Files
CST-agent/src/subagent.c
T
WHC 7d3ec9fcb9 Extensions: skills, plugins, MCP, sub-agents; configurable keys
- 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
2026-10-05 14:55:31 +08:00

704 lines
22 KiB
C

#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE
#endif
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE 700
#endif
#include "cst_subagent.h"
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#ifndef _WIN32
#include <dirent.h>
#endif
#include "cJSON.h"
#include "cst_agent.h"
#include "cst_skills.h"
#include "cst_tools.h"
#include "cst_util.h"
#include "os_compat.h"
#define ROLE_FILE_MAX (64 * 1024)
#define INDEX_MAX 2048
static pthread_mutex_t g_hook_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t g_seq_mu = PTHREAD_MUTEX_INITIALIZER;
static char g_ws[OS_PATH_MAX];
/* ---------- roles ---------- */
static const struct {
const char *name, *desc, *prompt;
int read_only;
} BUILTIN[] = {
{"general",
"General-purpose agent for multi-step work: researches, edits files and "
"runs commands as needed.",
"", 0},
{"explore",
"Fast read-only explorer: searches and reads the codebase to answer a "
"question (where is X, how does Y work, which files do Z). Several can "
"run at once.",
"You only read. Search broadly first (grep, glob, list_dir), then read "
"just the relevant parts. Report concrete findings: file paths with line "
"numbers, short excerpts when they matter, and a direct answer. Say what "
"you looked for and did not find.",
1},
{"plan",
"Read-only planner: investigates the code involved and returns a "
"step-by-step implementation plan.",
"You only read. Investigate the code involved, then return a concrete "
"plan: the files to change and what to change in each, risks and edge "
"cases, and how to verify. Sketch code only where it removes ambiguity.",
1},
};
#define NBUILTIN ((int)(sizeof(BUILTIN) / sizeof(BUILTIN[0])))
static char *slurp(const char *path, size_t max) {
FILE *fp = fopen(path, "rb");
if (!fp)
return NULL;
cst_buf_t b = {0};
char tmp[8192];
size_t n;
while ((n = fread(tmp, 1, sizeof(tmp), fp)) > 0) {
if (b.len + n > max) {
cst_buf_free(&b);
fclose(fp);
return NULL;
}
cst_buf_append(&b, tmp, n);
}
fclose(fp);
if (!b.buf)
cst_buf_puts(&b, "");
return cst_buf_steal(&b);
}
static int find_role(const cst_agent_role_t *v, int n, const char *name) {
for (int i = 0; i < n; i++)
if (strcmp(v[i].name, name) == 0)
return i;
return -1;
}
/* "a, b ,c" -> ",a,b,c," */
static void tool_list(const char *in, char *out, size_t sz) {
size_t o = 0;
out[0] = 0;
if (!in || !in[0])
return;
if (o + 1 < sz)
out[o++] = ',';
for (const char *p = in; *p && o + 2 < sz; p++) {
if (*p == ' ' || *p == '\t' || *p == '[' || *p == ']' || *p == '"')
continue;
if (*p == ',' && out[o - 1] == ',')
continue;
out[o++] = *p;
}
if (out[o - 1] != ',' && o + 1 < sz)
out[o++] = ',';
out[o] = 0;
if (strcmp(out, ",") == 0)
out[0] = 0;
}
static int truthy(const char *v) {
return strcmp(v, "true") == 0 || strcmp(v, "yes") == 0 ||
strcmp(v, "1") == 0;
}
static void load_dir(const char *dir, const char *source, cst_agent_role_t *v,
int *n, int max) {
#ifndef _WIN32
DIR *d = opendir(dir);
if (!d)
return;
struct dirent *de;
while ((de = readdir(d)) != NULL && *n < max) {
size_t len = strlen(de->d_name);
if (de->d_name[0] == '.' || len < 4 ||
strcmp(de->d_name + len - 3, ".md") != 0)
continue;
char p[OS_PATH_MAX];
if (snprintf(p, sizeof(p), "%s/%s", dir, de->d_name) >= (int)sizeof(p))
continue;
char *text = slurp(p, ROLE_FILE_MAX);
if (!text)
continue;
char name[64], val[1024];
if (!cst_fm_get(text, "name", name, sizeof(name)) || !name[0])
snprintf(name, sizeof(name), "%.*s", (int)(len - 3), de->d_name);
char clean[64];
cst_tool_sanitize_name(name, clean, sizeof(clean));
int at = find_role(v, *n, clean);
if (at >= 0 && strcmp(v[at].source, "builtin") != 0) {
free(text); /* the workspace one came first */
continue;
}
cst_agent_role_t *r = at >= 0 ? &v[at] : &v[(*n)++];
free(r->prompt);
memset(r, 0, sizeof(*r));
snprintf(r->name, sizeof(r->name), "%s", clean);
snprintf(r->source, sizeof(r->source), "%s", source);
if (cst_fm_get(text, "description", val, sizeof(val)))
snprintf(r->desc, sizeof(r->desc), "%.255s", val);
if (cst_fm_get(text, "model", val, sizeof(val)))
snprintf(r->model, sizeof(r->model), "%.127s", val);
if (cst_fm_get(text, "tools", val, sizeof(val)))
tool_list(val, r->tools, sizeof(r->tools));
if (cst_fm_get(text, "max_steps", val, sizeof(val)))
r->max_steps = atoi(val) > 0 ? atoi(val) : 0;
if (cst_fm_get(text, "readonly", val, sizeof(val)) ||
cst_fm_get(text, "read_only", val, sizeof(val)))
r->read_only = truthy(val);
const char *body = cst_fm_body(text);
while (*body == '\n' || *body == '\r')
body++;
r->prompt = strdup(body);
free(text);
}
closedir(d);
#else
(void)dir;
(void)source;
(void)v;
(void)n;
(void)max;
#endif
}
int cst_roles_list(const char *ws, cst_agent_role_t *out, int max) {
int n = 0;
for (int i = 0; i < NBUILTIN && n < max; i++, n++) {
memset(&out[n], 0, sizeof(out[n]));
snprintf(out[n].name, sizeof(out[n].name), "%s", BUILTIN[i].name);
snprintf(out[n].desc, sizeof(out[n].desc), "%s", BUILTIN[i].desc);
snprintf(out[n].source, sizeof(out[n].source), "builtin");
out[n].read_only = BUILTIN[i].read_only;
out[n].prompt = strdup(BUILTIN[i].prompt);
}
char p[OS_PATH_MAX];
if (ws && ws[0]) {
snprintf(p, sizeof(p), "%.4000s/.agent/agents", ws);
load_dir(p, "workspace", out, &n, max);
}
char cfg[OS_PATH_MAX];
if (os_config_dir(cfg, sizeof(cfg)) == 0) {
snprintf(p, sizeof(p), "%.4000s/agents", cfg);
load_dir(p, "user", out, &n, max);
}
/* custom roles after the built-ins, by name */
for (int i = NBUILTIN + 1; i < n; i++)
for (int j = i; j > NBUILTIN && strcmp(out[j - 1].name, out[j].name) > 0;
j--) {
cst_agent_role_t t = out[j];
out[j] = out[j - 1];
out[j - 1] = t;
}
return n;
}
void cst_roles_free(cst_agent_role_t *v, int n) {
for (int i = 0; i < n; i++) {
free(v[i].prompt);
v[i].prompt = NULL;
}
}
char *cst_subagent_index(const char *ws) {
cst_agent_role_t *v = calloc(CST_ROLES_MAX, sizeof(*v));
if (!v)
return NULL;
int n = cst_roles_list(ws, v, CST_ROLES_MAX);
cst_buf_t b = {0};
cst_buf_puts(&b, "\nSub-agents (task tool, agent=<name>; default "
"general):\n");
for (int i = 0; i < n && b.len < INDEX_MAX; i++)
cst_buf_printf(&b, "- %s%s: %.200s\n", v[i].name,
v[i].read_only ? " (read-only)" : "", v[i].desc);
cst_roles_free(v, n);
free(v);
return cst_buf_steal(&b);
}
/* ---------- transcripts ---------- */
static int sub_dir(const char *ws, const char *parent_id, char *out,
size_t sz) {
char dir[OS_PATH_MAX];
if (!parent_id || !parent_id[0] || strchr(parent_id, '/') ||
strchr(parent_id, '\\') ||
cst_agent_store_dir(ws, dir, sizeof(dir)) != 0)
return -1;
return snprintf(out, sz, "%s/%s", dir, parent_id) < (int)sz ? 0 : -1;
}
int cst_subagent_path(const char *ws, const char *parent_id, const char *file,
char *out, size_t sz) {
char dir[OS_PATH_MAX];
if (!file || strchr(file, '/') || strchr(file, '\\') ||
sub_dir(ws, parent_id, dir, sizeof(dir)) != 0)
return -1;
return snprintf(out, sz, "%s/%s.jsonl", dir, file) < (int)sz ? 0 : -1;
}
/* Reserve the next sub-<n>.jsonl. */
static int next_file(const char *dir, char *file, size_t fsz) {
pthread_mutex_lock(&g_seq_mu);
int best = 0;
#ifndef _WIN32
DIR *d = opendir(dir);
if (d) {
struct dirent *de;
while ((de = readdir(d)) != NULL) {
int k = 0;
if (sscanf(de->d_name, "sub-%d.jsonl", &k) == 1 && k > best)
best = k;
}
closedir(d);
}
#endif
int rc = -1;
for (int k = best + 1; k < best + 100; k++) {
char p[OS_PATH_MAX];
snprintf(file, fsz, "sub-%d", k);
if (snprintf(p, sizeof(p), "%s/%s.jsonl", dir, file) >= (int)sizeof(p))
break;
struct stat st;
if (stat(p, &st) == 0)
continue;
FILE *fp = fopen(p, "a");
if (fp) {
fclose(fp);
rc = 0;
}
break;
}
pthread_mutex_unlock(&g_seq_mu);
return rc;
}
static void write_meta(const char *dir, const char *file, const char *role,
const char *desc, const char *status, int steps,
int tools, double ms) {
char p[OS_PATH_MAX];
if (snprintf(p, sizeof(p), "%s/%s.json", dir, file) >= (int)sizeof(p))
return;
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "role", role);
cJSON_AddStringToObject(o, "description", desc);
cJSON_AddStringToObject(o, "status", status);
cJSON_AddNumberToObject(o, "steps", steps);
cJSON_AddNumberToObject(o, "tools", tools);
cJSON_AddNumberToObject(o, "ms", ms);
char *txt = cJSON_PrintUnformatted(o);
cJSON_Delete(o);
FILE *fp = txt ? fopen(p, "w") : NULL;
if (fp) {
fputs(txt, fp);
fputc('\n', fp);
fclose(fp);
}
free(txt);
}
int cst_subagent_list(const char *ws, const char *parent_id,
cst_subagent_rec_t *out, int max) {
char dir[OS_PATH_MAX];
if (sub_dir(ws, parent_id, dir, sizeof(dir)) != 0)
return 0;
int n = 0;
#ifndef _WIN32
DIR *d = opendir(dir);
if (!d)
return 0;
struct dirent *de;
while ((de = readdir(d)) != NULL && n < max) {
int k = 0;
char tail[8] = "";
if (sscanf(de->d_name, "sub-%d.%7s", &k, tail) != 2 ||
strcmp(tail, "json") != 0)
continue;
char p[OS_PATH_MAX];
if (snprintf(p, sizeof(p), "%s/%s", dir, de->d_name) >= (int)sizeof(p))
continue;
char *txt = slurp(p, 64 * 1024);
cJSON *o = txt ? cJSON_Parse(txt) : NULL;
free(txt);
cst_subagent_rec_t *r = &out[n];
memset(r, 0, sizeof(*r));
snprintf(r->file, sizeof(r->file), "sub-%d", k);
const cJSON *it;
if ((it = cJSON_GetObjectItemCaseSensitive(o, "role")) && it->valuestring)
snprintf(r->role, sizeof(r->role), "%s", it->valuestring);
if ((it = cJSON_GetObjectItemCaseSensitive(o, "description")) &&
it->valuestring)
snprintf(r->desc, sizeof(r->desc), "%s", it->valuestring);
if ((it = cJSON_GetObjectItemCaseSensitive(o, "status")) &&
it->valuestring)
snprintf(r->status, sizeof(r->status), "%s", it->valuestring);
if ((it = cJSON_GetObjectItemCaseSensitive(o, "steps")))
r->steps = it->valueint;
if ((it = cJSON_GetObjectItemCaseSensitive(o, "tools")))
r->tools = it->valueint;
if ((it = cJSON_GetObjectItemCaseSensitive(o, "ms")))
r->ms = it->valuedouble;
cJSON_Delete(o);
struct stat st;
if (stat(p, &st) == 0)
r->mtime = (long long)st.st_mtime;
n++;
}
closedir(d);
#else
(void)out;
(void)max;
#endif
/* newest first: higher number */
for (int i = 1; i < n; i++)
for (int j = i; j > 0 && atoi(out[j - 1].file + 4) < atoi(out[j].file + 4);
j--) {
cst_subagent_rec_t t = out[j];
out[j] = out[j - 1];
out[j - 1] = t;
}
return n;
}
/* ---------- the task tool ---------- */
typedef struct {
const cst_agent_hooks_t *ph;
const ctl_tool_call_t *pcall;
const char *role;
const char *ws;
char last[2][80];
int ntools;
volatile int *parent_cancel;
cst_agent_t *child;
atomic_int done;
} sub_run_t;
static const char *arg(const ctl_tool_call_t *c, const char *key) {
for (int i = 0; i < c->nargs; i++)
if (strcmp(c->args[i].key, key) == 0)
return c->args[i].value ? c->args[i].value : "";
return NULL;
}
/* "grep foo" / "read_file src/x.c" for the progress line. */
static void step_summary(const ctl_tool_call_t *c, const char *ws, char *out,
size_t sz) {
static const char *keys[] = {"path", "pattern", "cmd", "command", "url",
"query", "name", "file"};
const char *v = NULL;
for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]) && !v; i++)
v = arg(c, keys[i]);
char one[48] = "";
size_t wl = ws ? strlen(ws) : 0;
if (v && wl && strncmp(v, ws, wl) == 0 && v[wl] == '/' && v[wl + 1])
v += wl + 1;
if (v) {
size_t k = 0;
for (; v[k] && v[k] != '\n' && k < 40; k++)
one[k] = v[k];
while (k > 0 && ((unsigned char)one[k] & 0xC0) == 0x80 && v[k])
k--;
one[k] = 0;
}
snprintf(out, sz, "%.30s%s%.40s", c->name, one[0] ? " " : "", one);
}
static void sub_tool_event(const char *phase, const ctl_tool_call_t *call,
const char *detail, void *ud) {
(void)detail;
sub_run_t *r = ud;
if (!call || strcmp(phase, "run") != 0)
return;
r->ntools++;
memcpy(r->last[0], r->last[1], sizeof(r->last[0]));
step_summary(call, r->ws, r->last[1], sizeof(r->last[1]));
if (!r->ph || !r->ph->on_tool_event)
return;
char line[256];
snprintf(line, sizeof(line), "%s%s%s · %s", r->role,
r->last[0][0] ? " · " : "", r->last[0], r->last[1]);
pthread_mutex_lock(&g_hook_mu);
r->ph->on_tool_event("progress", r->pcall, line, r->ph->userdata);
pthread_mutex_unlock(&g_hook_mu);
}
static int sub_permission(const char *tool, const char *detail,
const char *preview, void *ud) {
sub_run_t *r = ud;
if (!r->ph || !r->ph->on_permission)
return CST_PERM_DENY;
cst_buf_t d = {0};
size_t sbl = strlen(CST_PERM_SANDBOX);
if (detail && strncmp(detail, CST_PERM_SANDBOX, sbl) == 0) {
cst_buf_printf(&d, "%s[%s] %s", CST_PERM_SANDBOX, r->role, detail + sbl);
} else {
cst_buf_printf(&d, "[%s] %s", r->role, detail ? detail : tool);
}
/* one question at a time, whichever sub-agent asks */
pthread_mutex_lock(&g_hook_mu);
int ans = r->ph->on_permission(tool, d.buf ? d.buf : tool, preview,
r->ph->userdata);
pthread_mutex_unlock(&g_hook_mu);
cst_buf_free(&d);
return ans;
}
static void *watch_main(void *p) {
sub_run_t *r = p;
while (!atomic_load(&r->done)) {
if (r->parent_cancel && *r->parent_cancel)
cst_agent_request_cancel(r->child);
struct timespec ts = {0, 50 * 1000000L};
nanosleep(&ts, NULL);
}
return NULL;
}
static int out_text(cst_tool_result_t *r, int code, const char *text) {
r->exit_code = code;
r->out = strdup(text);
r->out_len = r->out ? strlen(r->out) : 0;
return 0;
}
static int task_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r, void *ud) {
(void)ud;
if (!ctx->agent)
return out_text(r, 1, "error: task needs a running agent");
const cst_agent_config_t *pcfg = cst_agent_config(ctx->agent);
if (pcfg->depth > 0)
return out_text(r, 1, "error: a sub-agent cannot start sub-agents; do "
"the work yourself");
const char *desc = arg(c, "description");
const char *prompt = arg(c, "prompt");
const char *want = arg(c, "agent");
if (!want || !want[0])
want = "general";
if (!prompt || !prompt[0])
return out_text(r, 1, "error: prompt is required");
if (!desc || !desc[0])
desc = want;
cst_agent_role_t *roles = calloc(CST_ROLES_MAX, sizeof(*roles));
if (!roles)
return out_text(r, 1, "error: out of memory");
int nr = cst_roles_list(ctx->workspace_root, roles, CST_ROLES_MAX);
int ri = find_role(roles, nr, want);
if (ri < 0) {
cst_buf_t b = {0};
cst_buf_printf(&b, "error: no agent named \"%s\" (available:", want);
for (int i = 0; i < nr; i++)
cst_buf_printf(&b, "%s %s", i ? "," : "", roles[i].name);
cst_buf_puts(&b, ")");
cst_roles_free(roles, nr);
free(roles);
r->exit_code = 1;
r->out_len = b.len;
r->out = cst_buf_steal(&b);
return 0;
}
const cst_agent_role_t *role = &roles[ri];
char pid[256], dir[OS_PATH_MAX], file[64], path[OS_PATH_MAX];
cst_agent_session_id(ctx->agent, pid, sizeof(pid));
if (sub_dir(ctx->workspace_root, pid, dir, sizeof(dir)) != 0 ||
os_mkdir_p(dir, 0755) != 0 || next_file(dir, file, sizeof(file)) != 0 ||
snprintf(path, sizeof(path), "%s/%s.jsonl", dir, file) >=
(int)sizeof(path)) {
cst_roles_free(roles, nr);
free(roles);
return out_text(r, 1, "error: cannot create the sub-agent transcript");
}
cst_agent_config_t cc = *pcfg;
cc.depth = pcfg->depth + 1;
cc.plan_mode = 0;
cc.read_only = role->read_only || pcfg->plan_mode;
snprintf(cc.tools_only, sizeof(cc.tools_only), "%s", role->tools);
cst_tool_set_disabled(&cc, "ask_user", 1);
cc.role_prompt = role->prompt;
if (role->model[0])
snprintf(cc.model, sizeof(cc.model), "%s", role->model);
if (role->max_steps > 0)
cc.max_steps = role->max_steps;
snprintf(cc.session_path, sizeof(cc.session_path), "%s", path);
cst_agent_undo_key(ctx->agent, cc.undo_sid, sizeof(cc.undo_sid),
cc.undo_tag, sizeof(cc.undo_tag));
volatile int *parent_cancel = os_get_cancel_flag();
sub_run_t run;
memset(&run, 0, sizeof(run));
run.ph = ctx->hooks;
run.pcall = c;
run.role = role->name;
run.ws = ctx->workspace_root;
run.parent_cancel = parent_cancel;
run.child = cst_agent_create(&cc);
if (!run.child) {
cst_roles_free(roles, nr);
free(roles);
return out_text(r, 1, "error: cannot start the sub-agent");
}
write_meta(dir, file, role->name, desc, "running", 0, 0, 0);
cst_agent_hooks_t h;
memset(&h, 0, sizeof(h));
h.on_tool_event = sub_tool_event;
h.on_permission = sub_permission;
h.userdata = &run;
if (run.ph && run.ph->on_tool_event) {
char line[160];
snprintf(line, sizeof(line), "%s · starting", role->name);
pthread_mutex_lock(&g_hook_mu);
run.ph->on_tool_event("progress", c, line, run.ph->userdata);
pthread_mutex_unlock(&g_hook_mu);
}
pthread_t watcher;
int watching = pthread_create(&watcher, NULL, watch_main, &run) == 0;
double t0 = os_now_ms();
char err[512] = "";
int rc = cst_agent_run_turn_ex(run.child, prompt, &h, err, sizeof(err));
double ms = os_now_ms() - t0;
atomic_store(&run.done, 1);
if (watching)
pthread_join(watcher, NULL);
int cancelled = parent_cancel && *parent_cancel;
/* the report is the last assistant text after the prompt */
cst_session_t *s = cst_agent_session(run.child);
int n = cst_session_count(s), steps = 0;
const char *final = NULL;
int seen_user = 0;
for (int i = 0; i < n; i++) {
const cst_message_t *m = cst_session_get(s, i);
if (m->role == CST_ROLE_USER && !m->tool_call_id[0] && !seen_user) {
seen_user = 1;
continue;
}
if (!seen_user || m->role != CST_ROLE_ASSISTANT)
continue;
steps++;
if (m->content && m->content[0] && !m->tool_calls_json)
final = m->content;
}
long tokens = cst_agent_last_prompt_tokens(run.child);
cst_buf_t b = {0};
if (rc != 0)
cst_buf_printf(&b, "error: sub-agent %s: %s\n", cancelled ? "cancelled"
: "failed",
err[0] ? err : "unknown error");
if (final) {
while (*final == ' ' || *final == '\n')
final++;
cst_buf_puts(&b, final);
} else if (rc == 0) {
cst_buf_puts(&b, "(the sub-agent ended without a final report; its "
"transcript has the details)");
}
while (b.len && (b.buf[b.len - 1] == '\n' || b.buf[b.len - 1] == ' '))
b.buf[--b.len] = 0;
char tok[32] = "";
if (tokens > 0)
snprintf(tok, sizeof(tok), " · %.1fk tokens", (double)tokens / 1000.0);
cst_buf_printf(&b,
"\n\n[task %s · %d step%s · %d tool call%s · %.1f s%s · "
"transcript .agent/sessions/%s/%s.jsonl]",
role->name, steps, steps == 1 ? "" : "s", run.ntools,
run.ntools == 1 ? "" : "s", ms / 1000.0, tok, pid, file);
write_meta(dir, file, role->name, desc,
rc == 0 ? "done" : cancelled ? "cancelled" : "error", steps,
run.ntools, ms);
cst_agent_destroy(run.child);
/* run_turn and destroy re-pointed this thread's cancel flag */
os_set_cancel_flag(parent_cancel);
cst_roles_free(roles, nr);
free(roles);
r->exit_code = rc == 0 ? 0 : cancelled ? 130 : 1;
r->out_len = b.len;
r->out = cst_buf_steal(&b);
return 0;
}
/* Read-only roles may run side by side. */
static int task_parallel(const ctl_tool_call_t *c, void *ud) {
(void)ud;
const char *want = arg(c, "agent");
if (!want || !want[0])
return 0; /* general */
for (int i = 0; i < NBUILTIN; i++)
if (strcmp(BUILTIN[i].name, want) == 0)
return BUILTIN[i].read_only;
cst_agent_role_t *v = calloc(CST_ROLES_MAX, sizeof(*v));
if (!v)
return 0;
int n = cst_roles_list(g_ws, v, CST_ROLES_MAX);
int i = find_role(v, n, want);
int ro = i >= 0 && v[i].read_only;
cst_roles_free(v, n);
free(v);
return ro;
}
int cst_subagent_register_tool(const char *ws) {
snprintf(g_ws, sizeof(g_ws), "%s", ws ? ws : "");
if (cst_tool_find("task"))
return 0;
cst_agent_role_t *v = calloc(CST_ROLES_MAX, sizeof(*v));
if (!v)
return -1;
int n = cst_roles_list(g_ws, v, CST_ROLES_MAX);
cst_buf_t names = {0};
for (int i = 0; i < n; i++)
cst_buf_printf(&names, "%s%s", i ? ", " : "", v[i].name);
cst_roles_free(v, n);
free(v);
cst_buf_t schema = {0};
cst_buf_printf(
&schema,
"{\"type\":\"object\",\"properties\":{"
"\"description\":{\"type\":\"string\",\"description\":\"3-6 word "
"summary shown to the user\"},"
"\"prompt\":{\"type\":\"string\",\"description\":\"the complete task: "
"goal, context, constraints and what to report back\"},"
"\"agent\":{\"type\":\"string\",\"description\":\"one of: %s "
"(default general)\"}},"
"\"required\":[\"description\",\"prompt\"]}",
names.buf ? names.buf : "general");
cst_buf_free(&names);
cst_tool_def_t d;
memset(&d, 0, sizeof(d));
d.name = "task";
d.desc = "Hand a self-contained task to a sub-agent with its own context; "
"you get back only its final report. Use it for broad searches "
"or investigations that would flood your context, and for "
"independent subtasks. It sees none of this conversation, so put "
"everything it needs in prompt. Read-only agents (explore, plan) "
"called together in one step run in parallel.";
d.schema_json = schema.buf;
d.perm = CST_TP_ASK;
d.run_ex = task_run;
d.parallel = task_parallel;
d.owner = "builtin:task";
int rc = cst_tool_register(&d);
cst_buf_free(&schema);
return rc;
}