feat: add v2 task control and asynchronous isolated workers

This commit is contained in:
WHC
2026-10-20 14:46:57 +08:00
parent 143dba203b
commit d971cb02b0
26 changed files with 1590 additions and 66 deletions
+118
View File
@@ -19,6 +19,8 @@
#include "cst_agent.h"
#include "cst_skills.h"
#include "cst_tools.h"
#include "cst_runtime.h"
#include "cst_worktree.h"
#include "cst_util.h"
#include "os_compat.h"
@@ -174,6 +176,12 @@ char *cst_workers_config_get(const char *ws) {
char path[OS_PATH_MAX];
workers_path(ws, path, sizeof(path));
char *s = slurp(path, ROLE_FILE_MAX);
if (!s) {
char config[OS_PATH_MAX];
if (os_config_dir(config, sizeof(config)) == 0 &&
os_path_join(path, sizeof(path), config, "workers.json") == 0)
s = slurp(path, ROLE_FILE_MAX);
}
return s ? s : strdup("{\"orchestrate\":false,\"max_steps\":20,\"max_tasks\":8,\"max_requests\":100,\"profiles\":{},\"roles\":{}}");
}
static cJSON *workers_json(const char *ws) {
@@ -705,6 +713,8 @@ static cJSON *worker_snapshot(const char *path, cst_agent_config_t *cc,
}
j = cJSON_CreateObject();
cJSON_AddStringToObject(j, "role", role);
cJSON_AddStringToObject(j, "workspace", cc->workspace_root);
cJSON_AddStringToObject(j, "scope", cc->worker_scope);
cJSON_AddStringToObject(j, "provider", cc->provider);
cJSON_AddStringToObject(j, "api_base", cc->api_base);
cJSON_AddStringToObject(j, "model", cc->model);
@@ -834,6 +844,11 @@ static int task_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_agent_config_t cc = *pcfg;
cc.depth = pcfg->depth + 1;
cc.task_revision = cst_runtime_revision(pcfg->runtime);
const char *isolated = arg(c, "_worktree");
if (isolated && isolated[0]) snprintf(cc.workspace_root, sizeof(cc.workspace_root), "%s", isolated);
const char *scope = arg(c, "_scope");
if (scope) snprintf(cc.worker_scope, sizeof(cc.worker_scope), "%s", scope);
cc.plan_mode = 0;
cc.read_only = role->read_only || pcfg->plan_mode || pcfg->read_only;
snprintf(cc.tools_only, sizeof(cc.tools_only), "%s", role->tools);
@@ -883,6 +898,9 @@ static int task_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
}
cJSON_Delete(settings);
if (snapshot) {
const char *saved_workspace = json_string(snapshot, "workspace");
if (*saved_workspace) snprintf(cc.workspace_root, sizeof(cc.workspace_root), "%s", saved_workspace);
snprintf(cc.worker_scope, sizeof(cc.worker_scope), "%s", json_string(snapshot, "scope"));
snprintf(cc.provider, sizeof(cc.provider), "%s", json_string(snapshot, "provider"));
snprintf(cc.api_base, sizeof(cc.api_base), "%s", json_string(snapshot, "api_base"));
snprintf(cc.model, sizeof(cc.model), "%s", json_string(snapshot, "model"));
@@ -1137,6 +1155,105 @@ static int task_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
}
/* Read-only roles may run side by side. */
typedef struct {
cst_tool_ctx_t ctx;
cst_agent_hooks_t hooks;
ctl_tool_call_t call;
char worktree[OS_PATH_MAX];
} worker_job_t;
static void worker_job_free(void *p) {
worker_job_t *j = p; cst_tool_call_clear(&j->call); free(j);
}
static int worker_job_run(void *p, volatile int *cancel, cst_tool_result_t *out) {
worker_job_t *j = p; os_set_cancel_flag(cancel);
int rc = task_run(&j->ctx, &j->call, out, NULL);
if (j->worktree[0]) {
cst_buf_t b = {0}; cst_buf_printf(&b, "%s\n[isolated worktree=%s; use worker_diff then worker_integrate to accept changes]", out->out ? out->out : "", j->worktree);
free(out->out); out->out_len = b.len; out->out = cst_buf_steal(&b);
}
return rc;
}
static int async_worker_tool(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *out, void *ud) {
(void)ud;
if (!ctx->agent) return out_text(out, 1, "error: no agent");
const cst_agent_config_t *cfg = cst_agent_config(ctx->agent);
if (cfg->depth || !cfg->runtime) return out_text(out, 1, "error: only the main agent can schedule workers");
if (strcmp(c->name, "worker_start")) {
const char *wt = arg(c, "worktree"); int code;
if (wt && cst_runtime_resource_busy(cfg->runtime, wt)) return out_text(out, 1, "error: worker is still using this worktree; wait before review/integration");
char *text = wt ? cst_worktree_diff(ctx->workspace_root, wt, !strcmp(c->name, "worker_integrate"), &code) : NULL;
if (!text) return out_text(out, 1, "error: worktree is required");
out->out = text; out->out_len = strlen(text); out->exit_code = code; return 0;
}
if (!cst_runtime_check(cfg->runtime, cst_runtime_revision(cfg->runtime), 0, 1))
return out_text(out, 1, "error: delegation is disabled");
const char *kind = arg(c, "kind");
if (!kind || (strcmp(kind, "read") && strcmp(kind, "edit") && strcmp(kind, "external")))
return out_text(out, 1, "error: kind must be read, edit, or external");
if (!cst_runtime_check(cfg->runtime, cst_runtime_revision(cfg->runtime), strcmp(kind, "read") != 0, 1))
return out_text(out, 1, "error: task controls prohibit this worker kind");
worker_job_t *j = calloc(1, sizeof(*j)); if (!j) return -1;
j->ctx = *ctx; j->ctx.set_todo = NULL; j->ctx.todo_ud = NULL;
if (ctx->hooks) { j->hooks = *ctx->hooks; j->ctx.hooks = &j->hooks; }
cJSON *a = cJSON_CreateObject();
const char *prompt = arg(c, "prompt");
const char *description = arg(c, "description");
if (!prompt || !*prompt) { cJSON_Delete(a); free(j); return out_text(out, 1, "error: prompt required"); }
cJSON_AddStringToObject(a, "prompt", prompt);
cJSON_AddStringToObject(a, "description", description ? description : kind);
/* Typed routing prevents read-only investigations silently using Pro. */
cJSON_AddStringToObject(a, "agent", !strcmp(kind, "read") ? "explore" : "general");
const char *resume = arg(c, "worker_id"); if (resume && *resume) cJSON_AddStringToObject(a, "worker_id", resume);
if (resume && *resume) {
char sid[256], transcript[OS_PATH_MAX]; cst_agent_session_id(ctx->agent, sid, sizeof(sid));
cJSON *saved = cst_subagent_path(ctx->workspace_root, sid, resume, transcript, sizeof(transcript)) == 0 ? worker_snapshot(transcript, NULL, NULL, 0) : NULL;
if (!saved || (!strcmp(kind, "read") && !cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(saved, "readonly")))) {
cJSON_Delete(saved); cJSON_Delete(a); free(j); return out_text(out, 1, "error: invalid worker resume or mismatched task kind");
}
if (!strcmp(kind, "edit")) {
snprintf(j->worktree, sizeof(j->worktree), "%s", json_string(saved, "workspace"));
char prefix[OS_PATH_MAX]; snprintf(prefix, sizeof(prefix), "%s/.agent/v2/wt-", ctx->workspace_root);
if (strncmp(j->worktree, prefix, strlen(prefix)) || !*json_string(saved, "scope")) {
cJSON_Delete(saved); cJSON_Delete(a); free(j); return out_text(out, 1, "error: edit resume requires an isolated scoped worker");
}
}
cJSON_Delete(saved);
}
if (!strcmp(kind, "edit") && (!resume || !*resume)) {
char err[256];
if (cst_worktree_create(ctx->workspace_root, arg(c, "files"), j->worktree, sizeof(j->worktree), err, sizeof(err))) {
cJSON_Delete(a); free(j); return out_text(out, 1, err);
}
cJSON_AddStringToObject(a, "_worktree", j->worktree);
const char *files = arg(c, "files");
if (!files || strlen(files) >= sizeof(cfg->worker_scope)) { cJSON_Delete(a); free(j); return out_text(out, 1, "error: file scope is too large"); }
cJSON_AddStringToObject(a, "_scope", files);
}
const char *steps = arg(c, "max_steps"); if (steps && *steps) cJSON_AddNumberToObject(a, "max_steps", atoi(steps));
char *json = cJSON_PrintUnformatted(a); cJSON_Delete(a);
int parse = json ? cst_tool_call_from_native(&j->call, c->id, "task", json) : -1; free(json);
if (parse) { worker_job_free(j); return out_text(out, 1, "error: invalid worker arguments"); }
char id[32]; const char *resource = !strcmp(kind, "read") ? "" : j->worktree[0] ? j->worktree : "*";
char wt_copy[OS_PATH_MAX]; snprintf(wt_copy, sizeof(wt_copy), "%s", j->worktree);
if (cst_job_start(cfg->runtime, "worker", resource, strcmp(kind, "external") != 0, worker_job_run, j, worker_job_free, id, sizeof(id))) {
worker_job_free(j); return out_text(out, 1, "error: worker scheduler is full or paused");
}
char message[OS_PATH_MAX + 128]; snprintf(message, sizeof(message), "job_id=%s%s%s; running asynchronously; continue independent work or wait for the result", id, wt_copy[0] ? "; worktree=" : "", wt_copy);
return out_text(out, 0, message);
}
static int register_worker_tools(void) {
const char *names[] = {"worker_start", "worker_diff", "worker_integrate"};
for (int i = 0; i < 3; i++) if (!cst_tool_find(names[i])) {
cst_tool_def_t d = {0}; d.name = names[i]; d.owner = "builtin:worker-v2";
d.desc = i == 0 ? "Start an asynchronous typed worker. read uses Flash; edit uses Pro in an isolated Git worktree and requires files scope. external uses a protected serial resource. Use job_wait for results; small tasks should be done directly by the main agent." : i == 1 ? "Preview an isolated worker's scoped delta without changing the user's index." : "Integrate a reviewed worker delta. Conflicts preserve the worktree and patch; never overwrite user changes.";
d.schema_json = i == 0 ? "{\"type\":\"object\",\"properties\":{\"kind\":{\"type\":\"string\",\"enum\":[\"read\",\"edit\",\"external\"]},\"prompt\":{\"type\":\"string\"},\"description\":{\"type\":\"string\"},\"files\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"worker_id\":{\"type\":\"string\"},\"max_steps\":{\"type\":\"integer\",\"minimum\":1}},\"required\":[\"kind\",\"prompt\"]}" : "{\"type\":\"object\",\"properties\":{\"worktree\":{\"type\":\"string\"}},\"required\":[\"worktree\"]}";
d.perm = i == 2 ? CST_TP_WRITE : i == 1 ? CST_TP_READ : CST_TP_ASK;
d.run_ex = async_worker_tool; if (cst_tool_register(&d)) return -1;
}
return 0;
}
static int task_parallel(const ctl_tool_call_t *c, void *ud) {
(void)ud;
if (arg(c, "worker_id")) return 0;
@@ -1156,6 +1273,7 @@ static int task_parallel(const ctl_tool_call_t *c, void *ud) {
int cst_subagent_register_tool(const char *ws) {
snprintf(g_ws, sizeof(g_ws), "%s", ws ? ws : "");
if (register_worker_tools()) return -1;
if (cst_tool_find("task"))
return 0;
cst_agent_role_t *v = calloc(CST_ROLES_MAX, sizeof(*v));