Entropic 2.3.8
Local-first agentic inference engine
Loading...
Searching...
No Matches
compactor_registry.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
10
11#include <sstream>
12
13static auto logger = entropic::log::get("core.compactor_registry");
14
15namespace entropic {
16
17// ── JSON helpers (internal to this TU) ──────────────────
18
26static const char* json_escape_char(char c) {
27 const char* esc = nullptr;
28 switch (c) {
29 case '"': esc = "\\\""; break;
30 case '\\': esc = "\\\\"; break;
31 case '\n': esc = "\\n"; break;
32 case '\r': esc = "\\r"; break;
33 case '\t': esc = "\\t"; break;
34 default: break;
35 }
36 return esc;
37}
38
46static std::string json_escape(const std::string& input) {
47 std::ostringstream oss;
48 for (char c : input) {
49 const char* esc = json_escape_char(c);
50 if (esc != nullptr) { oss << esc; }
51 else { oss << c; }
52 }
53 return oss.str();
54}
55
63static std::string serialize_messages(
64 const std::vector<Message>& messages) {
65 std::ostringstream oss;
66 oss << '[';
67 for (size_t i = 0; i < messages.size(); ++i) {
68 if (i > 0) oss << ',';
69 oss << "{\"role\":\"" << messages[i].role
70 << "\",\"content\":\""
71 << json_escape(messages[i].content) << "\"}";
72 }
73 oss << ']';
74 return oss.str();
75}
76
86static std::string serialize_config(
87 const CompactionConfig& config,
88 const std::string& identity,
89 int token_count) {
90 std::ostringstream oss;
91 oss << "{\"identity\":\"" << json_escape(identity) << "\""
92 << ",\"token_count\":" << token_count
93 << ",\"max_tokens\":0"
94 << ",\"threshold_percent\":" << config.threshold_percent
95 << ",\"force\":true}";
96 return oss.str();
97}
98
99// ── Minimal JSON parser for message arrays ──────────────
100
107 const std::string& data;
108 size_t pos = 0;
109};
110
117static void json_skip_ws(JsonCursor& c) {
118 while (c.pos < c.data.size() && isspace(c.data[c.pos])) {
119 ++c.pos;
120 }
121}
122
130static char json_unescape_char(char ch) {
131 char result = ch;
132 switch (ch) {
133 case '"': result = '"'; break;
134 case '\\': result = '\\'; break;
135 case 'n': result = '\n'; break;
136 case 'r': result = '\r'; break;
137 case 't': result = '\t'; break;
138 default: break;
139 }
140 return result;
141}
142
151static bool json_read_string(JsonCursor& c, std::string& out) {
152 json_skip_ws(c);
153 if (c.pos >= c.data.size() || c.data[c.pos] != '"') {
154 return false;
155 }
156 ++c.pos;
157 out.clear();
158 while (c.pos < c.data.size() && c.data[c.pos] != '"') {
159 if (c.data[c.pos] == '\\' && c.pos + 1 < c.data.size()) {
160 out += json_unescape_char(c.data[c.pos + 1]);
161 c.pos += 2;
162 } else {
163 out += c.data[c.pos++];
164 }
165 }
166 if (c.pos >= c.data.size()) return false;
167 ++c.pos; // skip closing '"'
168 return true;
169}
170
178 char open = c.data[c.pos];
179 char close = (open == '[') ? ']' : '}';
180 int depth = 1;
181 ++c.pos;
182 while (c.pos < c.data.size() && depth > 0) {
183 char ch = c.data[c.pos];
184 if (ch == '"') {
185 std::string dummy;
186 json_read_string(c, dummy);
187 continue;
188 }
189 if (ch == open) ++depth;
190 else if (ch == close) --depth;
191 ++c.pos;
192 }
193}
194
202 json_skip_ws(c);
203 if (c.pos >= c.data.size()) return;
204 char ch = c.data[c.pos];
205 if (ch == '"') {
206 std::string dummy;
207 json_read_string(c, dummy);
208 return;
209 }
210 if (ch == '[' || ch == '{') {
212 return;
213 }
214 while (c.pos < c.data.size()
215 && c.data[c.pos] != ',' && c.data[c.pos] != '}'
216 && c.data[c.pos] != ']') {
217 ++c.pos;
218 }
219}
220
231 std::string& type_val,
232 std::string& text_val) {
233 std::string key;
234 if (!json_read_string(c, key)) return false;
235 json_skip_ws(c);
236 if (c.pos >= c.data.size() || c.data[c.pos] != ':') return false;
237 ++c.pos;
238 if (key == "type") { json_read_string(c, type_val); }
239 else if (key == "text") { json_read_string(c, text_val); }
240 else { json_skip_value(c); }
241 return true;
242}
243
254 std::string& type_val,
255 std::string& text_val) {
256 while (c.pos < c.data.size() && c.data[c.pos] != '}') {
257 json_skip_ws(c);
258 if (c.data[c.pos] == ',') { ++c.pos; continue; }
259 if (!json_read_content_part_field(c, type_val, text_val)) {
260 return false;
261 }
262 }
263 if (c.pos < c.data.size()) ++c.pos; // skip '}'
264 return true;
265}
266
275static void append_text_part(const std::string& type_val,
276 const std::string& text_val,
277 std::string& text) {
278 if (type_val != "text" || text_val.empty()) return;
279 if (!text.empty()) text += ' ';
280 text += text_val;
281}
282
303 while (c.pos < c.data.size()) {
304 json_skip_ws(c);
305 char ch = c.data[c.pos];
306 if (ch != ',') return ch;
307 ++c.pos;
308 }
309 return '\0';
310}
311
321 std::string& text) {
322 ++c.pos; // skip '{'
323 std::string type_val;
324 std::string text_val;
325 bool ok = json_parse_content_part(c, type_val, text_val);
326 if (ok) append_text_part(type_val, text_val, text);
327 return ok;
328}
329
343 std::string& text) {
344 if (c.pos >= c.data.size() || c.data[c.pos] != '[') {
345 return false;
346 }
347 ++c.pos; // skip '['
348 text.clear();
349
350 bool ok = true;
351 char ch = json_skip_to_element(c);
352 while (ok && ch == '{') {
354 ch = ok ? json_skip_to_element(c) : '\0';
355 }
356 if (ok && ch == ']') { ++c.pos; }
357 return ok && ch == ']';
358}
359
372static bool json_read_field(JsonCursor& c, Message& msg) {
373 std::string key;
374 bool ok = json_read_string(c, key);
375 json_skip_ws(c);
376 if (ok && c.pos < c.data.size() && c.data[c.pos] == ':') {
377 ++c.pos;
378 } else {
379 ok = false;
380 }
381
382 if (!ok) return false;
383
384 if (key == "role") {
385 ok = json_read_string(c, msg.role);
386 } else if (key == "content") {
387 json_skip_ws(c);
388 if (c.pos < c.data.size() && c.data[c.pos] == '[') {
390 } else {
391 ok = json_read_string(c, msg.content);
392 }
393 } else {
395 }
396 return ok;
397}
398
407static bool json_parse_message(JsonCursor& c, Message& msg) {
408 while (c.pos < c.data.size()) {
409 json_skip_ws(c);
410 if (c.data[c.pos] == '}') { ++c.pos; return true; }
411 if (c.data[c.pos] == ',') { ++c.pos; continue; }
412 if (!json_read_field(c, msg)) return false;
413 }
414 return false;
415}
416
426 std::vector<Message>& messages) {
427 bool ok = true;
428 while (ok && c.pos < c.data.size()) {
429 json_skip_ws(c);
430 if (c.data[c.pos] == ']') break;
431 if (c.data[c.pos] == ',') { ++c.pos; continue; }
432 ok = (c.data[c.pos] == '{');
433 if (!ok) break;
434 ++c.pos;
435
436 Message msg;
437 ok = json_parse_message(c, msg);
438 if (ok) messages.push_back(std::move(msg));
439 }
440 return ok;
441}
442
451static bool parse_messages(const char* json,
452 std::vector<Message>& messages) {
453 if (json == nullptr || json[0] != '[') return false;
454 messages.clear();
455 std::string input(json);
456 JsonCursor c{input, 1};
457 return json_parse_array(c, messages);
458}
459
460// ── CompactorRegistry ───────────────────────────────────
461
469 CompactionManager& default_manager)
470 : default_manager_(default_manager) {
471 default_compactor_ = [&mgr = default_manager_](
472 const std::vector<Message>& messages,
473 const CompactionConfig& /*config*/,
474 const std::string& /*identity*/) {
475 return mgr.compact_messages(messages);
476 };
477}
478
489 const std::string& identity,
490 entropic_compactor_fn compactor,
491 void* user_data) {
492 if (compactor == nullptr) {
493 logger->error("NULL compactor for identity '{}'", identity);
495 }
496
497 CompactorEntry entry;
498 entry.c_callback = compactor;
499 entry.user_data = user_data;
500 entry.cpp_fn = wrap_c_compactor(compactor, user_data);
501
502 std::unique_lock lock(mutex_);
503 compactors_[identity] = std::move(entry);
504
505 logger->info("Registered compactor for identity '{}'",
506 identity.empty() ? "(global)" : identity);
507 return ENTROPIC_OK;
508}
509
518 const std::string& identity) {
519 std::unique_lock lock(mutex_);
520 compactors_.erase(identity);
521
522 logger->info("Deregistered compactor for identity '{}'",
523 identity.empty() ? "(global)" : identity);
524 return ENTROPIC_OK;
525}
526
537 const std::string& identity,
538 const std::vector<Message>& messages,
539 const CompactionConfig& config) {
540 CompactorFn selected;
541 std::string source;
542 bool is_custom = false;
543
544 { // Snapshot under read lock, release before calling
545 std::shared_lock lock(mutex_);
546 auto it = compactors_.find(identity);
547 if (it != compactors_.end()) {
548 selected = it->second.cpp_fn;
549 source = identity;
550 is_custom = true;
551 } else {
552 it = compactors_.find("");
553 if (it != compactors_.end()) {
554 selected = it->second.cpp_fn;
555 source = "global_custom";
556 is_custom = true;
557 }
558 }
559 }
560
561 if (!is_custom) {
562 return run_default(identity, messages, config);
563 }
564 return run_custom(
565 selected, source, identity, messages, config);
566}
567
577CompactionResult CompactorRegistry::run_default(
578 const std::string& identity,
579 const std::vector<Message>& messages,
580 const CompactionConfig& config) {
581 auto result = default_compactor_(messages, config, identity);
582 result.identity = identity;
583 result.compactor_source = "default";
584 result.custom_compactor_used = false;
585 return result;
586}
587
599CompactionResult CompactorRegistry::run_custom(
600 const CompactorFn& selected,
601 const std::string& source,
602 const std::string& identity,
603 const std::vector<Message>& messages,
604 const CompactionConfig& config) {
605 auto result = selected(messages, config, identity);
606
607 if (result.compacted) {
608 result.identity = identity;
609 result.compactor_source = source;
610 result.custom_compactor_used = true;
611 return result;
612 }
613
614 logger->warn("Custom compactor '{}' failed for '{}', "
615 "falling back to default", source, identity);
616 return run_default(identity, messages, config);
617}
618
627 const std::string& identity) const {
628 std::shared_lock lock(mutex_);
629 if (compactors_.count(identity) > 0) {
630 return true;
631 }
632 return compactors_.count("") > 0;
633}
634
643CompactorFn CompactorRegistry::wrap_c_compactor(
644 entropic_compactor_fn compactor,
645 void* user_data) {
646 return [compactor, user_data](
647 const std::vector<Message>& messages,
648 const CompactionConfig& config,
649 const std::string& identity) -> CompactionResult {
650 auto msg_json = serialize_messages(messages);
651 auto cfg_json = serialize_config(config, identity, 0);
652
653 char* out_messages = nullptr;
654 char* out_summary = nullptr;
655 int rc = compactor(
656 msg_json.c_str(), cfg_json.c_str(),
657 &out_messages, &out_summary, user_data);
658
659 CompactionResult result;
660 if (rc != 0) {
661 result.compacted = false;
662 free(out_messages);
663 free(out_summary);
664 return result;
665 }
666
667 result.compacted = true;
668 if (out_summary != nullptr) {
669 result.summary = out_summary;
670 free(out_summary);
671 }
672 if (out_messages != nullptr) {
673 parse_messages(out_messages, result.messages);
674 result.preserved_messages =
675 static_cast<int>(result.messages.size());
676 free(out_messages);
677 }
678 return result;
679 };
680}
681
682} // namespace entropic
Manages automatic context compaction.
Definition compaction.h:113
entropic_error_t register_compactor(const std::string &identity, entropic_compactor_fn compactor, void *user_data)
Register a compactor for a specific identity.
CompactionResult compact(const std::string &identity, const std::vector< Message > &messages, const CompactionConfig &config)
Run compaction using the appropriate compactor.
CompactorRegistry(CompactionManager &default_manager)
Construct with default compactor.
bool has_custom_compactor(const std::string &identity) const
Check if a custom compactor is registered for an identity.
entropic_error_t deregister_compactor(const std::string &identity)
Deregister a compactor for a specific identity.
Per-identity compactor registration and dispatch.
int(* entropic_compactor_fn)(const char *messages_json, const char *config_json, char **out_messages, char **out_summary, void *user_data)
Compactor function type.
Definition entropic.h:1966
entropic_error_t
Error codes returned by all C API functions.
Definition error.h:35
@ ENTROPIC_OK
Success.
Definition error.h:36
@ ENTROPIC_ERROR_INVALID_CONFIG
Config validation failed (missing fields, bad values)
Definition error.h:38
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 void json_skip_composite(JsonCursor &c)
Skip a JSON array or object in cursor (nested-aware).
static void json_skip_value(JsonCursor &c)
Skip one JSON value (string, primitive, array, or object).
@ ok
Tool dispatched, returned non-empty content.
static std::string serialize_config(const CompactionConfig &config, const std::string &identity, int token_count)
Serialize compaction config + identity to JSON.
static const char * json_escape_char(char c)
Map a character to its JSON escape sequence.
static bool json_read_string(JsonCursor &c, std::string &out)
Read a JSON quoted string from cursor.
static bool json_extract_text_from_array(JsonCursor &c, std::string &text)
Extract concatenated text from a JSON content array.
static void json_skip_ws(JsonCursor &c)
Skip whitespace in a JSON cursor.
static bool json_parse_message(JsonCursor &c, Message &msg)
Parse one JSON object into a Message (role + content).
static std::string json_escape(const std::string &input)
Save pre-compaction snapshot via storage interface.
static bool json_parse_array(JsonCursor &c, std::vector< Message > &messages)
Parse message objects from a JSON array cursor.
static bool json_parse_content_part(JsonCursor &c, std::string &type_val, std::string &text_val)
Parse one content part object, extract type and text.
static std::string serialize_messages(const std::vector< Message > &messages)
Serialize messages to minimal JSON array.
static char json_skip_to_element(JsonCursor &c)
Read one content part object from an array cursor.
static bool json_read_content_part_field(JsonCursor &c, std::string &type_val, std::string &text_val)
Read one field of a content part object into type/text.
std::function< CompactionResult(const std::vector< Message > &messages, const CompactionConfig &config, const std::string &identity)> CompactorFn
Internal C++ compactor function type.
static char json_unescape_char(char ch)
Decode one JSON escape sequence character.
static bool parse_messages(const char *json, std::vector< Message > &messages)
Parse a JSON array of message objects.
static bool json_read_one_content_part(JsonCursor &c, std::string &text)
Parse one content part object and append text if applicable.
static void append_text_part(const std::string &type_val, const std::string &text_val, std::string &text)
Append text from a content part if type == "text".
static bool json_read_field(JsonCursor &c, Message &msg)
Read one key:value pair and apply to message fields.
Auto-compaction configuration.
Definition config.h:508
float threshold_percent
Compaction trigger (0.5–0.99)
Definition config.h:510
Result of a compaction operation.
Definition compaction.h:89
std::string summary
Generated summary text.
Definition compaction.h:93
std::vector< Message > messages
The compacted message list (v1.9.9)
Definition compaction.h:96
int preserved_messages
Messages kept after compaction.
Definition compaction.h:94
bool compacted
Whether compaction occurred.
Definition compaction.h:90
A registered compactor entry.
entropic_compactor_fn c_callback
C function pointer (NULL for C++ compactors)
CompactorFn cpp_fn
C++ function (wraps c_callback or native)
void * user_data
Opaque data for C callback.
Lightweight cursor into a JSON string.
const std::string & data
Source string.
size_t pos
Current position.
A message in a conversation.
Definition message.h:35
std::string content
Message text content (always populated)
Definition message.h:37
std::string role
Message role.
Definition message.h:36