2136 lines · 66.2 KB · C
RawDownload
// 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.