Initial CST-Agent: portable Linux/Termux/Windows coding agent.
Vendored sashidown, OpenAI-compatible env (OPENAI_*), and bootstrap scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+363
@@ -0,0 +1,363 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
#include "cst_agent.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
typedef long long cst_ssize_t;
|
||||
static cst_ssize_t cst_getline(char **lineptr, size_t *n, FILE *stream) {
|
||||
if (!lineptr || !n || !stream)
|
||||
return -1;
|
||||
if (!*lineptr || *n == 0) {
|
||||
*n = 256;
|
||||
*lineptr = (char *)malloc(*n);
|
||||
if (!*lineptr)
|
||||
return -1;
|
||||
}
|
||||
size_t len = 0;
|
||||
int c = 0;
|
||||
while ((c = fgetc(stream)) != EOF) {
|
||||
if (len + 2 > *n) {
|
||||
size_t nn = (*n) * 2;
|
||||
char *nb = (char *)realloc(*lineptr, nn);
|
||||
if (!nb)
|
||||
return -1;
|
||||
*lineptr = nb;
|
||||
*n = nn;
|
||||
}
|
||||
(*lineptr)[len++] = (char)c;
|
||||
if (c == '\n')
|
||||
break;
|
||||
}
|
||||
if (len == 0 && c == EOF)
|
||||
return -1;
|
||||
(*lineptr)[len] = 0;
|
||||
return (cst_ssize_t)len;
|
||||
}
|
||||
#define getline cst_getline
|
||||
#endif
|
||||
|
||||
struct cst_session {
|
||||
char path[4096];
|
||||
cst_message_t *msgs;
|
||||
int nmsgs;
|
||||
int cap;
|
||||
FILE *fp;
|
||||
};
|
||||
|
||||
static char *xstrdup(const char *s) {
|
||||
if (!s)
|
||||
return NULL;
|
||||
size_t n = strlen(s);
|
||||
char *p = malloc(n + 1);
|
||||
if (!p)
|
||||
return NULL;
|
||||
memcpy(p, s, n + 1);
|
||||
return p;
|
||||
}
|
||||
|
||||
static const char *role_str(cst_role_t r) {
|
||||
switch (r) {
|
||||
case CST_ROLE_SYSTEM:
|
||||
return "system";
|
||||
case CST_ROLE_USER:
|
||||
return "user";
|
||||
case CST_ROLE_ASSISTANT:
|
||||
return "assistant";
|
||||
case CST_ROLE_TOOL:
|
||||
return "tool";
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
|
||||
static cst_role_t role_from(const char *s) {
|
||||
if (strcmp(s, "system") == 0)
|
||||
return CST_ROLE_SYSTEM;
|
||||
if (strcmp(s, "assistant") == 0)
|
||||
return CST_ROLE_ASSISTANT;
|
||||
if (strcmp(s, "tool") == 0)
|
||||
return CST_ROLE_TOOL;
|
||||
return CST_ROLE_USER;
|
||||
}
|
||||
|
||||
static int json_escape_write(FILE *fp, const char *s) {
|
||||
fputc('"', fp);
|
||||
for (; s && *s; s++) {
|
||||
unsigned char c = (unsigned char)*s;
|
||||
if (c == '"' || c == '\\') {
|
||||
fputc('\\', fp);
|
||||
fputc(c, fp);
|
||||
} else if (c == '\n') {
|
||||
fputs("\\n", fp);
|
||||
} else if (c == '\r') {
|
||||
fputs("\\r", fp);
|
||||
} else if (c == '\t') {
|
||||
fputs("\\t", fp);
|
||||
} else if (c < 0x20) {
|
||||
fprintf(fp, "\\u%04x", c);
|
||||
} else {
|
||||
fputc(c, fp);
|
||||
}
|
||||
}
|
||||
fputc('"', fp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Minimal extract "key":"value" from a single JSONL object line. */
|
||||
static int extract_str(const char *line, const char *key, char *out,
|
||||
size_t out_sz) {
|
||||
char pat[64];
|
||||
snprintf(pat, sizeof(pat), "\"%s\":\"", key);
|
||||
const char *p = strstr(line, pat);
|
||||
if (!p)
|
||||
return -1;
|
||||
p += strlen(pat);
|
||||
size_t o = 0;
|
||||
while (*p && o + 1 < out_sz) {
|
||||
if (*p == '\\' && p[1]) {
|
||||
p++;
|
||||
if (*p == 'n')
|
||||
out[o++] = '\n';
|
||||
else if (*p == 'r')
|
||||
out[o++] = '\r';
|
||||
else if (*p == 't')
|
||||
out[o++] = '\t';
|
||||
else
|
||||
out[o++] = *p;
|
||||
p++;
|
||||
continue;
|
||||
}
|
||||
if (*p == '"') {
|
||||
out[o] = 0;
|
||||
return 0;
|
||||
}
|
||||
out[o++] = *p++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int session_grow(cst_session_t *s) {
|
||||
if (s->nmsgs < s->cap)
|
||||
return 0;
|
||||
int ncap = s->cap ? s->cap * 2 : 32;
|
||||
cst_message_t *nm = realloc(s->msgs, (size_t)ncap * sizeof(cst_message_t));
|
||||
if (!nm)
|
||||
return -1;
|
||||
s->msgs = nm;
|
||||
s->cap = ncap;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int session_push_mem(cst_session_t *s, cst_role_t role, const char *content,
|
||||
const char *tool_id, const char *tool_name,
|
||||
const char *tool_calls_json) {
|
||||
if (session_grow(s) != 0)
|
||||
return -1;
|
||||
cst_message_t *d = &s->msgs[s->nmsgs];
|
||||
memset(d, 0, sizeof(*d));
|
||||
d->role = role;
|
||||
d->content = xstrdup(content ? content : "");
|
||||
if (!d->content)
|
||||
return -1;
|
||||
if (tool_id)
|
||||
snprintf(d->tool_call_id, sizeof(d->tool_call_id), "%s", tool_id);
|
||||
if (tool_name)
|
||||
snprintf(d->tool_name, sizeof(d->tool_name), "%s", tool_name);
|
||||
if (tool_calls_json && tool_calls_json[0])
|
||||
d->tool_calls_json = xstrdup(tool_calls_json);
|
||||
s->nmsgs++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void load_existing(cst_session_t *s) {
|
||||
FILE *rp = fopen(s->path, "r");
|
||||
if (!rp)
|
||||
return;
|
||||
char *line = NULL;
|
||||
size_t cap = 0;
|
||||
while (getline(&line, &cap, rp) != -1) {
|
||||
char role[32] = {0};
|
||||
char *content = malloc(cap > 8 ? cap : 4096);
|
||||
char tid[CTL_MAX_ID] = {0};
|
||||
char tname[CTL_MAX_NAME] = {0};
|
||||
if (!content)
|
||||
continue;
|
||||
content[0] = 0;
|
||||
if (extract_str(line, "role", role, sizeof(role)) != 0) {
|
||||
free(content);
|
||||
continue;
|
||||
}
|
||||
/* content may be large — reuse line extract into growing buffer */
|
||||
free(content);
|
||||
content = malloc(strlen(line) + 1);
|
||||
if (!content)
|
||||
continue;
|
||||
if (extract_str(line, "content", content, strlen(line) + 1) != 0) {
|
||||
free(content);
|
||||
continue;
|
||||
}
|
||||
extract_str(line, "tool_call_id", tid, sizeof(tid));
|
||||
extract_str(line, "tool_name", tname, sizeof(tname));
|
||||
char *tcj = malloc(strlen(line) + 1);
|
||||
const char *tcjp = NULL;
|
||||
if (tcj) {
|
||||
tcj[0] = 0;
|
||||
if (extract_str(line, "tool_calls_json", tcj, strlen(line) + 1) == 0)
|
||||
tcjp = tcj;
|
||||
}
|
||||
session_push_mem(s, role_from(role), content, tid[0] ? tid : NULL,
|
||||
tname[0] ? tname : NULL, tcjp);
|
||||
free(tcj);
|
||||
free(content);
|
||||
}
|
||||
free(line);
|
||||
fclose(rp);
|
||||
}
|
||||
|
||||
cst_session_t *cst_session_open(const char *path) {
|
||||
cst_session_t *s = calloc(1, sizeof(*s));
|
||||
if (!s)
|
||||
return NULL;
|
||||
snprintf(s->path, sizeof(s->path), "%s", path ? path : "session.jsonl");
|
||||
load_existing(s);
|
||||
s->fp = fopen(s->path, "a+");
|
||||
if (!s->fp) {
|
||||
for (int i = 0; i < s->nmsgs; i++) {
|
||||
free(s->msgs[i].content);
|
||||
free(s->msgs[i].tool_calls_json);
|
||||
}
|
||||
free(s->msgs);
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
if (!s->msgs) {
|
||||
s->cap = 32;
|
||||
s->msgs = calloc((size_t)s->cap, sizeof(cst_message_t));
|
||||
if (!s->msgs) {
|
||||
fclose(s->fp);
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void cst_session_close(cst_session_t *s) {
|
||||
if (!s)
|
||||
return;
|
||||
for (int i = 0; i < s->nmsgs; i++) {
|
||||
free(s->msgs[i].content);
|
||||
free(s->msgs[i].tool_calls_json);
|
||||
}
|
||||
free(s->msgs);
|
||||
if (s->fp)
|
||||
fclose(s->fp);
|
||||
free(s);
|
||||
}
|
||||
|
||||
int cst_session_append(cst_session_t *s, const cst_message_t *msg) {
|
||||
if (!s || !msg)
|
||||
return -1;
|
||||
if (session_push_mem(s, msg->role, msg->content, msg->tool_call_id,
|
||||
msg->tool_name, msg->tool_calls_json) != 0)
|
||||
return -1;
|
||||
|
||||
fprintf(s->fp, "{\"role\":");
|
||||
json_escape_write(s->fp, role_str(msg->role));
|
||||
fprintf(s->fp, ",\"content\":");
|
||||
json_escape_write(s->fp, msg->content ? msg->content : "");
|
||||
if (msg->tool_call_id[0]) {
|
||||
fprintf(s->fp, ",\"tool_call_id\":");
|
||||
json_escape_write(s->fp, msg->tool_call_id);
|
||||
}
|
||||
if (msg->tool_name[0]) {
|
||||
fprintf(s->fp, ",\"tool_name\":");
|
||||
json_escape_write(s->fp, msg->tool_name);
|
||||
}
|
||||
if (msg->tool_calls_json && msg->tool_calls_json[0]) {
|
||||
fprintf(s->fp, ",\"tool_calls_json\":");
|
||||
json_escape_write(s->fp, msg->tool_calls_json);
|
||||
}
|
||||
fprintf(s->fp, "}\n");
|
||||
fflush(s->fp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cst_session_count(const cst_session_t *s) {
|
||||
return s ? s->nmsgs : 0;
|
||||
}
|
||||
|
||||
const cst_message_t *cst_session_get(const cst_session_t *s, int i) {
|
||||
if (!s || i < 0 || i >= s->nmsgs)
|
||||
return NULL;
|
||||
return &s->msgs[i];
|
||||
}
|
||||
|
||||
const char *cst_session_path(const cst_session_t *s) {
|
||||
return s ? s->path : NULL;
|
||||
}
|
||||
|
||||
int cst_session_clear(cst_session_t *s, int keep_system) {
|
||||
if (!s)
|
||||
return -1;
|
||||
cst_message_t sys = {0};
|
||||
int have_sys = 0;
|
||||
if (keep_system) {
|
||||
for (int i = 0; i < s->nmsgs; i++) {
|
||||
if (s->msgs[i].role == CST_ROLE_SYSTEM) {
|
||||
sys.role = CST_ROLE_SYSTEM;
|
||||
sys.content = xstrdup(s->msgs[i].content ? s->msgs[i].content : "");
|
||||
have_sys = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < s->nmsgs; i++) {
|
||||
free(s->msgs[i].content);
|
||||
free(s->msgs[i].tool_calls_json);
|
||||
}
|
||||
s->nmsgs = 0;
|
||||
if (s->fp) {
|
||||
fclose(s->fp);
|
||||
s->fp = NULL;
|
||||
}
|
||||
s->fp = fopen(s->path, "w+");
|
||||
if (!s->fp) {
|
||||
if (have_sys)
|
||||
free(sys.content);
|
||||
return -1;
|
||||
}
|
||||
if (have_sys) {
|
||||
cst_session_append(s, &sys);
|
||||
free(sys.content);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cst_session_rewrite(cst_session_t *s, const cst_message_t *msgs, int n) {
|
||||
if (!s || n < 0)
|
||||
return -1;
|
||||
for (int i = 0; i < s->nmsgs; i++) {
|
||||
free(s->msgs[i].content);
|
||||
free(s->msgs[i].tool_calls_json);
|
||||
}
|
||||
s->nmsgs = 0;
|
||||
if (s->fp) {
|
||||
fclose(s->fp);
|
||||
s->fp = NULL;
|
||||
}
|
||||
s->fp = fopen(s->path, "w+");
|
||||
if (!s->fp)
|
||||
return -1;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (cst_session_append(s, &msgs[i]) != 0)
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user