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:
@@ -0,0 +1,156 @@
|
||||
#include "ctl.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
int n_open;
|
||||
int n_arg;
|
||||
int n_ready;
|
||||
int n_text;
|
||||
int n_error;
|
||||
char last_name[CTL_MAX_NAME];
|
||||
char last_id[CTL_MAX_ID];
|
||||
char last_path[512];
|
||||
size_t text_bytes;
|
||||
} sink_t;
|
||||
|
||||
static void on_ev(const ctl_event_t *ev, void *ud) {
|
||||
sink_t *s = ud;
|
||||
switch (ev->type) {
|
||||
case CTL_EV_CALL_OPEN:
|
||||
s->n_open++;
|
||||
snprintf(s->last_id, sizeof(s->last_id), "%s", ev->call->id);
|
||||
snprintf(s->last_name, sizeof(s->last_name), "%s", ev->call->name);
|
||||
break;
|
||||
case CTL_EV_ARG:
|
||||
s->n_arg++;
|
||||
if (ev->call->nargs > 0) {
|
||||
const ctl_arg_t *a = &ev->call->args[ev->call->nargs - 1];
|
||||
if (strcmp(a->key, "path") == 0 && a->value)
|
||||
snprintf(s->last_path, sizeof(s->last_path), "%s", a->value);
|
||||
}
|
||||
break;
|
||||
case CTL_EV_CALL_READY:
|
||||
s->n_ready++;
|
||||
break;
|
||||
case CTL_EV_TEXT:
|
||||
s->n_text++;
|
||||
s->text_bytes += ev->text_len;
|
||||
break;
|
||||
case CTL_EV_ERROR:
|
||||
s->n_error++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static int expect(int cond, const char *msg) {
|
||||
if (!cond) {
|
||||
fprintf(stderr, "FAIL: %s\n", msg);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int test_chunked_confirm(void) {
|
||||
sink_t s = {0};
|
||||
ctl_parser_t *p = ctl_parser_create(on_ev, &s);
|
||||
const char *msg =
|
||||
"I'll read the file.\n"
|
||||
"call t1 read_file\n"
|
||||
"arg path 10\n"
|
||||
"README.mdx"
|
||||
"end\n";
|
||||
/* feed 1 byte at a time */
|
||||
for (size_t i = 0; msg[i]; i++) {
|
||||
if (ctl_parser_feed(p, &msg[i], 1) != CTL_OK) {
|
||||
fprintf(stderr, "feed failed at %zu\n", i);
|
||||
ctl_parser_destroy(p);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
int ok = 1;
|
||||
ok &= expect(ctl_parser_finish(p) == CTL_OK, "finish ok");
|
||||
ok &= expect(s.n_ready == 1, "one ready");
|
||||
ok &= expect(s.n_open == 1, "one open");
|
||||
ok &= expect(s.n_arg == 1, "one arg");
|
||||
ok &= expect(strcmp(s.last_name, "read_file") == 0, "name");
|
||||
ok &= expect(strcmp(s.last_path, "README.mdx") == 0, "path");
|
||||
ok &= expect(s.n_error == 0, "no error");
|
||||
ok &= expect(ctl_parser_pending(p) == 0, "pending 0");
|
||||
/* pending peak should be bounded by line/arg */
|
||||
ok &= expect(ctl_parser_pending_peak(p) <= CTL_MAX_LINE, "peak bounded");
|
||||
ctl_parser_destroy(p);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int test_truncate_no_execute(void) {
|
||||
sink_t s = {0};
|
||||
ctl_parser_t *p = ctl_parser_create(on_ev, &s);
|
||||
const char *msg =
|
||||
"call t1 read_file\n"
|
||||
"arg path 20\n"
|
||||
"partial"; /* incomplete: need 20 bytes, then end */
|
||||
ctl_parser_feed(p, msg, strlen(msg));
|
||||
int ok = 1;
|
||||
ok &= expect(s.n_ready == 0, "no ready before EOF");
|
||||
ok &= expect(s.n_open == 1, "open confirmed");
|
||||
ok &= expect(ctl_parser_finish(p) == CTL_ERR_CONTRACT, "finish error");
|
||||
ok &= expect(s.n_error >= 1, "error emitted");
|
||||
ok &= expect(s.n_ready == 0, "still no ready");
|
||||
ctl_parser_destroy(p);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int test_monotonic_no_retract(void) {
|
||||
/* Once CALL_READY fires, further feed of garbage should error, not retract */
|
||||
sink_t s = {0};
|
||||
ctl_parser_t *p = ctl_parser_create(on_ev, &s);
|
||||
const char *okmsg =
|
||||
"call a1 run_shell\n"
|
||||
"arg cmd 5\n"
|
||||
"ls -l"
|
||||
"end\n";
|
||||
ctl_parser_feed(p, okmsg, strlen(okmsg));
|
||||
int ready = s.n_ready;
|
||||
int ok = expect(ready == 1, "ready once");
|
||||
/* text after is fine */
|
||||
ctl_parser_feed(p, "done.\n", 6);
|
||||
ok &= expect(s.n_ready == 1, "ready unchanged");
|
||||
ctl_parser_finish(p);
|
||||
ctl_parser_destroy(p);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int test_text_before_call(void) {
|
||||
sink_t s = {0};
|
||||
ctl_parser_t *p = ctl_parser_create(on_ev, &s);
|
||||
const char *msg = "Hello\nWorld\n";
|
||||
ctl_parser_feed(p, msg, strlen(msg));
|
||||
ctl_parser_finish(p);
|
||||
int ok = expect(s.n_text == 2, "two text lines");
|
||||
ok &= expect(s.n_ready == 0, "no calls");
|
||||
ctl_parser_destroy(p);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
int nfail = 0;
|
||||
if (!test_chunked_confirm())
|
||||
nfail++;
|
||||
if (!test_truncate_no_execute())
|
||||
nfail++;
|
||||
if (!test_monotonic_no_retract())
|
||||
nfail++;
|
||||
if (!test_text_before_call())
|
||||
nfail++;
|
||||
if (nfail) {
|
||||
fprintf(stderr, "%d test(s) failed\n", nfail);
|
||||
return 1;
|
||||
}
|
||||
printf("test_ctl_frontier: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#include "cst_agent.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int expect(int cond, const char *msg) {
|
||||
if (!cond) {
|
||||
fprintf(stderr, "FAIL: %s\n", msg);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int test_complete_json(void) {
|
||||
const char *j =
|
||||
"Here is a call {\"name\":\"read_file\",\"arguments\":{\"path\":\"a.c\"}} "
|
||||
"done";
|
||||
cst_json_tool_t t;
|
||||
int ok = cst_json_try_parse_tool(j, strlen(j), &t);
|
||||
int pass = expect(ok == 1, "parse ok");
|
||||
pass &= expect(t.ok == 1, "ok flag");
|
||||
pass &= expect(strcmp(t.name, "read_file") == 0, "name");
|
||||
pass &= expect(strstr(t.arguments_json, "path") != NULL, "args");
|
||||
return pass;
|
||||
}
|
||||
|
||||
static int test_truncated_json(void) {
|
||||
const char *j = "{\"name\":\"read_file\",\"arguments\":{\"path\":\"a";
|
||||
cst_json_tool_t t;
|
||||
int ok = cst_json_try_parse_tool(j, strlen(j), &t);
|
||||
return expect(ok == 0, "truncated must fail");
|
||||
}
|
||||
|
||||
static int test_unclosed_brace(void) {
|
||||
const char *j = "{\"name\":\"run_shell\",\"arguments\":{\"cmd\":\"ls\"";
|
||||
cst_json_tool_t t;
|
||||
int ok = cst_json_try_parse_tool(j, strlen(j), &t);
|
||||
return expect(ok == 0, "unclosed brace fails");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
int nfail = 0;
|
||||
if (!test_complete_json())
|
||||
nfail++;
|
||||
if (!test_truncated_json())
|
||||
nfail++;
|
||||
if (!test_unclosed_brace())
|
||||
nfail++;
|
||||
if (nfail) {
|
||||
fprintf(stderr, "%d failed\n", nfail);
|
||||
return 1;
|
||||
}
|
||||
printf("test_json_baseline: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "mm_tool.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
int n_ready;
|
||||
int n_text;
|
||||
char name[64];
|
||||
char path[256];
|
||||
} sink_t;
|
||||
|
||||
static void on_ev(const ctl_event_t *ev, void *ud) {
|
||||
sink_t *s = ud;
|
||||
if (ev->type == CTL_EV_TEXT)
|
||||
s->n_text++;
|
||||
if (ev->type == CTL_EV_CALL_READY && ev->call) {
|
||||
s->n_ready++;
|
||||
snprintf(s->name, sizeof(s->name), "%s", ev->call->name);
|
||||
if (ev->call->nargs > 0 && ev->call->args[0].value)
|
||||
snprintf(s->path, sizeof(s->path), "%s", ev->call->args[0].value);
|
||||
}
|
||||
}
|
||||
|
||||
static int expect(int c, const char *m) {
|
||||
if (!c) {
|
||||
fprintf(stderr, "FAIL: %s\n", m);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
sink_t s = {0};
|
||||
mm_scan_t *mm = mm_scan_create(on_ev, &s);
|
||||
const char *msg =
|
||||
"我来读一下。\n"
|
||||
"<minimax:tool_call>\n"
|
||||
"invoke read_file\n"
|
||||
"<arg name=\"path\">./README.md</arg>\n"
|
||||
"</minimax:tool_call>\n"
|
||||
"读完了。\n";
|
||||
/* byte-at-a-time */
|
||||
for (size_t i = 0; msg[i]; i++)
|
||||
mm_scan_feed(mm, &msg[i], 1);
|
||||
mm_scan_finish(mm);
|
||||
int ok = 1;
|
||||
ok &= expect(s.n_ready == 1, "one ready");
|
||||
ok &= expect(strcmp(s.name, "read_file") == 0, "name");
|
||||
ok &= expect(strcmp(s.path, "./README.md") == 0, "path");
|
||||
ok &= expect(s.n_text >= 1, "text emitted");
|
||||
/* truncated: no execute */
|
||||
sink_t s2 = {0};
|
||||
mm_scan_t *mm2 = mm_scan_create(on_ev, &s2);
|
||||
const char *trunc = "<minimax:tool_call>\ninvoke read_file\n";
|
||||
mm_scan_feed(mm2, trunc, strlen(trunc));
|
||||
mm_scan_finish(mm2);
|
||||
ok &= expect(s2.n_ready == 0, "trunc no ready");
|
||||
mm_scan_destroy(mm);
|
||||
mm_scan_destroy(mm2);
|
||||
if (!ok)
|
||||
return 1;
|
||||
printf("test_mm_tool: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "ctl.h"
|
||||
#include "cst_agent.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* Starvation replay: same logical tool call, cut at several prefixes.
|
||||
* CTL+CST should never execute on incomplete prefixes; JSON-buffer fails to
|
||||
* parse (cannot execute). When the full stream is present, both succeed.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
int n_ready;
|
||||
size_t peak;
|
||||
} ctl_stats_t;
|
||||
|
||||
static void on_ev(const ctl_event_t *ev, void *ud) {
|
||||
ctl_stats_t *s = ud;
|
||||
if (ev->type == CTL_EV_CALL_READY)
|
||||
s->n_ready++;
|
||||
}
|
||||
|
||||
static int ctl_ready_on_prefix(const char *full, size_t ncut, size_t *peak) {
|
||||
ctl_stats_t st = {0};
|
||||
ctl_parser_t *p = ctl_parser_create(on_ev, &st);
|
||||
ctl_parser_feed(p, full, ncut);
|
||||
/* do not finish — simulate stream stall / truncation mid-turn */
|
||||
*peak = ctl_parser_pending_peak(p);
|
||||
int ready = st.n_ready;
|
||||
ctl_parser_destroy(p);
|
||||
return ready;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const char *ctl =
|
||||
"call x1 read_file\n"
|
||||
"arg path 5\n"
|
||||
"a.txt"
|
||||
"end\n";
|
||||
const char *json =
|
||||
"{\"name\":\"read_file\",\"arguments\":{\"path\":\"a.txt\"}}";
|
||||
|
||||
size_t ctl_len = strlen(ctl);
|
||||
size_t json_len = strlen(json);
|
||||
|
||||
int ctl_premature = 0, json_ok_full = 0, ctl_ok_full = 0;
|
||||
int json_fail_trunc = 0, ctl_safe_trunc = 0;
|
||||
|
||||
/* full streams */
|
||||
{
|
||||
size_t peak = 0;
|
||||
ctl_stats_t st = {0};
|
||||
ctl_parser_t *p = ctl_parser_create(on_ev, &st);
|
||||
ctl_parser_feed(p, ctl, ctl_len);
|
||||
ctl_parser_finish(p);
|
||||
peak = ctl_parser_pending_peak(p);
|
||||
ctl_ok_full = (st.n_ready == 1);
|
||||
printf("CTL full: ready=%d peak=%zu\n", st.n_ready, peak);
|
||||
ctl_parser_destroy(p);
|
||||
}
|
||||
{
|
||||
cst_json_tool_t t;
|
||||
json_ok_full = cst_json_try_parse_tool(json, json_len, &t);
|
||||
printf("JSON full: ok=%d name=%s\n", json_ok_full, t.name);
|
||||
}
|
||||
|
||||
/* truncate at 50% and 80% */
|
||||
size_t cuts[] = {ctl_len / 2, (ctl_len * 4) / 5, ctl_len - 2};
|
||||
for (size_t i = 0; i < sizeof(cuts) / sizeof(cuts[0]); i++) {
|
||||
size_t peak = 0;
|
||||
int ready = ctl_ready_on_prefix(ctl, cuts[i], &peak);
|
||||
printf("CTL cut %zu/%zu: ready=%d peak=%zu\n", cuts[i], ctl_len, ready,
|
||||
peak);
|
||||
if (ready)
|
||||
ctl_premature++;
|
||||
else
|
||||
ctl_safe_trunc++;
|
||||
}
|
||||
|
||||
size_t jcuts[] = {json_len / 2, (json_len * 4) / 5, json_len - 2};
|
||||
for (size_t i = 0; i < sizeof(jcuts) / sizeof(jcuts[0]); i++) {
|
||||
cst_json_tool_t t;
|
||||
int ok = cst_json_try_parse_tool(json, jcuts[i], &t);
|
||||
printf("JSON cut %zu/%zu: ok=%d\n", jcuts[i], json_len, ok);
|
||||
if (!ok)
|
||||
json_fail_trunc++;
|
||||
}
|
||||
|
||||
printf("\n=== summary ===\n");
|
||||
printf("CTL full success: %s\n", ctl_ok_full ? "yes" : "no");
|
||||
printf("JSON full success: %s\n", json_ok_full ? "yes" : "no");
|
||||
printf("CTL premature executes on trunc: %d (want 0)\n", ctl_premature);
|
||||
printf("CTL safe truncations: %d\n", ctl_safe_trunc);
|
||||
printf("JSON trunc failures: %d\n", json_fail_trunc);
|
||||
|
||||
if (!ctl_ok_full || !json_ok_full || ctl_premature != 0)
|
||||
return 1;
|
||||
printf("replay_starvation: OK\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user