forked from WHC/Hermit
Usability pass: tool registry, TUI polish, saved keys and settings
Tools - Table-driven tool registry with permission classes and default/minimal profiles; new tools: edit_file, list_dir, glob, grep, delete_file, move_file, todo_write, web_fetch, ask_user (plus read_file paging). - Built-in Myers diff; write tools return unified diffs. - Persistent permission rules (.agent/permissions.json), per-turn undo snapshots (.agent/undo), AGENTS.md project notes, auto-compact at 80% of the model context, cancellable run_shell with process-group kill. TUI - Table-driven slash commands: /provider /key /effort /status /tools /permissions /yolo /undo /retry /diff /export /copy /rename /delete /init /max-steps /todo, with / and @ completion. - Permission modal with diff and prefix rules, ask_user modal, todo panel, header with model/effort/context, bracketed paste, line editing, wide-char cursor, NO_COLOR and ASCII fallback; Esc never quits. Startup and configuration - API keys are entered in the TUI (hidden) and saved per provider in <config>/credentials (0600); env variables are only a fallback. - Last provider, per-provider model and reasoning effort are remembered in <config>/settings.json; --effort/-e and /effort send reasoning_effort (low|medium|high|max) to models that accept it. - DeepSeek preset uses deepseek-flash / deepseek-v4-pro. - Session flags --new/--continue/--resume/--list-sessions, --yolo, --confirm. Compatibility and tests - ncurses detection, macOS/Homebrew support, make install, XDG config dir, POSIX cleanups; Gitea CI (gcc/clang/asan/musl/macOS). - Unit tests for diff, fs tools, registry, undo/perms and config; e2e fixtures multi_file_refactor and ask_or_todo; vendored cJSON.
This commit is contained in:
+194
-14
@@ -11,7 +11,9 @@ extern "C" {
|
||||
|
||||
#define CST_AGENT_MAX_MSG 256
|
||||
#define CST_AGENT_MAX_CONTENT (512 * 1024)
|
||||
#define CST_AGENT_CTX_WINDOW 40 /* max non-system msgs sent to LLM */
|
||||
#define CST_AGENT_CTX_WINDOW 80 /* max non-system msgs sent to LLM */
|
||||
#define CST_DEFAULT_MAX_STEPS 30
|
||||
#define CST_DEFAULT_TOOL_OUT_CAP (16 * 1024)
|
||||
|
||||
typedef enum {
|
||||
CST_ROLE_SYSTEM = 0,
|
||||
@@ -20,12 +22,39 @@ typedef enum {
|
||||
CST_ROLE_TOOL
|
||||
} cst_role_t;
|
||||
|
||||
/* How tool calls travel between model and runtime.
|
||||
* native-eos : OpenAI tools[]; execute after the stream ends (baseline).
|
||||
* native-inc : OpenAI tools[]; execute as soon as one call's arguments are
|
||||
* complete (next index started / finish_reason).
|
||||
* ctl : CTL frames in plain text, length-prefixed args; execute on
|
||||
* confirmed `end` while the stream is still running.
|
||||
* ctl-heredoc: same, prompt teaches `arg <key> <<TAG ... TAG` bodies.
|
||||
* xml : MiniMax <minimax:tool_call> text; execute after the stream. */
|
||||
typedef enum {
|
||||
CST_PROTO_NATIVE_EOS = 0,
|
||||
CST_PROTO_NATIVE_INC,
|
||||
CST_PROTO_CTL,
|
||||
CST_PROTO_CTL_HEREDOC,
|
||||
CST_PROTO_XML
|
||||
} cst_protocol_t;
|
||||
|
||||
const char *cst_protocol_name(cst_protocol_t p);
|
||||
int cst_protocol_parse(const char *s, cst_protocol_t *out);
|
||||
int cst_protocol_is_native(cst_protocol_t p);
|
||||
int cst_protocol_is_ctl(cst_protocol_t p);
|
||||
/* Text protocols carry calls/results in message text (no tools[]). */
|
||||
int cst_protocol_is_text(cst_protocol_t p);
|
||||
/* Eager protocols execute a call during the stream, at confirmation time. */
|
||||
int cst_protocol_is_eager(cst_protocol_t p);
|
||||
|
||||
typedef struct {
|
||||
cst_role_t role;
|
||||
char *content;
|
||||
char tool_call_id[CTL_MAX_ID]; /* for tool results */
|
||||
char tool_call_id[CTL_MAX_ID]; /* tool results (role=tool, or text-protocol
|
||||
result frames sent as role=user) */
|
||||
char tool_name[CTL_MAX_NAME];
|
||||
char *tool_calls_json; /* assistant: OpenAI tool_calls array JSON, or NULL */
|
||||
char *meta_json; /* optional JSON object (kind, protocol, ...) */
|
||||
} cst_message_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -34,10 +63,18 @@ typedef struct {
|
||||
char api_base[512];
|
||||
char api_key[512];
|
||||
char model[128];
|
||||
char provider[32];
|
||||
char effort[16]; /* low | medium | high | max */
|
||||
cst_protocol_t protocol;
|
||||
int max_steps; /* model calls per user turn */
|
||||
size_t tool_out_cap; /* bytes of one tool output sent to the model */
|
||||
int headless;
|
||||
int confirm_shell; /* confirm run_shell */
|
||||
int confirm_write; /* confirm write_file / apply_patch */
|
||||
int auto_approve; /* headless / CST_AUTO_APPROVE=1 */
|
||||
int confirm_write; /* confirm file-changing tools */
|
||||
int confirm_net; /* confirm web_fetch */
|
||||
int auto_approve; /* headless / CST_AUTO_APPROVE=1 / --yolo */
|
||||
int tool_profile; /* cst_tool_profile_t (CST_TOOLS) */
|
||||
unsigned long long tools_off; /* bit i = registry tool i disabled */
|
||||
} cst_agent_config_t;
|
||||
|
||||
typedef struct cst_session cst_session_t;
|
||||
@@ -56,6 +93,10 @@ const cst_message_t *cst_session_get(const cst_session_t *s, int i);
|
||||
const char *cst_session_path(const cst_session_t *s);
|
||||
int cst_session_clear(cst_session_t *s, int keep_system);
|
||||
int cst_session_rewrite(cst_session_t *s, const cst_message_t *msgs, int n);
|
||||
/* Readers on another thread must hold the lock while using pointers returned
|
||||
* by cst_session_get (append may reallocate). Append locks internally. */
|
||||
void cst_session_lock(cst_session_t *s);
|
||||
void cst_session_unlock(cst_session_t *s);
|
||||
|
||||
/* --- workspace .agent/sessions store (OpenCode-like) --- */
|
||||
int cst_agent_store_ensure(const char *workspace);
|
||||
@@ -70,6 +111,18 @@ int cst_agent_store_resolve_default(const char *workspace, char *path_out,
|
||||
size_t path_sz, char *id_out,
|
||||
size_t id_sz);
|
||||
int cst_agent_store_list(const char *workspace, char ids[][128], int max_ids);
|
||||
typedef struct {
|
||||
char id[128];
|
||||
long long mtime;
|
||||
int messages;
|
||||
char title[128]; /* set via /rename; empty when none */
|
||||
char first_user[128]; /* first user message, one line */
|
||||
} cst_session_info_t;
|
||||
int cst_agent_store_info(const char *workspace, const char *id,
|
||||
cst_session_info_t *info);
|
||||
int cst_agent_store_title_set(const char *workspace, const char *id,
|
||||
const char *title);
|
||||
int cst_agent_store_delete(const char *workspace, const char *id);
|
||||
|
||||
/* --- tools --- */
|
||||
typedef struct {
|
||||
@@ -80,6 +133,12 @@ typedef struct {
|
||||
|
||||
void cst_tool_result_free(cst_tool_result_t *r);
|
||||
|
||||
int cst_tool_known(const char *name);
|
||||
int cst_tool_is_shell(const char *name);
|
||||
int cst_tool_is_write(const char *name);
|
||||
/* OpenAI tools[] array for every tool (cJSON*, caller frees). */
|
||||
void *cst_tools_schema(void);
|
||||
|
||||
int cst_tool_read_file(const char *workspace_root, const ctl_tool_call_t *call,
|
||||
cst_tool_result_t *r);
|
||||
int cst_tool_run_shell(const char *workspace_root, const ctl_tool_call_t *call,
|
||||
@@ -91,30 +150,72 @@ int cst_tool_write_file(const char *workspace_root, const ctl_tool_call_t *call,
|
||||
int cst_tool_dispatch(const char *workspace_root, const ctl_tool_call_t *call,
|
||||
cst_tool_result_t *r);
|
||||
|
||||
/* Build ctl_tool_call from native OpenAI tool_call fields. */
|
||||
/* Build ctl_tool_call from native OpenAI tool_call fields. Returns -1 when
|
||||
* args_json is not a JSON object (call still gets id/name). */
|
||||
int cst_tool_call_from_native(ctl_tool_call_t *out, const char *id,
|
||||
const char *name, const char *args_json);
|
||||
void cst_tool_call_clear(ctl_tool_call_t *c);
|
||||
|
||||
/* Apply unified-diff hunks (text after the ---/+++ header) to one buffer.
|
||||
* All hunks must be complete and locate unambiguously, else nothing changes.
|
||||
* On success *out is malloc'd. */
|
||||
int cst_patch_apply_buffer(const char *orig, size_t olen, const char *hunks,
|
||||
size_t hlen, char **out, size_t *out_len,
|
||||
int *nhunks, char *err, size_t err_sz);
|
||||
|
||||
/* --- LLM --- */
|
||||
typedef void (*cst_llm_on_delta_fn)(const char *data, size_t len, void *ud);
|
||||
/* args_json == NULL means the arguments were not valid JSON / too large. */
|
||||
typedef void (*cst_llm_on_native_tool_fn)(const char *id, const char *name,
|
||||
const char *args_json, void *ud);
|
||||
typedef void (*cst_llm_on_native_open_fn)(int index, const char *id,
|
||||
const char *name, void *ud);
|
||||
|
||||
typedef struct {
|
||||
long http_status;
|
||||
char finish_reason[32];
|
||||
long prompt_tokens;
|
||||
long completion_tokens;
|
||||
long cached_tokens;
|
||||
double t_first_byte_ms; /* relative to request start; -1 if none */
|
||||
double t_end_ms;
|
||||
int client_stopped; /* stream ended by stop_flag (not an error) */
|
||||
int retries;
|
||||
char error[512]; /* in-stream or HTTP error message */
|
||||
} cst_llm_stats_t;
|
||||
|
||||
typedef struct {
|
||||
const char *api_base;
|
||||
const char *api_key;
|
||||
const char *model;
|
||||
const char *reasoning_effort; /* NULL = not sent */
|
||||
const cst_message_t *msgs;
|
||||
int nmsgs;
|
||||
cst_llm_on_delta_fn on_delta;
|
||||
cst_llm_on_native_tool_fn on_native_tool; /* fired once per completed call */
|
||||
/* Fired once per call, as soon as that call's arguments are complete. */
|
||||
cst_llm_on_native_tool_fn on_native_tool;
|
||||
cst_llm_on_native_open_fn on_native_open;
|
||||
void *userdata;
|
||||
int enable_tools; /* send tools[] schema */
|
||||
const cst_agent_config_t *tool_cfg; /* active tool set; NULL = all */
|
||||
int tool_choice_none; /* "tool_choice":"none" */
|
||||
volatile int *cancel_flag; /* non-zero → abort curl */
|
||||
size_t tool_out_cap; /* wire cap for role=tool content; 0 = default */
|
||||
volatile int *cancel_flag; /* non-zero → abort (error "cancelled") */
|
||||
volatile int *stop_flag; /* non-zero → stop reading, not an error */
|
||||
cst_llm_stats_t *stats; /* optional */
|
||||
} cst_llm_request_t;
|
||||
|
||||
int cst_llm_chat_stream(const cst_llm_request_t *req, char *err, size_t err_sz);
|
||||
/* JSON request body (malloc'd) exactly as sent on the wire. */
|
||||
char *cst_llm_build_body(const cst_llm_request_t *req);
|
||||
/* Reset the CST_RECORD_SSE / CST_REPLAY_SSE sequence counter. */
|
||||
void cst_llm_trace_reset(void);
|
||||
|
||||
/* on_permission answers */
|
||||
#define CST_PERM_DENY 0
|
||||
#define CST_PERM_ONCE 1
|
||||
#define CST_PERM_ALWAYS_TOOL 2 /* saved to .agent/permissions.json */
|
||||
#define CST_PERM_ALWAYS_PREFIX 3 /* run_shell: allow this command prefix */
|
||||
|
||||
/* Live UI / observability hooks for a turn */
|
||||
typedef struct {
|
||||
@@ -123,8 +224,20 @@ typedef struct {
|
||||
void (*on_tool_event)(const char *phase, const ctl_tool_call_t *call,
|
||||
const char *detail, void *ud);
|
||||
void (*on_think_delta)(const char *text, size_t len, void *ud);
|
||||
/* Return 1=allow, 0=deny. NULL → use config auto_approve / defaults. */
|
||||
int (*on_permission)(const char *tool_name, const char *detail, void *ud);
|
||||
/* Return a CST_PERM_* answer. NULL → deny unless auto_approve. preview is
|
||||
* the diff a write call would make, the full command for run_shell, or
|
||||
* NULL. */
|
||||
int (*on_permission)(const char *tool_name, const char *detail,
|
||||
const char *preview, void *ud);
|
||||
/* ask_user tool. Fill ans and return 0, or -1 when dismissed. NULL means
|
||||
* nobody can answer (headless). */
|
||||
int (*on_ask_user)(const char *question, const char *options, char *ans,
|
||||
size_t ans_sz, void *ud);
|
||||
/* todo_write replaced the task list (one item per line). */
|
||||
void (*on_todo)(const char *text, void *ud);
|
||||
/* Full tool output, fired right after execution (before it is persisted). */
|
||||
void (*on_tool_result)(const ctl_tool_call_t *call, const char *out,
|
||||
size_t len, int exit_code, void *ud);
|
||||
void *userdata;
|
||||
} cst_agent_hooks_t;
|
||||
|
||||
@@ -143,14 +256,58 @@ void cst_agent_request_cancel(cst_agent_t *a);
|
||||
int cst_agent_new_session(cst_agent_t *a, char *id_out, size_t id_sz);
|
||||
int cst_agent_switch_session(cst_agent_t *a, const char *id);
|
||||
int cst_agent_compact(cst_agent_t *a, char *err, size_t err_sz);
|
||||
/* Bare id of the current session (file name without .jsonl). */
|
||||
void cst_agent_session_id(const cst_agent_t *a, char *out, size_t out_sz);
|
||||
/* Latest todo_write list of this session, or NULL. */
|
||||
const char *cst_agent_todo(const cst_agent_t *a);
|
||||
/* AGENTS.md (or CST.md) text injected into the system prompt, or NULL. */
|
||||
const char *cst_agent_notes(const cst_agent_t *a);
|
||||
void cst_agent_reload_notes(cst_agent_t *a);
|
||||
/* Roll back the file changes of the newest undoable turn. */
|
||||
int cst_agent_undo(cst_agent_t *a, char *report, size_t report_sz);
|
||||
int cst_agent_undo_available(const cst_agent_t *a);
|
||||
/* Persistent permission rules (see cst_perms.h). */
|
||||
struct cst_perms *cst_agent_perms(cst_agent_t *a);
|
||||
/* /retry: remove the last user prompt and its replies; caller frees. */
|
||||
char *cst_agent_rewind_last_turn(cst_agent_t *a);
|
||||
/* Last prompt_tokens reported by the model (0 if unknown). */
|
||||
long cst_agent_last_prompt_tokens(const cst_agent_t *a);
|
||||
|
||||
const char *cst_agent_system_prompt(void);
|
||||
const char *cst_agent_system_prompt_for(cst_protocol_t p);
|
||||
/* Prompt for this config's tool set and protocol, plus optional project
|
||||
* instructions (AGENTS.md). malloc'd. */
|
||||
char *cst_agent_build_prompt(const cst_agent_config_t *cfg, cst_protocol_t p,
|
||||
const char *project_notes);
|
||||
|
||||
/* Credentials: zenity + ~/.config/cst-agent/credentials (0600) */
|
||||
int cst_creds_load_api_key(char *out, size_t out_sz);
|
||||
int cst_creds_save_api_key(const char *key);
|
||||
int cst_creds_prompt_api_key(char *out, size_t out_sz);
|
||||
int cst_creds_ensure_api_key(char *out, size_t out_sz);
|
||||
/* Per-provider API keys in <config>/credentials (mode 0600), one
|
||||
* "<provider>.api_key=..." line each. */
|
||||
int cst_creds_load(const char *provider, char *out, size_t out_sz);
|
||||
int cst_creds_save(const char *provider, const char *key);
|
||||
/* Saved key, then the provider's env variable. Returns 0 when found;
|
||||
* *from (optional) gets "saved" or the variable name. */
|
||||
int cst_creds_resolve(const char *provider, char *out, size_t out_sz,
|
||||
const char **from);
|
||||
/* Hidden console prompt (no echo). */
|
||||
int cst_creds_prompt_console(const char *provider, char *out, size_t out_sz);
|
||||
|
||||
/* Last used provider, model per provider and effort, in
|
||||
* <config>/settings.json. */
|
||||
#define CST_SETTINGS_MAX_MODELS 16
|
||||
typedef struct {
|
||||
char provider[32];
|
||||
char effort[16];
|
||||
int nmodels;
|
||||
struct {
|
||||
char provider[32];
|
||||
char model[128];
|
||||
} models[CST_SETTINGS_MAX_MODELS];
|
||||
} cst_settings_t;
|
||||
int cst_settings_load(cst_settings_t *s);
|
||||
int cst_settings_save(const cst_settings_t *s);
|
||||
const char *cst_settings_model(const cst_settings_t *s, const char *provider);
|
||||
/* Record cfg's provider, model and effort as the last used ones. */
|
||||
int cst_settings_remember(const cst_agent_config_t *cfg);
|
||||
|
||||
/* JSON baseline parser for experiments (buffer until valid object). */
|
||||
typedef struct {
|
||||
@@ -161,6 +318,29 @@ typedef struct {
|
||||
|
||||
int cst_json_try_parse_tool(const char *buf, size_t len, cst_json_tool_t *out);
|
||||
|
||||
/* Provider presets (base URL + default model). */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *api_base;
|
||||
const char *model;
|
||||
const char *key_env; /* provider-specific key variable, e.g. DEEPSEEK_API_KEY */
|
||||
} cst_provider_t;
|
||||
|
||||
const cst_provider_t *cst_provider_find(const char *name);
|
||||
const cst_provider_t *cst_provider_default(void);
|
||||
int cst_provider_count(void);
|
||||
const cst_provider_t *cst_provider_at(int i);
|
||||
/* Context window of a model in tokens (CST_CTX_TOKENS overrides). */
|
||||
long cst_model_context_tokens(const char *model);
|
||||
|
||||
/* Reasoning effort: low | medium | high | max. */
|
||||
#define CST_DEFAULT_EFFORT "medium"
|
||||
int cst_effort_valid(const char *effort);
|
||||
/* Value for "reasoning_effort", or NULL when this provider/model does not
|
||||
* take the parameter. */
|
||||
const char *cst_effort_wire(const char *provider, const char *model,
|
||||
const char *effort);
|
||||
|
||||
/* Built-in / CST_MODELS model picker list. Writes up to max_out names.
|
||||
* Always includes `current` if non-empty and not already listed. */
|
||||
int cst_models_list(const char *current, char out[][128], int max_out);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef CST_METRICS_H
|
||||
#define CST_METRICS_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Append-only experiment log: <workspace>/.agent/metrics/<session>.jsonl
|
||||
* (override with CST_METRICS_PATH, disable with CST_METRICS=0). */
|
||||
typedef struct cst_metrics cst_metrics_t;
|
||||
|
||||
cst_metrics_t *cst_metrics_open(const char *workspace, const char *session_path);
|
||||
void cst_metrics_close(cst_metrics_t *m);
|
||||
/* Writes one line and frees obj (a cJSON*). NULL-safe. */
|
||||
void cst_metrics_emit(cst_metrics_t *m, void *obj);
|
||||
const char *cst_metrics_path(const cst_metrics_t *m);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef CST_PERMS_H
|
||||
#define CST_PERMS_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct cst_perms cst_perms_t;
|
||||
|
||||
/* Loads <workspace>/.agent/permissions.json (missing file = no rules). */
|
||||
cst_perms_t *cst_perms_open(const char *workspace);
|
||||
void cst_perms_close(cst_perms_t *p);
|
||||
|
||||
int cst_perms_tool_allowed(const cst_perms_t *p, const char *tool);
|
||||
/* Prefix rule match; commands with ; & | ` $ < > ( ) never match. */
|
||||
int cst_perms_shell_allowed(const cst_perms_t *p, const char *cmd);
|
||||
/* Add a rule and save. */
|
||||
int cst_perms_allow_tool(cst_perms_t *p, const char *tool);
|
||||
int cst_perms_allow_prefix(cst_perms_t *p, const char *prefix);
|
||||
int cst_perms_reset(cst_perms_t *p);
|
||||
char *cst_perms_describe(const cst_perms_t *p);
|
||||
/* Reasonable prefix rule for a command ("" when it should not have one). */
|
||||
void cst_perms_suggest_prefix(const char *cmd, char *out, size_t out_sz);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,107 @@
|
||||
#ifndef CST_TOOLS_H
|
||||
#define CST_TOOLS_H
|
||||
|
||||
#include "cst_agent.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define CST_TOOL_MAX_PARAMS 6
|
||||
#define CST_TOOL_MAX_TARGETS 16
|
||||
|
||||
/* Up to max workspace-relative regular files (ignores .git, node_modules,
|
||||
* root .gitignore ...). Caller frees each entry and the array. */
|
||||
int cst_workspace_files(const char *ws, char ***out, int max);
|
||||
|
||||
/* What a call can touch; drives confirmation, undo and duplicate-read logic. */
|
||||
typedef enum {
|
||||
CST_TP_READ = 0, /* reads the workspace */
|
||||
CST_TP_WRITE, /* changes files (snapshotted for /undo) */
|
||||
CST_TP_SHELL, /* arbitrary commands */
|
||||
CST_TP_NET, /* network access */
|
||||
CST_TP_ASK, /* talks to the user */
|
||||
CST_TP_META /* agent bookkeeping (todo list) */
|
||||
} cst_tool_perm_t;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *type; /* "string" | "integer" | "boolean" */
|
||||
int required;
|
||||
const char *desc;
|
||||
} cst_tool_param_t;
|
||||
|
||||
typedef struct cst_tool_ctx {
|
||||
const char *workspace_root;
|
||||
/* ask_user: fills ans; returns 0, or -1 when nobody can answer. */
|
||||
int (*ask)(const char *question, const char *options, char *ans,
|
||||
size_t ans_sz, void *ud);
|
||||
void *ask_ud;
|
||||
/* todo_write: the list was replaced (text form, one item per line). */
|
||||
void (*set_todo)(const char *text, void *ud);
|
||||
void *todo_ud;
|
||||
} cst_tool_ctx_t;
|
||||
|
||||
typedef int (*cst_tool_run_fn)(const cst_tool_ctx_t *ctx,
|
||||
const ctl_tool_call_t *call,
|
||||
cst_tool_result_t *r);
|
||||
/* Write tools: compute the diff a call would produce without touching disk.
|
||||
* Returns 0 and a malloc'd text, or -1 with an error text in *out. */
|
||||
typedef int (*cst_tool_preview_fn)(const cst_tool_ctx_t *ctx,
|
||||
const ctl_tool_call_t *call, char **out);
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *desc;
|
||||
/* `--tools minimal` schema text (the original wording; required params
|
||||
* only), so paper runs keep sending byte-identical requests. */
|
||||
const char *min_desc;
|
||||
cst_tool_param_t params[CST_TOOL_MAX_PARAMS];
|
||||
cst_tool_perm_t perm;
|
||||
int minimal; /* member of `--tools minimal` (the original four) */
|
||||
cst_tool_run_fn run;
|
||||
cst_tool_preview_fn preview;
|
||||
} cst_tool_def_t;
|
||||
|
||||
typedef enum { CST_TOOLS_DEFAULT = 0, CST_TOOLS_MINIMAL } cst_tool_profile_t;
|
||||
|
||||
int cst_tool_profile_parse(const char *s, int *out);
|
||||
const char *cst_tool_profile_name(int p);
|
||||
|
||||
int cst_tool_count(void);
|
||||
const cst_tool_def_t *cst_tool_at(int i);
|
||||
const cst_tool_def_t *cst_tool_find(const char *name);
|
||||
int cst_tool_index(const char *name);
|
||||
|
||||
/* Enabled for this config (profile + /tools switches). cfg may be NULL. */
|
||||
int cst_tool_active(const cst_agent_config_t *cfg, const cst_tool_def_t *t);
|
||||
/* OpenAI tools[] for the active set (cJSON*, caller frees). */
|
||||
void *cst_tools_schema_cfg(const cst_agent_config_t *cfg);
|
||||
/* "Tools:\n- name(a, b?): ...\n" for the active set (malloc'd). */
|
||||
char *cst_tools_prompt(const cst_agent_config_t *cfg);
|
||||
/* Comma-separated active tool names (malloc'd). */
|
||||
char *cst_tools_names(const cst_agent_config_t *cfg);
|
||||
|
||||
/* Missing required args / bad integers. 0 = ok. */
|
||||
int cst_tool_validate(const cst_tool_def_t *t, const ctl_tool_call_t *call,
|
||||
char *err, size_t err_sz);
|
||||
int cst_tool_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *call,
|
||||
cst_tool_result_t *r);
|
||||
int cst_tool_preview(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *call,
|
||||
char **out);
|
||||
|
||||
/* Workspace-relative files a write call may change (for /undo snapshots).
|
||||
* Returns the count written into paths. */
|
||||
int cst_tool_targets(const ctl_tool_call_t *call, char paths[][1024], int max);
|
||||
|
||||
/* Unified diff (3 lines of context, GNU `diff -u` layout) with
|
||||
* `--- a/<label>` / `+++ b/<label>` headers. Returns malloc'd text; "" when
|
||||
* identical. */
|
||||
char *cst_diff_unified(const char *label, const char *a, size_t alen,
|
||||
const char *b, size_t blen);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef CST_UNDO_H
|
||||
#define CST_UNDO_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Save rel (workspace-relative) once per (session, tag) before it changes.
|
||||
* A missing file is recorded as absent so undo deletes it again. */
|
||||
int cst_undo_snapshot(const char *ws, const char *sid, const char *tag,
|
||||
const char *rel);
|
||||
/* Roll back the newest tag and drop it. report lists what happened. */
|
||||
int cst_undo_restore_last(const char *ws, const char *sid, char *report,
|
||||
size_t report_sz);
|
||||
/* Number of turns that can be undone. */
|
||||
int cst_undo_count(const char *ws, const char *sid);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef CST_UTIL_H
|
||||
#define CST_UTIL_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
char *buf;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
} cst_buf_t;
|
||||
|
||||
int cst_buf_append(cst_buf_t *b, const char *s, size_t n);
|
||||
int cst_buf_puts(cst_buf_t *b, const char *s);
|
||||
int cst_buf_printf(cst_buf_t *b, const char *fmt, ...);
|
||||
void cst_buf_free(cst_buf_t *b);
|
||||
/* Take ownership of the NUL-terminated buffer (never NULL on success). */
|
||||
char *cst_buf_steal(cst_buf_t *b);
|
||||
|
||||
/* Largest n' <= n that does not split a UTF-8 sequence of s. */
|
||||
size_t cst_utf8_floor(const char *s, size_t n);
|
||||
/* Copy s[0..n) replacing invalid UTF-8 with '?'. malloc'd, NUL-terminated. */
|
||||
char *cst_utf8_sanitize(const char *s, size_t n, size_t *out_len);
|
||||
|
||||
uint64_t cst_fnv1a64(uint64_t h, const void *data, size_t n);
|
||||
#define CST_FNV_INIT 1469598103934665603ULL
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -71,6 +71,31 @@ typedef struct {
|
||||
|
||||
typedef void (*ctl_on_event_fn)(const ctl_event_t *ev, void *userdata);
|
||||
|
||||
/* Why the stream left the contract (for protocol-adherence experiments). */
|
||||
typedef enum {
|
||||
CTL_EK_NONE = 0,
|
||||
CTL_EK_BAD_CALL, /* malformed `call` line */
|
||||
CTL_EK_BAD_ARG, /* malformed `arg` header / bad heredoc tag */
|
||||
CTL_EK_UNEXPECTED_LINE, /* non arg/end line inside a call (nbytes too small) */
|
||||
CTL_EK_BODY_EOF, /* EOF inside a length-prefixed body (nbytes too big) */
|
||||
CTL_EK_HEREDOC_EOF, /* EOF before the heredoc terminator */
|
||||
CTL_EK_LINE_TOO_LONG,
|
||||
CTL_EK_LIMIT, /* too many args / body over CTL_MAX_ARG_BYTES */
|
||||
CTL_EK_OOM
|
||||
} ctl_error_kind_t;
|
||||
|
||||
typedef struct {
|
||||
size_t calls_open;
|
||||
size_t calls_ready;
|
||||
size_t args_nbytes; /* arg <key> <n> */
|
||||
size_t args_inline; /* arg <key> <value on the same line> */
|
||||
size_t args_heredoc; /* arg <key> <<TAG ... TAG */
|
||||
size_t pending_peak;
|
||||
ctl_error_kind_t error_kind;
|
||||
} ctl_parser_stats_t;
|
||||
|
||||
const char *ctl_error_kind_name(ctl_error_kind_t k);
|
||||
|
||||
typedef struct ctl_parser ctl_parser_t;
|
||||
|
||||
ctl_parser_t *ctl_parser_create(ctl_on_event_fn on_event, void *userdata);
|
||||
@@ -91,6 +116,8 @@ size_t ctl_parser_pending_peak(const ctl_parser_t *p);
|
||||
/* Reset for a new assistant turn. */
|
||||
void ctl_parser_reset(ctl_parser_t *p);
|
||||
|
||||
void ctl_parser_stats(const ctl_parser_t *p, ctl_parser_stats_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -24,6 +24,7 @@ int os_path_under(const char *root, const char *path);
|
||||
|
||||
/* --- dirs / config --- */
|
||||
int os_mkdir(const char *path, int mode);
|
||||
int os_mkdir_p(const char *path, int mode);
|
||||
int os_config_dir(char *out, size_t out_sz);
|
||||
int os_temp_dir(char *out, size_t out_sz);
|
||||
/* Create unique temp file; writes path into out; returns fd or -1. Caller closes. */
|
||||
@@ -56,6 +57,15 @@ int os_run_capture(const char *cwd, const char *const argv[], size_t max_out,
|
||||
int os_run_shell_c(const char *cwd, const char *cmd, size_t max_out,
|
||||
os_run_result_t *r);
|
||||
|
||||
/* Child processes get stdin=/dev/null, their own process group, and are
|
||||
* killed after the timeout (CST_SHELL_TIMEOUT seconds, default 120) or when
|
||||
* *flag becomes non-zero. Exit code 124 = timeout, 130 = cancelled. */
|
||||
void os_set_cancel_flag(volatile int *flag);
|
||||
void os_set_run_timeout(int seconds);
|
||||
|
||||
/* Monotonic milliseconds (arbitrary epoch). */
|
||||
double os_now_ms(void);
|
||||
|
||||
/* --- misc --- */
|
||||
void os_localtime(const time_t *t, struct tm *out);
|
||||
int os_getpid(void);
|
||||
|
||||
Reference in New Issue
Block a user