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:
+419
@@ -0,0 +1,419 @@
|
||||
#include "ctl.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef enum {
|
||||
ST_TEXT = 0, /* outside a call; accumulate text lines */
|
||||
ST_LINE, /* collecting a line (call/arg/end or text) */
|
||||
ST_ARG_BODY, /* reading nbytes of arg body */
|
||||
ST_ERROR
|
||||
} ctl_state_t;
|
||||
|
||||
struct ctl_parser {
|
||||
ctl_on_event_fn on_event;
|
||||
void *userdata;
|
||||
ctl_state_t state;
|
||||
char line[CTL_MAX_LINE];
|
||||
size_t line_len;
|
||||
ctl_tool_call_t call;
|
||||
int in_call;
|
||||
/* current arg being filled */
|
||||
char arg_key[CTL_MAX_KEY];
|
||||
char *arg_buf;
|
||||
size_t arg_cap;
|
||||
size_t arg_got;
|
||||
size_t arg_need;
|
||||
size_t pending;
|
||||
size_t pending_peak;
|
||||
char errbuf[256];
|
||||
};
|
||||
|
||||
static void bump_pending(ctl_parser_t *p, size_t n) {
|
||||
p->pending = n;
|
||||
if (n > p->pending_peak)
|
||||
p->pending_peak = n;
|
||||
}
|
||||
|
||||
static void emit(ctl_parser_t *p, ctl_event_t *ev) {
|
||||
if (p->on_event)
|
||||
p->on_event(ev, p->userdata);
|
||||
}
|
||||
|
||||
static void fail(ctl_parser_t *p, const char *msg) {
|
||||
snprintf(p->errbuf, sizeof(p->errbuf), "%s", msg);
|
||||
p->state = ST_ERROR;
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_ERROR;
|
||||
ev.error = p->errbuf;
|
||||
emit(p, &ev);
|
||||
}
|
||||
|
||||
/* free args in current call */
|
||||
static void clear_call_args(ctl_tool_call_t *c) {
|
||||
for (int i = 0; i < c->nargs; i++) {
|
||||
free(c->args[i].value);
|
||||
c->args[i].value = NULL;
|
||||
c->args[i].value_len = 0;
|
||||
}
|
||||
c->nargs = 0;
|
||||
c->id[0] = 0;
|
||||
c->name[0] = 0;
|
||||
}
|
||||
|
||||
static void reset_arg_body(ctl_parser_t *p) {
|
||||
free(p->arg_buf);
|
||||
p->arg_buf = NULL;
|
||||
p->arg_cap = 0;
|
||||
p->arg_got = 0;
|
||||
p->arg_need = 0;
|
||||
p->arg_key[0] = 0;
|
||||
}
|
||||
|
||||
ctl_parser_t *ctl_parser_create(ctl_on_event_fn on_event, void *userdata) {
|
||||
ctl_parser_t *p = calloc(1, sizeof(*p));
|
||||
if (!p)
|
||||
return NULL;
|
||||
p->on_event = on_event;
|
||||
p->userdata = userdata;
|
||||
p->state = ST_TEXT;
|
||||
return p;
|
||||
}
|
||||
|
||||
void ctl_parser_destroy(ctl_parser_t *p) {
|
||||
if (!p)
|
||||
return;
|
||||
clear_call_args(&p->call);
|
||||
reset_arg_body(p);
|
||||
free(p);
|
||||
}
|
||||
|
||||
void ctl_parser_reset(ctl_parser_t *p) {
|
||||
if (!p)
|
||||
return;
|
||||
clear_call_args(&p->call);
|
||||
reset_arg_body(p);
|
||||
p->state = ST_TEXT;
|
||||
p->line_len = 0;
|
||||
p->in_call = 0;
|
||||
p->pending = 0;
|
||||
/* keep pending_peak for experiment */
|
||||
}
|
||||
|
||||
size_t ctl_parser_pending(const ctl_parser_t *p) {
|
||||
return p ? p->pending : 0;
|
||||
}
|
||||
|
||||
size_t ctl_parser_pending_peak(const ctl_parser_t *p) {
|
||||
return p ? p->pending_peak : 0;
|
||||
}
|
||||
|
||||
static int starts_with(const char *s, const char *pfx) {
|
||||
return strncmp(s, pfx, strlen(pfx)) == 0;
|
||||
}
|
||||
|
||||
static int parse_call_line(ctl_parser_t *p, const char *line) {
|
||||
/* call <id> <name> */
|
||||
const char *s = line + 5;
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
if (!*s)
|
||||
return -1;
|
||||
const char *sp = strchr(s, ' ');
|
||||
if (!sp)
|
||||
return -1;
|
||||
size_t idlen = (size_t)(sp - s);
|
||||
if (idlen == 0 || idlen >= CTL_MAX_ID)
|
||||
return -1;
|
||||
while (*sp == ' ' || *sp == '\t')
|
||||
sp++;
|
||||
if (!*sp)
|
||||
return -1;
|
||||
size_t nlen = strlen(sp);
|
||||
if (nlen == 0 || nlen >= CTL_MAX_NAME)
|
||||
return -1;
|
||||
for (size_t i = 0; i < nlen; i++) {
|
||||
if (!(isalnum((unsigned char)sp[i]) || sp[i] == '_' || sp[i] == '-'))
|
||||
return -1;
|
||||
}
|
||||
clear_call_args(&p->call);
|
||||
memcpy(p->call.id, s, idlen);
|
||||
p->call.id[idlen] = 0;
|
||||
memcpy(p->call.name, sp, nlen);
|
||||
p->call.name[nlen] = 0;
|
||||
p->in_call = 1;
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_CALL_OPEN;
|
||||
ev.call = &p->call;
|
||||
emit(p, &ev);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int finish_arg(ctl_parser_t *p);
|
||||
|
||||
static int parse_arg_header(ctl_parser_t *p, const char *line) {
|
||||
/* Forms:
|
||||
* arg <key> <nbytes> → length-prefixed body follows
|
||||
* arg <key> <value...> → value is rest of line (line-mode, confirmed now)
|
||||
*/
|
||||
const char *s = line + 4;
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
if (!*s)
|
||||
return -1;
|
||||
const char *sp = strchr(s, ' ');
|
||||
if (!sp)
|
||||
return -1;
|
||||
size_t klen = (size_t)(sp - s);
|
||||
if (klen == 0 || klen >= CTL_MAX_KEY)
|
||||
return -1;
|
||||
while (*sp == ' ' || *sp == '\t')
|
||||
sp++;
|
||||
if (!*sp)
|
||||
return -1;
|
||||
if (p->call.nargs >= CTL_MAX_ARGS)
|
||||
return -1;
|
||||
|
||||
char *end = NULL;
|
||||
unsigned long nbytes = strtoul(sp, &end, 10);
|
||||
int length_prefixed = (end != sp && (*end == '\0' || *end == ' ' || *end == '\t'));
|
||||
if (length_prefixed && end && *end != '\0') {
|
||||
while (*end == ' ' || *end == '\t')
|
||||
end++;
|
||||
if (*end != '\0')
|
||||
length_prefixed = 0; /* e.g. "9foo" or value starting with digits+text */
|
||||
}
|
||||
|
||||
memcpy(p->arg_key, s, klen);
|
||||
p->arg_key[klen] = 0;
|
||||
|
||||
if (!length_prefixed) {
|
||||
/* line-mode: entire remainder is the value */
|
||||
size_t vlen = strlen(sp);
|
||||
if (vlen > CTL_MAX_ARG_BYTES)
|
||||
return -1;
|
||||
free(p->arg_buf);
|
||||
p->arg_buf = malloc(vlen + 1);
|
||||
if (!p->arg_buf)
|
||||
return -2;
|
||||
memcpy(p->arg_buf, sp, vlen);
|
||||
p->arg_buf[vlen] = 0;
|
||||
p->arg_need = vlen;
|
||||
p->arg_got = vlen;
|
||||
p->arg_cap = vlen;
|
||||
finish_arg(p);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (nbytes > CTL_MAX_ARG_BYTES)
|
||||
return -1;
|
||||
p->arg_need = (size_t)nbytes;
|
||||
p->arg_got = 0;
|
||||
free(p->arg_buf);
|
||||
p->arg_buf = NULL;
|
||||
p->arg_cap = 0;
|
||||
if (p->arg_need > 0) {
|
||||
p->arg_buf = malloc(p->arg_need + 1);
|
||||
if (!p->arg_buf)
|
||||
return -2;
|
||||
p->arg_cap = p->arg_need;
|
||||
}
|
||||
p->state = ST_ARG_BODY;
|
||||
bump_pending(p, p->arg_need);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int finish_arg(ctl_parser_t *p) {
|
||||
int i = p->call.nargs;
|
||||
snprintf(p->call.args[i].key, CTL_MAX_KEY, "%s", p->arg_key);
|
||||
p->call.args[i].value = p->arg_buf;
|
||||
p->call.args[i].value_len = p->arg_got;
|
||||
if (p->arg_buf)
|
||||
p->arg_buf[p->arg_got] = 0;
|
||||
p->arg_buf = NULL; /* ownership moved */
|
||||
p->call.nargs++;
|
||||
p->arg_need = 0;
|
||||
p->arg_got = 0;
|
||||
p->arg_key[0] = 0;
|
||||
p->state = ST_LINE;
|
||||
bump_pending(p, 0);
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_ARG;
|
||||
ev.call = &p->call;
|
||||
emit(p, &ev);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void emit_text_line(ctl_parser_t *p, const char *line, size_t len) {
|
||||
/* include newline as confirmed text */
|
||||
char tmp[CTL_MAX_LINE + 1];
|
||||
if (len + 1 >= sizeof(tmp))
|
||||
len = sizeof(tmp) - 2;
|
||||
memcpy(tmp, line, len);
|
||||
tmp[len] = '\n';
|
||||
tmp[len + 1] = 0;
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_TEXT;
|
||||
ev.text = tmp;
|
||||
ev.text_len = len + 1;
|
||||
emit(p, &ev);
|
||||
}
|
||||
|
||||
static int is_fence_line(const char *line) {
|
||||
if (strncmp(line, "```", 3) == 0)
|
||||
return 1;
|
||||
if (strncmp(line, "~~~", 3) == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int handle_line(ctl_parser_t *p, const char *line) {
|
||||
/* Ignore markdown fences so models can wrap CTL in code blocks. */
|
||||
if (is_fence_line(line)) {
|
||||
bump_pending(p, 0);
|
||||
return 0;
|
||||
}
|
||||
/* Ignore blank lines between CTL frames. */
|
||||
if (line[0] == '\0') {
|
||||
bump_pending(p, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!p->in_call) {
|
||||
if (starts_with(line, "call ")) {
|
||||
if (parse_call_line(p, line) != 0) {
|
||||
fail(p, "invalid call line");
|
||||
return -1;
|
||||
}
|
||||
bump_pending(p, 0);
|
||||
return 0;
|
||||
}
|
||||
/* confirmed text line */
|
||||
emit_text_line(p, line, strlen(line));
|
||||
bump_pending(p, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* in call */
|
||||
if (starts_with(line, "arg ")) {
|
||||
int rc = parse_arg_header(p, line);
|
||||
if (rc == -2) {
|
||||
fail(p, "oom");
|
||||
return -1;
|
||||
}
|
||||
if (rc != 0) {
|
||||
fail(p, "invalid arg header");
|
||||
return -1;
|
||||
}
|
||||
if (p->arg_need == 0)
|
||||
finish_arg(p);
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(line, "end") == 0) {
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_CALL_READY;
|
||||
ev.call = &p->call;
|
||||
emit(p, &ev);
|
||||
clear_call_args(&p->call);
|
||||
p->in_call = 0;
|
||||
bump_pending(p, 0);
|
||||
p->state = ST_LINE;
|
||||
return 0;
|
||||
}
|
||||
fail(p, "unexpected line inside call");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctl_status_t ctl_parser_feed(ctl_parser_t *p, const char *data, size_t len) {
|
||||
if (!p)
|
||||
return CTL_ERR_CONTRACT;
|
||||
if (p->state == ST_ERROR)
|
||||
return CTL_ERR_CONTRACT;
|
||||
|
||||
size_t i = 0;
|
||||
while (i < len) {
|
||||
if (p->state == ST_ARG_BODY) {
|
||||
size_t left = p->arg_need - p->arg_got;
|
||||
size_t take = len - i;
|
||||
if (take > left)
|
||||
take = left;
|
||||
if (take > 0) {
|
||||
memcpy(p->arg_buf + p->arg_got, data + i, take);
|
||||
p->arg_got += take;
|
||||
i += take;
|
||||
bump_pending(p, p->arg_need - p->arg_got);
|
||||
}
|
||||
if (p->arg_got == p->arg_need) {
|
||||
finish_arg(p);
|
||||
/* continue with remaining bytes in LINE mode */
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* line accumulation */
|
||||
p->state = ST_LINE;
|
||||
unsigned char c = (unsigned char)data[i++];
|
||||
if (c == '\r')
|
||||
continue; /* normalize CRLF */
|
||||
if (c == '\n') {
|
||||
p->line[p->line_len] = 0;
|
||||
if (handle_line(p, p->line) != 0)
|
||||
return CTL_ERR_CONTRACT;
|
||||
p->line_len = 0;
|
||||
if (p->state == ST_ARG_BODY && p->arg_need == 0) {
|
||||
/* already finished empty arg */
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (p->line_len + 1 >= CTL_MAX_LINE) {
|
||||
fail(p, "line too long");
|
||||
return CTL_ERR_LIMIT;
|
||||
}
|
||||
p->line[p->line_len++] = (char)c;
|
||||
bump_pending(p, p->line_len);
|
||||
}
|
||||
return CTL_OK;
|
||||
}
|
||||
|
||||
ctl_status_t ctl_parser_finish(ctl_parser_t *p) {
|
||||
if (!p)
|
||||
return CTL_ERR_CONTRACT;
|
||||
if (p->state == ST_ERROR)
|
||||
return CTL_ERR_CONTRACT;
|
||||
/* Mid-argument body cannot be confirmed at EOF. */
|
||||
if (p->state == ST_ARG_BODY) {
|
||||
fail(p, "incomplete arg body at EOF");
|
||||
return CTL_ERR_CONTRACT;
|
||||
}
|
||||
/* Trailing text without final newline is still confirmed text. */
|
||||
if (!p->in_call && p->line_len > 0) {
|
||||
p->line[p->line_len] = 0;
|
||||
emit_text_line(p, p->line, p->line_len);
|
||||
p->line_len = 0;
|
||||
}
|
||||
/* EOF confirms an open call whose args are complete (implicit end).
|
||||
* Matches paper EOF adequacy: stream end closes the frontier. */
|
||||
if (p->in_call) {
|
||||
if (p->line_len > 0) {
|
||||
p->line[p->line_len] = 0;
|
||||
/* leftover line must be end or fence/blank */
|
||||
if (strcmp(p->line, "end") == 0 || is_fence_line(p->line) ||
|
||||
p->line[0] == '\0') {
|
||||
p->line_len = 0;
|
||||
} else {
|
||||
fail(p, "unexpected trailing line in call at EOF");
|
||||
return CTL_ERR_CONTRACT;
|
||||
}
|
||||
}
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_CALL_READY;
|
||||
ev.call = &p->call;
|
||||
emit(p, &ev);
|
||||
clear_call_args(&p->call);
|
||||
p->in_call = 0;
|
||||
}
|
||||
bump_pending(p, 0);
|
||||
return CTL_OK;
|
||||
}
|
||||
Reference in New Issue
Block a user