Extensions: skills, plugins, MCP, sub-agents; configurable keys
ci / linux (clang) (push) Canceled after 0s
ci / linux (gcc) (push) Canceled after 0s
ci / sanitizers (push) Canceled after 0s
ci / musl (push) Canceled after 0s
ci / macos (push) Canceled after 0s

- Skills (SKILL.md discovery, skill tool, /skills /skill)
- plugin.json exec tools with permission prompts, timeouts and workspace trust (/plugins)
- MCP stdio client: JSON-RPC initialize/tools/list/tools/call, mcp__server__tool, /mcp and sidebar status
- task tool with general/explore/plan and .agent/agents/*.md roles, parallel read-only runs, cancel propagation, /agents
- Keymap with Ctrl+X leader, keybinds.json overrides and /keys panel
- Collapsed thinking, throttled streaming Markdown, leaner tool output, parallel read-only tools
- Spawn: close-on-exec pipes and a process-wide fork lock
- Tests for keymap, skills, plugins, MCP, sub-agents, shell compaction; docs/plugins.md
This commit is contained in:
WHC
2026-10-05 14:55:31 +08:00
parent c81df42f0b
commit 7d3ec9fcb9
38 changed files with 8356 additions and 399 deletions
+589 -105
View File
@@ -7,6 +7,8 @@
#include "cst_metrics.h"
#include "cst_perms.h"
#include "cst_sandbox.h"
#include "cst_skills.h"
#include "cst_subagent.h"
#include "cst_tools.h"
#include "cst_undo.h"
#include "cst_util.h"
@@ -16,6 +18,9 @@
#include <stdint.h>
#include <stdio.h>
#include <time.h>
#ifndef _WIN32
#include <sys/utsname.h>
#endif
#include <stdlib.h>
#include <string.h>
@@ -211,20 +216,156 @@ static void put_workflow(cst_buf_t *b, const cst_agent_config_t *cfg) {
#define PROJECT_NOTES_MAX (16 * 1024)
/* Default-profile prompt: static sections first (prefix-cache friendly),
* then the protocol, then per-session facts. */
static const char DEF_INTRO[] =
"You are CST-Agent, a coding agent working in the user's local "
"workspace. All tool paths are relative to the workspace root and shell "
"commands run there.\n\n"
"Principles:\n"
"- Reply in the user's language. Be concise: lead with the result, no "
"filler.\n"
"- Understand before changing: look at the relevant code instead of "
"guessing; never invent files, APIs or tool results.\n"
"- Make the smallest change that fully solves the task, in the style of "
"the surrounding code; leave unrelated code alone.\n"
"- Verify (build, tests, or running it) before saying it works; if you "
"could not verify, say so.\n"
"- Keep going until the task is done or truly blocked.\n\n";
static void put_tool_use(cst_buf_t *b, const cst_agent_config_t *cfg) {
cst_buf_puts(b, "\nTool use:\n");
if (tool_on(cfg, "run_shell") &&
(tool_on(cfg, "grep") || tool_on(cfg, "glob")))
cst_buf_puts(b, "- Use the file tools rather than run_shell to look "
"around: read_file (not cat/head/tail), grep (not "
"grep/rg), glob and list_dir (not find/ls).\n");
cst_buf_puts(b, "- Independent read-only calls can go out together in one "
"reply; they run in parallel.\n");
if (tool_on(cfg, "read_file"))
cst_buf_puts(b, "- Read a file before editing it and again after a failed "
"edit. The `N|` prefixes in read_file output are not part "
"of the file.\n");
cst_buf_puts(b, "- When a call fails, read the error and change approach "
"instead of repeating the same call.\n");
}
static void put_safety(cst_buf_t *b, const cst_agent_config_t *cfg) {
cst_buf_puts(b, "\nSafety:\n"
"- Do not commit, push or rewrite git history unless asked. "
"Do not delete or overwrite files you did not create unless "
"the task requires it. Never print secrets or API keys.\n");
if (tool_on(cfg, "run_shell"))
cst_buf_puts(b, "- run_shell may run in a sandbox (writes only inside the "
"workspace and temp dir, no network). If a command is "
"blocked the user is asked whether to rerun it outside; "
"do not try to work around the sandbox.\n");
}
/* Current branch from .git/HEAD of the workspace or a parent directory. */
static void git_branch(const char *ws, char *out, size_t n) {
out[0] = 0;
char dir[4096];
snprintf(dir, sizeof(dir), "%s", ws ? ws : ".");
for (int depth = 0; depth < 8; depth++) {
char p[4200], line[512];
snprintf(p, sizeof(p), "%s/.git", dir);
FILE *fp = fopen(p, "r");
if (fp) {
/* worktree / submodule: ".git" is a file "gitdir: <path>" */
char gd[4096] = "";
if (fgets(line, sizeof(line), fp) && strncmp(line, "gitdir: ", 8) == 0) {
line[strcspn(line, "\r\n")] = 0;
if (line[8] == '/')
snprintf(gd, sizeof(gd), "%s", line + 8);
else
snprintf(gd, sizeof(gd), "%.2000s/%.2000s", dir, line + 8);
}
fclose(fp);
if (gd[0])
snprintf(p, sizeof(p), "%s/HEAD", gd);
else
snprintf(p, sizeof(p), "%s/.git/HEAD", dir);
fp = fopen(p, "r");
if (fp) {
if (fgets(line, sizeof(line), fp)) {
line[strcspn(line, "\r\n")] = 0;
if (strncmp(line, "ref: refs/heads/", 16) == 0)
snprintf(out, n, "%.100s", line + 16);
else if (strlen(line) >= 12)
snprintf(out, n, "detached at %.12s", line);
}
fclose(fp);
}
return;
}
char *sl = strrchr(dir, '/');
if (!sl || sl == dir)
return;
*sl = 0;
}
}
static void os_label(char *out, size_t n) {
#ifdef _WIN32
snprintf(out, n, "Windows");
#else
struct utsname u;
if (uname(&u) == 0)
snprintf(out, n, "%s %s %s", u.sysname, u.release, u.machine);
else
snprintf(out, n, "Unix");
#endif
}
static void put_env(cst_buf_t *b, const cst_agent_config_t *cfg) {
char osl[256], br[128], day[32];
os_label(osl, sizeof(osl));
time_t now = time(NULL);
struct tm tmv;
os_localtime(&now, &tmv);
strftime(day, sizeof(day), "%Y-%m-%d", &tmv);
git_branch(cfg->workspace_root, br, sizeof(br));
cst_buf_printf(b, "\nEnvironment:\n- OS: %s; date: %s\n- Workspace: %s%s%s%s\n",
osl, day, cfg->workspace_root[0] ? cfg->workspace_root : ".",
br[0] ? " (git: " : " (not a git repo", br,
")");
const char *ap = cfg->auto_approve ? "yolo (no confirmations)"
: cfg->approval == CST_APPROVAL_ASK
? "ask (the user confirms writes and commands)"
: cfg->approval == CST_APPROVAL_AUTO
? "auto (edits run; risky commands need confirmation)"
: "yolo (no confirmations)";
if (tool_on(cfg, "run_shell")) {
int sb = cst_sandbox_probe();
cst_buf_printf(b, "- Approval: %s; shell sandbox: %s\n", ap,
sb == CST_SANDBOX_NONE ? "none" : cst_sandbox_name(sb));
} else {
cst_buf_printf(b, "- Approval: %s\n", ap);
}
}
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);
cst_buf_puts(&b, minimal ? PROMPT_BASE : DEF_INTRO);
if (cst_protocol_is_native(p))
cst_buf_puts(&b, SEC_NATIVE);
cst_buf_puts(&b, minimal ? SEC_NATIVE
: "Call tools through the native "
"function-calling interface.\n");
if (minimal) {
cst_buf_puts(&b, PROMPT_TOOLS);
} else {
char *tools = cst_tools_prompt(cfg);
cst_buf_puts(&b, tools ? tools : "");
free(tools);
/* native requests carry the descriptions in tools[] already */
if (!cst_protocol_is_native(p)) {
char *tools = cst_tools_prompt(cfg);
cst_buf_puts(&b, tools ? tools : "");
free(tools);
}
put_workflow(&b, cfg);
put_tool_use(&b, cfg);
put_safety(&b, cfg);
}
switch (p) {
case CST_PROTO_CTL:
@@ -241,12 +382,19 @@ char *cst_agent_build_prompt(const cst_agent_config_t *cfg, cst_protocol_t p,
}
if (!minimal && cst_protocol_is_ctl(p))
cst_buf_puts(&b, SEC_CTL_NUMBERS);
if (cfg && cfg->plan_mode)
cst_buf_puts(&b, "\nPLAN MODE: you may only read and research (no file "
"changes, no shell). Investigate, then answer with a "
"concrete step-by-step plan: files to touch, what to "
"change, how to verify. The user switches to Build mode "
"to carry it out.\n");
/* per-workspace, rarely changing */
if (!minimal && tool_on(cfg, "skill")) {
char *ix = cst_skills_index(cfg->workspace_root);
if (ix)
cst_buf_puts(&b, ix);
free(ix);
}
if (!minimal && tool_on(cfg, "task")) {
char *ix = cst_subagent_index(cfg->workspace_root);
if (ix)
cst_buf_puts(&b, ix);
free(ix);
}
if (project_notes && project_notes[0]) {
size_t n = strlen(project_notes);
if (n > PROJECT_NOTES_MAX)
@@ -256,6 +404,29 @@ char *cst_agent_build_prompt(const cst_agent_config_t *cfg, cst_protocol_t p,
cst_buf_append(&b, project_notes, n);
cst_buf_puts(&b, "\n");
}
/* per-session facts last */
if (!minimal)
put_env(&b, cfg);
if (!minimal && cfg->depth > 0) {
cst_buf_puts(&b, "\nSUB-AGENT: another agent delegated one task to you "
"with the task tool. It sees none of your tool calls or "
"outputs, only your final message, and the user is not "
"watching this conversation (you cannot ask questions). "
"Work autonomously, then end with one self-contained "
"report: what you found or changed, with file paths "
"and line numbers, and anything left open.\n");
if (cfg->role_prompt && cfg->role_prompt[0]) {
cst_buf_puts(&b, "\nRole instructions:\n");
cst_buf_puts(&b, cfg->role_prompt);
cst_buf_puts(&b, "\n");
}
}
if (cfg && cfg->plan_mode)
cst_buf_puts(&b, "\nPLAN MODE: you may only read and research (no file "
"changes, no shell). Investigate, then answer with a "
"concrete step-by-step plan: files to touch, what to "
"change, how to verify. The user switches to Build mode "
"to carry it out.\n");
return cst_buf_steal(&b);
}
@@ -285,6 +456,8 @@ static const char NUDGE_FINAL[] =
/* 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 FAIL_FP_MAX 32
#define PAR_MAX 4
#define MAX_CTL_RETRIES 3
/* --- agent --------------------------------------------------------------- */
@@ -302,6 +475,14 @@ struct cst_agent {
unsigned write_epoch;
read_fp_t reads[READ_FP_MAX];
int nreads;
/* this user turn: failing calls (hash -> count) and applied writes */
struct {
uint64_t hash;
int count;
} fails[FAIL_FP_MAX];
int nfails;
read_fp_t writes[FAIL_FP_MAX];
int nwrites;
int turn_no;
char *notes; /* AGENTS.md text, or NULL */
char *todo; /* latest todo_write list, or NULL */
@@ -489,6 +670,15 @@ static void set_output(pcall_t *pc, const char *s, size_t n, int code) {
pc->exit_code = code;
}
static void append_output(pcall_t *pc, const char *note) {
cst_buf_t b = {0};
cst_buf_append(&b, pc->output ? pc->output : "", pc->output_len);
cst_buf_puts(&b, note);
free(pc->output);
pc->output_len = b.len;
pc->output = cst_buf_steal(&b);
}
static void emit_call_metrics(turn_t *t, const pcall_t *pc) {
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "type", "call");
@@ -516,11 +706,23 @@ static void undo_snapshot(cst_agent_t *a, const ctl_tool_call_t *c) {
if (n <= 0)
return;
char sid[256];
cst_agent_session_id(a, sid, sizeof(sid));
if (a->cfg.undo_sid[0])
snprintf(sid, sizeof(sid), "%s", a->cfg.undo_sid);
else
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]);
}
void cst_agent_undo_key(const cst_agent_t *a, char *sid, size_t sid_sz,
char *tag, size_t tag_sz) {
if (a->cfg.undo_sid[0])
snprintf(sid, sid_sz, "%s", a->cfg.undo_sid);
else
cst_agent_session_id(a, sid, sid_sz);
snprintf(tag, tag_sz, "%s", a->undo_tag);
}
static void agent_set_todo(const char *text, void *ud) {
turn_t *t = ud;
free(t->a->todo);
@@ -613,10 +815,34 @@ static void sandbox_escalate(turn_t *t, cst_tool_ctx_t *ctx,
tr->out = cst_buf_steal(&b);
}
/* Execute (or refuse) one confirmed call. Irreversible once started. */
static void exec_call(turn_t *t, pcall_t *pc) {
if (pc->done)
return;
/* One call through approval, execution and bookkeeping. exec_begin and
* exec_end run on the agent thread (they call hooks); exec_run may run on a
* worker thread for read-only batches. */
typedef struct {
cst_tool_ctx_t ctx;
const cst_tool_def_t *def;
const char *cmd;
uint64_t h;
int run;
int mutating;
cst_tool_result_t tr;
} exec_t;
static int fail_count(cst_agent_t *a, uint64_t h, int bump) {
for (int i = 0; i < a->nfails; i++) {
if (a->fails[i].hash == h)
return bump ? ++a->fails[i].count : a->fails[i].count;
}
if (!bump)
return 0;
int slot = a->nfails < FAIL_FP_MAX ? a->nfails++ : (int)(h % FAIL_FP_MAX);
a->fails[slot].hash = h;
a->fails[slot].count = 1;
return 1;
}
static void exec_begin(turn_t *t, pcall_t *pc, exec_t *x) {
memset(x, 0, sizeof(*x));
pc->done = 1;
cst_agent_t *a = t->a;
ctl_tool_call_t *c = &pc->call;
@@ -624,22 +850,27 @@ static void exec_call(turn_t *t, pcall_t *pc) {
char detail[512];
args_detail(c, detail, sizeof(detail));
const cst_tool_def_t *def = cst_tool_find(c->name);
x->def = def;
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;
cst_tool_ctx_t *ctx = &x->ctx;
ctx->workspace_root = a->cfg.workspace_root;
ctx->compact = a->cfg.tool_profile != CST_TOOLS_MINIMAL;
ctx->set_todo = agent_set_todo;
ctx->todo_ud = t;
ctx->agent = a;
ctx->hooks = t->hooks;
if (t->hooks && t->hooks->on_ask_user) {
ctx.ask = agent_ask;
ctx.ask_ud = t;
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)) {
return;
}
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)",
@@ -648,99 +879,151 @@ static void exec_call(turn_t *t, pcall_t *pc) {
set_output(pc, msg, strlen(msg), 1);
pc->status = PC_UNKNOWN;
hook_status(t, "unknown tool");
} else if (pc->invalid) {
return;
}
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;
}
return;
}
/* a sub-agent that may write changes files like a write tool does */
x->mutating = def->perm == CST_TP_SHELL || def->perm == CST_TP_WRITE ||
(strcmp(def->name, "task") == 0 &&
!(def->parallel && def->parallel(c, def->ud)));
uint64_t h = call_hash(c);
x->h = h;
int dup = 0;
/* plugin and MCP results can change between identical calls */
int external = def->owner && strncmp(def->owner, "builtin:", 8) != 0;
if (def->perm == CST_TP_READ && !external) {
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, ask = 0;
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))) {
ask = 1;
if (a->cfg.approval == CST_APPROVAL_AUTO) {
if (def->perm == CST_TP_WRITE) {
} else if (def->perm == CST_TP_WRITE && ctx->compact) {
/* the same write already applied and nothing changed since */
for (int i = 0; i < a->nwrites; i++) {
if (a->writes[i].hash == h && a->writes[i].epoch == a->write_epoch) {
dup = 2;
break;
}
}
}
int allow = 1, ask = 0;
const char *cmd = def->perm == CST_TP_SHELL ? tool_cmd(c) : NULL;
x->cmd = cmd;
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))) {
ask = 1;
if (a->cfg.approval == CST_APPROVAL_AUTO) {
if (def->perm == CST_TP_WRITE) {
ask = 0;
} else if (def->perm == CST_TP_SHELL) {
if (cst_sandbox_probe() != CST_SANDBOX_NONE) {
ctx->sandbox = 1;
ask = 0;
} else if (cmd && cst_shell_is_safe(cmd)) {
ask = 0;
} else if (def->perm == CST_TP_SHELL) {
if (cst_sandbox_probe() != CST_SANDBOX_NONE) {
ctx.sandbox = 1;
ask = 0;
} else if (cmd && cst_shell_is_safe(cmd)) {
ask = 0;
}
}
}
}
if (ask) {
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 (ask) {
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);
}
}
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);
if (ctx.sandbox && cmd &&
cst_sandbox_blocked(tr.out, tr.out_len, tr.exit_code))
sandbox_escalate(t, &ctx, c, cmd, &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;
allow = 0;
}
}
if (dup == 2) {
const char *msg = "note: this exact edit was already applied earlier in "
"this turn and nothing changed since; it was not run "
"again. Re-read the file if unsure of its state.";
set_output(pc, msg, strlen(msg), 0);
pc->status = PC_DUP;
hook_status(t, "skip repeated edit");
} else 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);
x->run = 1;
}
}
static void exec_run(exec_t *x, const ctl_tool_call_t *c) {
cst_tool_run(&x->ctx, c, &x->tr);
}
static void exec_end(turn_t *t, pcall_t *pc, exec_t *x) {
cst_agent_t *a = t->a;
ctl_tool_call_t *c = &pc->call;
if (x->run) {
if (x->ctx.sandbox && x->cmd &&
cst_sandbox_blocked(x->tr.out, x->tr.out_len, x->tr.exit_code))
sandbox_escalate(t, &x->ctx, c, x->cmd, &x->tr);
cst_tool_result_t *tr = &x->tr;
if (x->ctx.compact) {
if (tr->exit_code != 0 && fail_count(a, x->h, 1) >= 2) {
cst_buf_t b = {0};
cst_buf_append(&b, tr->out ? tr->out : "", tr->out_len);
cst_buf_printf(&b, "\n[cst: this exact call has now failed %d times "
"in this turn. Do not repeat it: read the error, "
"inspect the code, and change approach.]",
fail_count(a, x->h, 0));
free(tr->out);
tr->out_len = b.len;
tr->out = cst_buf_steal(&b);
}
}
set_output(pc, tr->out ? tr->out : "", tr->out_len, tr->exit_code);
cst_tool_result_free(tr);
if (x->mutating)
a->write_epoch++;
if (x->def->perm == CST_TP_WRITE && pc->exit_code == 0) {
int slot = a->nwrites < FAIL_FP_MAX ? a->nwrites++
: (int)(x->h % FAIL_FP_MAX);
a->writes[slot].hash = x->h;
a->writes[slot].epoch = a->write_epoch;
} else if (pc->exit_code == 0 && x->def->perm == CST_TP_READ) {
int slot = a->nreads < READ_FP_MAX ? a->nreads++ : (int)(x->h % READ_FP_MAX);
a->reads[slot].hash = x->h;
a->reads[slot].epoch = a->write_epoch;
}
}
pc->t_exec_end = rel_ms(t);
if (t->hooks && t->hooks->on_tool_result)
@@ -756,6 +1039,82 @@ static void exec_call(turn_t *t, pcall_t *pc) {
emit_call_metrics(t, pc);
}
/* Execute (or refuse) one confirmed call. Irreversible once started. */
static void exec_call(turn_t *t, pcall_t *pc) {
if (pc->done)
return;
exec_t x;
exec_begin(t, pc, &x);
if (x.run)
exec_run(&x, &pc->call);
exec_end(t, pc, &x);
}
/* Read-only calls that may share a parallel batch. */
static int parallel_ok(const turn_t *t, const pcall_t *pc) {
if (pc->done || pc->invalid)
return 0;
const cst_tool_def_t *def = cst_tool_find(pc->call.name);
if (!def || !cst_tool_active(&t->a->cfg, def))
return 0;
return def->perm == CST_TP_READ ||
(def->parallel && def->parallel(&pc->call, def->ud));
}
typedef struct {
exec_t *x;
const ctl_tool_call_t *c;
volatile int *cancel;
} par_job_t;
static void par_main(void *p) {
par_job_t *j = p;
/* not reset after: the job may run inline on the agent thread */
os_set_cancel_flag(j->cancel);
exec_run(j->x, j->c);
}
static void exec_parallel(turn_t *t, int from, int to) {
exec_t xs[PAR_MAX];
par_job_t jobs[PAR_MAX];
void *args[PAR_MAX];
int n = 0;
for (int i = from; i < to; i++)
exec_begin(t, &t->calls[i], &xs[i - from]);
for (int i = from; i < to; i++) {
if (!xs[i - from].run)
continue;
jobs[n].x = &xs[i - from];
jobs[n].c = &t->calls[i].call;
jobs[n].cancel = &t->a->cancel;
args[n] = &jobs[n];
n++;
}
if (n > 1)
hook_status(t, "running reads in parallel");
os_run_parallel(par_main, args, n);
for (int i = from; i < to; i++)
exec_end(t, &t->calls[i], &xs[i - from]);
}
/* Calls left after the stream: consecutive read-only calls run together. */
static void exec_pending(turn_t *t) {
int par = t->a->cfg.tool_profile != CST_TOOLS_MINIMAL;
for (int i = 0; i < t->ncalls;) {
int j = i;
while (par && j < t->ncalls && j - i < PAR_MAX &&
parallel_ok(t, &t->calls[j]))
j++;
if (j - i >= 2) {
exec_parallel(t, i, j);
i = j;
} else {
exec_call(t, &t->calls[i]);
i++;
}
}
}
/* --- stream routing ------------------------------------------------------ */
static void on_text(turn_t *t, const char *s, size_t n) {
@@ -1061,6 +1420,59 @@ static int ctx_window(void) {
return CST_AGENT_CTX_WINDOW;
}
/* Big tool outputs from turns this far back go out as a one-line stub (the
* session file keeps them whole). */
#define STUB_TURNS 3
#define STUB_MIN (2 * 1024)
/* "path"/"pattern"/"cmd"/"url" argument of call id in a tool_calls array. */
static void call_hint(const cJSON *calls, const char *id, char *out,
size_t n) {
out[0] = 0;
const cJSON *tc = NULL;
cJSON_ArrayForEach(tc, calls) {
const cJSON *cid = cJSON_GetObjectItemCaseSensitive(tc, "id");
if (!cJSON_IsString(cid) || strcmp(cid->valuestring, id) != 0)
continue;
const cJSON *fn = cJSON_GetObjectItemCaseSensitive(tc, "function");
const cJSON *ar = cJSON_GetObjectItemCaseSensitive(fn, "arguments");
cJSON *args = cJSON_IsString(ar) ? cJSON_Parse(ar->valuestring) : NULL;
static const char *const keys[] = {"path", "pattern", "cmd", "command",
"url"};
for (size_t k = 0; k < sizeof(keys) / sizeof(keys[0]); k++) {
const cJSON *v = cJSON_GetObjectItemCaseSensitive(args, keys[k]);
if (cJSON_IsString(v)) {
size_t l = strlen(v->valuestring);
size_t keep = cst_utf8_floor(v->valuestring, l > 80 ? 80 : l);
snprintf(out, n, " %.*s%s", (int)keep, v->valuestring,
keep < l ? "…" : "");
for (char *q = out; *q; q++)
if (*q == '\n')
*q = ' ';
break;
}
}
cJSON_Delete(args);
return;
}
}
static char *stub_text(const char *name, const char *hint, const char *body,
size_t n) {
size_t lines = 0;
for (size_t i = 0; i < n; i++)
lines += body[i] == '\n';
if (n && body[n - 1] != '\n')
lines++;
cst_buf_t b = {0};
cst_buf_printf(&b,
"[output elided: %s%s returned %zu line%s (%.1f KB) in an "
"earlier turn; call it again if you still need it]",
name && name[0] ? name : "tool", hint, lines,
lines == 1 ? "" : "s", (double)n / 1024.0);
return cst_buf_steal(&b);
}
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));
@@ -1084,14 +1496,37 @@ static int build_request_msgs(cst_agent_t *a, cst_protocol_t proto,
int skip = nonsys > ctx_window() ? nonsys - ctx_window() : 0;
int text_proto = cst_protocol_is_text(proto);
int leading = 1;
/* real user messages after each index (drives stubbing) */
int *later = NULL;
if (a->cfg.tool_profile != CST_TOOLS_MINIMAL && n > 0) {
later = calloc((size_t)n, sizeof(int));
int cnt = 0;
for (int i = n - 1; later && i >= 0; i--) {
later[i] = cnt;
const cst_message_t *m = cst_session_get(a->session, i);
if (m->role == CST_ROLE_USER && !m->tool_call_id[0])
cnt++;
}
}
cJSON *calls = NULL;
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 (m->role == CST_ROLE_ASSISTANT && later) {
cJSON_Delete(calls);
calls = m->tool_calls_json && later[i] >= STUB_TURNS
? cJSON_Parse(m->tool_calls_json)
: NULL;
}
if (skip > 0) {
skip--;
continue;
}
int stub = later && later[i] >= STUB_TURNS && m->content &&
strlen(m->content) > STUB_MIN &&
(m->role == CST_ROLE_TOOL ||
(m->role == CST_ROLE_USER && m->tool_call_id[0]));
/* never start the window with orphan results */
if (leading && (m->role == CST_ROLE_TOOL ||
(m->role == CST_ROLE_USER && m->tool_call_id[0])))
@@ -1111,12 +1546,52 @@ static int build_request_msgs(cst_agent_t *a, cst_protocol_t proto,
const char *nl = strchr(body, '\n');
if (strncmp(body, "[tool ", 6) == 0 && nl)
body = nl + 1;
char *st = NULL;
if (stub) {
char hint[128];
call_hint(calls, m->tool_call_id, hint, sizeof(hint));
st = stub_text(m->tool_name, hint, body, strlen(body));
if (st)
body = st;
}
mm.role = CST_ROLE_USER;
mm.content = own(r, result_frame(m->tool_call_id, code, body,
strlen(body), a->cfg.tool_out_cap));
free(st);
} else if (stub && m->role == CST_ROLE_TOOL) {
const char *nl = strchr(m->content, '\n');
size_t hl = nl && strncmp(m->content, "[tool ", 6) == 0
? (size_t)(nl - m->content) + 1
: 0;
char hint[128];
call_hint(calls, m->tool_call_id, hint, sizeof(hint));
char *st = stub_text(m->tool_name, hint, m->content + hl,
strlen(m->content + hl));
cst_buf_t b = {0};
cst_buf_append(&b, m->content, hl);
cst_buf_puts(&b, st ? st : "");
free(st);
mm.content = own(r, cst_buf_steal(&b));
} else if (stub && text_proto && strncmp(m->content, "result ", 7) == 0) {
/* text-protocol frame: "result <id> exit=<c> <n>\n<body>\nend" */
const char *ex = strstr(m->content, " exit=");
const char *nl = strchr(m->content, '\n');
size_t len = strlen(m->content);
if (ex && nl && len >= 4 && strcmp(m->content + len - 4, "\nend") == 0 &&
nl < m->content + len - 4) {
const char *body = nl + 1;
size_t bn = (size_t)(m->content + len - 4 - body);
char *st = stub_text(m->tool_name, "", body, bn);
if (st)
mm.content = own(r, result_frame(m->tool_call_id, atoi(ex + 6), st,
strlen(st), 0));
free(st);
}
}
r->msgs[r->n++] = mm;
}
cJSON_Delete(calls);
free(later);
if (nudge) {
r->msgs[r->n].role = CST_ROLE_USER;
r->msgs[r->n].content = (char *)nudge;
@@ -1540,6 +2015,8 @@ int cst_agent_run_turn_ex(cst_agent_t *a, const char *user_text,
maybe_auto_compact(a, hooks);
a->turn_no++;
a->nreads = 0;
a->nfails = 0;
a->nwrites = 0;
{
time_t now = time(NULL);
struct tm tmv;
@@ -1548,6 +2025,8 @@ int cst_agent_run_turn_ex(cst_agent_t *a, const char *user_text,
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);
if (a->cfg.undo_tag[0])
snprintf(a->undo_tag, sizeof(a->undo_tag), "%s", a->cfg.undo_tag);
}
os_set_cancel_flag(&a->cancel);
const cst_protocol_t proto = a->cfg.protocol;
@@ -1662,8 +2141,13 @@ int cst_agent_run_turn_ex(cst_agent_t *a, const char *user_text,
break;
}
for (int i = 0; i < t.ncalls; i++)
exec_call(&t, &t.calls[i]); /* no-op for eagerly executed calls */
exec_pending(&t); /* eagerly executed calls are already done */
if (t.ncalls > 0 && a->cfg.tool_profile != CST_TOOLS_MINIMAL &&
a->cfg.max_steps - (step + 1) == 3)
append_output(&t.calls[t.ncalls - 1],
"\n[cst: 3 model calls left in this turn. Finish the "
"essential part now, verify it, and then summarize what "
"is done and what remains.]");
if (a->cancel) {
if (t.ncalls > 0)
persist_calls(&t);