// SPDX-License-Identifier: ISC#include <errno.h>#include <fnmatch.h>#include <signal.h>#include <stdbool.h>#include <stdio.h>#include <stdlib.h>#include <string.h> #include <dirent.h>#include <fcntl.h>#include <sys/stat.h> #include <cjson/cJSON.h> #include "clm/tools.h"#include "clm/internal.h"#include "clm/log.h"#include "clm/cleanup.h"#include "useful.h"#include "banned.h" #define CLM_TOOL_OUTPUT_CAP_DEFAULT (64 * 1024) /* bytes returned to model */#define CLM_TOOL_OUTPUT_CAP_MAX (1024 * 1024) /* ceiling for overrides */#define CLM_TOOL_TIMEOUT_MAX_MS 600000u /* 10 minutes */#define CLM_READ_DEFAULT_LIMIT 200 /* lines */ /* * One in-flight tool call. Owned by its batch. Completed exactly once via * clm_tool_complete/clm_tool_fail (or the timeout path), which records a tool * result into history and decrements the batch's pending count. */struct clm_tool_invocation; /* Opaque to the frontend; references the parked invocation it authorizes. */struct clm_permission_req { struct clm_tool_invocation *inv;}; static void run_invoke(struct clm_tool_invocation *inv); struct clm_tool_invocation { struct clm_tool_batch *batch; const struct clm_tool *def; /* resolved tool, or NULL if unknown */ char *id; char *name; char *args; /* raw JSON args string */ size_t output_cap; /* effective for this call */ uint64_t timeout_ms; /* effective for this call */ struct clm_timer *timer; /* per-call timeout, via host; NULL if none */ bool timed_out; bool completed; void (*cancel)(struct clm_tool_invocation *, void *); void *cancel_user; /* Permission gate: while awaiting a decision the invocation is parked * * (on_permission fired, invoke deferred). perm_req is the opaque handle * * that the frontend answers against; it points back to this invocation. */ bool awaiting_perm; struct clm_permission_req perm_req; /* Rate-limit deferral: when the token bucket is empty, the invocation * * is parked on rl_timer and dispatched when tokens refill. */ struct clm_timer *rl_timer;}; /* A batch of tool calls dispatched together for one assistant message. */struct clm_tool_batch { struct clm_agent *agent; struct clm_tool_invocation *inv; size_t n; size_t pending; /* outstanding completions + a dispatch hold */ size_t done; /* completed calls, for progress reporting */ int status; /* first hard error encountered, else 0 */ /* Set by clm_tools_detach() when the owning agent is torn down while * this batch still has invocations genuinely in flight. Once set, * `agent` is a dead pointer: nothing on the completion path may read * through it. See inv_finalize(). */ bool detached;}; /* ------------------------------------------------------------------ *//* Argument helpers *//* ------------------------------------------------------------------ */ static char *arg_string(cJSON *args, const char *key){ cJSON *v = NULL; if (!(v = cJSON_GetObjectItemCaseSensitive(args, key))) return NULL; if (!cJSON_IsString(v)) return NULL; return strdup(v->valuestring);} /* * Expand a leading "~/" to "$HOME/". Always returns a new allocation * (caller frees). Returns strdup(path) if no expansion needed. */static char *expand_tilde(const char *path){ const char *home; size_t hlen, plen; char *out; if (path == NULL) return NULL; if (path[0] != '~' || path[1] != '/') return strdup(path); home = getenv("HOME"); if (home == NULL || home[0] == '\0') return strdup(path); hlen = strlen(home); plen = strlen(path + 1); /* includes the '/' */ out = malloc(hlen + plen + 1); if (out == NULL) return strdup(path); memcpy(out, home, hlen); memcpy(out + hlen, path + 1, plen + 1); /* copy '/' + rest + NUL */ return out;} static intarg_int(cJSON *args, const char *key, int dflt){ cJSON *v = NULL; if (!(v = cJSON_GetObjectItemCaseSensitive(args, key))) return dflt; if (!cJSON_IsNumber(v)) return dflt; return (int)v->valuedouble;} /* ------------------------------------------------------------------ *//* Registration *//* ------------------------------------------------------------------ */ /* * A "removed" node is a zombie kept alive only until its last invocation * finishes (see the struct clm_tool comment in clm/tools.h); it must never be * resolved again, so lookups skip it. */static const struct clm_tool *find_tool(const struct clm_agent *agent, const char *name){ const struct clm_tool *t; TAILQ_FOREACH(t, &agent->tools, entries) { if (!t->removed && strcmp(t->name, name) == 0) return t; } return NULL;} intclm_tool_add(struct clm_agent *agent, const struct clm_tool_def *def){ struct clm_tool *t; ASSERT_RETURN(agent != NULL, -EINVAL); ASSERT_RETURN(def != NULL, -EINVAL); ASSERT_RETURN(def->name != NULL, -EINVAL); ASSERT_RETURN(def->invoke != NULL, -EINVAL); if (find_tool(agent, def->name) != NULL) return -EEXIST; t = calloc(1, sizeof(*t)); if (t == NULL) return -ENOMEM; t->name = strdup(def->name); t->description = def->description ? strdup(def->description) : NULL; t->params_schema = def->params_schema ? strdup(def->params_schema) : NULL; if (t->name == NULL || (def->description != NULL && t->description == NULL) || (def->params_schema != NULL && t->params_schema == NULL)) { free(t->name); free(t->description); free(t->params_schema); free(t); return -ENOMEM; } t->invoke = def->invoke; t->detach = def->detach; t->user = def->user; t->output_cap = def->output_cap; t->timeout_ms = def->timeout_ms; t->flags = def->flags; TAILQ_INSERT_TAIL(&agent->tools, t, entries); agent->tool_count++; return 0;} static voidtool_detach(struct clm_tool *t){ if (t->detach == NULL) return; t->detach(t->user); t->detach = NULL;} /* Free a node's owned strings and itself. Does not unlink or touch pending * invocations; callers must already know it is safe (unlinked + inflight==0, * or agent teardown). */static voidtool_node_free(struct clm_tool *t){ tool_detach(t); free(t->name); free(t->description); free(t->params_schema); free(t);} intclm_tool_remove(struct clm_agent *agent, const char *name){ struct clm_tool *t; ASSERT_RETURN(agent != NULL, -EINVAL); ASSERT_RETURN(name != NULL, -EINVAL); TAILQ_FOREACH(t, &agent->tools, entries) { if (t->removed || strcmp(t->name, name) != 0) continue; tool_detach(t); TAILQ_REMOVE(&agent->tools, t, entries); agent->tool_count--; if (t->inflight == 0) tool_node_free(t); else t->removed = true; /* freed once the last invocation finalizes */ return 0; } return -ENOENT;} voidclm_tools_free_registry(struct clm_tool_list *tools){ struct clm_tool *t, *tmp; if (tools == NULL) return; t = TAILQ_FIRST(tools); while (t != NULL) { tmp = TAILQ_NEXT(t, entries); TAILQ_REMOVE(tools, t, entries); tool_node_free(t); t = tmp; }} /* ------------------------------------------------------------------ *//* Schema construction *//* ------------------------------------------------------------------ */ static cJSON *int_prop(const char *desc){ cJSON *obj = cJSON_CreateObject(); if (obj == NULL) return NULL; cJSON_AddItemToObject(obj, "type", cJSON_CreateString("integer")); cJSON_AddItemToObject(obj, "description", cJSON_CreateString(desc)); return obj;} static voidinject_prop(cJSON *props, const char *name, const char *desc){ cJSON *exist = cJSON_GetObjectItemCaseSensitive(props, name); if (exist != NULL) { clm_debug("schema: skip reserved param '%s' (tool defines it)", name); return; } cJSON *p = int_prop(desc); if (p != NULL) cJSON_AddItemToObject(props, name, p);} static cJSON *tool_schema(const struct clm_tool *t){ cJSON *tool = cJSON_CreateObject(); cJSON *func, *params, *props; if (tool == NULL) return NULL; cJSON_AddItemToObject(tool, "type", cJSON_CreateString("function")); func = cJSON_CreateObject(); if (func == NULL) { cJSON_Delete(tool); return NULL; } cJSON_AddItemToObject(tool, "function", func); cJSON_AddItemToObject(func, "name", cJSON_CreateString(t->name)); cJSON_AddItemToObject(func, "description", cJSON_CreateString(t->description ? t->description : "")); /* Parse the tool's parameters schema, or synthesize an empty object. */ params = t->params_schema ? cJSON_Parse(t->params_schema) : NULL; if (params == NULL || cJSON_GetObjectItem(params, "type") == NULL) { if (params != NULL) cJSON_Delete(params); params = cJSON_CreateObject(); if (params == NULL) { cJSON_Delete(tool); return NULL; } } cJSON_AddItemToObject(func, "parameters", params); if (cJSON_GetObjectItemCaseSensitive(params, "type") == NULL) cJSON_AddItemToObject(params, "type", cJSON_CreateString("object")); props = cJSON_GetObjectItemCaseSensitive(params, "properties"); if (props == NULL) { props = cJSON_CreateObject(); if (props == NULL) { cJSON_Delete(tool); return NULL; } cJSON_AddItemToObject(params, "properties", props); } if (t->flags & CLM_TOOL_TIMEOUT_OVERRIDABLE) inject_prop(props, "timeout_ms", "optional: max milliseconds before this call is aborted"); if (t->flags & CLM_TOOL_OUTPUT_CAP_OVERRIDABLE) inject_prop(props, "output_cap", "optional: max bytes of output to return"); return tool;} cJSON *clm_tools_build_schema(const struct clm_agent *agent){ cJSON *arr = cJSON_CreateArray(); const struct clm_tool *t; if (agent == NULL) return NULL; if (arr == NULL) return NULL; TAILQ_FOREACH(t, &agent->tools, entries) { if (t->removed || (t->flags & CLM_TOOL_HIDDEN)) continue; cJSON *sch = tool_schema(t); if (sch != NULL) cJSON_AddItemToArray(arr, sch); /* steals reference */ } return arr;} /* ------------------------------------------------------------------ *//* Invocation accessors *//* ------------------------------------------------------------------ */ const char *clm_tool_invocation_name(const struct clm_tool_invocation *inv){ return inv ? inv->name : NULL;} const char *clm_tool_invocation_args(const struct clm_tool_invocation *inv){ return inv ? inv->args : NULL;} void *clm_tool_invocation_loop(const struct clm_tool_invocation *inv){ struct clm_host *host = inv ? inv->batch->agent->host : NULL; return host ? host->native_loop : NULL;} const char *clm_permission_req_name(const struct clm_permission_req *req){ return (req && req->inv) ? req->inv->name : NULL;} const char *clm_permission_req_args(const struct clm_permission_req *req){ return (req && req->inv) ? req->inv->args : NULL;} /* Mutable lookup of a registered tool by name (for recording decisions). */static struct clm_tool *find_tool_mut(struct clm_agent *agent, const char *name){ struct clm_tool *t; TAILQ_FOREACH(t, &agent->tools, entries) { if (!t->removed && strcmp(t->name, name) == 0) return t; } return NULL;} intclm_tool_permission_respond(struct clm_agent *agent, const struct clm_permission_req *req, enum clm_permission_decision decision){ struct clm_tool_invocation *inv; bool allow; ASSERT_RETURN(agent != NULL, -EINVAL); ASSERT_RETURN(req != NULL && req->inv != NULL, -EINVAL); inv = req->inv; if (!inv->awaiting_perm) return -EINVAL; /* already answered, or not parked */ inv->awaiting_perm = false; allow = (decision == CLM_PERM_ALLOW_ONCE || decision == CLM_PERM_ALLOW_ALWAYS); /* Remember _ALWAYS decisions for the rest of the session, per tool. */ if (decision == CLM_PERM_ALLOW_ALWAYS || decision == CLM_PERM_DENY_ALWAYS) { struct clm_tool *t = find_tool_mut(agent, inv->name); if (t != NULL) { t->remembered = true; t->remember_allow = allow; } } if (allow) run_invoke(inv); else clm_tool_fail(inv, "denied by user"); return 0;} size_tclm_tool_invocation_output_cap(const struct clm_tool_invocation *inv){ return inv ? inv->output_cap : 0;} uint64_tclm_tool_invocation_timeout_ms(const struct clm_tool_invocation *inv){ return inv ? inv->timeout_ms : 0;} voidclm_tool_invocation_set_cancel(struct clm_tool_invocation *inv, void (*cancel)(struct clm_tool_invocation *, void *), void *user){ if (inv == NULL) return; inv->cancel = cancel; inv->cancel_user = user;} /* ------------------------------------------------------------------ *//* Batch teardown *//* ------------------------------------------------------------------ */ static voidbatch_really_free(struct clm_tool_batch *batch){ size_t i; for (i = 0; i < batch->n; i++) { free(batch->inv[i].id); free(batch->inv[i].name); free(batch->inv[i].args); } free(batch->inv); free(batch);} static voidbatch_finalize(struct clm_tool_batch *batch){ struct clm_agent *agent = batch->agent; int status = batch->status; size_t i; if (agent->active_batch == batch) agent->active_batch = NULL; /* Advance the turn (or end it on error). This may start a new, * independent batch asynchronously; this one is torn down below. */ clm_agent_tools_done(agent, status);Lines 1–500 of 1479. Load more
// SPDX-License-Identifier: ISC #include <errno.h> #include <fnmatch.h> #include <signal.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <dirent.h> #include <fcntl.h> #include <sys/stat.h> #include <cjson/cJSON.h> #include "clm/tools.h" #include "clm/internal.h" #include "clm/log.h" #include "clm/cleanup.h" #include "useful.h" #include "banned.h" #define CLM_TOOL_OUTPUT_CAP_DEFAULT (64 * 1024) /* bytes returned to model */ #define CLM_TOOL_OUTPUT_CAP_MAX (1024 * 1024) /* ceiling for overrides */ #define CLM_TOOL_TIMEOUT_MAX_MS 600000u /* 10 minutes */ #define CLM_READ_DEFAULT_LIMIT 200 /* lines */ /* * One in-flight tool call. Owned by its batch. Completed exactly once via * clm_tool_complete/clm_tool_fail (or the timeout path), which records a tool * result into history and decrements the batch's pending count. */ struct clm_tool_invocation; /* Opaque to the frontend; references the parked invocation it authorizes. */ struct clm_permission_req { struct clm_tool_invocation *inv; }; static void run_invoke(struct clm_tool_invocation *inv); struct clm_tool_invocation { struct clm_tool_batch *batch; const struct clm_tool *def; /* resolved tool, or NULL if unknown */ char *id; char *name; char *args; /* raw JSON args string */ size_t output_cap; /* effective for this call */ uint64_t timeout_ms; /* effective for this call */ struct clm_timer *timer; /* per-call timeout, via host; NULL if none */ bool timed_out; bool completed; void (*cancel)(struct clm_tool_invocation *, void *); void *cancel_user; /* Permission gate: while awaiting a decision the invocation is parked * * (on_permission fired, invoke deferred). perm_req is the opaque handle * * that the frontend answers against; it points back to this invocation. */ bool awaiting_perm; struct clm_permission_req perm_req; /* Rate-limit deferral: when the token bucket is empty, the invocation * * is parked on rl_timer and dispatched when tokens refill. */ struct clm_timer *rl_timer; }; /* A batch of tool calls dispatched together for one assistant message. */ struct clm_tool_batch { struct clm_agent *agent; struct clm_tool_invocation *inv; size_t n; size_t pending; /* outstanding completions + a dispatch hold */ size_t done; /* completed calls, for progress reporting */ int status; /* first hard error encountered, else 0 */ /* Set by clm_tools_detach() when the owning agent is torn down while * this batch still has invocations genuinely in flight. Once set, * `agent` is a dead pointer: nothing on the completion path may read * through it. See inv_finalize(). */ bool detached; }; /* ------------------------------------------------------------------ */ /* Argument helpers */ /* ------------------------------------------------------------------ */ static char * arg_string(cJSON *args, const char *key) { cJSON *v = NULL; if (!(v = cJSON_GetObjectItemCaseSensitive(args, key))) return NULL; if (!cJSON_IsString(v)) return NULL; return strdup(v->valuestring); } /* * Expand a leading "~/" to "$HOME/". Always returns a new allocation * (caller frees). Returns strdup(path) if no expansion needed. */ static char * expand_tilde(const char *path) { const char *home; size_t hlen, plen; char *out; if (path == NULL) return NULL; if (path[0] != '~' || path[1] != '/') return strdup(path); home = getenv("HOME"); if (home == NULL || home[0] == '\0') return strdup(path); hlen = strlen(home); plen = strlen(path + 1); /* includes the '/' */ out = malloc(hlen + plen + 1); if (out == NULL) return strdup(path); memcpy(out, home, hlen); memcpy(out + hlen, path + 1, plen + 1); /* copy '/' + rest + NUL */ return out; } static int arg_int(cJSON *args, const char *key, int dflt) { cJSON *v = NULL; if (!(v = cJSON_GetObjectItemCaseSensitive(args, key))) return dflt; if (!cJSON_IsNumber(v)) return dflt; return (int)v->valuedouble; } /* ------------------------------------------------------------------ */ /* Registration */ /* ------------------------------------------------------------------ */ /* * A "removed" node is a zombie kept alive only until its last invocation * finishes (see the struct clm_tool comment in clm/tools.h); it must never be * resolved again, so lookups skip it. */ static const struct clm_tool * find_tool(const struct clm_agent *agent, const char *name) { const struct clm_tool *t; TAILQ_FOREACH(t, &agent->tools, entries) { if (!t->removed && strcmp(t->name, name) == 0) return t; } return NULL; } int clm_tool_add(struct clm_agent *agent, const struct clm_tool_def *def) { struct clm_tool *t; ASSERT_RETURN(agent != NULL, -EINVAL); ASSERT_RETURN(def != NULL, -EINVAL); ASSERT_RETURN(def->name != NULL, -EINVAL); ASSERT_RETURN(def->invoke != NULL, -EINVAL); if (find_tool(agent, def->name) != NULL) return -EEXIST; t = calloc(1, sizeof(*t)); if (t == NULL) return -ENOMEM; t->name = strdup(def->name); t->description = def->description ? strdup(def->description) : NULL; t->params_schema = def->params_schema ? strdup(def->params_schema) : NULL; if (t->name == NULL || (def->description != NULL && t->description == NULL) || (def->params_schema != NULL && t->params_schema == NULL)) { free(t->name); free(t->description); free(t->params_schema); free(t); return -ENOMEM; } t->invoke = def->invoke; t->detach = def->detach; t->user = def->user; t->output_cap = def->output_cap; t->timeout_ms = def->timeout_ms; t->flags = def->flags; TAILQ_INSERT_TAIL(&agent->tools, t, entries); agent->tool_count++; return 0; } static void tool_detach(struct clm_tool *t) { if (t->detach == NULL) return; t->detach(t->user); t->detach = NULL; } /* Free a node's owned strings and itself. Does not unlink or touch pending * invocations; callers must already know it is safe (unlinked + inflight==0, * or agent teardown). */ static void tool_node_free(struct clm_tool *t) { tool_detach(t); free(t->name); free(t->description); free(t->params_schema); free(t); } int clm_tool_remove(struct clm_agent *agent, const char *name) { struct clm_tool *t; ASSERT_RETURN(agent != NULL, -EINVAL); ASSERT_RETURN(name != NULL, -EINVAL); TAILQ_FOREACH(t, &agent->tools, entries) { if (t->removed || strcmp(t->name, name) != 0) continue; tool_detach(t); TAILQ_REMOVE(&agent->tools, t, entries); agent->tool_count--; if (t->inflight == 0) tool_node_free(t); else t->removed = true; /* freed once the last invocation finalizes */ return 0; } return -ENOENT; } void clm_tools_free_registry(struct clm_tool_list *tools) { struct clm_tool *t, *tmp; if (tools == NULL) return; t = TAILQ_FIRST(tools); while (t != NULL) { tmp = TAILQ_NEXT(t, entries); TAILQ_REMOVE(tools, t, entries); tool_node_free(t); t = tmp; } } /* ------------------------------------------------------------------ */ /* Schema construction */ /* ------------------------------------------------------------------ */ static cJSON * int_prop(const char *desc) { cJSON *obj = cJSON_CreateObject(); if (obj == NULL) return NULL; cJSON_AddItemToObject(obj, "type", cJSON_CreateString("integer")); cJSON_AddItemToObject(obj, "description", cJSON_CreateString(desc)); return obj; } static void inject_prop(cJSON *props, const char *name, const char *desc) { cJSON *exist = cJSON_GetObjectItemCaseSensitive(props, name); if (exist != NULL) { clm_debug("schema: skip reserved param '%s' (tool defines it)", name); return; } cJSON *p = int_prop(desc); if (p != NULL) cJSON_AddItemToObject(props, name, p); } static cJSON * tool_schema(const struct clm_tool *t) { cJSON *tool = cJSON_CreateObject(); cJSON *func, *params, *props; if (tool == NULL) return NULL; cJSON_AddItemToObject(tool, "type", cJSON_CreateString("function")); func = cJSON_CreateObject(); if (func == NULL) { cJSON_Delete(tool); return NULL; } cJSON_AddItemToObject(tool, "function", func); cJSON_AddItemToObject(func, "name", cJSON_CreateString(t->name)); cJSON_AddItemToObject(func, "description", cJSON_CreateString(t->description ? t->description : "")); /* Parse the tool's parameters schema, or synthesize an empty object. */ params = t->params_schema ? cJSON_Parse(t->params_schema) : NULL; if (params == NULL || cJSON_GetObjectItem(params, "type") == NULL) { if (params != NULL) cJSON_Delete(params); params = cJSON_CreateObject(); if (params == NULL) { cJSON_Delete(tool); return NULL; } } cJSON_AddItemToObject(func, "parameters", params); if (cJSON_GetObjectItemCaseSensitive(params, "type") == NULL) cJSON_AddItemToObject(params, "type", cJSON_CreateString("object")); props = cJSON_GetObjectItemCaseSensitive(params, "properties"); if (props == NULL) { props = cJSON_CreateObject(); if (props == NULL) { cJSON_Delete(tool); return NULL; } cJSON_AddItemToObject(params, "properties", props); } if (t->flags & CLM_TOOL_TIMEOUT_OVERRIDABLE) inject_prop(props, "timeout_ms", "optional: max milliseconds before this call is aborted"); if (t->flags & CLM_TOOL_OUTPUT_CAP_OVERRIDABLE) inject_prop(props, "output_cap", "optional: max bytes of output to return"); return tool; } cJSON * clm_tools_build_schema(const struct clm_agent *agent) { cJSON *arr = cJSON_CreateArray(); const struct clm_tool *t; if (agent == NULL) return NULL; if (arr == NULL) return NULL; TAILQ_FOREACH(t, &agent->tools, entries) { if (t->removed || (t->flags & CLM_TOOL_HIDDEN)) continue; cJSON *sch = tool_schema(t); if (sch != NULL) cJSON_AddItemToArray(arr, sch); /* steals reference */ } return arr; } /* ------------------------------------------------------------------ */ /* Invocation accessors */ /* ------------------------------------------------------------------ */ const char * clm_tool_invocation_name(const struct clm_tool_invocation *inv) { return inv ? inv->name : NULL; } const char * clm_tool_invocation_args(const struct clm_tool_invocation *inv) { return inv ? inv->args : NULL; } void * clm_tool_invocation_loop(const struct clm_tool_invocation *inv) { struct clm_host *host = inv ? inv->batch->agent->host : NULL; return host ? host->native_loop : NULL; } const char * clm_permission_req_name(const struct clm_permission_req *req) { return (req && req->inv) ? req->inv->name : NULL; } const char * clm_permission_req_args(const struct clm_permission_req *req) { return (req && req->inv) ? req->inv->args : NULL; } /* Mutable lookup of a registered tool by name (for recording decisions). */ static struct clm_tool * find_tool_mut(struct clm_agent *agent, const char *name) { struct clm_tool *t; TAILQ_FOREACH(t, &agent->tools, entries) { if (!t->removed && strcmp(t->name, name) == 0) return t; } return NULL; } int clm_tool_permission_respond(struct clm_agent *agent, const struct clm_permission_req *req, enum clm_permission_decision decision) { struct clm_tool_invocation *inv; bool allow; ASSERT_RETURN(agent != NULL, -EINVAL); ASSERT_RETURN(req != NULL && req->inv != NULL, -EINVAL); inv = req->inv; if (!inv->awaiting_perm) return -EINVAL; /* already answered, or not parked */ inv->awaiting_perm = false; allow = (decision == CLM_PERM_ALLOW_ONCE || decision == CLM_PERM_ALLOW_ALWAYS); /* Remember _ALWAYS decisions for the rest of the session, per tool. */ if (decision == CLM_PERM_ALLOW_ALWAYS || decision == CLM_PERM_DENY_ALWAYS) { struct clm_tool *t = find_tool_mut(agent, inv->name); if (t != NULL) { t->remembered = true; t->remember_allow = allow; } } if (allow) run_invoke(inv); else clm_tool_fail(inv, "denied by user"); return 0; } size_t clm_tool_invocation_output_cap(const struct clm_tool_invocation *inv) { return inv ? inv->output_cap : 0; } uint64_t clm_tool_invocation_timeout_ms(const struct clm_tool_invocation *inv) { return inv ? inv->timeout_ms : 0; } void clm_tool_invocation_set_cancel(struct clm_tool_invocation *inv, void (*cancel)(struct clm_tool_invocation *, void *), void *user) { if (inv == NULL) return; inv->cancel = cancel; inv->cancel_user = user; } /* ------------------------------------------------------------------ */ /* Batch teardown */ /* ------------------------------------------------------------------ */ static void batch_really_free(struct clm_tool_batch *batch) { size_t i; for (i = 0; i < batch->n; i++) { free(batch->inv[i].id); free(batch->inv[i].name); free(batch->inv[i].args); } free(batch->inv); free(batch); } static void batch_finalize(struct clm_tool_batch *batch) { struct clm_agent *agent = batch->agent; int status = batch->status; size_t i; if (agent->active_batch == batch) agent->active_batch = NULL; /* Advance the turn (or end it on error). This may start a new, * independent batch asynchronously; this one is torn down below. */ clm_agent_tools_done(agent, status);