#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #include "cJSON.h" #include "cst_agent.h" #include "cst_metrics.h" #include "cst_perms.h" #include "cst_tools.h" #include "cst_undo.h" #include "cst_util.h" #include "mm_tool.h" #include "os_compat.h" #include #include #include #include #include /* --- protocols ----------------------------------------------------------- */ static const char *const PROTO_NAMES[] = {"native-eos", "native-inc", "ctl", "ctl-heredoc", "xml"}; const char *cst_protocol_name(cst_protocol_t p) { if ((int)p < 0 || (size_t)p >= sizeof(PROTO_NAMES) / sizeof(PROTO_NAMES[0])) return "native-eos"; return PROTO_NAMES[p]; } int cst_protocol_parse(const char *s, cst_protocol_t *out) { if (!s || !out) return -1; if (strcmp(s, "native") == 0) { *out = CST_PROTO_NATIVE_EOS; return 0; } for (size_t i = 0; i < sizeof(PROTO_NAMES) / sizeof(PROTO_NAMES[0]); i++) { if (strcmp(s, PROTO_NAMES[i]) == 0) { *out = (cst_protocol_t)i; return 0; } } return -1; } int cst_protocol_is_native(cst_protocol_t p) { return p == CST_PROTO_NATIVE_EOS || p == CST_PROTO_NATIVE_INC; } int cst_protocol_is_ctl(cst_protocol_t p) { return p == CST_PROTO_CTL || p == CST_PROTO_CTL_HEREDOC; } int cst_protocol_is_text(cst_protocol_t p) { return !cst_protocol_is_native(p); } int cst_protocol_is_eager(cst_protocol_t p) { return p == CST_PROTO_NATIVE_INC || cst_protocol_is_ctl(p); } /* --- system prompts ------------------------------------------------------ */ #define PROMPT_BASE \ "You are CST-Agent, a coding assistant working inside a local workspace. " \ "All tool paths are relative to the workspace root and shell commands run " \ "there.\n" \ "Reply in the user's language. Be concise.\n\n" #define PROMPT_TOOLS \ "Tools:\n" \ "- read_file(path): read a text file.\n" \ "- run_shell(cmd): run a non-interactive shell command (stdin is empty; " \ "long-running commands are killed).\n" \ "- write_file(path, content): create or overwrite a whole file.\n" \ "- apply_patch(patch): unified diff with ---/+++ headers and " \ "@@ -a,b +c,d @@ hunks (keep 3 context lines). Prefer it for small edits " \ "to existing files.\n\n" \ "Workflow:\n" \ "1) Inspect only what you need (read_file; ls/grep via run_shell).\n" \ "2) Make the change.\n" \ "3) Verify with the project's build/tests via run_shell; fix and re-run " \ "until they pass.\n" \ "4) Finish with a short plain-text summary of what changed.\n" \ "Never invent other tool names.\n" #define PROMPT_RESULTS \ "- After your last frame, STOP writing and wait. Results arrive in the " \ "next user message as:\n" \ " result exit= \n" \ " \n" \ " end\n" \ " Never write result frames yourself.\n" static const char SEC_NATIVE[] = "Call tools through the native function-calling interface.\n\n"; static const char SEC_CTL[] = "\nHow to call tools: write CTL frames as plain text in your reply. There " "is NO function-calling API here; do not use JSON or XML.\n\n" "call \n" "arg \n" "\n" "end\n\n" "Rules:\n" "- is a short unique token per call (c1, c2, ...). Every frame line " "starts at the beginning of a line.\n" "- nbytes counts UTF-8 BYTES of the raw value (ASCII = 1 byte, most CJK " "characters = 3 bytes), including every newline inside it. The value is " "copied verbatim, with no escaping. One newline after the value, before " "`end`, is optional and not counted.\n" "- Short single-line values may be inline instead: `arg path src/main.c` " "(the value is the rest of the line; it must not be only digits).\n" "- You may write several frames in one reply.\n" PROMPT_RESULTS "\nExample (the content value is \"hello world\\n\" = 12 bytes):\n" "call c1 read_file\n" "arg path README.md\n" "end\n" "call c2 write_file\n" "arg path hello.txt\n" "arg content 12\n" "hello world\n" "end\n"; static const char SEC_CTL_HEREDOC[] = "\nHow to call tools: write CTL frames as plain text in your reply. There " "is NO function-calling API here; do not use JSON or XML.\n\n" "call \n" "arg <\n" "TAG\n" "end\n\n" "Rules:\n" "- is a short unique token per call (c1, c2, ...). Every frame line " "starts at the beginning of a line.\n" "- TAG is a word of letters/digits/underscore that never appears alone on " "a line inside the value (e.g. EOF_1). The value is the lines between the " "header and the TAG line, each ending with a newline. No escaping.\n" "- Short single-line values may be inline instead: `arg path src/main.c` " "(the value is the rest of the line; it must not be only digits).\n" "- You may write several frames in one reply.\n" PROMPT_RESULTS "\nExample:\n" "call c1 read_file\n" "arg path README.md\n" "end\n" "call c2 write_file\n" "arg path hello.txt\n" "arg content <\n" "\n" "README.md\n" "\n" "\n\n" "- Parameter values are verbatim text (no escaping).\n" "- You may write several blocks in one reply.\n" PROMPT_RESULTS; static const char SEC_CTL_NUMBERS[] = "- A purely numeric inline value must be quoted (`arg limit \"50\"`); " "bare digits are read as a byte count.\n"; static int tool_on(const cst_agent_config_t *cfg, const char *name) { return cst_tool_active(cfg, cst_tool_find(name)); } /* Workflow section for the default tool profile; names only active tools. */ static void put_workflow(cst_buf_t *b, const cst_agent_config_t *cfg) { cst_buf_puts(b, "\nWorkflow:\n1) Inspect only what you need"); const char *explore[] = {"list_dir", "glob", "grep"}; int n = 0; for (int i = 0; i < 3; i++) { if (tool_on(cfg, explore[i])) cst_buf_printf(b, "%s%s", n++ ? ", " : " (", explore[i]); } if (n) cst_buf_puts(b, ")"); if (tool_on(cfg, "read_file")) cst_buf_puts(b, "; read big files in slices with read_file offset/limit"); cst_buf_puts(b, ".\n2) Make the change"); n = 0; if (tool_on(cfg, "edit_file")) { cst_buf_puts(b, ": edit_file for small exact replacements"); n++; } if (tool_on(cfg, "apply_patch")) cst_buf_printf(b, "%sapply_patch for multi-hunk or multi-file diffs", n++ ? ", " : ": "); if (tool_on(cfg, "write_file")) cst_buf_printf(b, "%swrite_file for new files or full rewrites", n++ ? ", " : ": "); cst_buf_puts(b, ".\n"); if (tool_on(cfg, "run_shell")) cst_buf_puts(b, "3) Verify with the project's build/tests via run_shell; " "fix and re-run until they pass.\n"); if (tool_on(cfg, "todo_write")) cst_buf_puts(b, "- For work with several steps keep a todo list with " "todo_write and update it as you go.\n"); if (tool_on(cfg, "ask_user")) cst_buf_puts(b, "- Use ask_user only when a decision is truly the user's; " "otherwise choose sensibly and say so.\n"); cst_buf_puts(b, "Finish with a short plain-text summary of what changed.\n" "Never invent other tool names.\n"); } #define PROJECT_NOTES_MAX (16 * 1024) char *cst_agent_build_prompt(const cst_agent_config_t *cfg, cst_protocol_t p, const char *project_notes) { int minimal = !cfg || cfg->tool_profile == CST_TOOLS_MINIMAL; cst_buf_t b = {0}; cst_buf_puts(&b, PROMPT_BASE); if (cst_protocol_is_native(p)) cst_buf_puts(&b, SEC_NATIVE); if (minimal) { cst_buf_puts(&b, PROMPT_TOOLS); } else { char *tools = cst_tools_prompt(cfg); cst_buf_puts(&b, tools ? tools : ""); free(tools); put_workflow(&b, cfg); } switch (p) { case CST_PROTO_CTL: cst_buf_puts(&b, SEC_CTL); break; case CST_PROTO_CTL_HEREDOC: cst_buf_puts(&b, SEC_CTL_HEREDOC); break; case CST_PROTO_XML: cst_buf_puts(&b, SEC_XML); break; default: break; } if (!minimal && cst_protocol_is_ctl(p)) cst_buf_puts(&b, SEC_CTL_NUMBERS); if (project_notes && project_notes[0]) { size_t n = strlen(project_notes); if (n > PROJECT_NOTES_MAX) n = cst_utf8_floor(project_notes, PROJECT_NOTES_MAX); cst_buf_puts(&b, "\nProject instructions (from the workspace's " "AGENTS.md; follow them):\n"); cst_buf_append(&b, project_notes, n); cst_buf_puts(&b, "\n"); } return cst_buf_steal(&b); } /* Legacy prompts: minimal tool set, no project notes. */ const char *cst_agent_system_prompt_for(cst_protocol_t p) { static char *cache[5]; int i = (int)p < 0 || (int)p > 4 ? 0 : (int)p; if (!cache[i]) cache[i] = cst_agent_build_prompt(NULL, (cst_protocol_t)i, NULL); return cache[i] ? cache[i] : PROMPT_BASE; } const char *cst_agent_system_prompt(void) { return cst_agent_system_prompt_for(CST_PROTO_NATIVE_EOS); } /* Request-only nudges (never persisted). */ static const char NUDGE_EMPTY[] = "[system] Your previous reply was empty. If the task is not finished, " "continue with tool calls; otherwise give the final answer now in plain " "text, in the user's language."; static const char NUDGE_FINAL[] = "[system] The tool budget for this turn is exhausted. Do NOT call any " "tools. Answer now in plain text, in the user's language: summarize what " "was done and what remains."; /* Session keeps tool output up to this size; the wire cap is separate. */ #define TOOL_OUT_CAP (48 * 1024) #define READ_FP_MAX 256 #define MAX_CTL_RETRIES 3 /* --- agent --------------------------------------------------------------- */ typedef struct { uint64_t hash; unsigned epoch; } read_fp_t; struct cst_agent { cst_agent_config_t cfg; cst_session_t *session; cst_metrics_t *metrics; volatile int cancel; unsigned write_epoch; read_fp_t reads[READ_FP_MAX]; int nreads; int turn_no; char *notes; /* AGENTS.md text, or NULL */ char *todo; /* latest todo_write list, or NULL */ char undo_tag[64]; long last_prompt_tokens; cst_perms_t *perms; }; typedef enum { PC_OK = 0, PC_DENIED, PC_DUP, PC_UNKNOWN, PC_INVALID } pc_status_t; typedef struct { ctl_tool_call_t call; /* owns arg values */ char *raw_args; /* native: arguments string as streamed */ int invalid; int done; int exit_code; pc_status_t status; char *output; size_t output_len; const char *origin; /* native / ctl / xml */ double t_open, t_ready, t_exec_start, t_exec_end; } pcall_t; typedef struct { cst_agent_t *a; const cst_agent_hooks_t *hooks; cst_protocol_t proto; int step; int final_pass; double t0; cst_buf_t text; /* confirmed answer text (no tool syntax) */ cst_buf_t raw; /* visible stream with tool syntax, think stripped */ size_t raw_mark; int in_think; char tagbuf[16]; size_t taglen; ctl_parser_t *ctl; mm_scan_t *mm; pcall_t *calls; int ncalls; int cap; int n_native; int saw_ready; volatile int stop; int parse_error; char perr[256]; double native_open_t[64]; double ctl_open_t; } turn_t; static double rel_ms(const turn_t *t) { return os_now_ms() - t->t0; } static void hook_status(const turn_t *t, const char *msg) { if (t->hooks && t->hooks->on_status) t->hooks->on_status(msg, t->hooks->userdata); } static double ms2(double v) { return v < 0 ? v : (double)(long long)(v * 100.0 + 0.5) / 100.0; } static void hook_tool(const turn_t *t, const char *phase, const ctl_tool_call_t *c, const char *detail) { if (t->hooks && t->hooks->on_tool_event) t->hooks->on_tool_event(phase, c, detail, t->hooks->userdata); } static int text_is_meaningful(const char *s, size_t n) { size_t visible = 0; for (size_t i = 0; i < n; i++) { if ((unsigned char)s[i] > 0x20) visible++; } return visible >= 2; } static const char *trim_left(const char *s) { while (*s == ' ' || *s == '\n' || *s == '\r' || *s == '\t') s++; return s; } static void copy_call(ctl_tool_call_t *dst, const ctl_tool_call_t *src) { memset(dst, 0, sizeof(*dst)); memcpy(dst->id, src->id, sizeof(dst->id)); memcpy(dst->name, src->name, sizeof(dst->name)); for (int i = 0; i < src->nargs; i++) { ctl_arg_t *d = &dst->args[dst->nargs]; memcpy(d->key, src->args[i].key, sizeof(d->key)); d->value_len = src->args[i].value_len; d->value = malloc(d->value_len + 1); if (!d->value) continue; if (d->value_len) memcpy(d->value, src->args[i].value, d->value_len); d->value[d->value_len] = 0; dst->nargs++; } } static pcall_t *push_call(turn_t *t) { if (t->ncalls == t->cap) { int nc = t->cap ? t->cap * 2 : 4; pcall_t *nv = realloc(t->calls, (size_t)nc * sizeof(pcall_t)); if (!nv) return NULL; t->calls = nv; t->cap = nc; } pcall_t *pc = &t->calls[t->ncalls++]; memset(pc, 0, sizeof(*pc)); pc->t_exec_start = pc->t_exec_end = -1; return pc; } static void turn_free(turn_t *t) { for (int i = 0; i < t->ncalls; i++) { cst_tool_call_clear(&t->calls[i].call); free(t->calls[i].raw_args); free(t->calls[i].output); } free(t->calls); t->calls = NULL; t->ncalls = t->cap = 0; cst_buf_free(&t->text); cst_buf_free(&t->raw); if (t->mm) mm_scan_destroy(t->mm); if (t->ctl) ctl_parser_destroy(t->ctl); t->mm = NULL; t->ctl = NULL; } static uint64_t call_hash(const ctl_tool_call_t *c) { uint64_t h = cst_fnv1a64(CST_FNV_INIT, c->name, strlen(c->name)); for (int i = 0; i < c->nargs; i++) { h = cst_fnv1a64(h, "\x1f", 1); h = cst_fnv1a64(h, c->args[i].key, strlen(c->args[i].key)); h = cst_fnv1a64(h, "=", 1); if (c->args[i].value) h = cst_fnv1a64(h, c->args[i].value, c->args[i].value_len); } return h; } static void args_detail(const ctl_tool_call_t *c, char *out, size_t n) { size_t used = 0; out[0] = 0; for (int i = 0; i < c->nargs && used + 8 < n; i++) { const char *v = c->args[i].value ? c->args[i].value : ""; int k = snprintf(out + used, n - used, "%s%s=%.*s", i ? " " : "", c->args[i].key, (int)(c->args[i].value_len > 160 ? 160 : c->args[i].value_len), v); if (k < 0) break; used += (size_t)k; if (used >= n) used = n - 1; } } static void set_output(pcall_t *pc, const char *s, size_t n, int code) { free(pc->output); size_t keep = n; int trunc = 0; if (keep > TOOL_OUT_CAP) { keep = cst_utf8_floor(s, TOOL_OUT_CAP); trunc = 1; } cst_buf_t b = {0}; cst_buf_append(&b, s ? s : "", keep); if (trunc) cst_buf_printf(&b, "\n...(truncated, %zu bytes total)", n); pc->output_len = b.len; pc->output = cst_buf_steal(&b); pc->exit_code = code; } static void emit_call_metrics(turn_t *t, const pcall_t *pc) { cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "type", "call"); cJSON_AddNumberToObject(o, "turn", t->a->turn_no); cJSON_AddNumberToObject(o, "step", t->step); cJSON_AddStringToObject(o, "protocol", cst_protocol_name(t->proto)); cJSON_AddStringToObject(o, "origin", pc->origin ? pc->origin : ""); cJSON_AddStringToObject(o, "id", pc->call.id); cJSON_AddStringToObject(o, "name", pc->call.name); cJSON_AddNumberToObject(o, "t_call_open", ms2(pc->t_open)); cJSON_AddNumberToObject(o, "t_call_ready", ms2(pc->t_ready)); cJSON_AddNumberToObject(o, "t_exec_start", ms2(pc->t_exec_start)); cJSON_AddNumberToObject(o, "t_exec_end", ms2(pc->t_exec_end)); cJSON_AddNumberToObject(o, "exit", pc->exit_code); cJSON_AddNumberToObject(o, "bytes_out", (double)pc->output_len); static const char *const st[] = {"ok", "denied", "dup", "unknown", "invalid_args"}; cJSON_AddStringToObject(o, "status", st[pc->status]); cst_metrics_emit(t->a->metrics, o); } static void undo_snapshot(cst_agent_t *a, const ctl_tool_call_t *c) { char paths[CST_TOOL_MAX_TARGETS][1024]; int n = cst_tool_targets(c, paths, CST_TOOL_MAX_TARGETS); if (n <= 0) return; char sid[256]; cst_agent_session_id(a, sid, sizeof(sid)); for (int i = 0; i < n; i++) cst_undo_snapshot(a->cfg.workspace_root, sid, a->undo_tag, paths[i]); } static void agent_set_todo(const char *text, void *ud) { turn_t *t = ud; free(t->a->todo); t->a->todo = text ? strdup(text) : NULL; if (t->hooks && t->hooks->on_todo) t->hooks->on_todo(text, t->hooks->userdata); } static int agent_ask(const char *q, const char *opts, char *ans, size_t n, void *ud) { turn_t *t = ud; return t->hooks->on_ask_user(q, opts, ans, n, t->hooks->userdata); } static const char *tool_cmd(const ctl_tool_call_t *c) { for (int i = 0; i < c->nargs; i++) { if (strcmp(c->args[i].key, "cmd") == 0 || strcmp(c->args[i].key, "command") == 0) return c->args[i].value ? c->args[i].value : ""; } return NULL; } static int needs_confirm(const cst_agent_config_t *cfg, cst_tool_perm_t p) { switch (p) { case CST_TP_SHELL: return cfg->confirm_shell; case CST_TP_WRITE: return cfg->confirm_write; case CST_TP_NET: return cfg->confirm_net; default: return 0; } } /* Execute (or refuse) one confirmed call. Irreversible once started. */ static void exec_call(turn_t *t, pcall_t *pc) { if (pc->done) return; pc->done = 1; cst_agent_t *a = t->a; ctl_tool_call_t *c = &pc->call; pc->t_exec_start = rel_ms(t); char detail[512]; args_detail(c, detail, sizeof(detail)); const cst_tool_def_t *def = cst_tool_find(c->name); cst_tool_ctx_t ctx; memset(&ctx, 0, sizeof(ctx)); ctx.workspace_root = a->cfg.workspace_root; ctx.set_todo = agent_set_todo; ctx.todo_ud = t; if (t->hooks && t->hooks->on_ask_user) { ctx.ask = agent_ask; ctx.ask_ud = t; } if (a->cancel) { const char *msg = "error: cancelled by the user before it ran"; set_output(pc, msg, strlen(msg), 130); pc->status = PC_DENIED; } else if (!def || !cst_tool_active(&a->cfg, def)) { char *names = cst_tools_names(&a->cfg); char msg[768]; snprintf(msg, sizeof(msg), "error: unknown tool \"%.64s\" (allowed: %s)", c->name, names ? names : ""); free(names); set_output(pc, msg, strlen(msg), 1); pc->status = PC_UNKNOWN; hook_status(t, "unknown tool"); } else if (pc->invalid) { const char *msg = "error: tool arguments are not a valid JSON object (or " "exceed 1 MiB); nothing was executed"; set_output(pc, msg, strlen(msg), 1); pc->status = PC_INVALID; } else { int mutating = def->perm == CST_TP_SHELL || def->perm == CST_TP_WRITE; uint64_t h = call_hash(c); int dup = 0; if (def->perm == CST_TP_READ) { for (int i = 0; i < a->nreads; i++) { if (a->reads[i].hash == h && a->reads[i].epoch == a->write_epoch) { dup = 1; break; } } } int allow = 1; const char *cmd = def->perm == CST_TP_SHELL ? tool_cmd(c) : NULL; if (!dup && needs_confirm(&a->cfg, def->perm) && !a->cfg.auto_approve && !cst_perms_tool_allowed(a->perms, c->name) && !(cmd && cst_perms_shell_allowed(a->perms, cmd))) { if (t->hooks && t->hooks->on_permission) { char *preview = NULL; if (def->preview) cst_tool_preview(&ctx, c, &preview); else if (cmd) preview = strdup(cmd); int ans = t->hooks->on_permission( c->name, detail[0] ? detail : c->name, preview, t->hooks->userdata); free(preview); allow = ans != CST_PERM_DENY; if (ans == CST_PERM_ALWAYS_TOOL) { cst_perms_allow_tool(a->perms, c->name); } else if (ans == CST_PERM_ALWAYS_PREFIX && cmd) { char pre[128]; cst_perms_suggest_prefix(cmd, pre, sizeof(pre)); if (pre[0]) cst_perms_allow_prefix(a->perms, pre); } } else { allow = 0; } } if (dup) { const char *msg = strcmp(c->name, "read_file") == 0 ? "note: this exact read_file already ran earlier in this turn " "and no file changed since; reuse that output." : "note: this exact call already ran earlier in this turn and " "no file changed since; reuse that output."; set_output(pc, msg, strlen(msg), 0); pc->status = PC_DUP; hook_status(t, "skip duplicate read"); } else if (!allow) { set_output(pc, "denied by user", 14, 1); pc->status = PC_DENIED; hook_tool(t, "denied", c, detail); } else { hook_tool(t, "run", c, NULL); if (def->perm == CST_TP_WRITE) undo_snapshot(a, c); cst_tool_result_t tr = {0}; cst_tool_run(&ctx, c, &tr); set_output(pc, tr.out ? tr.out : "", tr.out_len, tr.exit_code); cst_tool_result_free(&tr); if (mutating) a->write_epoch++; else if (pc->exit_code == 0 && def->perm == CST_TP_READ) { int slot = a->nreads < READ_FP_MAX ? a->nreads++ : (int)(h % READ_FP_MAX); a->reads[slot].hash = h; a->reads[slot].epoch = a->write_epoch; } } } pc->t_exec_end = rel_ms(t); if (t->hooks && t->hooks->on_tool_result) t->hooks->on_tool_result(c, pc->output, pc->output_len, pc->exit_code, t->hooks->userdata); if (pc->status != PC_DENIED) { char d[96]; snprintf(d, sizeof(d), "%sexit=%d bytes=%zu", pc->status == PC_DUP ? "skipped duplicate · " : "", pc->exit_code, pc->output_len); hook_tool(t, "done", c, d); } emit_call_metrics(t, pc); } /* --- stream routing ------------------------------------------------------ */ static void on_text(turn_t *t, const char *s, size_t n) { if (t->stop || !n) return; cst_buf_append(&t->text, s, n); if (t->hooks && t->hooks->on_assistant_delta) t->hooks->on_assistant_delta(s, n, t->hooks->userdata); } static int is_blank(const char *s, size_t n) { for (size_t i = 0; i < n; i++) { if (s[i] != ' ' && s[i] != '\t' && s[i] != '\r' && s[i] != '\n') return 0; } return 1; } static void add_text_call(turn_t *t, const ctl_tool_call_t *c, const char *origin) { if (t->stop) return; if (t->final_pass) { t->stop = 1; /* no tools allowed now: keep the text, drop the call */ return; } pcall_t *pc = push_call(t); if (!pc) return; copy_call(&pc->call, c); if (!pc->call.id[0]) snprintf(pc->call.id, sizeof(pc->call.id), "%c%d", origin[0], t->ncalls); pc->origin = origin; pc->t_open = cst_protocol_is_ctl(t->proto) ? t->ctl_open_t : rel_ms(t); pc->t_ready = rel_ms(t); hook_tool(t, "ready", &pc->call, NULL); t->saw_ready = 1; t->raw_mark = t->raw.len; if (cst_protocol_is_eager(t->proto)) exec_call(t, &t->calls[t->ncalls - 1]); } static void on_ctl(const ctl_event_t *ev, void *ud) { turn_t *t = ud; switch (ev->type) { case CTL_EV_TEXT: if (t->saw_ready && !is_blank(ev->text, ev->text_len)) { /* prose after a confirmed call: likely an invented result. Stop. */ t->stop = 1; return; } on_text(t, ev->text, ev->text_len); break; case CTL_EV_CALL_OPEN: t->ctl_open_t = rel_ms(t); if (!t->stop) hook_tool(t, "open", ev->call, NULL); break; case CTL_EV_ARG: if (!t->stop) hook_tool(t, "arg", ev->call, NULL); break; case CTL_EV_CALL_READY: add_text_call(t, ev->call, "ctl"); break; case CTL_EV_ERROR: t->parse_error = 1; snprintf(t->perr, sizeof(t->perr), "%s", ev->error ? ev->error : "ctl"); break; default: break; } } static void on_mm(const ctl_event_t *ev, void *ud) { turn_t *t = ud; switch (ev->type) { case CTL_EV_TEXT: on_text(t, ev->text, ev->text_len); break; case CTL_EV_CALL_OPEN: case CTL_EV_ARG: hook_tool(t, ev->type == CTL_EV_ARG ? "arg" : "open", ev->call, NULL); break; case CTL_EV_CALL_READY: /* native modes: XML is only a fallback when no native call came in */ if (cst_protocol_is_native(t->proto) && t->n_native > 0) return; add_text_call(t, ev->call, "xml"); break; case CTL_EV_ERROR: t->parse_error = 1; snprintf(t->perr, sizeof(t->perr), "%s", ev->error ? ev->error : "xml"); break; default: break; } } static void feed_visible(turn_t *t, const char *data, size_t len) { if (!len || t->stop) return; cst_buf_append(&t->raw, data, len); if (t->ctl) ctl_parser_feed(t->ctl, data, len); else if (t->mm) mm_scan_feed(t->mm, data, len); } static void think_out(turn_t *t, const char *s, size_t n) { if (t->hooks && t->hooks->on_think_delta && n) t->hooks->on_think_delta(s, n, t->hooks->userdata); } /* Strip ... (MiniMax inline / reasoning_content wrapper). */ static void on_delta(const char *data, size_t len, void *ud) { turn_t *t = ud; for (size_t i = 0; i < len; i++) { char c = data[i]; const char *want = t->in_think ? "" : ""; size_t wl = strlen(want); if (t->taglen == 0 && c != '<') { if (t->in_think) think_out(t, &c, 1); else feed_visible(t, &c, 1); continue; } t->tagbuf[t->taglen++] = c; if (t->taglen <= wl && strncmp(want, t->tagbuf, t->taglen) == 0) { if (t->taglen == wl) { t->in_think = !t->in_think; t->taglen = 0; } continue; } /* not a tag: flush all but a possible new '<' start */ size_t flush = t->taglen; int restart = 0; if (c == '<' && t->taglen > 1) { flush--; restart = 1; } if (t->in_think) think_out(t, t->tagbuf, flush); else feed_visible(t, t->tagbuf, flush); t->taglen = 0; if (restart) t->tagbuf[t->taglen++] = '<'; } } /* A native call starts: release prose still held by the splitter or * the XML scanner so it reaches the UI before the tool card. */ static void flush_held_text(turn_t *t) { if (t->taglen && !t->in_think) { feed_visible(t, t->tagbuf, t->taglen); t->taglen = 0; } if (t->mm) mm_scan_finish(t->mm); } static void on_native_open(int index, const char *id, const char *name, void *ud) { turn_t *t = ud; flush_held_text(t); if (index >= 0 && index < 64) t->native_open_t[index] = rel_ms(t); ctl_tool_call_t tmp; memset(&tmp, 0, sizeof(tmp)); snprintf(tmp.id, sizeof(tmp.id), "%s", id ? id : ""); snprintf(tmp.name, sizeof(tmp.name), "%s", name ? name : ""); hook_tool(t, "open", &tmp, NULL); } static void on_native_tool(const char *id, const char *name, const char *args_json, void *ud) { turn_t *t = ud; if (!name || t->final_pass) return; pcall_t *pc = push_call(t); if (!pc) return; int idx = t->n_native++; if (cst_tool_call_from_native(&pc->call, id, name, args_json) != 0) pc->invalid = 1; if (!pc->call.id[0]) snprintf(pc->call.id, sizeof(pc->call.id), "call_%d_%d", t->step, idx + 1); pc->raw_args = strdup(args_json && !pc->invalid ? args_json : "{}"); pc->origin = "native"; pc->t_open = idx < 64 && t->native_open_t[idx] > 0 ? t->native_open_t[idx] : rel_ms(t); pc->t_ready = rel_ms(t); hook_tool(t, "ready", &pc->call, NULL); if (t->proto == CST_PROTO_NATIVE_INC) exec_call(t, &t->calls[t->ncalls - 1]); } /* --- request building ---------------------------------------------------- */ typedef struct { cst_message_t *msgs; int n; char **owned; int nowned; } req_msgs_t; static void req_msgs_free(req_msgs_t *r) { for (int i = 0; i < r->nowned; i++) free(r->owned[i]); free(r->owned); free(r->msgs); memset(r, 0, sizeof(*r)); } static char *own(req_msgs_t *r, char *s) { if (s) r->owned[r->nowned++] = s; return s; } static char *result_frame(const char *id, int exit_code, const char *body, size_t n, size_t cap) { size_t keep = n; cst_buf_t b = {0}; char note[96]; note[0] = 0; if (cap && n > cap) { keep = cst_utf8_floor(body, cap); snprintf(note, sizeof(note), "\n…(%zu more bytes not shown)", n - keep); } cst_buf_printf(&b, "result %s exit=%d %zu\n", id, exit_code, keep + strlen(note)); cst_buf_append(&b, body, keep); cst_buf_puts(&b, note); cst_buf_puts(&b, "\nend"); return cst_buf_steal(&b); } static void render_value(cst_buf_t *b, cst_protocol_t p, const char *key, const char *v) { size_t n = strlen(v); if (p == CST_PROTO_XML) { cst_buf_printf(b, "%s\n", key, v); } else if (p == CST_PROTO_CTL_HEREDOC) { cst_buf_printf(b, "arg %s <valuestring : "tool"; if (p == CST_PROTO_XML) cst_buf_printf(&b, "\n\n", name); else cst_buf_printf(&b, "call %s %s\n", cJSON_IsString(id) ? id->valuestring : "c", name); cJSON *args = cJSON_IsString(ar) ? cJSON_Parse(ar->valuestring) : NULL; const cJSON *it = NULL; cJSON_ArrayForEach(it, args) { if (cJSON_IsString(it)) { render_value(&b, p, it->string, it->valuestring); } else { char *v = cJSON_PrintUnformatted(it); render_value(&b, p, it->string, v ? v : ""); free(v); } } cJSON_Delete(args); cst_buf_puts(&b, p == CST_PROTO_XML ? "\n\n" : "end\n"); } cJSON_Delete(arr); return cst_buf_steal(&b); } static int ctx_window(void) { const char *e = getenv("CST_CTX_WINDOW"); if (e && atoi(e) > 0) return atoi(e); return CST_AGENT_CTX_WINDOW; } static int build_request_msgs(cst_agent_t *a, cst_protocol_t proto, const char *nudge, req_msgs_t *r) { memset(r, 0, sizeof(*r)); int n = cst_session_count(a->session); r->msgs = calloc((size_t)n + 2, sizeof(cst_message_t)); r->owned = calloc((size_t)n + 3, sizeof(char *)); if (!r->msgs || !r->owned) { req_msgs_free(r); return -1; } r->msgs[r->n].role = CST_ROLE_SYSTEM; r->msgs[r->n].content = own(r, cst_agent_build_prompt(&a->cfg, proto, a->notes)); r->n++; int nonsys = 0; for (int i = 0; i < n; i++) { if (cst_session_get(a->session, i)->role != CST_ROLE_SYSTEM) nonsys++; } int skip = nonsys > ctx_window() ? nonsys - ctx_window() : 0; int text_proto = cst_protocol_is_text(proto); int leading = 1; for (int i = 0; i < n; i++) { const cst_message_t *m = cst_session_get(a->session, i); if (m->role == CST_ROLE_SYSTEM) continue; if (skip > 0) { skip--; continue; } /* never start the window with orphan results */ if (leading && (m->role == CST_ROLE_TOOL || (m->role == CST_ROLE_USER && m->tool_call_id[0]))) continue; leading = 0; cst_message_t mm = *m; if (text_proto && m->role == CST_ROLE_ASSISTANT && m->tool_calls_json) { mm.content = own(r, render_calls_as_text(proto, m->content, m->tool_calls_json)); mm.tool_calls_json = NULL; } else if (text_proto && m->role == CST_ROLE_TOOL) { int code = 0; const char *ex = m->content ? strstr(m->content, " exit=") : NULL; if (ex) code = atoi(ex + 6); const char *body = m->content ? m->content : ""; const char *nl = strchr(body, '\n'); if (strncmp(body, "[tool ", 6) == 0 && nl) body = nl + 1; mm.role = CST_ROLE_USER; mm.content = own(r, result_frame(m->tool_call_id, code, body, strlen(body), a->cfg.tool_out_cap)); } r->msgs[r->n++] = mm; } if (nudge) { r->msgs[r->n].role = CST_ROLE_USER; r->msgs[r->n].content = (char *)nudge; r->n++; } return 0; } /* --- persistence --------------------------------------------------------- */ static char *meta_json(const char *kind, cst_protocol_t p) { cJSON *o = cJSON_CreateObject(); if (kind) cJSON_AddStringToObject(o, "kind", kind); cJSON_AddStringToObject(o, "protocol", cst_protocol_name(p)); char *s = cJSON_PrintUnformatted(o); cJSON_Delete(o); return s; } static void append_msg(cst_agent_t *a, cst_role_t role, const char *content, const char *tool_id, const char *tool_name, const char *tool_calls_json, char *meta) { cst_message_t m; memset(&m, 0, sizeof(m)); m.role = role; m.content = (char *)(content ? content : ""); if (tool_id) snprintf(m.tool_call_id, sizeof(m.tool_call_id), "%s", tool_id); if (tool_name) snprintf(m.tool_name, sizeof(m.tool_name), "%s", tool_name); m.tool_calls_json = (char *)tool_calls_json; m.meta_json = meta; cst_session_append(a->session, &m); free(meta); } static char *build_tool_calls_json(const turn_t *t) { cJSON *arr = cJSON_CreateArray(); for (int i = 0; i < t->ncalls; i++) { const pcall_t *pc = &t->calls[i]; cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "id", pc->call.id); cJSON_AddStringToObject(o, "type", "function"); cJSON *fn = cJSON_AddObjectToObject(o, "function"); cJSON_AddStringToObject(fn, "name", pc->call.name); if (pc->raw_args) { cJSON_AddStringToObject(fn, "arguments", pc->raw_args); } else { cJSON *args = cJSON_CreateObject(); for (int k = 0; k < pc->call.nargs; k++) cJSON_AddStringToObject(args, pc->call.args[k].key, pc->call.args[k].value ? pc->call.args[k].value : ""); char *s = cJSON_PrintUnformatted(args); cJSON_Delete(args); cJSON_AddStringToObject(fn, "arguments", s ? s : "{}"); free(s); } cJSON_AddItemToArray(arr, o); } char *s = cJSON_PrintUnformatted(arr); cJSON_Delete(arr); return s; } static void persist_calls(turn_t *t) { cst_agent_t *a = t->a; if (cst_protocol_is_text(t->proto)) { size_t n = t->stop ? t->raw_mark : t->raw.len; char *raw = malloc(n + 1); if (raw) { memcpy(raw, t->raw.buf ? t->raw.buf : "", n); raw[n] = 0; } append_msg(a, CST_ROLE_ASSISTANT, raw ? raw : "", NULL, NULL, NULL, meta_json(NULL, t->proto)); free(raw); for (int i = 0; i < t->ncalls; i++) { pcall_t *pc = &t->calls[i]; char *frame = result_frame(pc->call.id, pc->exit_code, pc->output ? pc->output : "", pc->output_len, a->cfg.tool_out_cap); append_msg(a, CST_ROLE_USER, frame, pc->call.id, pc->call.name, NULL, meta_json("tool_result", t->proto)); free(frame); } return; } char *tcj = build_tool_calls_json(t); append_msg(a, CST_ROLE_ASSISTANT, t->text.buf ? trim_left(t->text.buf) : "", NULL, NULL, tcj, meta_json(NULL, t->proto)); free(tcj); for (int i = 0; i < t->ncalls; i++) { pcall_t *pc = &t->calls[i]; cst_buf_t b = {0}; cst_buf_printf(&b, "[tool %s id=%s exit=%d]\n", pc->call.name, pc->call.id, pc->exit_code); cst_buf_append(&b, pc->output ? pc->output : "", pc->output_len); append_msg(a, CST_ROLE_TOOL, b.buf, pc->call.id, pc->call.name, NULL, meta_json(NULL, t->proto)); cst_buf_free(&b); } } /* --- agent lifecycle ----------------------------------------------------- */ void cst_agent_session_id(const cst_agent_t *a, char *out, size_t out_sz) { if (!out || !out_sz) return; out[0] = 0; if (!a) return; const char *p = a->cfg.session_path; const char *sl = strrchr(p, '/'); snprintf(out, out_sz, "%s", sl ? sl + 1 : p); size_t n = strlen(out); if (n > 6 && strcmp(out + n - 6, ".jsonl") == 0) out[n - 6] = 0; } const char *cst_agent_todo(const cst_agent_t *a) { return a ? a->todo : NULL; } const char *cst_agent_notes(const cst_agent_t *a) { return a ? a->notes : NULL; } long cst_agent_last_prompt_tokens(const cst_agent_t *a) { return a ? a->last_prompt_tokens : 0; } void cst_agent_reload_notes(cst_agent_t *a) { if (!a) return; free(a->notes); a->notes = NULL; const char *e = getenv("CST_NO_PROJECT_NOTES"); if (e && e[0] == '1') return; static const char *const names[] = {"AGENTS.md", "CST.md"}; for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { char p[4200]; snprintf(p, sizeof(p), "%s/%s", a->cfg.workspace_root, names[i]); FILE *fp = fopen(p, "rb"); if (!fp) continue; cst_buf_t b = {0}; char tmp[4096]; size_t n; while ((n = fread(tmp, 1, sizeof(tmp), fp)) > 0 && b.len < PROJECT_NOTES_MAX) cst_buf_append(&b, tmp, n); fclose(fp); if (b.len && b.buf) { size_t clean_len = 0; a->notes = cst_utf8_sanitize(b.buf, b.len, &clean_len); } cst_buf_free(&b); return; } } /* Latest todo_write result in the session (tool output lists `[ ] x`). */ /* Rough context size of a reopened session until the next real usage * report (about 4 bytes per token). */ static void estimate_tokens(cst_agent_t *a) { size_t bytes = 0; cst_session_lock(a->session); for (int i = 0; i < cst_session_count(a->session); i++) { const cst_message_t *m = cst_session_get(a->session, i); bytes += (m->content ? strlen(m->content) : 0) + (m->tool_calls_json ? strlen(m->tool_calls_json) : 0); } cst_session_unlock(a->session); a->last_prompt_tokens = (long)(bytes / 4); } static void restore_todo(cst_agent_t *a) { free(a->todo); a->todo = NULL; cst_session_lock(a->session); int n = cst_session_count(a->session); for (int i = n - 1; i >= 0; i--) { const cst_message_t *m = cst_session_get(a->session, i); if (!m || strcmp(m->tool_name, "todo_write") != 0 || !m->content) continue; cst_buf_t b = {0}; const char *p = m->content; while (*p) { const char *e = strchr(p, '\n'); size_t len = e ? (size_t)(e - p) : strlen(p); if (len >= 3 && p[0] == '[' && p[2] == ']' && (p[1] == ' ' || p[1] == 'x' || p[1] == '~')) { cst_buf_append(&b, p, len); cst_buf_puts(&b, "\n"); } if (!e) break; p = e + 1; } if (b.len) { a->todo = cst_buf_steal(&b); break; } cst_buf_free(&b); } cst_session_unlock(a->session); } int cst_agent_undo(cst_agent_t *a, char *report, size_t report_sz) { if (!a || !report || !report_sz) return -1; char sid[256]; cst_agent_session_id(a, sid, sizeof(sid)); int rc = cst_undo_restore_last(a->cfg.workspace_root, sid, report, report_sz); if (rc == 0) a->write_epoch++; return rc; } cst_perms_t *cst_agent_perms(cst_agent_t *a) { return a ? a->perms : NULL; } /* Drop the last user prompt and everything after it; returns the prompt. */ char *cst_agent_rewind_last_turn(cst_agent_t *a) { if (!a || !a->session) return NULL; cst_session_t *s = a->session; cst_session_lock(s); int n = cst_session_count(s), idx = -1; for (int i = n - 1; i > 0; i--) { const cst_message_t *m = cst_session_get(s, i); if (m->role == CST_ROLE_USER && !m->tool_call_id[0] && m->content && strncmp(m->content, "[session compacted", 18) != 0) { idx = i; break; } } char *prompt = idx > 0 ? strdup(cst_session_get(s, idx)->content) : NULL; cst_session_unlock(s); if (!prompt) return NULL; if (cst_session_rewrite(s, cst_session_get(s, 0), idx) != 0) { free(prompt); return NULL; } a->write_epoch++; return prompt; } int cst_agent_undo_available(const cst_agent_t *a) { if (!a) return 0; char sid[256]; cst_agent_session_id(a, sid, sizeof(sid)); return cst_undo_count(a->cfg.workspace_root, sid); } static void seed_system(cst_agent_t *a) { if (cst_session_count(a->session) == 0) { char *sp = cst_agent_build_prompt(&a->cfg, a->cfg.protocol, a->notes); append_msg(a, CST_ROLE_SYSTEM, sp, NULL, NULL, NULL, meta_json(NULL, a->cfg.protocol)); free(sp); } } cst_agent_t *cst_agent_create(const cst_agent_config_t *cfg) { if (!cfg) return NULL; cst_agent_t *a = calloc(1, sizeof(*a)); if (!a) return NULL; a->cfg = *cfg; if (a->cfg.max_steps <= 0) a->cfg.max_steps = CST_DEFAULT_MAX_STEPS; if (a->cfg.tool_out_cap == 0) a->cfg.tool_out_cap = CST_DEFAULT_TOOL_OUT_CAP; a->session = cst_session_open(cfg->session_path[0] ? cfg->session_path : "session.jsonl"); if (!a->session) { free(a); return NULL; } a->metrics = cst_metrics_open(a->cfg.workspace_root, cst_session_path(a->session)); snprintf(a->cfg.session_path, sizeof(a->cfg.session_path), "%s", cst_session_path(a->session)); a->perms = cst_perms_open(a->cfg.workspace_root); cst_agent_reload_notes(a); seed_system(a); restore_todo(a); estimate_tokens(a); return a; } void cst_agent_destroy(cst_agent_t *a) { if (!a) return; os_set_cancel_flag(NULL); cst_metrics_close(a->metrics); cst_session_close(a->session); free(a->notes); free(a->todo); cst_perms_close(a->perms); free(a); } cst_session_t *cst_agent_session(cst_agent_t *a) { return a ? a->session : NULL; } const cst_agent_config_t *cst_agent_config(const cst_agent_t *a) { return a ? &a->cfg : NULL; } cst_agent_config_t *cst_agent_config_mut(cst_agent_t *a) { return a ? &a->cfg : NULL; } void cst_agent_request_cancel(cst_agent_t *a) { if (a) a->cancel = 1; } int cst_agent_run_turn(cst_agent_t *a, const char *user_text, char *err, size_t err_sz) { return cst_agent_run_turn_ex(a, user_text, NULL, err, err_sz); } static void emit_step_metrics(turn_t *t, const cst_llm_stats_t *st, int lrc) { cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "type", "step"); cJSON_AddNumberToObject(o, "turn", t->a->turn_no); cJSON_AddNumberToObject(o, "step", t->step); cJSON_AddStringToObject(o, "protocol", cst_protocol_name(t->proto)); cJSON_AddStringToObject(o, "model", t->a->cfg.model); cJSON_AddBoolToObject(o, "final_pass", t->final_pass); cJSON_AddBoolToObject(o, "ok", lrc == 0); cJSON_AddNumberToObject(o, "http", (double)st->http_status); cJSON_AddStringToObject(o, "finish_reason", st->finish_reason); cJSON_AddNumberToObject(o, "t_first_byte", ms2(st->t_first_byte_ms)); cJSON_AddNumberToObject(o, "t_stream_end", ms2(st->t_end_ms)); cJSON_AddNumberToObject(o, "prompt_tokens", (double)st->prompt_tokens); cJSON_AddNumberToObject(o, "completion_tokens", (double)st->completion_tokens); cJSON_AddNumberToObject(o, "cached_tokens", (double)st->cached_tokens); cJSON_AddNumberToObject(o, "retries", st->retries); cJSON_AddBoolToObject(o, "client_stopped", st->client_stopped); cJSON_AddNumberToObject(o, "calls", t->ncalls); cJSON_AddNumberToObject(o, "text_bytes", (double)t->text.len); if (t->ctl) { ctl_parser_stats_t cs; ctl_parser_stats(t->ctl, &cs); cJSON *c = cJSON_AddObjectToObject(o, "ctl"); cJSON_AddNumberToObject(c, "calls_open", (double)cs.calls_open); cJSON_AddNumberToObject(c, "calls_ready", (double)cs.calls_ready); cJSON_AddNumberToObject(c, "args_nbytes", (double)cs.args_nbytes); cJSON_AddNumberToObject(c, "args_inline", (double)cs.args_inline); cJSON_AddNumberToObject(c, "args_heredoc", (double)cs.args_heredoc); cJSON_AddNumberToObject(c, "pending_peak", (double)cs.pending_peak); cJSON_AddStringToObject(c, "error_kind", ctl_error_kind_name(cs.error_kind)); } if (t->parse_error) cJSON_AddStringToObject(o, "parse_error", t->perr); if (lrc != 0 && st->error[0]) cJSON_AddStringToObject(o, "error", st->error); cst_metrics_emit(t->a->metrics, o); } static void emit_turn_metrics(cst_agent_t *a, int steps, int rc, double total_ms, const char *err) { cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "type", "turn"); cJSON_AddNumberToObject(o, "turn", a->turn_no); cJSON_AddStringToObject(o, "protocol", cst_protocol_name(a->cfg.protocol)); cJSON_AddStringToObject(o, "model", a->cfg.model); cJSON_AddNumberToObject(o, "steps", steps); cJSON_AddNumberToObject(o, "rc", rc); cJSON_AddNumberToObject(o, "t_total", ms2(total_ms)); if (err && err[0]) cJSON_AddStringToObject(o, "error", err); cst_metrics_emit(a->metrics, o); } /* Compact before a turn when the last request used most of the window. */ static void maybe_auto_compact(cst_agent_t *a, const cst_agent_hooks_t *hooks) { const char *e = getenv("CST_AUTO_COMPACT"); if (e && e[0] == '0') return; long win = cst_model_context_tokens(a->cfg.model); if (a->last_prompt_tokens <= 0 || win <= 0 || a->last_prompt_tokens * 10 < win * 8) return; if (hooks && hooks->on_status) { char msg[128]; snprintf(msg, sizeof(msg), "context %ld%% full — compacting...", a->last_prompt_tokens * 100 / win); hooks->on_status(msg, hooks->userdata); } char err[256]; if (cst_agent_compact(a, err, sizeof(err)) == 0) { a->last_prompt_tokens = 0; if (hooks && hooks->on_status) hooks->on_status("context compacted", hooks->userdata); } } int cst_agent_run_turn_ex(cst_agent_t *a, const char *user_text, const cst_agent_hooks_t *hooks, char *err, size_t err_sz) { char dummy[8]; if (!err || !err_sz) { err = dummy; err_sz = sizeof(dummy); } err[0] = 0; if (!a || !user_text) return -1; a->cancel = 0; maybe_auto_compact(a, hooks); a->turn_no++; a->nreads = 0; { time_t now = time(NULL); struct tm tmv; os_localtime(&now, &tmv); char ts[32]; strftime(ts, sizeof(ts), "%Y%m%d-%H%M%S", &tmv); snprintf(a->undo_tag, sizeof(a->undo_tag), "%s-%d-%03d", ts, os_getpid(), a->turn_no % 1000); } os_set_cancel_flag(&a->cancel); const cst_protocol_t proto = a->cfg.protocol; const double turn_t0 = os_now_ms(); append_msg(a, CST_ROLE_USER, user_text, NULL, NULL, NULL, NULL); int empty_retries = 0, ctl_retries = 0, tools_ran = 0; const char *nudge = NULL; int rc = -1; int step = 0; for (step = 0; step < a->cfg.max_steps; step++) { turn_t t; memset(&t, 0, sizeof(t)); t.a = a; t.hooks = hooks; t.proto = proto; t.step = step; t.final_pass = tools_ran && step == a->cfg.max_steps - 1; if (t.final_pass) nudge = NUDGE_FINAL; if (cst_protocol_is_ctl(proto)) t.ctl = ctl_parser_create(on_ctl, &t); else t.mm = mm_scan_create(on_mm, &t); if (!t.ctl && !t.mm) { snprintf(err, err_sz, "parser create failed"); break; } req_msgs_t rm; if (build_request_msgs(a, proto, nudge, &rm) != 0) { turn_free(&t); snprintf(err, err_sz, "oom"); break; } nudge = NULL; { char s[64]; snprintf(s, sizeof(s), "calling model (step %d)...", step + 1); hook_status(&t, t.final_pass ? "final answer pass..." : s); } cst_llm_stats_t st; cst_llm_request_t req; memset(&req, 0, sizeof(req)); req.api_base = a->cfg.api_base; req.api_key = a->cfg.api_key; req.model = a->cfg.model; req.reasoning_effort = cst_effort_wire(a->cfg.provider, a->cfg.model, a->cfg.effort); req.msgs = rm.msgs; req.nmsgs = rm.n; req.on_delta = on_delta; req.userdata = &t; req.cancel_flag = &a->cancel; req.stop_flag = &t.stop; req.stats = &st; req.tool_out_cap = a->cfg.tool_out_cap; if (cst_protocol_is_native(proto)) { req.on_native_tool = on_native_tool; req.on_native_open = on_native_open; req.enable_tools = 1; req.tool_cfg = &a->cfg; req.tool_choice_none = t.final_pass; } t.t0 = os_now_ms(); char lerr[512]; int lrc = cst_llm_chat_stream(&req, lerr, sizeof(lerr)); req_msgs_free(&rm); /* flush a dangling partial tag */ if (t.taglen && !t.in_think) feed_visible(&t, t.tagbuf, t.taglen); t.taglen = 0; if (t.mm) mm_scan_finish(t.mm); if (t.ctl && !t.stop) ctl_parser_finish(t.ctl); /* EOF may confirm a final call */ if (proto == CST_PROTO_XML && t.ncalls == 0 && !t.parse_error && t.raw.buf && (strstr(t.raw.buf, " 0) a->last_prompt_tokens = st.prompt_tokens; if (lrc != 0) { /* side effects of eagerly executed calls must still be recorded */ int kept = 0; for (int i = 0; i < t.ncalls; i++) { if (t.calls[i].done) { t.calls[kept++] = t.calls[i]; } else { cst_tool_call_clear(&t.calls[i].call); free(t.calls[i].raw_args); free(t.calls[i].output); } } t.ncalls = kept; if (t.ncalls > 0) persist_calls(&t); turn_free(&t); snprintf(err, err_sz, "llm: %s", lerr); break; } for (int i = 0; i < t.ncalls; i++) exec_call(&t, &t.calls[i]); /* no-op for eagerly executed calls */ if (a->cancel) { if (t.ncalls > 0) persist_calls(&t); else if (t.text.len) append_msg(a, CST_ROLE_ASSISTANT, trim_left(t.text.buf), NULL, NULL, NULL, meta_json("cancelled", proto)); turn_free(&t); snprintf(err, err_sz, "cancelled"); break; } const char *body = t.text.buf ? trim_left(t.text.buf) : ""; int has_text = text_is_meaningful(body, strlen(body)); if (t.ncalls > 0) { persist_calls(&t); tools_ran = 1; empty_retries = 0; if (t.parse_error && cst_protocol_is_text(proto)) { char msg[768]; snprintf(msg, sizeof(msg), "[%s] contract error after your last confirmed call: %s. " "Frames after it were ignored; re-send them with exact " "syntax.", proto == CST_PROTO_XML ? "xml" : "ctl", t.perr); append_msg(a, CST_ROLE_USER, msg, NULL, NULL, NULL, meta_json("ctl_error", proto)); } turn_free(&t); hook_status(&t, "tool round done..."); continue; } if (t.parse_error && cst_protocol_is_text(proto)) { if (t.raw.len) append_msg(a, CST_ROLE_ASSISTANT, t.raw.buf, NULL, NULL, NULL, meta_json(NULL, proto)); char msg[768]; if (proto == CST_PROTO_XML) snprintf(msg, sizeof(msg), "[xml] contract error: %s. Nothing was executed. Re-send the " "tool block with exact syntax:\n\n" "\nVALUE" "\n\n", t.perr); else snprintf(msg, sizeof(msg), "[ctl] contract error: %s. Nothing was executed. Re-send the " "call with exact CTL syntax (byte counts must be exact; use " "inline values for short single-line arguments).", t.perr); append_msg(a, CST_ROLE_USER, msg, NULL, NULL, NULL, meta_json("ctl_error", proto)); turn_free(&t); if (++ctl_retries > MAX_CTL_RETRIES) { snprintf(err, err_sz, "%s: repeated contract errors", cst_protocol_name(proto)); break; } hook_status(&t, "contract error — asking model to re-send..."); continue; } if (has_text) { append_msg(a, CST_ROLE_ASSISTANT, body, NULL, NULL, NULL, meta_json(NULL, proto)); turn_free(&t); hook_status(&t, "done"); rc = 0; break; } turn_free(&t); if (empty_retries < 2 && step + 1 < a->cfg.max_steps) { empty_retries++; nudge = NUDGE_EMPTY; hook_status(&t, "empty response — retrying..."); continue; } /* Last resort: surface the latest tool output so the turn isn't blank. */ if (tools_ran) { int n = cst_session_count(a->session); for (int i = n - 1; i >= 0; i--) { const cst_message_t *m = cst_session_get(a->session, i); if (!m->tool_call_id[0] || !m->content) continue; cst_buf_t b = {0}; cst_buf_puts(&b, "(模型未返回正文,以下为最近工具输出)\n"); cst_buf_append(&b, m->content, cst_utf8_floor(m->content, strlen(m->content) > 2000 ? 2000 : strlen(m->content))); append_msg(a, CST_ROLE_ASSISTANT, b.buf, NULL, NULL, NULL, meta_json("fallback", proto)); cst_buf_free(&b); rc = 0; break; } if (rc == 0) break; } snprintf(err, err_sz, "empty model response"); break; } if (rc != 0 && !err[0]) snprintf(err, err_sz, "step limit (%d) reached", a->cfg.max_steps); emit_turn_metrics(a, step + (step < a->cfg.max_steps ? 1 : 0), rc, os_now_ms() - turn_t0, err); return rc; } static int agent_reopen_session(cst_agent_t *a, const char *path) { cst_session_t *ns = cst_session_open(path); if (!ns) return -1; cst_session_close(a->session); a->session = ns; snprintf(a->cfg.session_path, sizeof(a->cfg.session_path), "%s", path); cst_metrics_close(a->metrics); a->metrics = cst_metrics_open(a->cfg.workspace_root, path); seed_system(a); restore_todo(a); estimate_tokens(a); return 0; } int cst_agent_new_session(cst_agent_t *a, char *id_out, size_t id_sz) { if (!a) return -1; if (cst_agent_store_ensure(a->cfg.workspace_root) != 0) return -1; char id[128]; if (cst_agent_store_new_id(id, sizeof(id)) != 0) return -1; char path[4096]; if (cst_agent_store_path_for_id(a->cfg.workspace_root, id, path, sizeof(path)) != 0) return -1; if (agent_reopen_session(a, path) != 0) return -1; cst_agent_store_set_current(a->cfg.workspace_root, id); if (id_out && id_sz) snprintf(id_out, id_sz, "%s", id); return 0; } int cst_agent_switch_session(cst_agent_t *a, const char *id) { if (!a || !id || !id[0]) return -1; char path[4096]; if (cst_agent_store_path_for_id(a->cfg.workspace_root, id, path, sizeof(path)) != 0) return -1; if (agent_reopen_session(a, path) != 0) return -1; char bare[128]; snprintf(bare, sizeof(bare), "%s", id); size_t n = strlen(bare); if (n > 6 && strcmp(bare + n - 6, ".jsonl") == 0) bare[n - 6] = 0; cst_agent_store_set_current(a->cfg.workspace_root, bare); return 0; } static void acc_feed(const char *data, size_t len, void *ud) { cst_buf_append((cst_buf_t *)ud, data, len); } int cst_agent_compact(cst_agent_t *a, char *err, size_t err_sz) { char dummy[8]; if (!err || !err_sz) { err = dummy; err_sz = sizeof(dummy); } err[0] = 0; if (!a || !a->session) return -1; int n = cst_session_count(a->session); if (n <= 1) { snprintf(err, err_sz, "nothing to compact"); return -1; } cst_buf_t blob = {0}; cst_buf_puts(&blob, "Summarize the coding session below so work can continue from " "the summary alone. Keep: the user's goals, files and paths " "involved, decisions made, current state (what works, what " "fails), and open items. Write it in the language the user " "used. Do not call tools; output only the summary.\n\n---\n"); for (int i = 0; i < n && blob.len < 120000; i++) { const cst_message_t *m = cst_session_get(a->session, i); if (!m || m->role == CST_ROLE_SYSTEM) continue; const char *role = m->role == CST_ROLE_USER ? (m->tool_call_id[0] ? "tool" : "user") : m->role == CST_ROLE_ASSISTANT ? "assistant" : "tool"; const char *c = m->content ? m->content : ""; size_t cl = strlen(c); if (cl > 2000) cl = cst_utf8_floor(c, 2000); cst_buf_printf(&blob, "\n[%s]\n", role); cst_buf_append(&blob, c, cl); } cst_message_t reqs[2]; memset(reqs, 0, sizeof(reqs)); reqs[0].role = CST_ROLE_SYSTEM; reqs[0].content = (char *)"You summarize coding-agent sessions. No tools."; reqs[1].role = CST_ROLE_USER; reqs[1].content = blob.buf; cst_buf_t acc = {0}; cst_llm_request_t req; memset(&req, 0, sizeof(req)); req.api_base = a->cfg.api_base; req.api_key = a->cfg.api_key; req.model = a->cfg.model; req.reasoning_effort = cst_effort_wire(a->cfg.provider, a->cfg.model, a->cfg.effort); req.msgs = reqs; req.nmsgs = 2; req.on_delta = acc_feed; req.userdata = &acc; req.cancel_flag = &a->cancel; char lerr[512]; lerr[0] = 0; int rc = cst_llm_chat_stream(&req, lerr, sizeof(lerr)); cst_buf_free(&blob); if (rc != 0 || !acc.buf || acc.len == 0) { cst_buf_free(&acc); snprintf(err, err_sz, "compact llm: %s", lerr[0] ? lerr : "empty"); return -1; } /* Strip ... blocks from summary. */ char *src = acc.buf, *dst = acc.buf; while (*src) { if (strncmp(src, "", 7) == 0) { char *end = strstr(src, ""); if (end) { src = end + 8; continue; } } *dst++ = *src++; } *dst = 0; acc.len = (size_t)(dst - acc.buf); while (acc.len > 0 && (acc.buf[acc.len - 1] == '\n' || acc.buf[acc.len - 1] == ' ')) acc.buf[--acc.len] = 0; if (acc.len < 8) { cst_buf_free(&acc); snprintf(err, err_sz, "compact produced empty summary"); return -1; } cst_buf_t summary = {0}; cst_buf_printf(&summary, "[session compacted — summary of earlier " "conversation]\n%s", trim_left(acc.buf)); cst_buf_free(&acc); cst_message_t keep[2]; memset(keep, 0, sizeof(keep)); keep[0].role = CST_ROLE_SYSTEM; keep[0].content = cst_agent_build_prompt(&a->cfg, a->cfg.protocol, a->notes); keep[1].role = CST_ROLE_USER; keep[1].content = summary.buf; rc = cst_session_rewrite(a->session, keep, 2); free(keep[0].content); cst_buf_free(&summary); if (rc != 0) { snprintf(err, err_sz, "rewrite session failed"); return -1; } return 0; }