Entropic 2.14.3
Local-first agentic inference engine
Loading...
Searching...
No Matches
compaction.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
11
12#include <sstream>
13
14static auto logger = entropic::log::get("core.compaction");
15
16namespace entropic {
17
18// ── TokenCounter ─────────────────────────────────────────
19
27 : max_tokens(max_tokens) {}
28
36int TokenCounter::count_text(const std::string& text) {
37 if (text.empty()) {
38 return 0;
39 }
40 return static_cast<int>(text.size()) / 4 + 1;
41}
42
57 return count_text(msg.content) + 4; // +4 for role tokens
58}
59
68 const std::vector<Message>& messages) const {
69 int total = 0;
70 for (const auto& msg : messages) {
72 }
73 return total;
74}
75
84 const std::vector<Message>& messages) const {
85 if (max_tokens == 0) {
86 return 0.0f;
87 }
88 return static_cast<float>(count_messages(messages))
89 / static_cast<float>(max_tokens);
90}
91
103
104// ── CompactionManager ────────────────────────────────────
105
114 const CompactionConfig& config,
115 TokenCounter& counter)
116 : config(config), counter(counter) {}
117
127 std::vector<Message>& messages,
128 bool force,
129 const std::string& conversation_id) {
130 int current = counter.count_messages(messages);
131 int threshold = static_cast<int>(
132 static_cast<float>(counter.max_tokens) * config.threshold_percent);
133
134 if (!force && current < threshold) {
135 return {false, current, current};
136 }
137
138 if (!config.enabled) {
139 logger->warn("Context at {}/{} tokens, compaction disabled",
141 return {false, current, current};
142 }
143
144 logger->info("Compacting conversation ({} tokens)", current);
145
146 // Save full history before compacting (v1.8.8)
147 if (config.save_full_history && !conversation_id.empty()) {
148 save_snapshot(conversation_id, messages);
149 }
150
151 std::string summary;
152 int stripped = 0;
153 auto compacted = compact(messages, summary, stripped);
154
156 int new_count = counter.count_messages(compacted);
157
158 if (new_count >= current) {
159 logger->error("Compaction did not reduce tokens: {} -> {}",
161 }
162
163 logger->info("Compacted {} -> {} tokens", current, new_count);
164 messages = std::move(compacted);
165
166 CompactionResult result;
167 result.compacted = true;
168 result.old_token_count = current;
169 result.new_token_count = new_count;
170 result.summary = summary;
171 result.preserved_messages = static_cast<int>(messages.size()) - 1;
173 return result;
174}
175
186static void partition_messages(const std::vector<Message>& messages,
187 size_t start,
188 std::vector<const Message*>& user_msgs,
189 std::vector<const Message*>& assistant_msgs,
190 int& stripped_count) {
191 stripped_count = 0;
192 for (size_t i = start; i < messages.size(); ++i) {
193 const auto& msg = messages[i];
194 auto src = msg.metadata.find("source");
195 if (src != msg.metadata.end() && src->second == "user") {
196 user_msgs.push_back(&msg);
197 } else if (msg.role == "assistant") {
198 assistant_msgs.push_back(&msg);
199 } else {
201 }
202 }
203}
204
215static std::vector<Message> assemble_compacted(
217 const std::vector<const Message*>& user_msgs,
218 const std::vector<const Message*>& assistant_msgs) {
219 std::vector<Message> result;
220 if (system_msg != nullptr) { result.push_back(*system_msg); }
221 result.push_back(std::move(summary_msg));
222 for (const auto* m : user_msgs) { result.push_back(*m); }
223 if (!assistant_msgs.empty()) {
224 result.push_back(*assistant_msgs.back());
225 }
226 return result;
227}
228
238std::vector<Message> CompactionManager::compact(
239 const std::vector<Message>& messages,
240 std::string& summary,
241 int& stripped_count) {
242 Message const* system_msg = nullptr;
243 size_t start = 0;
244 if (!messages.empty() && messages[0].role == "system") {
245 system_msg = &messages[0];
246 start = 1;
247 }
248
249 std::vector<const Message*> user_msgs;
250 std::vector<const Message*> assistant_msgs;
253
254 std::vector<Message> working(messages.begin() + static_cast<long>(start),
255 messages.end());
256 logger->info("Compact: {} user, {} assistant, {} stripped",
257 user_msgs.size(), assistant_msgs.size(), stripped_count);
258 summary = structured_summary(working);
259 Message summary_msg;
260 summary_msg.role = "user";
261 summary_msg.content = format_summary(
262 summary, static_cast<int>(working.size()));
263
264 return assemble_compacted(system_msg, std::move(summary_msg),
266}
267
275std::string CompactionManager::structured_summary(
276 const std::vector<Message>& messages) {
277 std::string lines = "Original task: "
278 + extract_original_task(messages);
279
280 auto tool_log = extract_tool_log(messages);
281 if (!tool_log.empty()) {
282 lines += "\n\nTool calls made (oldest first):";
283 for (const auto& [name, brief] : tool_log) {
284 lines += "\n- " + name + ": " + brief;
285 }
286 }
287 return lines;
288}
289
304static bool is_tool_result(const Message& msg) {
305 if (msg.content.rfind("Tool `", 0) == 0) { return true; }
306 auto src = msg.metadata.find("source");
307 return src != msg.metadata.end() && src->second == "tool";
308}
309
318static std::string find_tagged(
319 const std::vector<Message>& messages,
320 const std::string& source) {
321 for (const auto& msg : messages) {
322 auto src = msg.metadata.find("source");
323 if (src != msg.metadata.end() && src->second == source) {
324 return msg.content;
325 }
326 }
327 return {};
328}
329
337static std::string find_first_user_task(
338 const std::vector<Message>& messages) {
339 for (const auto& msg : messages) {
340 if (msg.role != "user" || msg.content.empty()) { continue; }
341 if (is_tool_result(msg)) { continue; }
342 return msg.content;
343 }
344 return {};
345}
346
361std::string CompactionManager::extract_original_task(
362 const std::vector<Message>& messages) {
363 std::string task = find_tagged(messages, "user");
364 if (task.empty()) { task = find_first_user_task(messages); }
365 if (task.empty()) { return "(no user message found)"; }
366 if (task.size() > 500) { return task.substr(0, 500) + "..."; }
367 return task;
368}
369
377std::vector<std::pair<std::string, std::string>>
378CompactionManager::extract_tool_log(
379 const std::vector<Message>& messages) {
380 std::vector<std::pair<std::string, std::string>> log;
381 for (const auto& msg : messages) {
382 auto it = msg.metadata.find("tool_name");
383 if (it == msg.metadata.end()) {
384 continue;
385 }
386 const auto& name = it->second;
387 if (msg.content.rfind("[Previous:", 0) == 0) {
388 log.emplace_back(name, "(pruned)");
389 continue;
390 }
391 auto nl = msg.content.find('\n');
392 std::string brief = msg.content.substr(0, std::min(nl, size_t{100}));
393 log.emplace_back(name, brief);
394 }
395 return log;
396}
397
406std::string CompactionManager::format_summary(
407 const std::string& summary,
408 int message_count) {
409 return "[CONVERSATION SUMMARY]\n"
410 "The following summarizes "
411 + std::to_string(message_count)
412 + " previous messages that have been compacted"
413 " to save context space.\n\n"
414 + summary
415 + "\n\n[END SUMMARY - Recent conversation continues below]";
416}
417
431 const std::vector<Message>& messages) {
432 int old_count = counter.count_messages(messages);
433
434 std::string summary;
435 int stripped = 0;
436 auto compacted = compact(messages, summary, stripped);
437
439 int new_count = counter.count_messages(compacted);
440
441 CompactionResult result;
442 result.compacted = true;
443 result.old_token_count = old_count;
444 result.new_token_count = new_count;
445 result.summary = summary;
446 result.preserved_messages =
447 static_cast<int>(compacted.size()) - 1;
449 result.messages = compacted;
450 result.compactor_source = "default";
451 return result;
452}
453
461 storage_ = storage;
462}
463
478static std::string json_escape(const std::string& input) {
479 std::ostringstream oss;
480 for (char c : input) {
481 if (c == '"') oss << "\\\"";
482 else if (c == '\\') oss << "\\\\";
483 else if (c == '\n') oss << "\\n";
484 else oss << c;
485 }
486 return oss.str();
487}
488
496static std::string serialize_messages_json(
497 const std::vector<Message>& messages) {
498 std::ostringstream oss;
499 oss << '[';
500 for (size_t i = 0; i < messages.size(); ++i) {
501 if (i > 0) oss << ',';
502 oss << "{\"role\":\"" << messages[i].role
503 << "\",\"content\":\"" << json_escape(messages[i].content)
504 << "\"}";
505 }
506 oss << ']';
507 return oss.str();
508}
509
517void CompactionManager::save_snapshot(
518 const std::string& conversation_id,
519 const std::vector<Message>& messages) {
520 if (!storage_ || !storage_->save_snapshot) {
521 return;
522 }
523
524 auto json_str = serialize_messages_json(messages);
525 storage_->save_snapshot(
526 conversation_id.c_str(), json_str.c_str(),
527 storage_->user_data);
528 logger->info("Saved compaction snapshot for {} ({} messages)",
529 conversation_id, messages.size());
530}
531
532} // namespace entropic
CompactionResult compact_messages(const std::vector< Message > &messages)
Compact messages using the value-density strategy.
CompactionConfig config
Compaction configuration.
Definition compaction.h:181
TokenCounter & counter
Shared token counter.
Definition compaction.h:182
CompactionResult check_and_compact(std::vector< Message > &messages, bool force=false, const std::string &conversation_id="")
Check if compaction is needed and perform if so.
void set_storage(const struct StorageInterface *storage)
Set storage interface for compaction snapshots.
CompactionManager(const CompactionConfig &config, TokenCounter &counter)
Construct a compaction manager.
Track token usage across conversation.
Definition compaction.h:32
int max_tokens
Maximum context window size.
Definition compaction.h:94
void clear_cache()
Clear the token count cache — retained as a no-op (gh#158).
int count_messages(const std::vector< Message > &messages) const
Count total tokens in a message list.
float usage_percent(const std::vector< Message > &messages) const
Get usage as fraction of context window (0.0–1.0).
TokenCounter(int max_tokens)
Construct a token counter.
int count_message(const Message &msg) const
Count tokens in a single message.
Auto-compaction for context management.
Types for the agentic loop engine.
json lines
Lines taken so far.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
Activate model on GPU (WARM → ACTIVE).
static std::string find_tagged(const std::vector< Message > &messages, const std::string &source)
Find the first message matching a source tag.
static std::vector< Message > assemble_compacted(const Message *system_msg, Message summary_msg, const std::vector< const Message * > &user_msgs, const std::vector< const Message * > &assistant_msgs)
Assemble the compacted list: system, summary, users, last asst.
@ optional
optional: every escape asks the host's path approver
static void partition_messages(const std::vector< Message > &messages, size_t start, std::vector< const Message * > &user_msgs, std::vector< const Message * > &assistant_msgs, int &stripped_count)
Partition messages (from start) into user/assistant/stripped.
static std::string serialize_messages_json(const std::vector< Message > &messages)
Serialize messages to minimal JSON array.
static bool is_tool_result(const Message &msg)
Extract original user task from messages.
static std::string json_escape(const std::string &input)
Save pre-compaction snapshot via storage interface.
static std::string find_first_user_task(const std::vector< Message > &messages)
Find the first user-role message that isn't a tool result.
std::string config
–config FILE
Definition serve.cpp:38
Auto-compaction configuration.
Definition config.h:1059
bool save_full_history
Save full history before compaction.
Definition config.h:1065
float threshold_percent
Compaction trigger (0.5–0.99)
Definition config.h:1061
bool enabled
Enable auto-compaction.
Definition config.h:1060
Result of a compaction operation.
Definition compaction.h:111
int messages_summarized
Messages stripped into summary.
Definition compaction.h:117
std::string summary
Generated summary text.
Definition compaction.h:115
std::vector< Message > messages
The compacted message list (v1.9.9)
Definition compaction.h:118
int preserved_messages
Messages kept after compaction.
Definition compaction.h:116
int old_token_count
Token count before compaction.
Definition compaction.h:113
int new_token_count
Token count after compaction.
Definition compaction.h:114
bool compacted
Whether compaction occurred.
Definition compaction.h:112
std::string compactor_source
"default", "global_custom", or identity name
Definition compaction.h:122
A message in a conversation.
Definition message.h:36
Storage interface for conversation persistence.
bool(* save_snapshot)(const char *conversation_id, const char *messages_json, void *user_data)
Save a compaction snapshot (full history before compaction).
void * user_data
Opaque pointer (storage backend)