Files
CST-agent/src/tools/session_tools.c
T
WHC 042d7f6999 v0.9.7: a turn that cannot hang, a session list that reads nothing twice, and PDFs
- consent: on_permission / on_ask_user wait at most two / five minutes, and in
  the background (a phone off screen, a window that is gone) the answer is
  refused at once with a reason -- a dialog nobody can see blocked the worker
  for good, which is what "the app hangs while the agent works" looked like.
- finishTurn() no longer joins the thread it is called from; the event loop
  reaps the Turn instead.
- AndroidTools: the hop to the GUI thread pings first and gives up after 3 s
  instead of waiting forever on a busy GUI thread.
- the session list stopped reading every transcript: cst_agent_store_info()
  counts newlines in 64 KiB blocks (first_user only within the first 256 KiB),
  and AgentCore::sessions() reuses a row while (mtime, size) is unchanged.
- session_search gets a budget (max_bytes 32 MB, max_seconds 5 s), skips lines
  over 64 KiB, and says when the budget stopped the walk.
- the header shows the phase ("shell: make -j8"), so a long turn no longer
  looks like a stuck one.
- md_to_pdf: the front end's own Qt tool (owner app:qt, every platform):
  markdown or a workspace .md/.html becomes an A4 PDF with 15 mm margins,
  paginated by hand, written into the workspace and delivered as a file card.
  No PrintSupport, so the phone gets it too.
- --uitest runs md_to_pdf through the registry with Chinese text and checks the
  bytes are a PDF; app/CMakeLists.txt no longer drops src/web_md.c from the
  standalone engine build, which the window needs to render markdown.

Version 0.9.7.
2026-10-11 19:26:36 +08:00

291 lines
10 KiB
C

#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE
#endif
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE 700
#endif
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <sys/utsname.h>
#include <unistd.h>
#else
#include <windows.h>
#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);
}