// SPDX-License-Identifier: ISC#include <errno.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <time.h> #include <cjson/cJSON.h> #include "clm/agent.h"#include "clm/http.h"#include "clm/host.h"#include "clm/llm.h"#include "clm/tools.h"#include "clm/history.h"#include "clm/internal.h"#include "clm/cleanup.h"#include "clm/log.h"#include "clm/provider.h"#include "useful.h"#include "banned.h" static const char *default_system_prompt = "You are a helpful assistant. Answer from your own knowledge whenever you can. " "Use the provided tools only when the task requires reading or modifying the " "user's files or running a command on their system. When you already have the " "answer, reply directly without using tools."; /* Minimum spacing between injected "current time" context updates. */#define CLM_TIME_STAMP_INTERVAL 600 /* seconds (10 minutes) */ /* * Explains the automatic time-context injections to the model. Appended to the * system prompt once, so a mid-conversation "[context update]" line is treated * as silent ambient background rather than something the user said. The Qwen3 * chat template rejects any system message that is not first, so per-turn * updates ride in a clearly framed user-role message instead (see * clm_agent_submit); this convention line is what keeps that transparent. */static const char *time_context_note = "\n\nYou may periodically receive a line beginning with \"[context update]\" " "carrying the current date and time. Treat it as silent ambient context, not " "as a message from the user. Never announce, repeat, or comment on the time " "unless the user explicitly asks about the date or time."; /* Format the current local time as an RFC 2822 date string. */static voidfmt_rfc2822(char *buf, size_t len){ time_t now = time(NULL); struct tm tm; if (localtime_r(&now, &tm) == NULL || strftime(buf, len, "%a, %d %b %Y %H:%M:%S %z", &tm) == 0) { if (len > 0) buf[0] = '\0'; }} /* * Build the session-start system prompt: the base prompt, a current-time stamp, * and the note explaining future time updates. Returns a malloc'd string the * caller must free, or NULL on OOM. */static char *build_system_prompt(const char *base){ char stamp[64]; autofree char *out = NULL; size_t len; fmt_rfc2822(stamp, sizeof(stamp)); len = strlen(base) + strlen(stamp) + strlen(time_context_note) + 20; out = malloc(len); if (out == NULL) return NULL; snprintf(out, len, "%s\n\ncurrent time: %s%s", base, stamp, time_context_note); char *ret = out; out = NULL; return ret;} voidclm_agent_set_error(struct clm_agent *agent, const char *msg){ char *dup = msg ? strdup(msg) : NULL; free(agent->last_error); agent->last_error = dup;} /* * Derive the /v1/models URL used for health probes from the chat-completions * base URL. The models endpoint sits beside completions, so replace a trailing * "/chat/completions" with "/models"; otherwise place "/models" next to the * last path segment. Returns a malloc'd string, or NULL on OOM. */static char *clm_derive_models_url(const char *base_url){ static const char comp[] = "/chat/completions"; static const char models[] = "/models"; size_t blen = strlen(base_url); size_t clen = sizeof(comp) - 1; const char *slash; size_t prefix; char *out; if (blen >= clen && strcmp(base_url + blen - clen, comp) == 0) prefix = blen - clen; else if ((slash = strrchr(base_url, '/')) != NULL) prefix = (size_t)(slash - base_url); else return strdup(base_url); /* can't derive; probe as-is */ out = malloc(prefix + sizeof(models)); if (out == NULL) return NULL; memcpy(out, base_url, prefix); memcpy(out + prefix, models, sizeof(models)); return out;} /* * llama.cpp serves GET /props at the server root, not beside the chat * endpoint, so this cuts base_url back to scheme://authority and appends * "/props". Returns a malloc'd string, or NULL if the authority can't be * located (e.g. no "//"). */static char *clm_derive_props_url(const char *base_url){ static const char props[] = "/props"; const char *authority, *slash; size_t prefix; char *out; authority = strstr(base_url, "//"); if (authority == NULL) return NULL; authority += 2; /* past "//" */ slash = strchr(authority, '/'); prefix = slash ? (size_t)(slash - base_url) : strlen(base_url); out = malloc(prefix + sizeof(props)); if (out == NULL) return NULL; memcpy(out, base_url, prefix); memcpy(out + prefix, props, sizeof(props)); return out;} /* * Start an HTTP request through the host transport. All of the agent's requests * use the configured API key and no extra headers or client suffix, so this * wraps the common shape; callers vary only url/body/callbacks/user/out. */static intagent_http_post(struct clm_agent *agent, const char *url, const char *body, clm_http_success_cb success, clm_http_error_cb error, clm_http_data_cb data, void *user, struct clm_http_call **out){ const struct clm_provider_ops *ops = clm_provider_ops_get(agent->llm->provider); autofreev char **auth_headers = NULL; struct clm_http_req req = { .url = url, .api_key = agent->llm->api_key, .body = body, .headers = NULL, .client_suffix = NULL, }; /* A provider with its own auth scheme (e.g. Anthropic's * x-api-key/anthropic-version instead of a bearer token) supplies its * own header set here and the default bearer header is suppressed -- * see clm/provider.h. */ if (ops->build_auth_headers != NULL) { auth_headers = ops->build_auth_headers(agent->llm); if (auth_headers != NULL) { req.api_key = NULL; req.headers = (const char *const *)auth_headers; } } return agent->host->http_post(agent->host->ctx, &req, success, error, data, user, out);} intclm_agent_new(const struct clm_cfg *cfg, struct clm_host *host, const struct clm_callbacks *cb, void *user, struct clm_agent **out){ struct clm_agent *agent; int r; ASSERT_RETURN(out != NULL, -EINVAL); ASSERT_RETURN(cfg != NULL, -EINVAL); ASSERT_RETURN(cfg->api_key != NULL, -EINVAL); ASSERT_RETURN(cfg->base_url != NULL, -EINVAL); ASSERT_RETURN(host != NULL, -EINVAL); ASSERT_RETURN(host->http_post != NULL, -EINVAL); agent = calloc(1, sizeof(*agent)); if (agent == NULL) return -ENOMEM; agent->host = host; agent->state = CLM_STATE_IDLE; agent->stream = cfg->stream; agent->backend = cfg->backend; agent->max_iterations = cfg->max_iterations; /* 0 = unlimited */ clm_history_init(&agent->history); TAILQ_INIT(&agent->tools); if (cb != NULL) { agent->cb_on_assistant_text = cb->on_assistant_text; agent->cb_on_reasoning = cb->on_reasoning; agent->cb_on_tool_begin = cb->on_tool_begin; agent->cb_on_permission = cb->on_permission; agent->cb_on_tool_result = cb->on_tool_result; agent->cb_on_tool_batch = cb->on_tool_batch; agent->cb_on_finish_reason = cb->on_finish_reason; agent->cb_on_usage = cb->on_usage; agent->cb_on_connection = cb->on_connection; agent->cb_on_state = cb->on_state; agent->cb_on_turn_done = cb->on_turn_done; agent->cb_on_notice = cb->on_notice; agent->cb_on_message = cb->on_message; } agent->cb_user = user; agent->models_url = clm_derive_models_url(cfg->base_url); agent->props_url = clm_derive_props_url(cfg->base_url); r = clm_llm_new(&agent->llm, cfg->provider, cfg->api_key, cfg->base_url, cfg->model ? cfg->model : "local-model", cfg->disable_parallel_tool_calls); if (r < 0) { free(agent); return r; } if (cfg->system_prompt != NULL) { agent->system_prompt_base = strdup(cfg->system_prompt); if (agent->system_prompt_base == NULL) { clm_agent_free(agent); return -ENOMEM; } } { const char *base = agent->system_prompt_base ? agent->system_prompt_base : default_system_prompt; autofree char *sys = build_system_prompt(base); struct clm_message *m; if (sys == NULL || (m = clm_history_add_system(&agent->history, sys, agent->compressor)) == NULL) { clm_agent_free(agent); return -ENOMEM; } clm_agent_emit_message(agent, m); } agent->last_time_stamp = time(NULL); if (clm_tools_register_builtins(agent) < 0) { clm_agent_free(agent); return -ENOMEM; } /* Tool dispatch rate limiter: 1 token/sec refill, burst of 8. */ if (clm_ratelimit_new(&agent->tool_rl, 1, 8) < 0) { clm_agent_free(agent); return -ENOMEM; } /* LLM request rate limiter, estimated-token bucket -- see llm_rl in * internal.h for why this exists separately from tool_rl, and why * the fallback rate is deliberately very high rather than * "conservative". Overridable per provider via cfg. */ { int64_t rps = cfg->rate_tokens_per_sec > 0 ? cfg->rate_tokens_per_sec : CLM_DEFAULT_LLM_RL_TOKENS_PER_SEC; int64_t burst = cfg->rate_burst > 0 ? cfg->rate_burst : CLM_DEFAULT_LLM_RL_BURST; if (clm_ratelimit_new(&agent->llm_rl, (size_t)rps, (size_t)burst) < 0) { clm_agent_free(agent); return -ENOMEM; } } /* Apply provider overrides from config */ if (cfg->context_size > 0) agent->ctx_max = cfg->context_size; if (cfg->autocompact_pct > 0) agent->autocompact_pct = cfg->autocompact_pct; agent->volatile_tools = cfg->volatile_tools; *out = agent; return 0;} voidclm_agent_free(struct clm_agent *agent){ if (agent == NULL) return; clm_tools_detach(agent); clm_llm_free(agent->llm); clm_history_free(&agent->history); free(agent->system_prompt_base); free(agent->last_error); free(agent->models_url); free(agent->props_url); free(agent->compact_body); free(agent->pending_notify); clm_tools_free_registry(&agent->tools); clm_ratelimit_free(agent->tool_rl); if (agent->llm_rl_timer != NULL && agent->host != NULL && agent->host->timer_cancel != NULL) agent->host->timer_cancel(agent->llm_rl_timer); clm_ratelimit_free(agent->llm_rl); free(agent);} voidclm_agent_free_ptr(struct clm_agent **agent){ if (agent && *agent) { clm_agent_free(*agent); *agent = NULL; }} voidclm_agent_set_compressor(struct clm_agent *agent, const struct clm_compressor *cz){ if (agent == NULL) return; agent->compressor = cz;} enum clm_agent_stateclm_agent_get_state(const struct clm_agent *agent){ return agent ? agent->state : CLM_STATE_ERROR;} /* Per-conversation context budget in tokens, or 0 if unknown (non-llama.cpp * backend, or /props not yet fetched). */int64_tclm_agent_get_ctx_max(const struct clm_agent *agent){ return agent ? agent->ctx_max : 0;} /* Auto-compact threshold. Only meaningful as a percentage when ctx_max is * known -- from probing GET /props against a llama.cpp backend, or from an * explicit context_size provider override -- which hosted backends * (Anthropic, OpenAI, etc.) don't expose. Against those, ctx_max stays 0, * so CLM_AUTOCOMPACT_FALLBACK_TOKENS below is used instead of a percentage * -- see clm_agent_over_autocompact_threshold(). */#define CLM_AUTOCOMPACT_PCT 70 /* * Absolute token fallback for backends with no known ctx_max (see above). * This exists purely because a real billing incident happened without it: * running an agent against a paid API with no working compaction meant * every turn resent the entire accumulated history, and that cost real * money fast with no ceiling in place. Picked to be comfortably small for * a paid API rather than tuned to any specific model's real context * window -- the whole point is capping *cost*, not maximizing context * use, when the actual window size is unknown. */#define CLM_AUTOCOMPACT_FALLBACK_TOKENS 100000 /* * True if context usage (tracked in emit_usage() below) is at/above the * autocompact threshold. Shared by clm_agent_tools_done()'s mid-chain * check and tui.c's own end-of-turn check, so both use exactly the same * calc instead of tui.c keeping a second copy in sync by hand. */boolclm_agent_over_autocompact_threshold(const struct clm_agent *agent){ if (agent == NULL || agent->ctx_used <= 0) return false; if (agent->ctx_max <= 0) return agent->ctx_used >= CLM_AUTOCOMPACT_FALLBACK_TOKENS; int pct = agent->autocompact_pct > 0 ? agent->autocompact_pct : CLM_AUTOCOMPACT_PCT; return (agent->ctx_used * 100) / agent->ctx_max >= pct;} const char *clm_agent_get_last_error(const struct clm_agent *agent){ if (agent == NULL || agent->last_error == NULL) return ""; return agent->last_error;} boolclm_agent_take_mid_chain_compact_error(struct clm_agent *agent){ bool failed; if (agent == NULL) return false; failed = agent->mid_chain_compact_failed; agent->mid_chain_compact_failed = false; return failed;} boolclm_agent_take_mid_chain_compact_started(struct clm_agent *agent){ bool started; if (agent == NULL) return false; started = agent->mid_chain_compact_started; agent->mid_chain_compact_started = false; return started;} boolclm_agent_take_mid_chain_compact_succeeded(struct clm_agent *agent){ bool succeeded; if (agent == NULL) return false; succeeded = agent->mid_chain_compact_succeeded; agent->mid_chain_compact_succeeded = false; return succeeded;} static void clm_agent_start_turn(struct clm_agent *agent); intclm_agent_submit(struct clm_agent *agent, const char *prompt){ ASSERT_RETURN(agent != NULL, -EINVAL); ASSERT_RETURN(prompt != NULL, -EINVAL); /* * Reject only while a turn is genuinely in flight. A previous turn that * finished, errored, or was cancelled must not lock out new prompts: * the user should be able to just type again to recover. */ if (agent->state == CLM_STATE_THINKING || agent->state == CLM_STATE_CALLING_TOOL) { clm_agent_set_error(agent, "turn already in progress"); return -EBUSY; } /* * Refresh the model's sense of time on a new turn once enough has passed. * The Qwen3 template forbids a non-leading system message, so this rides * in a user-role message framed as ambient context; the system prompt's * note tells the model to treat "[context update]" lines silently. It is * appended near the newest turn (outside the cached prefix) so it does not * invalidate the server's prompt cache for the earlier conversation. */ { time_t now = time(NULL); if (now - agent->last_time_stamp >= CLM_TIME_STAMP_INTERVAL) { char stamp[64]; autofree char *msg = NULL; fmt_rfc2822(stamp, sizeof(stamp)); msg = malloc(strlen(stamp) + 100); if (msg != NULL) { snprintf(msg, strlen(stamp) + 100, "[context update] current time: %s\n" "(automatic context, not user input; do not acknowledge)", stamp); clm_agent_emit_message(agent, clm_history_add_user(&agent->history, msg, agent->compressor)); } agent->last_time_stamp = now; } } { struct clm_message *m = clm_history_add_user(&agent->history, prompt, agent->compressor); if (m == NULL) { clm_agent_set_error(agent, "out of memory"); agent->state = CLM_STATE_ERROR; return -ENOMEM; }Lines 1–500 of 2136. Load more
// SPDX-License-Identifier: ISC #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <cjson/cJSON.h> #include "clm/agent.h" #include "clm/http.h" #include "clm/host.h" #include "clm/llm.h" #include "clm/tools.h" #include "clm/history.h" #include "clm/internal.h" #include "clm/cleanup.h" #include "clm/log.h" #include "clm/provider.h" #include "useful.h" #include "banned.h" static const char *default_system_prompt = "You are a helpful assistant. Answer from your own knowledge whenever you can. " "Use the provided tools only when the task requires reading or modifying the " "user's files or running a command on their system. When you already have the " "answer, reply directly without using tools."; /* Minimum spacing between injected "current time" context updates. */ #define CLM_TIME_STAMP_INTERVAL 600 /* seconds (10 minutes) */ /* * Explains the automatic time-context injections to the model. Appended to the * system prompt once, so a mid-conversation "[context update]" line is treated * as silent ambient background rather than something the user said. The Qwen3 * chat template rejects any system message that is not first, so per-turn * updates ride in a clearly framed user-role message instead (see * clm_agent_submit); this convention line is what keeps that transparent. */ static const char *time_context_note = "\n\nYou may periodically receive a line beginning with \"[context update]\" " "carrying the current date and time. Treat it as silent ambient context, not " "as a message from the user. Never announce, repeat, or comment on the time " "unless the user explicitly asks about the date or time."; /* Format the current local time as an RFC 2822 date string. */ static void fmt_rfc2822(char *buf, size_t len) { time_t now = time(NULL); struct tm tm; if (localtime_r(&now, &tm) == NULL || strftime(buf, len, "%a, %d %b %Y %H:%M:%S %z", &tm) == 0) { if (len > 0) buf[0] = '\0'; } } /* * Build the session-start system prompt: the base prompt, a current-time stamp, * and the note explaining future time updates. Returns a malloc'd string the * caller must free, or NULL on OOM. */ static char * build_system_prompt(const char *base) { char stamp[64]; autofree char *out = NULL; size_t len; fmt_rfc2822(stamp, sizeof(stamp)); len = strlen(base) + strlen(stamp) + strlen(time_context_note) + 20; out = malloc(len); if (out == NULL) return NULL; snprintf(out, len, "%s\n\ncurrent time: %s%s", base, stamp, time_context_note); char *ret = out; out = NULL; return ret; } void clm_agent_set_error(struct clm_agent *agent, const char *msg) { char *dup = msg ? strdup(msg) : NULL; free(agent->last_error); agent->last_error = dup; } /* * Derive the /v1/models URL used for health probes from the chat-completions * base URL. The models endpoint sits beside completions, so replace a trailing * "/chat/completions" with "/models"; otherwise place "/models" next to the * last path segment. Returns a malloc'd string, or NULL on OOM. */ static char * clm_derive_models_url(const char *base_url) { static const char comp[] = "/chat/completions"; static const char models[] = "/models"; size_t blen = strlen(base_url); size_t clen = sizeof(comp) - 1; const char *slash; size_t prefix; char *out; if (blen >= clen && strcmp(base_url + blen - clen, comp) == 0) prefix = blen - clen; else if ((slash = strrchr(base_url, '/')) != NULL) prefix = (size_t)(slash - base_url); else return strdup(base_url); /* can't derive; probe as-is */ out = malloc(prefix + sizeof(models)); if (out == NULL) return NULL; memcpy(out, base_url, prefix); memcpy(out + prefix, models, sizeof(models)); return out; } /* * llama.cpp serves GET /props at the server root, not beside the chat * endpoint, so this cuts base_url back to scheme://authority and appends * "/props". Returns a malloc'd string, or NULL if the authority can't be * located (e.g. no "//"). */ static char * clm_derive_props_url(const char *base_url) { static const char props[] = "/props"; const char *authority, *slash; size_t prefix; char *out; authority = strstr(base_url, "//"); if (authority == NULL) return NULL; authority += 2; /* past "//" */ slash = strchr(authority, '/'); prefix = slash ? (size_t)(slash - base_url) : strlen(base_url); out = malloc(prefix + sizeof(props)); if (out == NULL) return NULL; memcpy(out, base_url, prefix); memcpy(out + prefix, props, sizeof(props)); return out; } /* * Start an HTTP request through the host transport. All of the agent's requests * use the configured API key and no extra headers or client suffix, so this * wraps the common shape; callers vary only url/body/callbacks/user/out. */ static int agent_http_post(struct clm_agent *agent, const char *url, const char *body, clm_http_success_cb success, clm_http_error_cb error, clm_http_data_cb data, void *user, struct clm_http_call **out) { const struct clm_provider_ops *ops = clm_provider_ops_get(agent->llm->provider); autofreev char **auth_headers = NULL; struct clm_http_req req = { .url = url, .api_key = agent->llm->api_key, .body = body, .headers = NULL, .client_suffix = NULL, }; /* A provider with its own auth scheme (e.g. Anthropic's * x-api-key/anthropic-version instead of a bearer token) supplies its * own header set here and the default bearer header is suppressed -- * see clm/provider.h. */ if (ops->build_auth_headers != NULL) { auth_headers = ops->build_auth_headers(agent->llm); if (auth_headers != NULL) { req.api_key = NULL; req.headers = (const char *const *)auth_headers; } } return agent->host->http_post(agent->host->ctx, &req, success, error, data, user, out); } int clm_agent_new(const struct clm_cfg *cfg, struct clm_host *host, const struct clm_callbacks *cb, void *user, struct clm_agent **out) { struct clm_agent *agent; int r; ASSERT_RETURN(out != NULL, -EINVAL); ASSERT_RETURN(cfg != NULL, -EINVAL); ASSERT_RETURN(cfg->api_key != NULL, -EINVAL); ASSERT_RETURN(cfg->base_url != NULL, -EINVAL); ASSERT_RETURN(host != NULL, -EINVAL); ASSERT_RETURN(host->http_post != NULL, -EINVAL); agent = calloc(1, sizeof(*agent)); if (agent == NULL) return -ENOMEM; agent->host = host; agent->state = CLM_STATE_IDLE; agent->stream = cfg->stream; agent->backend = cfg->backend; agent->max_iterations = cfg->max_iterations; /* 0 = unlimited */ clm_history_init(&agent->history); TAILQ_INIT(&agent->tools); if (cb != NULL) { agent->cb_on_assistant_text = cb->on_assistant_text; agent->cb_on_reasoning = cb->on_reasoning; agent->cb_on_tool_begin = cb->on_tool_begin; agent->cb_on_permission = cb->on_permission; agent->cb_on_tool_result = cb->on_tool_result; agent->cb_on_tool_batch = cb->on_tool_batch; agent->cb_on_finish_reason = cb->on_finish_reason; agent->cb_on_usage = cb->on_usage; agent->cb_on_connection = cb->on_connection; agent->cb_on_state = cb->on_state; agent->cb_on_turn_done = cb->on_turn_done; agent->cb_on_notice = cb->on_notice; agent->cb_on_message = cb->on_message; } agent->cb_user = user; agent->models_url = clm_derive_models_url(cfg->base_url); agent->props_url = clm_derive_props_url(cfg->base_url); r = clm_llm_new(&agent->llm, cfg->provider, cfg->api_key, cfg->base_url, cfg->model ? cfg->model : "local-model", cfg->disable_parallel_tool_calls); if (r < 0) { free(agent); return r; } if (cfg->system_prompt != NULL) { agent->system_prompt_base = strdup(cfg->system_prompt); if (agent->system_prompt_base == NULL) { clm_agent_free(agent); return -ENOMEM; } } { const char *base = agent->system_prompt_base ? agent->system_prompt_base : default_system_prompt; autofree char *sys = build_system_prompt(base); struct clm_message *m; if (sys == NULL || (m = clm_history_add_system(&agent->history, sys, agent->compressor)) == NULL) { clm_agent_free(agent); return -ENOMEM; } clm_agent_emit_message(agent, m); } agent->last_time_stamp = time(NULL); if (clm_tools_register_builtins(agent) < 0) { clm_agent_free(agent); return -ENOMEM; } /* Tool dispatch rate limiter: 1 token/sec refill, burst of 8. */ if (clm_ratelimit_new(&agent->tool_rl, 1, 8) < 0) { clm_agent_free(agent); return -ENOMEM; } /* LLM request rate limiter, estimated-token bucket -- see llm_rl in * internal.h for why this exists separately from tool_rl, and why * the fallback rate is deliberately very high rather than * "conservative". Overridable per provider via cfg. */ { int64_t rps = cfg->rate_tokens_per_sec > 0 ? cfg->rate_tokens_per_sec : CLM_DEFAULT_LLM_RL_TOKENS_PER_SEC; int64_t burst = cfg->rate_burst > 0 ? cfg->rate_burst : CLM_DEFAULT_LLM_RL_BURST; if (clm_ratelimit_new(&agent->llm_rl, (size_t)rps, (size_t)burst) < 0) { clm_agent_free(agent); return -ENOMEM; } } /* Apply provider overrides from config */ if (cfg->context_size > 0) agent->ctx_max = cfg->context_size; if (cfg->autocompact_pct > 0) agent->autocompact_pct = cfg->autocompact_pct; agent->volatile_tools = cfg->volatile_tools; *out = agent; return 0; } void clm_agent_free(struct clm_agent *agent) { if (agent == NULL) return; clm_tools_detach(agent); clm_llm_free(agent->llm); clm_history_free(&agent->history); free(agent->system_prompt_base); free(agent->last_error); free(agent->models_url); free(agent->props_url); free(agent->compact_body); free(agent->pending_notify); clm_tools_free_registry(&agent->tools); clm_ratelimit_free(agent->tool_rl); if (agent->llm_rl_timer != NULL && agent->host != NULL && agent->host->timer_cancel != NULL) agent->host->timer_cancel(agent->llm_rl_timer); clm_ratelimit_free(agent->llm_rl); free(agent); } void clm_agent_free_ptr(struct clm_agent **agent) { if (agent && *agent) { clm_agent_free(*agent); *agent = NULL; } } void clm_agent_set_compressor(struct clm_agent *agent, const struct clm_compressor *cz) { if (agent == NULL) return; agent->compressor = cz; } enum clm_agent_state clm_agent_get_state(const struct clm_agent *agent) { return agent ? agent->state : CLM_STATE_ERROR; } /* Per-conversation context budget in tokens, or 0 if unknown (non-llama.cpp * backend, or /props not yet fetched). */ int64_t clm_agent_get_ctx_max(const struct clm_agent *agent) { return agent ? agent->ctx_max : 0; } /* Auto-compact threshold. Only meaningful as a percentage when ctx_max is * known -- from probing GET /props against a llama.cpp backend, or from an * explicit context_size provider override -- which hosted backends * (Anthropic, OpenAI, etc.) don't expose. Against those, ctx_max stays 0, * so CLM_AUTOCOMPACT_FALLBACK_TOKENS below is used instead of a percentage * -- see clm_agent_over_autocompact_threshold(). */ #define CLM_AUTOCOMPACT_PCT 70 /* * Absolute token fallback for backends with no known ctx_max (see above). * This exists purely because a real billing incident happened without it: * running an agent against a paid API with no working compaction meant * every turn resent the entire accumulated history, and that cost real * money fast with no ceiling in place. Picked to be comfortably small for * a paid API rather than tuned to any specific model's real context * window -- the whole point is capping *cost*, not maximizing context * use, when the actual window size is unknown. */ #define CLM_AUTOCOMPACT_FALLBACK_TOKENS 100000 /* * True if context usage (tracked in emit_usage() below) is at/above the * autocompact threshold. Shared by clm_agent_tools_done()'s mid-chain * check and tui.c's own end-of-turn check, so both use exactly the same * calc instead of tui.c keeping a second copy in sync by hand. */ bool clm_agent_over_autocompact_threshold(const struct clm_agent *agent) { if (agent == NULL || agent->ctx_used <= 0) return false; if (agent->ctx_max <= 0) return agent->ctx_used >= CLM_AUTOCOMPACT_FALLBACK_TOKENS; int pct = agent->autocompact_pct > 0 ? agent->autocompact_pct : CLM_AUTOCOMPACT_PCT; return (agent->ctx_used * 100) / agent->ctx_max >= pct; } const char * clm_agent_get_last_error(const struct clm_agent *agent) { if (agent == NULL || agent->last_error == NULL) return ""; return agent->last_error; } bool clm_agent_take_mid_chain_compact_error(struct clm_agent *agent) { bool failed; if (agent == NULL) return false; failed = agent->mid_chain_compact_failed; agent->mid_chain_compact_failed = false; return failed; } bool clm_agent_take_mid_chain_compact_started(struct clm_agent *agent) { bool started; if (agent == NULL) return false; started = agent->mid_chain_compact_started; agent->mid_chain_compact_started = false; return started; } bool clm_agent_take_mid_chain_compact_succeeded(struct clm_agent *agent) { bool succeeded; if (agent == NULL) return false; succeeded = agent->mid_chain_compact_succeeded; agent->mid_chain_compact_succeeded = false; return succeeded; } static void clm_agent_start_turn(struct clm_agent *agent); int clm_agent_submit(struct clm_agent *agent, const char *prompt) { ASSERT_RETURN(agent != NULL, -EINVAL); ASSERT_RETURN(prompt != NULL, -EINVAL); /* * Reject only while a turn is genuinely in flight. A previous turn that * finished, errored, or was cancelled must not lock out new prompts: * the user should be able to just type again to recover. */ if (agent->state == CLM_STATE_THINKING || agent->state == CLM_STATE_CALLING_TOOL) { clm_agent_set_error(agent, "turn already in progress"); return -EBUSY; } /* * Refresh the model's sense of time on a new turn once enough has passed. * The Qwen3 template forbids a non-leading system message, so this rides * in a user-role message framed as ambient context; the system prompt's * note tells the model to treat "[context update]" lines silently. It is * appended near the newest turn (outside the cached prefix) so it does not * invalidate the server's prompt cache for the earlier conversation. */ { time_t now = time(NULL); if (now - agent->last_time_stamp >= CLM_TIME_STAMP_INTERVAL) { char stamp[64]; autofree char *msg = NULL; fmt_rfc2822(stamp, sizeof(stamp)); msg = malloc(strlen(stamp) + 100); if (msg != NULL) { snprintf(msg, strlen(stamp) + 100, "[context update] current time: %s\n" "(automatic context, not user input; do not acknowledge)", stamp); clm_agent_emit_message(agent, clm_history_add_user(&agent->history, msg, agent->compressor)); } agent->last_time_stamp = now; } } { struct clm_message *m = clm_history_add_user(&agent->history, prompt, agent->compressor); if (m == NULL) { clm_agent_set_error(agent, "out of memory"); agent->state = CLM_STATE_ERROR; return -ENOMEM; }