#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef _XOPEN_SOURCE #define _XOPEN_SOURCE 700 #endif #include #include #include #include #include "cJSON.h" #include "cst_agent.h" #include "cst_sandbox.h" #include "cst_tools.h" #include "cst_util.h" #include "cst_version.h" #include "os_compat.h" #include "tools_internal.h" #ifndef _WIN32 #include #include #else #include #endif /* A search that does not care about case: the model asks "where did we talk * about the store", and nobody remembers how it was capitalised. */ static const char *find_in(const char *hay, const char *needle, int icase) { size_t nn = strlen(needle); if (!nn) return NULL; for (const char *p = hay; *p; p++) { if (!icase) { if (strncmp(p, needle, nn) == 0) return p; continue; } size_t i = 0; while (i < nn && p[i] && tolower((unsigned char)p[i]) == tolower((unsigned char)needle[i])) i++; if (i == nn) return p; } return NULL; } /* A line of context around a match: control characters become spaces (one * transcript line is one JSON object anyway) and the tail is marked. */ static void say_context(cst_buf_t *b, const char *from, size_t max) { size_t i = 0; while (from[i] && i < max) { char c = from[i]; if (c == '\n' || c == '\r' || c == '\t') c = ' '; cst_buf_append(b, &c, 1); i++; } if (from[i]) cst_buf_puts(b, " ..."); } /* --- session_search ------------------------------------------------------ */ /* What was said before: every stored session's transcript, line by line. The * store is the same one the CLI, the TUI, the WebUI and the app use, so this * is how "we solved this last week" is answered without a shell. */ int cst_tool_session_search(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r) { const char *query = tool_arg(c, "query"); if (!query || !query[0]) return tool_set_result(r, "error: missing query", 1); while (*query == ' ') query++; if (!query[0]) return tool_set_result(r, "error: missing query", 1); long want = tool_arg_long(c, "max_matches", 20); if (want <= 0) want = 20; if (want > 100) want = 100; int icase = tool_arg_bool(c, "ignore_case", 1); /* A store grows without bound and this walks all of it, so it gets a * budget: the walk stops when it is spent and says so, instead of making a * turn sit for minutes on a workspace with a few hundred sessions. */ long budget_bytes = tool_arg_long(c, "max_bytes", 32L << 20); if (budget_bytes <= 0) budget_bytes = 32L << 20; long budget_ms = tool_arg_long(c, "max_seconds", 5); if (budget_ms <= 0) budget_ms = 5; budget_ms *= 1000; const double t_start = os_now_ms(); long scanned_bytes = 0; int cut = 0; /* the budget stopped the walk */ static char ids[200][128]; int n = cst_agent_store_list(ctx->workspace_root, ids, 200); cst_buf_t hits = {0}; int matches = 0, in_sessions = 0, scanned_sessions = 0; for (int i = 0; i < n && matches < (int)want; i++) { if (scanned_bytes >= budget_bytes || os_now_ms() - t_start > (double)budget_ms) { cut = 1; break; } char path[OS_PATH_MAX]; if (cst_agent_store_path_for_id(ctx->workspace_root, ids[i], path, sizeof(path)) != 0) continue; FILE *fp = fopen(path, "rb"); if (!fp) continue; scanned_sessions++; cst_session_info_t in; int have_info = cst_agent_store_info(ctx->workspace_root, ids[i], &in) == 0; int in_this = 0, lineno = 0; char *line = NULL; size_t cap = 0; cst_buf_t one = {0}; ssize_t got; while ((got = getline(&line, &cap, fp)) > 0) { lineno++; scanned_bytes += got; /* A tool output line can be megabytes (a whole file read back): such a * line is not scanned -- one cJSON tree per block of it would be the * slowest thing in the turn. */ if (got <= 1 || got > (1 << 16)) { if (scanned_bytes >= budget_bytes) { cut = 1; break; } continue; } /* the clock is asked every so often: a clock_gettime per line would be * the next slowest thing after the parse */ if ((lineno & 0xFF) == 0 && ((scanned_bytes >= budget_bytes || os_now_ms() - t_start > (double)budget_ms))) { cut = 1; break; } cJSON *o = cJSON_Parse(line); if (!o) continue; const cJSON *role = cJSON_GetObjectItemCaseSensitive(o, "role"); const cJSON *content = cJSON_GetObjectItemCaseSensitive(o, "content"); const char *txt = cJSON_IsString(content) ? content->valuestring : NULL; const char *at = txt ? find_in(txt, query, icase) : NULL; int stop = 0; if (at) { if (!in_this) { in_this = 1; in_sessions++; cst_buf_printf(&one, "\n%s", ids[i]); if (have_info && in.messages) cst_buf_printf(&one, " · %d msgs", in.messages); if (have_info && in.title[0]) cst_buf_printf(&one, " · %s", in.title); else if (have_info && in.first_user[0]) cst_buf_printf(&one, " · %s", in.first_user); cst_buf_puts(&one, "\n"); } const char *who = cJSON_IsString(role) ? role->valuestring : "?"; const char *meta = NULL; const cJSON *kind = cJSON_GetObjectItemCaseSensitive( cJSON_GetObjectItemCaseSensitive(o, "meta"), "kind"); if (cJSON_IsString(kind)) meta = kind->valuestring; /* a little before the match, a line's worth after it */ const char *start = at; for (int back = 0; start > txt && back < 60; back++) start--; cst_buf_printf(&one, " #%d %s%s: ", lineno, who, meta && strcmp(meta, "note") == 0 ? " (note)" : ""); say_context(&one, start, (size_t)(at - start) + strlen(query) + 160); cst_buf_puts(&one, "\n"); matches++; stop = matches >= (int)want; } cJSON_Delete(o); if (stop) break; } cst_buf_append(&hits, one.buf, one.buf ? one.len : 0); cst_buf_free(&one); free(line); fclose(fp); } if (matches == 0) { cst_buf_free(&hits); return tool_set_resultf(r, 0, "no match for \"%.80s\" in %d of %d stored session%s" " (%.1f MB scanned%s)\n", query, scanned_sessions, n, scanned_sessions == 1 ? "" : "s", (double)scanned_bytes / (1024.0 * 1024.0), cut ? "; the budget stopped the walk -- raise " "max_bytes or max_seconds" : ""); } cst_buf_t out = {0}; cst_buf_printf(&out, "%d match%s for \"%.80s\" in %d of %d stored session%s\n", matches, matches == 1 ? "" : "es", query, in_sessions, scanned_sessions, scanned_sessions == 1 ? "" : "s"); cst_buf_append(&out, hits.buf, hits.len); if (matches >= (int)want) cst_buf_puts(&out, "\n(more may match; raise max_matches or narrow the " "query)\n"); if (cut) cst_buf_printf(&out, "\n(the walk stopped after %.1f MB / %.1f s of a %d MB store: " "older sessions were not looked at -- raise max_bytes or " "max_seconds to go further)\n", (double)scanned_bytes / (1024.0 * 1024.0), (os_now_ms() - t_start) / 1000.0, n); cst_buf_free(&hits); return tool_take_buf(r, &out, 0); } /* --- device_info --------------------------------------------------------- */ /* Where this agent is running: enough for the model to stop guessing (a * phone in a pocket and a desktop in a terminal answer differently), and no * more than that -- no serial numbers, no accounts. */ int cst_tool_device_info(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r) { (void)c; cst_buf_t b = {0}; cst_buf_printf(&b, "cst-agent %s\n", CST_VERSION); #if defined(__ANDROID__) const char *plat = "android"; #elif defined(_WIN32) const char *plat = "windows"; #elif defined(__APPLE__) const char *plat = "macos"; #elif defined(__linux__) const char *plat = "linux"; #else const char *plat = "unknown"; #endif #ifdef _WIN32 char host[160] = ""; os_windows_version(host, sizeof(host)); SYSTEM_INFO si; memset(&si, 0, sizeof(si)); GetSystemInfo(&si); cst_buf_printf(&b, "system: %s (%d-bit, %lu CPUs)\n", host[0] ? host : plat, (int)(sizeof(void *) * 8), (unsigned long)si.dwNumberOfProcessors); #else struct utsname u; long cpus = sysconf(_SC_NPROCESSORS_ONLN); if (uname(&u) == 0) cst_buf_printf(&b, "system: %s %.80s %.60s (%d-bit, %ld CPUs)\n", u.sysname, u.release, u.machine, (int)(sizeof(void *) * 8), cpus > 0 ? cpus : 0L); else cst_buf_printf(&b, "system: %s (%d-bit)\n", plat, (int)(sizeof(void *) * 8)); #endif cst_buf_printf(&b, "workspace: %s\n", ctx->workspace_root ? ctx->workspace_root : "?"); cst_buf_printf(&b, "sandbox: %s\n", cst_sandbox_name(cst_sandbox_probe())); const cst_agent_config_t *cfg = ctx->agent ? cst_agent_config(ctx->agent) : NULL; if (cfg) { char sid[128] = ""; cst_agent_session_id(ctx->agent, sid, sizeof(sid)); cst_buf_printf(&b, "session: %.120s\n", sid[0] ? sid : "(none)"); cst_buf_printf(&b, "model: %.60s · %.30s · effort %.12s · mode %.12s\n", cfg->model, cfg->provider, cfg->effort, cfg->mode); cst_buf_printf(&b, "permissions: approval %s%s%s\n", cst_approval_name(cfg->approval), cfg->plan_mode ? ", plan mode (read-only)" : "", cfg->auto_approve ? ", unattended" : ""); cst_buf_printf(&b, "steps: %d model calls per turn max\n", cfg->max_steps); } char dir[OS_PATH_MAX]; if (os_config_dir(dir, sizeof(dir)) == 0) cst_buf_printf(&b, "config: %s\n", dir); if (os_temp_dir(dir, sizeof(dir)) == 0) cst_buf_printf(&b, "temp: %s\n", dir); if (os_exe_dir(dir, sizeof(dir)) == 0) cst_buf_printf(&b, "program: %s\n", dir); cst_buf_printf(&b, "process: pid %d\n", (int)os_getpid()); return tool_take_buf(r, &b, 0); }