2026-01-29 00:38:21 +01:00
|
|
|
// retoor <retoor@molodetz.nl>
|
|
|
|
|
#include "context_manager.h"
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
2026-01-29 06:01:05 +01:00
|
|
|
#define MIN_KEEP_CHARS 500
|
2026-01-29 00:38:21 +01:00
|
|
|
#define TRUNCATE_MARKER "\n\n[... content truncated for context management ...]\n\n"
|
2026-01-29 06:01:05 +01:00
|
|
|
static const char *get_message_role(struct json_object *msg) {
|
2026-01-29 00:38:21 +01:00
|
|
|
struct json_object *role_obj;
|
|
|
|
|
if (json_object_object_get_ex(msg, "role", &role_obj)) {
|
2026-01-29 06:01:05 +01:00
|
|
|
return json_object_get_string(role_obj);
|
2026-01-29 00:38:21 +01:00
|
|
|
}
|
2026-01-29 07:42:06 +01:00
|
|
|
// Tool results don't have a 'role' field in some implementations,
|
|
|
|
|
// they have 'tool_call_id'.
|
|
|
|
|
if (json_object_object_get_ex(msg, "tool_call_id", &role_obj)) {
|
|
|
|
|
return "tool";
|
|
|
|
|
}
|
2026-01-29 06:01:05 +01:00
|
|
|
return "";
|
2026-01-29 00:38:21 +01:00
|
|
|
}
|
2026-01-29 07:42:06 +01:00
|
|
|
static bool has_tool_calls(struct json_object *msg) {
|
|
|
|
|
struct json_object *tool_calls;
|
|
|
|
|
if (json_object_object_get_ex(msg, "tool_calls", &tool_calls)) {
|
|
|
|
|
return json_object_array_length(tool_calls) > 0;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-01-29 06:01:05 +01:00
|
|
|
static size_t get_message_content_len(struct json_object *msg) {
|
|
|
|
|
struct json_object *content_obj;
|
|
|
|
|
const char *content = NULL;
|
|
|
|
|
if (json_object_object_get_ex(msg, "content", &content_obj)) {
|
|
|
|
|
content = json_object_get_string(content_obj);
|
|
|
|
|
} else if (json_object_object_get_ex(msg, "tool_result", &content_obj)) {
|
|
|
|
|
content = json_object_get_string(content_obj);
|
|
|
|
|
}
|
|
|
|
|
return content ? strlen(content) : 0;
|
|
|
|
|
}
|
|
|
|
|
static size_t calculate_total_size(messages_handle msgs) {
|
|
|
|
|
size_t total = 0;
|
|
|
|
|
int count = messages_count(msgs);
|
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
|
|
|
total += get_message_content_len(messages_get_object(msgs, i));
|
|
|
|
|
}
|
|
|
|
|
return total;
|
|
|
|
|
}
|
|
|
|
|
static r_status_t perform_truncate(messages_handle msgs, int index, double ratio) {
|
2026-01-29 00:38:21 +01:00
|
|
|
struct json_object *msg = messages_get_object(msgs, index);
|
|
|
|
|
if (!msg) return R_ERROR_NOT_FOUND;
|
|
|
|
|
struct json_object *content_obj;
|
|
|
|
|
const char *content = NULL;
|
|
|
|
|
bool is_tool_result = false;
|
|
|
|
|
if (json_object_object_get_ex(msg, "content", &content_obj)) {
|
|
|
|
|
content = json_object_get_string(content_obj);
|
|
|
|
|
} else if (json_object_object_get_ex(msg, "tool_result", &content_obj)) {
|
|
|
|
|
content = json_object_get_string(content_obj);
|
|
|
|
|
is_tool_result = true;
|
|
|
|
|
}
|
|
|
|
|
if (!content) return R_SUCCESS;
|
|
|
|
|
size_t len = strlen(content);
|
2026-01-29 06:01:05 +01:00
|
|
|
size_t target_len = (size_t)(len * ratio);
|
|
|
|
|
if (target_len < MIN_KEEP_CHARS * 2) target_len = MIN_KEEP_CHARS * 2;
|
|
|
|
|
if (target_len >= len) return R_SUCCESS;
|
|
|
|
|
size_t keep_each = target_len / 2;
|
|
|
|
|
|
2026-04-03 10:42:43 +02:00
|
|
|
size_t marker_len = strlen(TRUNCATE_MARKER);
|
|
|
|
|
char *new_content = malloc(keep_each * 2 + marker_len + 1);
|
2026-01-29 00:38:21 +01:00
|
|
|
if (!new_content) return R_ERROR_OUT_OF_MEMORY;
|
2026-04-03 10:42:43 +02:00
|
|
|
memcpy(new_content, content, keep_each);
|
|
|
|
|
memcpy(new_content + keep_each, TRUNCATE_MARKER, marker_len);
|
|
|
|
|
size_t offset = keep_each + marker_len;
|
|
|
|
|
memcpy(new_content + offset, content + len - keep_each, keep_each);
|
|
|
|
|
new_content[offset + keep_each] = '\0';
|
2026-01-29 07:42:06 +01:00
|
|
|
struct json_object *new_msg = json_tokener_parse(json_object_to_json_string(msg));
|
2026-01-29 00:38:21 +01:00
|
|
|
if (is_tool_result) {
|
|
|
|
|
json_object_object_add(new_msg, "tool_result", json_object_new_string(new_content));
|
|
|
|
|
} else {
|
|
|
|
|
json_object_object_add(new_msg, "content", json_object_new_string(new_content));
|
|
|
|
|
}
|
|
|
|
|
free(new_content);
|
|
|
|
|
return messages_replace_at(msgs, index, new_msg);
|
|
|
|
|
}
|
|
|
|
|
r_status_t context_manager_shrink(messages_handle msgs) {
|
|
|
|
|
if (!msgs) return R_ERROR_INVALID_ARG;
|
|
|
|
|
int count = messages_count(msgs);
|
2026-01-29 07:42:06 +01:00
|
|
|
if (count <= 2) return R_ERROR_API_ERROR;
|
2026-01-29 06:01:05 +01:00
|
|
|
size_t initial_size = calculate_total_size(msgs);
|
2026-01-29 07:42:06 +01:00
|
|
|
size_t target_size = (size_t)(initial_size * 0.5);
|
|
|
|
|
if (target_size < 20000) target_size = 20000;
|
|
|
|
|
fprintf(stderr, " \033[2m-> Context overflow (%zu chars). Middle-out shrinking to %zu...\033[0m\n",
|
2026-01-29 06:01:05 +01:00
|
|
|
initial_size, target_size);
|
2026-01-29 07:42:06 +01:00
|
|
|
// Strategy 1: Truncate very large messages first (safe, doesn't break sequence)
|
|
|
|
|
for (int i = 0; i < messages_count(msgs); i++) {
|
|
|
|
|
struct json_object *msg = messages_get_object(msgs, i);
|
|
|
|
|
if (get_message_content_len(msg) > 50000) {
|
|
|
|
|
perform_truncate(msgs, i, 0.2);
|
2026-01-29 00:38:21 +01:00
|
|
|
}
|
2026-01-29 07:42:06 +01:00
|
|
|
}
|
|
|
|
|
// Strategy 2: Remove messages from the middle until size is within target
|
|
|
|
|
// We keep:
|
|
|
|
|
// - System message (usually index 0)
|
|
|
|
|
// - Most recent 4 messages (usually current task context)
|
|
|
|
|
while (calculate_total_size(msgs) > target_size && messages_count(msgs) > 6) {
|
|
|
|
|
int middle_idx = 1; // Start after system
|
|
|
|
|
struct json_object *msg = messages_get_object(msgs, middle_idx);
|
|
|
|
|
const char *role = get_message_role(msg);
|
|
|
|
|
int remove_count = 1;
|
|
|
|
|
if (strcmp(role, "assistant") == 0 && has_tool_calls(msg)) {
|
|
|
|
|
// Must also remove the following tool results to maintain sequence
|
|
|
|
|
int search_idx = middle_idx + 1;
|
|
|
|
|
while (search_idx < messages_count(msgs) - 4) {
|
|
|
|
|
struct json_object *next_msg = messages_get_object(msgs, search_idx);
|
|
|
|
|
if (strcmp(get_message_role(next_msg), "tool") == 0) {
|
|
|
|
|
remove_count++;
|
|
|
|
|
search_idx++;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-01-29 06:01:05 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-29 07:42:06 +01:00
|
|
|
// Ensure we don't eat into the "recent" buffer
|
|
|
|
|
if (middle_idx + remove_count > messages_count(msgs) - 4) {
|
2026-01-29 00:38:21 +01:00
|
|
|
break;
|
|
|
|
|
}
|
2026-01-29 07:42:06 +01:00
|
|
|
messages_remove_range(msgs, middle_idx, remove_count);
|
2026-01-29 00:38:21 +01:00
|
|
|
}
|
2026-01-29 07:42:06 +01:00
|
|
|
size_t final_size = calculate_total_size(msgs);
|
|
|
|
|
fprintf(stderr, " \033[2m-> Context shrunk to %zu chars. Remaining messages: %d\033[0m\n",
|
|
|
|
|
final_size, messages_count(msgs));
|
2026-01-29 06:01:05 +01:00
|
|
|
return R_SUCCESS;
|
2026-01-29 07:42:06 +01:00
|
|
|
}
|