- 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.
291 lines
10 KiB
C
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);
|
|
}
|