/* context.c — Hot-path context assembly for small-model agent framework. * * The small model has a fixed context budget (default 3072 tokens). This * module assembles a prompt from tiered priority sources with strict budgets. * Everything is token-counted; nothing overflows. Built as a shared lib * callable from Python via ctypes. * * Pri0: System + tool specs (512 tok, immutable, cron-updated) * Pri1: Active task (768 tok, per-task reset) * Pri2: Persistent memory (1024 tok, scored + deduplicated) * Pri3: Recent conversation (768 tok, FIFO sliding window) * * Compile: * cc -O3 -std=c11 -fPIC -shared context.c -o libcontext.so * * Token estimation: ~4 chars/token for English text (conservative). */ #include #include #include #include #define MAX_TOKENS 3072 #define PRI0_TOKENS 512 #define PRI1_TOKENS 768 #define PRI2_TOKENS 1024 #define PRI3_TOKENS 768 #define CHARS_PER_TOKEN 4 #define MAX_BUF (MAX_TOKENS * CHARS_PER_TOKEN * 2) /* generous */ static char g_buf[MAX_BUF]; /* ---- Token estimation ---- */ static int est_tokens(const char *s) { int chars = 0; while (*s) { chars++; s++; } return (chars + CHARS_PER_TOKEN - 1) / CHARS_PER_TOKEN; } /* Truncate string to fit max_tokens, adding "..." if truncated. * Returns bytes written (not counting null). */ static int truncate(char *dst, const char *src, int max_tokens) { int max_chars = max_tokens * CHARS_PER_TOKEN; int n = 0; while (src[n] && n < max_chars - 3) n++; memcpy(dst, src, n); if (src[n]) { dst[n++] = '.'; dst[n++] = '.'; dst[n++] = '.'; } dst[n] = '\0'; return n; } /* ---- Public API ---- */ /* Score a memory entry by novelty. Returns 0.0-1.0. * Simple: count word overlap with existing entries. Lower overlap = higher novelty. */ double context_memory_score(const char *entry, const char **existing, int n_existing) { if (n_existing == 0) return 1.0; int total_words = 0, overlap = 0; const char *p = entry; char word[64]; while (*p) { while (*p == ' ' || *p == '\n') p++; if (!*p) break; int len = 0; while (p[len] && p[len] != ' ' && p[len] != '\n' && len < 63) len++; memcpy(word, p, len); word[len] = '\0'; p += len; total_words++; for (int i = 0; i < n_existing; i++) { const char *found = strstr(existing[i], word); if (found) { int at_start = (found == existing[i]); int at_boundary = at_start || (found[-1] == ' ' || found[-1] == '\n'); if (at_boundary) { overlap++; break; } } } } if (total_words == 0) return 0.0; return 1.0 - (double)overlap / (double)total_words; } /* Build the full context buffer from tiered sources. * * pri0: system prompt * pri1: current task description * pri2: array of memory entries (pre-sorted by score) * npri2: number of memory entries * pri3: array of recent messages (newest last) * npri3: number of recent messages * * Returns the assembled prompt string. Static buffer — copy if needed. */ const char *context_assemble( const char *pri0, const char *pri1, const char **pri2, int npri2, const char **pri3, int npri3) { char *p = g_buf; int remaining = MAX_TOKENS; /* Pri0: system prompt — always included, truncated if needed */ int tok = est_tokens(pri0); if (tok > PRI0_TOKENS) tok = PRI0_TOKENS; truncate(p, pri0, tok); p += strlen(p); *p++ = '\n'; *p = '\0'; remaining -= tok; /* Pri1: task — always included */ tok = est_tokens(pri1); if (tok > PRI1_TOKENS) tok = PRI1_TOKENS; if (tok <= remaining) { truncate(p, pri1, tok); p += strlen(p); *p++ = '\n'; *p = '\0'; remaining -= tok; } /* Pri2: scored memories — fill remaining up to PRI2_TOKENS */ int pri2_budget = PRI2_TOKENS; if (pri2_budget > remaining) pri2_budget = remaining; if (pri2_budget > 0 && npri2 > 0) { p += sprintf(p, "--- RELEVANT MEMORIES ---\n"); for (int i = 0; i < npri2 && pri2_budget > 0; i++) { int len = strlen(pri2[i]); int et = est_tokens(pri2[i]); if (et > pri2_budget) et = pri2_budget; if (et <= 0) continue; truncate(p, pri2[i], et); int wrote = strlen(p); p += wrote; *p++ = '\n'; *p = '\0'; pri2_budget -= et; } *p++ = '\n'; *p = '\0'; remaining -= (PRI2_TOKENS - pri2_budget); } /* Pri3: recent messages — fill remaining */ if (remaining > 0 && npri3 > 0) { /* Include from newest backward */ int start = npri3 - 1; int budget = PRI3_TOKENS; if (budget > remaining) budget = remaining; /* Find how many messages fit */ int msgs_to_include = 0; int used = 0; for (int i = start; i >= 0; i--) { int et = est_tokens(pri3[i]); if (used + et > budget) break; msgs_to_include++; used += et; } /* Write them oldest-first */ for (int i = start - msgs_to_include + 1; i <= start; i++) { if (i >= 0) { int len = strlen(pri3[i]); memcpy(p, pri3[i], len); p += len; *p++ = '\n'; *p = '\0'; } } remaining -= used; } return g_buf; } /* Compress tool output to gist. Returns static buffer. * Simple heuristic version (full compression uses LLM pass). */ const char *context_compress_output(const char *raw, int max_tokens) { char *p = g_buf; /* Count lines and total chars */ int lines = 0, chars = 0; const char *s; for (s = raw; *s; s++) { chars++; if (*s == '\n') lines++; } /* Extract first and last few lines + stats */ int budget = max_tokens * CHARS_PER_TOKEN; p += snprintf(p, budget, "[%d lines, %d chars]\n", lines, chars); /* First 2 non-empty lines */ const char *line_start = raw; int lc = 0; for (s = raw; *s && lc < 4; s++) { if (*s == '\n' || *s == '\0') { if (s > line_start && p - g_buf < budget - 20) { int n = s - line_start; if (n > 80) n = 80; /* max 80 chars per line */ memcpy(p, line_start, n); p += n; if (s - line_start > 80) { *p++ = '.'; *p++ = '.'; *p++ = '.'; } *p++ = '\n'; lc++; } line_start = s + 1; if (*s == '\0') break; } } /* Last line if there's space */ if (lines > 4 && p - g_buf < budget - 40) { p += snprintf(p, 40, "...\n"); /* Find last non-empty line */ const char *last = raw + strlen(raw) - 1; while (last > raw && *last == '\n') last--; const char *ll = last; while (ll > raw && ll[-1] != '\n') ll--; int n = last - ll + 1; if (n > 80) { ll = last - 79; n = 80; } memcpy(p, ll, n); p += n; } *p = '\0'; return g_buf; } /* Quick response format check — does it match the THOUGHT/ACTION template? */ int context_validate_response(const char *response) { return (strstr(response, "THOUGHT:") != NULL || strstr(response, "ACTION:") != NULL); }