Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions src/core/agent.c
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,25 @@ static int copy_printed_session_json(char *dst, size_t dst_size, char *printed,

static const char SUMMARIZE_SYSTEM[] = "Summarize the following conversation in one short paragraph. Output only the summary, no preamble.";

/** Summarize oldest messages when over max_ctx; replace with one summary + trailing. */
/** Append one "role: content" line, or roll back if the whole line does not fit. */
static int summary_source_append(char *buf, size_t cap, size_t *used,
const char *role, const char *content)
{
int n;
if (!buf || !used || *used >= cap - 1)
return 0;
n = snprintf(buf + *used, cap - *used, "%s: %s\n",
role ? role : "user", content ? content : "");
if (n < 0 || (size_t)n >= cap - *used) {
buf[*used] = '\0';
return 0;
}
*used += (size_t)n;
return 1;
}

/** Summarize oldest messages when over max_ctx; replace with one summary + trailing.
* Messages that do not fit in the summary source stay in the session. */
static int compact_session_via_llm(const char *session_id, char *session_buf, size_t session_buf_size,
int msg_count, int max_ctx, const provider_t *provider)
{
Expand All @@ -97,17 +115,27 @@ static int compact_session_via_llm(const char *session_id, char *session_buf, si
if (!source_buf) { cJSON_Delete(root); return -1; }
source_buf[0] = '\0';
size_t used = 0;
for (int i = 0; i < to_summarize_n && used < SUMMARY_SOURCE_MAX - 1; i++) {
int next_unsummarized = to_summarize_n;
for (int i = 0; i < to_summarize_n; i++) {
const cJSON *item = cJSON_GetArrayItem(root, i);
if (!item || !cJSON_IsObject(item)) continue;
const char *role = "user";
const char *content = "";
const cJSON *r = cJSON_GetObjectItem(item, "role");
const cJSON *c = cJSON_GetObjectItem(item, "content");
const cJSON *r;
const cJSON *c;
if (!item || !cJSON_IsObject(item)) continue;
r = cJSON_GetObjectItem(item, "role");
c = cJSON_GetObjectItem(item, "content");
if (cJSON_IsString(r)) role = r->valuestring;
if (cJSON_IsString(c)) content = c->valuestring;
int n = snprintf(source_buf + used, SUMMARY_SOURCE_MAX - used, "%s: %s\n", role, content ? content : "");
if (n > 0 && (size_t)n < SUMMARY_SOURCE_MAX - used) used += (size_t)n;
if (!summary_source_append(source_buf, SUMMARY_SOURCE_MAX, &used, role, content)) {
next_unsummarized = i;
break;
}
}
if (used == 0 || next_unsummarized == 0) {
free(source_buf);
cJSON_Delete(root);
return -1;
}
provider_message_t summ_msgs[2] = {
{ .role = "system", .content = SUMMARIZE_SYSTEM, .tool_calls = NULL, .tool_calls_count = 0, .tool_use_id = NULL },
Expand Down Expand Up @@ -136,7 +164,8 @@ static int compact_session_via_llm(const char *session_id, char *session_buf, si
cJSON_AddItemToArray(new_arr, summ_obj);
}
int keep = max_ctx - 1;
int start = msg_count - keep;
int start = (next_unsummarized < to_summarize_n)
? next_unsummarized : (msg_count - keep);
if (start < 0) start = 0;
for (int i = start; i < msg_count; i++) {
const cJSON *item = cJSON_GetArrayItem(root, i);
Expand Down
82 changes: 82 additions & 0 deletions tests/test_agent.c
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,87 @@ static int test_context_compaction_when_history_exceeds_max(void)
return failed;
}

/* A message past the summary-source cap must stay in the session. The summarizer
* only receives lines that fit; deleting the rest loses history the model never saw. */
static int test_compaction_keeps_message_that_does_not_fit_summary(void)
{
enum { BIG = 65 * 1024, JSON_CAP = 96 * 1024, LOAD_CAP = 128 * 1024 };
int failed = 1;
config_t *cfg = NULL;
char *session_json = NULL;
char *loaded = NULL;
char *big = NULL;
const char *db_path = "build/test_agent_compact_fit.db";
const char *config_path = "build/test_agent_compact_fit.toml";
size_t off;
int i;
char response_buf[4096];
char errbuf[256];
FILE *cf;

memory_cleanup();
compaction_call_count = 0;
if (memory_init(db_path) != 0)
goto cleanup_early;
session_json = malloc(JSON_CAP);
big = malloc((size_t)BIG + 1);
loaded = malloc(LOAD_CAP);
if (!session_json || !big || !loaded)
goto cleanup;
memset(big, 'A', (size_t)BIG);
memcpy(big + BIG - 16, "BIG_MARKER_END!!", 16);
big[BIG] = '\0';
off = 0;
off += (size_t)snprintf(session_json + off, JSON_CAP - off, "[");
off += (size_t)snprintf(session_json + off, JSON_CAP - off,
"{\"role\":\"user\",\"content\":\"alpha_small\"}");
off += (size_t)snprintf(session_json + off, JSON_CAP - off,
",{\"role\":\"user\",\"content\":\"%s\"}", big);
for (i = 2; i < 6; i++) {
off += (size_t)snprintf(session_json + off, JSON_CAP - off,
",{\"role\":\"user\",\"content\":\"m%d\"}", i);
}
off += (size_t)snprintf(session_json + off, JSON_CAP - off, "]");
if (off >= JSON_CAP)
goto cleanup;
if (session_save("cli:compactfit", session_json) != 0)
goto cleanup;
cf = fopen(config_path, "w");
if (!cf)
goto cleanup;
fprintf(cf, "[agent]\nmodel = \"test\"\nmax_context_messages = 4\n[memory]\npath = \"%s\"\n",
db_path);
fclose(cf);
if (config_load(config_path, &cfg, errbuf, sizeof(errbuf)) != 0)
goto cleanup;
response_buf[0] = '\0';
if (agent_run(cfg, "cli:compactfit", "hi", &compaction_provider, NULL, 0,
response_buf, sizeof(response_buf)) != 0)
goto cleanup;
if (session_load("cli:compactfit", loaded, LOAD_CAP) != 0)
goto cleanup;
if (strstr(loaded, "BIG_MARKER_END!!") == NULL) {
fprintf(stderr,
"FAIL: compacted session dropped the message that did not fit the summary\n");
goto cleanup;
}
if (strstr(loaded, "Summary of earlier") == NULL) {
fprintf(stderr, "FAIL: expected a summary of the messages that did fit\n");
goto cleanup;
}
failed = 0;
cleanup:
config_free(cfg);
free(session_json);
free(big);
free(loaded);
cleanup_early:
remove(config_path);
remove(db_path);
memory_cleanup();
return failed;
}

static int test_local_offline_note_when_active_is_local(void)
{
const char *path = "build/test_agent_local_note.toml";
Expand Down Expand Up @@ -1186,6 +1267,7 @@ int main(void)
RUN(test_react_loop_preserves_prior_tool_results());
RUN(test_session_persisted_after_exchange());
RUN(test_context_compaction_when_history_exceeds_max());
RUN(test_compaction_keeps_message_that_does_not_fit_summary());
RUN(test_local_offline_note_when_active_is_local());
RUN(test_local_offline_note_skipped_for_non_local());
RUN(test_session_overflow_does_not_corrupt_history());
Expand Down
Loading