#include "ds4.h" #include "ds4_distributed.h" #include "ds4_help.h" #include "ds4_kvstore.h" #include "ds4_web.h" #include "linenoise.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* This is intentionally not in linenoise.h, but it is part of the existing * multiplexed editor implementation. The agent uses it only to restore text * after Enter is pressed while the model is still busy. */ int linenoiseEditInsert(struct linenoiseState *l, const char *c, size_t clen); static int set_nonblock(int fd, bool on, int *old_flags); static bool agent_parse_bool_default(const char *s, bool def); /* ============================================================================ * Configuration, Worker State, And Streaming Types * ============================================================================ * * The agent is intentionally a single process: the UI thread owns terminal * input/output, while the worker thread owns the live DS4 session and KV state. * These types define the shared state and the small streaming state machines * used to render sampled assistant text and DSML tool calls as they arrive. */ typedef struct { const char *prompt; const char *system; const char *trace_path; int n_predict; int ctx_size; float temperature; float top_p; float min_p; uint64_t seed; ds4_think_mode think_mode; } agent_generation_options; typedef struct { ds4_engine_options engine; agent_generation_options gen; const char *chdir_path; bool non_interactive; } agent_config; typedef enum { AGENT_WORKER_IDLE, AGENT_WORKER_PREFILL, AGENT_WORKER_GENERATING, AGENT_WORKER_COMPACTING, AGENT_WORKER_DRAINING, AGENT_WORKER_SAVING, AGENT_WORKER_ERROR, AGENT_WORKER_STOPPED, } agent_worker_state; typedef struct { agent_worker_state state; int prefill_done; int prefill_total; unsigned prefill_label; double prefill_tps; int generated; double gen_tps; bool greedy_sampling; int ctx_used; int ctx_size; int power_percent; char error[256]; } agent_status; typedef struct agent_bash_job agent_bash_job; typedef struct { ds4_engine *engine; agent_config *cfg; ds4_session *session; ds4_tokens transcript; char *cache_dir; char *sysprompt_path; char session_sha[41]; char *session_title; uint64_t session_created_at; char *legacy_session_path_to_delete; bool user_activity; bool session_dirty; pthread_t thread; pthread_mutex_t mu; pthread_cond_t cond; int wake_fd[2]; FILE *trace; bool wake_pending; bool stop; bool interrupt; bool initialized; bool save_requested; bool compact_requested; bool power_requested; int requested_power; int progress_base; double progress_started_at; int last_system_prompt_reminder_at; char *cmd_text; agent_status status; char *out; size_t out_len; size_t out_cap; ds4_web *web; bool web_approval_pending; bool web_approval_answered; bool web_approval_result; char web_approval_message[256]; char web_approval_error[160]; bool queued_user_drain_pending; bool queued_user_drain_answered; char *queued_user_drain_text; bool datetime_context_injected; char more_path[PATH_MAX]; int more_next_line; bool more_bare; bool more_valid; agent_bash_job *bash_jobs; int next_bash_job_id; bool raw_mode_needs_restore; } agent_worker; static unsigned agent_next_prefill_label(void); typedef struct agent_tail_capture { char *buf; size_t cap; size_t start; size_t len; size_t total; } agent_tail_capture; typedef enum { AGENT_MD_PENDING_NONE, AGENT_MD_PENDING_STAR, AGENT_MD_PENDING_BACKTICK, } agent_markdown_pending; typedef struct agent_syntax agent_syntax; typedef struct { ds4_engine *engine; agent_worker *worker; bool format_thinking; bool format_markdown; bool in_think; bool color_open; bool use_color; bool last_output_newline; bool wrote_visible_output; bool md_bold; bool md_italic; bool md_inline_code; bool md_code_block; bool md_fence_info; bool md_code_line_start; bool md_code_in_ml_comment; bool md_syntax_silent; bool md_syntax_has_highlight; agent_markdown_pending md_pending; size_t md_pending_len; const agent_syntax *md_syntax; char md_fence_lang[32]; size_t md_fence_lang_len; const char *md_code_line_prefix; const char *md_code_line_prefix_color; bool md_code_highlight_upto; char *md_code_line; size_t md_code_line_len; size_t md_code_line_cap; char pending[16]; size_t pending_len; char utf8_pending[4]; size_t utf8_pending_len; size_t utf8_pending_need; agent_tail_capture *capture; } agent_token_renderer; typedef struct { char *name; char *value; bool is_string; } agent_tool_arg; typedef struct { char *name; agent_tool_arg *args; int argc; int argcap; } agent_tool_call; typedef struct { agent_tool_call *v; int len; int cap; } agent_tool_calls; typedef enum { AGENT_DSML_SEARCH, AGENT_DSML_STRUCTURAL, AGENT_DSML_PARAM_VALUE, AGENT_DSML_DONE, AGENT_DSML_ERROR, } agent_dsml_state; typedef struct { agent_dsml_state state; char search_tail[64]; size_t search_len; char *raw; size_t raw_len; size_t raw_cap; size_t parse_pos; agent_tool_call current; char *param_name; bool param_is_string; size_t param_value_start; bool param_close_prefix; agent_tool_calls calls; char error[160]; } agent_dsml_parser; typedef enum { AGENT_TOOL_PARAM_NORMAL, AGENT_TOOL_PARAM_PATH, AGENT_TOOL_PARAM_OFFSET, AGENT_TOOL_PARAM_CONTENT, AGENT_TOOL_PARAM_DIFF_OLD, AGENT_TOOL_PARAM_DIFF_NEW, AGENT_TOOL_PARAM_BASH_COMMAND, } agent_tool_param_kind; typedef struct { bool active; bool tool_announced; bool param_active; bool at_line_start; bool last_output_newline; agent_tool_param_kind param_kind; char tool_name[64]; char param_name[64]; char param_end_tail[64]; size_t param_end_len; bool read_style; bool read_prefix_rendered; bool read_line_rendered; char read_path[512]; char read_start[32]; char read_max[32]; char read_whole[8]; char tool_path[512]; bool code_param_active; } agent_tool_visualizer; typedef struct { char tail[32]; size_t len; } agent_dsml_marker_detector; typedef struct { agent_token_renderer *renderer; agent_dsml_parser *parser; agent_tool_visualizer viz; bool in_think; bool dsml_active; bool dsml_ignored; bool replay; char pending[16]; size_t pending_len; char dsml_start_tail[64]; size_t dsml_start_len; agent_dsml_marker_detector plain_dsml; agent_dsml_marker_detector think_dsml; bool dsml_in_think; bool dsml_in_think_reported; bool post_think_gap; bool tool_preflight_error; char tool_preflight_error_msg[256]; } agent_stream_renderer; typedef struct { bool active; bool done; } agent_edit_upto_forcer; static volatile sig_atomic_t agent_sigint; static agent_worker *agent_completion_worker; static void worker_apply_pending_power(agent_worker *w); static void agent_trace(agent_worker *w, const char *fmt, ...); static void agent_trace_text(agent_worker *w, const char *label, const char *text, size_t len); static void agent_publish_system_status(agent_worker *w, const char *msg); static int agent_web_confirm(void *privdata, const char *message, char *err, size_t err_len); static void agent_web_log(void *privdata, const char *message); static bool agent_preflight_edit_old(agent_worker *w, const agent_tool_call *call, char *err, size_t err_len); static int agent_worker_sync_tokens(agent_worker *w, const ds4_tokens *tokens, bool publish_progress, char *err, size_t err_len); /* ============================================================================ * Small Utilities And Command-Line Parsing * ============================================================================ */ static void agent_sigint_handler(int sig) { (void)sig; agent_sigint = 1; } static void *xmalloc(size_t n) { void *p = malloc(n ? n : 1); if (!p) { perror("ds4-agent: malloc"); exit(1); } return p; } static char *xstrdup(const char *s) { if (!s) s = ""; size_t n = strlen(s); char *p = xmalloc(n + 1); memcpy(p, s, n + 1); return p; } static char *xstrndup(const char *s, size_t n) { char *p = xmalloc(n + 1); memcpy(p, s, n); p[n] = '\0'; return p; } static void *xrealloc(void *ptr, size_t n) { void *p = realloc(ptr, n ? n : 1); if (!p) { perror("ds4-agent: realloc"); exit(1); } return p; } static void write_all(int fd, const char *p, size_t n) { while (n) { ssize_t wr = write(fd, p, n); if (wr < 0) { if (errno == EINTR) continue; return; } p += wr; n -= (size_t)wr; } } typedef struct { char *ptr; size_t len; size_t cap; } agent_input_buf; static void agent_input_buf_append(agent_input_buf *b, const char *s, size_t n) { if (!n) return; if (b->len + n + 1 > b->cap) { size_t cap = b->cap ? b->cap * 2 : 4096; while (cap < b->len + n + 1) cap *= 2; b->ptr = xrealloc(b->ptr, cap); b->cap = cap; } memcpy(b->ptr + b->len, s, n); b->len += n; b->ptr[b->len] = '\0'; } static char *agent_input_buf_take(agent_input_buf *b) { if (!b->ptr) return xstrdup(""); char *p = b->ptr; memset(b, 0, sizeof(*b)); return p; } static void agent_input_buf_free(agent_input_buf *b) { free(b->ptr); memset(b, 0, sizeof(*b)); } static int parse_int(const char *s, const char *opt) { char *end = NULL; long v = strtol(s, &end, 10); if (s[0] == '\0' || *end != '\0' || v <= 0 || v > INT32_MAX) { fprintf(stderr, "ds4-agent: invalid value for %s: %s\n", opt, s); exit(2); } return (int)v; } static bool parse_power_percent(const char *arg, int *out) { char *end = NULL; long v = strtol(arg, &end, 10); if (!arg[0] || *end != '\0' || v < 1 || v > 100) return false; *out = (int)v; return true; } static bool agent_slash_command_with_args(const char *cmd, const char *name) { size_t len = strlen(name); return !strncmp(cmd, name, len) && (cmd[len] == '\0' || isspace((unsigned char)cmd[len])); } static bool agent_slash_command_known(const char *cmd) { return !strcmp(cmd, "/help") || !strcmp(cmd, "/save") || !strcmp(cmd, "/compact") || !strcmp(cmd, "/list") || !strcmp(cmd, "/quit") || !strcmp(cmd, "/exit") || !strcmp(cmd, "/new") || agent_slash_command_with_args(cmd, "/power") || agent_slash_command_with_args(cmd, "/switch") || agent_slash_command_with_args(cmd, "/del") || agent_slash_command_with_args(cmd, "/strip") || agent_slash_command_with_args(cmd, "/history"); } static uint64_t parse_u64(const char *s, const char *opt) { char *end = NULL; unsigned long long v = strtoull(s, &end, 10); if (s[0] == '\0' || *end != '\0' || v == 0) { fprintf(stderr, "ds4-agent: invalid value for %s: %s\n", opt, s); exit(2); } return (uint64_t)v; } static float parse_float_range(const char *s, const char *opt, float min, float max) { char *end = NULL; float v = strtof(s, &end); if (s[0] == '\0' || *end != '\0' || !isfinite(v) || v < min || v > max) { fprintf(stderr, "ds4-agent: invalid value for %s: %s\n", opt, s); exit(2); } return v; } static ds4_backend parse_backend(const char *s) { if (!strcmp(s, "metal")) return DS4_BACKEND_METAL; if (!strcmp(s, "cuda")) return DS4_BACKEND_CUDA; if (!strcmp(s, "cpu")) return DS4_BACKEND_CPU; fprintf(stderr, "ds4-agent: invalid backend: %s\n", s); exit(2); } static ds4_backend default_backend(void) { #ifdef DS4_NO_GPU return DS4_BACKEND_CPU; #elif defined(__APPLE__) return DS4_BACKEND_METAL; #else return DS4_BACKEND_CUDA; #endif } static double now_sec(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec * 1.0e-9; } static void usage(FILE *fp, const char *topic) { ds4_help_print(fp, DS4_HELP_AGENT, topic); } static const char *need_arg(int *i, int argc, char **argv, const char *opt) { if (*i + 1 >= argc) { fprintf(stderr, "ds4-agent: missing value for %s\n", opt); exit(2); } return argv[++(*i)]; } static agent_config parse_options(int argc, char **argv) { agent_config c = { .engine = { .model_path = "ds4flash.gguf", .backend = default_backend(), .mtp_draft_tokens = 1, .mtp_margin = 3.0f, }, .gen = { .system = "You are a helpful coding assistant running inside ds4-agent.", .n_predict = 50000, .ctx_size = 100000, .temperature = DS4_DEFAULT_TEMPERATURE, .top_p = DS4_DEFAULT_TOP_P, .min_p = DS4_DEFAULT_MIN_P, .think_mode = DS4_THINK_HIGH, }, }; bool steering_scale_set = false; for (int i = 1; i < argc; i++) { const char *arg = argv[i]; if (!strcmp(arg, "-h") || !strcmp(arg, "--help")) { const char *topic = (i + 1 < argc && argv[i + 1][0] != '-') ? argv[i + 1] : NULL; usage(stdout, topic); exit(0); } char dist_parse_err[256] = {0}; ds4_dist_cli_parse_result dist_parse = ds4_dist_parse_cli_arg(arg, &i, argc, argv, &c.engine.distributed, dist_parse_err, sizeof(dist_parse_err)); if (dist_parse == DS4_DIST_CLI_ERROR) { fprintf(stderr, "ds4-agent: %s\n", dist_parse_err[0] ? dist_parse_err : "invalid distributed option"); exit(2); } if (dist_parse == DS4_DIST_CLI_MATCHED) continue; if (!strcmp(arg, "-p") || !strcmp(arg, "--prompt")) { c.gen.prompt = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--non-interactive")) { c.non_interactive = true; } else if (!strcmp(arg, "-sys") || !strcmp(arg, "--system")) { c.gen.system = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--trace")) { c.gen.trace_path = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "-m") || !strcmp(arg, "--model")) { c.engine.model_path = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--mtp")) { c.engine.mtp_path = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--mtp-draft")) { c.engine.mtp_draft_tokens = parse_int(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--mtp-margin")) { c.engine.mtp_margin = parse_float_range(need_arg(&i, argc, argv, arg), arg, 0.0f, 1000.0f); } else if (!strcmp(arg, "-c") || !strcmp(arg, "--ctx")) { c.gen.ctx_size = parse_int(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "-n") || !strcmp(arg, "--tokens")) { c.gen.n_predict = parse_int(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--temp")) { c.gen.temperature = parse_float_range(need_arg(&i, argc, argv, arg), arg, 0.0f, 100.0f); } else if (!strcmp(arg, "--top-p")) { c.gen.top_p = parse_float_range(need_arg(&i, argc, argv, arg), arg, 0.0f, 1.0f); } else if (!strcmp(arg, "--min-p")) { c.gen.min_p = parse_float_range(need_arg(&i, argc, argv, arg), arg, 0.0f, 1.0f); } else if (!strcmp(arg, "--seed")) { c.gen.seed = parse_u64(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--think")) { c.gen.think_mode = DS4_THINK_HIGH; } else if (!strcmp(arg, "--think-max")) { c.gen.think_mode = DS4_THINK_MAX; } else if (!strcmp(arg, "--nothink")) { c.gen.think_mode = DS4_THINK_NONE; } else if (!strcmp(arg, "--backend")) { c.engine.backend = parse_backend(need_arg(&i, argc, argv, arg)); } else if (!strcmp(arg, "--metal")) { c.engine.backend = DS4_BACKEND_METAL; } else if (!strcmp(arg, "--cuda")) { c.engine.backend = DS4_BACKEND_CUDA; } else if (!strcmp(arg, "--cpu")) { c.engine.backend = DS4_BACKEND_CPU; } else if (!strcmp(arg, "-t") || !strcmp(arg, "--threads")) { c.engine.n_threads = parse_int(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--chdir")) { c.chdir_path = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--quality")) { c.engine.quality = true; } else if (!strcmp(arg, "--ssd-streaming")) { c.engine.ssd_streaming = true; } else if (!strcmp(arg, "--ssd-streaming-cold")) { c.engine.ssd_streaming_cold = true; } else if (!strcmp(arg, "--ssd-streaming-cache-experts")) { uint32_t experts = 0; uint64_t bytes = 0; if (!ds4_parse_streaming_cache_experts_arg( need_arg(&i, argc, argv, arg), &experts, &bytes)) { fprintf(stderr, "ds4-agent: --ssd-streaming-cache-experts must be a positive count or GB\n"); exit(2); } c.engine.ssd_streaming_cache_experts = experts; c.engine.ssd_streaming_cache_bytes = bytes; } else if (!strcmp(arg, "--ssd-streaming-preload-experts")) { int v = parse_int(need_arg(&i, argc, argv, arg), arg); if (v <= 0) { fprintf(stderr, "ds4-agent: --ssd-streaming-preload-experts must be positive\n"); exit(2); } c.engine.ssd_streaming_preload_experts = (uint32_t)v; } else if (!strcmp(arg, "--simulate-used-memory")) { if (!ds4_parse_gib_arg(need_arg(&i, argc, argv, arg), &c.engine.simulate_used_memory_bytes)) { fprintf(stderr, "ds4-agent: --simulate-used-memory must be a positive GiB value, e.g. 64GB\n"); exit(2); } } else if (!strcmp(arg, "--prefill-chunk")) { int v = parse_int(need_arg(&i, argc, argv, arg), arg); if (v <= 0) { fprintf(stderr, "ds4-agent: --prefill-chunk must be positive\n"); exit(2); } c.engine.prefill_chunk = (uint32_t)v; } else if (!strcmp(arg, "--power")) { c.engine.power_percent = parse_int(need_arg(&i, argc, argv, arg), arg); if (c.engine.power_percent < 1 || c.engine.power_percent > 100) { fprintf(stderr, "ds4-agent: --power must be between 1 and 100\n"); exit(2); } } else if (!strcmp(arg, "--warm-weights")) { c.engine.warm_weights = true; } else if (!strcmp(arg, "--dir-steering-file")) { c.engine.directional_steering_file = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--dir-steering-ffn")) { c.engine.directional_steering_ffn = parse_float_range(need_arg(&i, argc, argv, arg), arg, -100.0f, 100.0f); steering_scale_set = true; } else if (!strcmp(arg, "--dir-steering-attn")) { c.engine.directional_steering_attn = parse_float_range(need_arg(&i, argc, argv, arg), arg, -100.0f, 100.0f); steering_scale_set = true; } else { fprintf(stderr, "ds4-agent: unknown option: %s\n", arg); usage(stderr, NULL); exit(2); } } if (c.engine.directional_steering_file && !steering_scale_set) c.engine.directional_steering_ffn = 1.0f; char dist_err[256]; if (ds4_dist_prepare_engine_options(&c.engine.distributed, &c.engine, dist_err, sizeof(dist_err)) != 0) { fprintf(stderr, "ds4-agent: %s\n", dist_err); exit(2); } if (c.engine.distributed.role == DS4_DISTRIBUTED_WORKER) { fprintf(stderr, "ds4-agent: --role worker is a serving mode; start workers with ./ds4\n"); exit(2); } return c; } static void log_context_memory(ds4_backend backend, int ctx_size, uint32_t prefill_chunk) { ds4_context_memory m = ds4_context_memory_estimate_with_prefill(backend, ctx_size, prefill_chunk); fprintf(stderr, "ds4-agent: context buffers %.2f MiB (ctx=%d, backend=%s, prefill_chunk=%u, raw_kv_rows=%u, compressed_kv_rows=%u)\n", (double)m.total_bytes / (1024.0 * 1024.0), ctx_size, ds4_backend_name(backend), m.prefill_cap, m.raw_cap, m.comp_cap); } static ds4_think_mode effective_think_mode(const agent_config *cfg) { return ds4_think_mode_for_context(cfg->gen.think_mode, cfg->gen.ctx_size); } /* ============================================================================ * System Prompt Rendering And Worker Output Queues * ============================================================================ */ static const char agent_tools_prompt_intro[] = "You are a coding agent running in a local workspace. Use tools for local file and system work. " "Avoid printing large file contents or large code blocks as answers; create or edit files with tools, " "then summarize results briefly.\n\n" "## Tools\n\n" "You have access to native DSML tools. Invoke tools by writing exactly this shape:\n\n" "<|DSML|tool_calls>\n" "<|DSML|invoke name=\"$TOOL_NAME\">\n" "<|DSML|parameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE\n" "\n" "\n\n" "Tool calls are not allowed inside ; finish thinking before emitting DSML.\n\n" "String parameters use raw text and string=\"true\". Numbers and booleans use JSON text and string=\"false\".\n\n" "Read defaults to a bounded chunk: path alone returns the first 500 lines, not the whole file. " "If read says more lines are available, call more with count= to read the next chunk; " "more defaults to the next 500 lines. " "The read result also reports continue_offset=N, which is the next start_line if you need to jump manually. " "If the user explicitly asks you to read a complete file into context, call read with whole=true. " "A whole-file read may fail if the result would not fit the current context; then explain that and use chunks.\n\n"; static const char agent_tools_prompt_edit_line[] = "## Editing files\n\n" "Use write for new files or deliberate whole-file replacement. Use edit with path, old, and new for changes. " "For edit, always put the edited file path as the first parameter. " "The old text must match exactly once in the current file; otherwise edit fails for safety.\n" "For large replacements, prefer anchored old text: write the first lines, then [upto], then the final lines. " "The tool replaces everything from the head through the tail. If the head or tail is ambiguous, the edit fails.\n" "After [upto], always write unique final lines before closing old; never close old immediately after [upto].\n" "Do not use a generic tail anchor like:\n" "- BigNum bignum_add(BigNum *a, BigNum *b) {\n" "- [upto]\n" "- }\n" "because the closing brace may match many functions. Instead include final lines that are unique near that function, " "for example its last calculation and return line before the brace.\n" "Example anchored edit:\n" "<|DSML|tool_calls>\n" "<|DSML|invoke name=\"edit\">\n" "<|DSML|parameter name=\"path\" string=\"true\">/tmp/example.c\n" "<|DSML|parameter name=\"old\" string=\"true\">static int parse(void) {\n" " int ok = 0;\n" "[upto]\n" " return ok;\n" "}\n" "<|DSML|parameter name=\"new\" string=\"true\">static int parse(void) {\n" " return parse_impl();\n" "}\n" "\n" "\n" "To insert text, use edit with old set to an exact unique anchor and new set to that anchor plus the added text.\n" "Use read raw=true only when you need plain file text without line numbers or read annotations.\n\n"; static const char agent_tools_prompt_after_edit[] = "For long-running bash commands, pass refresh_sec. If a bash job is still running, use " "bash_status to check it early or bash_stop to terminate it.\n\n" "Use google_search to find web pages. Use visit_page to read a known URL with a visible browser. " "The first web call may ask the user for permission to start Chrome.\n\n" "### Available Tool Schemas\n\n" "{\n" " \"type\": \"function\",\n" " \"function\": {\n" " \"name\": \"google_search\",\n" " \"description\": \"Search Google in a visible browser and return compact Markdown links.\",\n" " \"parameters\": {\n" " \"type\": \"object\",\n" " \"properties\": {\n" " \"query\": {\"type\": \"string\"}\n" " },\n" " \"required\": [\"query\"]\n" " }\n" " }\n" "}\n\n" "{\n" " \"type\": \"function\",\n" " \"function\": {\n" " \"name\": \"visit_page\",\n" " \"description\": \"Open a URL in a visible browser and return rendered page Markdown.\",\n" " \"parameters\": {\n" " \"type\": \"object\",\n" " \"properties\": {\n" " \"url\": {\"type\": \"string\"}\n" " },\n" " \"required\": [\"url\"]\n" " }\n" " }\n" "}\n\n" "{\n" " \"type\": \"function\",\n" " \"function\": {\n" " \"name\": \"bash\",\n" " \"description\": \"Run a shell command.\",\n" " \"parameters\": {\n" " \"type\": \"object\",\n" " \"properties\": {\n" " \"command\": {\"type\": \"string\"},\n" " \"timeout_sec\": {\"type\": \"number\"},\n" " \"refresh_sec\": {\"type\": \"number\"}\n" " },\n" " \"required\": [\"command\"]\n" " }\n" " }\n" "}\n\n" "{\n" " \"type\": \"function\",\n" " \"function\": {\n" " \"name\": \"bash_status\",\n" " \"description\": \"Report current status and new output for a bash job.\",\n" " \"parameters\": {\n" " \"type\": \"object\",\n" " \"properties\": {\n" " \"job\": {\"type\": \"number\"},\n" " \"pid\": {\"type\": \"number\"},\n" " \"refresh_sec\": {\"type\": \"number\"}\n" " },\n" " \"required\": [\"job\"]\n" " }\n" " }\n" "}\n\n" "{\n" " \"type\": \"function\",\n" " \"function\": {\n" " \"name\": \"bash_stop\",\n" " \"description\": \"Terminate a running bash job and report its final output.\",\n" " \"parameters\": {\n" " \"type\": \"object\",\n" " \"properties\": {\n" " \"job\": {\"type\": \"number\"},\n" " \"pid\": {\"type\": \"number\"},\n" " \"refresh_sec\": {\"type\": \"number\"}\n" " },\n" " \"required\": [\"job\"]\n" " }\n" " }\n" "}\n\n" "{\n" " \"type\": \"function\",\n" " \"function\": {\n" " \"name\": \"read\",\n" " \"description\": \"Read a text file or a range of lines.\",\n" " \"parameters\": {\n" " \"type\": \"object\",\n" " \"properties\": {\n" " \"path\": {\"type\": \"string\"},\n" " \"start_line\": {\"type\": \"number\"},\n" " \"max_lines\": {\"type\": \"number\"},\n" " \"whole\": {\"type\": \"boolean\"},\n" " \"raw\": {\"type\": \"boolean\"}\n" " },\n" " \"required\": [\"path\"]\n" " }\n" " }\n" "}\n\n" "{\n" " \"type\": \"function\",\n" " \"function\": {\n" " \"name\": \"more\",\n" " \"description\": \"Continue the previous read-like output.\",\n" " \"parameters\": {\n" " \"type\": \"object\",\n" " \"properties\": {\n" " \"count\": {\"type\": \"number\"}\n" " }\n" " }\n" " }\n" "}\n\n" "{\n" " \"type\": \"function\",\n" " \"function\": {\n" " \"name\": \"write\",\n" " \"description\": \"Create or overwrite a text file.\",\n" " \"parameters\": {\n" " \"type\": \"object\",\n" " \"properties\": {\n" " \"path\": {\"type\": \"string\"},\n" " \"content\": {\"type\": \"string\"}\n" " },\n" " \"required\": [\"path\", \"content\"]\n" " }\n" " }\n" "}\n\n" "{\n" " \"type\": \"function\",\n" " \"function\": {\n" " \"name\": \"edit\",\n" " \"description\": \"Replace exactly one old text match; old may contain [upto] between unique head and tail anchors.\",\n" " \"parameters\": {\n" " \"type\": \"object\",\n" " \"properties\": {\n" " \"path\": {\"type\": \"string\"},\n" " \"old\": {\"type\": \"string\"},\n" " \"new\": {\"type\": \"string\"}\n" " },\n" " \"required\": [\"path\", \"old\", \"new\"]\n" " }\n" " }\n" "}\n\n" "{\n" " \"type\": \"function\",\n" " \"function\": {\n" " \"name\": \"search\",\n" " \"description\": \"Search files and return compact edit-friendly matches.\",\n" " \"parameters\": {\n" " \"type\": \"object\",\n" " \"properties\": {\n" " \"query\": {\"type\": \"string\"},\n" " \"path\": {\"type\": \"string\"},\n" " \"mode\": {\"type\": \"string\"},\n" " \"glob\": {\"type\": \"string\"},\n" " \"context\": {\"type\": \"number\"},\n" " \"max_results\": {\"type\": \"number\"},\n" " \"case_sensitive\": {\"type\": \"boolean\"}\n" " },\n" " \"required\": [\"query\"]\n" " }\n" " }\n" "}\n\n" "{\n" " \"type\": \"function\",\n" " \"function\": {\n" " \"name\": \"list\",\n" " \"description\": \"List one directory compactly.\",\n" " \"parameters\": {\n" " \"type\": \"object\",\n" " \"properties\": {\n" " \"path\": {\"type\": \"string\"}\n" " },\n" " \"required\": [\"path\"]\n" " }\n" " }\n" "}\n" "\n" "# Rules\n\n" "- Always use strict syntax for DSML tool stanzas.\n" "- This system runs on local inference of a few hundred tokens/s of prefill, " "and a few tens of tokens/s decoding speed. Use read/search to get the " "anchors you need, then use anchored edit to avoid having to " "retype large text.\n" "- Write code that is reliable and works well; always have a mental model of " "what is going on in complex parts of the code.\n" "- Work in a way that preserves the current system configuration integrity, " "unless explicitly asked otherwise by the user.\n"; static char *agent_build_tools_prompt(void) { const char *edit = agent_tools_prompt_edit_line; size_t a = strlen(agent_tools_prompt_intro); size_t b = strlen(edit); size_t c = strlen(agent_tools_prompt_after_edit); char *out = xmalloc(a + b + c + 1); memcpy(out, agent_tools_prompt_intro, a); memcpy(out + a, edit, b); memcpy(out + a + b, agent_tools_prompt_after_edit, c + 1); return out; } static const char agent_dsml_syntax_reminder[] = "DSML syntax reminder:\n" "<|DSML|tool_calls>\n" "<|DSML|invoke name=\"$TOOL_NAME\">\n" "<|DSML|parameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE\n" "\n" "\n"; #define AGENT_SYSTEM_PROMPT_REMINDER_TOKENS 50000 static char *agent_build_system_prompt_reminder(void) { char *tools = agent_build_tools_prompt(); const char *start = "\n\n[System prompt reminder follows.]\n"; const char *end = "[End system prompt reminder.]\n\n"; size_t len = strlen(start) + strlen(tools) + strlen(end) + 1; char *out = xmalloc(len); out[0] = '\0'; strcat(out, start); strcat(out, tools); strcat(out, end); free(tools); return out; } static void agent_append_system_prompt(ds4_engine *engine, ds4_tokens *tokens, const char *extra) { /* The built-in tool prompt is trusted DS4 control text. Tokenize it like a * rendered chat prompt so the literal |DSML| markers in the examples become * the model's dedicated DSML token. Do not apply that tokenizer to user * supplied -sys text: arbitrary user text containing <|User|>, , or * |DSML| must remain plain content, not control tokens. */ char *tools_prompt = agent_build_tools_prompt(); ds4_tokenize_rendered_chat(engine, tools_prompt, tokens); free(tools_prompt); if (!extra || !extra[0]) return; size_t n = strlen(extra); char *plain = xmalloc(n + 3); memcpy(plain, "\n\n", 2); memcpy(plain + 2, extra, n + 1); ds4_chat_append_message(engine, tokens, "system", plain); free(plain); } static void agent_worker_note_system_prompt_seen(agent_worker *w) { w->last_system_prompt_reminder_at = w->transcript.len; } static void agent_worker_maybe_append_datetime_context(agent_worker *w) { if (w->datetime_context_injected) return; time_t now = time(NULL); struct tm tm; localtime_r(&now, &tm); char when[128]; if (strftime(when, sizeof(when), "%Y-%m-%d %H:%M:%S %Z", &tm) == 0) snprintf(when, sizeof(when), "%lld", (long long)now); char msg[256]; snprintf(msg, sizeof(msg), "Current local date and time at session start: %s. " "Use this only when date or time matters.", when); ds4_chat_append_message(w->engine, &w->transcript, "system", msg); agent_trace_text(w, "datetime-context", msg, strlen(msg)); w->datetime_context_injected = true; } /* The full tool/system reminder is separate from DSML syntax errors: it is a * pressure-controlled refresh of the same trusted prompt shape used at startup. * The built-in prompt is tokenized as rendered chat so DSML markers stay native * control tokens; arbitrary -sys text remains ordinary text. */ static void agent_worker_maybe_append_system_prompt_reminder(agent_worker *w) { if (w->last_system_prompt_reminder_at <= 0) { agent_worker_note_system_prompt_seen(w); return; } if (w->transcript.len - w->last_system_prompt_reminder_at < AGENT_SYSTEM_PROMPT_REMINDER_TOKENS) { return; } char *reminder = agent_build_system_prompt_reminder(); agent_publish_system_status(w, "Re-injecting system prompt reminder..."); agent_trace(w, "system prompt reminder injected at transcript=%d", w->transcript.len); ds4_tokenize_rendered_chat(w->engine, reminder, &w->transcript); free(reminder); const char *extra = w->cfg->gen.system; if (extra && extra[0]) { ds4_tokenize_text(w->engine, "\nAdditional system instructions reminder:\n", &w->transcript); ds4_tokenize_text(w->engine, extra, &w->transcript); ds4_tokenize_text(w->engine, "\n[End additional system instructions reminder.]\n\n", &w->transcript); } agent_worker_note_system_prompt_seen(w); } /* Wake the UI thread after changing worker-visible state. The byte in * wake_fd is level-triggered with wake_pending so bursts of sampled tokens do * not flood the pipe. */ static void agent_wake_locked(agent_worker *w) { if (w->wake_pending) return; w->wake_pending = true; char c = 'x'; ssize_t wr = write(w->wake_fd[1], &c, 1); (void)wr; } /* Queue rendered output for the UI thread. The worker never writes directly * to the terminal, which keeps linenoise redraws serialized in one place. */ static void agent_publish(agent_worker *w, const char *s, size_t n) { if (!n) return; pthread_mutex_lock(&w->mu); if (w->out_len + n + 1 > w->out_cap) { size_t cap = w->out_cap ? w->out_cap * 2 : 4096; while (cap < w->out_len + n + 1) cap *= 2; char *p = realloc(w->out, cap); if (!p) { pthread_mutex_unlock(&w->mu); return; } w->out = p; w->out_cap = cap; } memcpy(w->out + w->out_len, s, n); w->out_len += n; w->out[w->out_len] = '\0'; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } static void agent_publishf(agent_worker *w, const char *fmt, ...) { char stack[1024]; va_list ap; va_start(ap, fmt); int n = vsnprintf(stack, sizeof(stack), fmt, ap); va_end(ap); if (n <= 0) return; if ((size_t)n < sizeof(stack)) { agent_publish(w, stack, (size_t)n); return; } char *heap = xmalloc((size_t)n + 1); va_start(ap, fmt); vsnprintf(heap, (size_t)n + 1, fmt, ap); va_end(ap); agent_publish(w, heap, (size_t)n); free(heap); } static bool worker_is_idle(agent_worker *w); static void agent_set_status(agent_worker *w, agent_worker_state state) { pthread_mutex_lock(&w->mu); w->status.state = state; if (state != AGENT_WORKER_PREFILL) w->status.prefill_tps = 0.0; if (state != AGENT_WORKER_GENERATING) w->status.greedy_sampling = false; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } static void agent_set_error(agent_worker *w, const char *msg) { pthread_mutex_lock(&w->mu); w->status.state = AGENT_WORKER_ERROR; w->status.prefill_tps = 0.0; w->status.greedy_sampling = false; snprintf(w->status.error, sizeof(w->status.error), "%s", msg ? msg : "unknown error"); agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } /* ============================================================================ * Trace Logging * ============================================================================ */ static void agent_trace_time(FILE *fp) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); struct tm tm; localtime_r(&ts.tv_sec, &tm); fprintf(fp, "%04d-%02d-%02d %02d:%02d:%02d.%03ld", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, ts.tv_nsec / 1000000); } static void agent_trace(agent_worker *w, const char *fmt, ...) { if (!w || !w->trace) return; pthread_mutex_lock(&w->mu); agent_trace_time(w->trace); fputs(" ", w->trace); va_list ap; va_start(ap, fmt); vfprintf(w->trace, fmt, ap); va_end(ap); fputc('\n', w->trace); fflush(w->trace); pthread_mutex_unlock(&w->mu); } static void agent_trace_escaped(FILE *fp, const char *s, size_t n) { for (size_t i = 0; i < n; i++) { unsigned char c = (unsigned char)s[i]; switch (c) { case '\\': fputs("\\\\", fp); break; case '\n': fputs("\\n", fp); break; case '\r': fputs("\\r", fp); break; case '\t': fputs("\\t", fp); break; case '"': fputs("\\\"", fp); break; default: if (c < 32 || c == 127) fprintf(fp, "\\x%02x", c); else fputc(c, fp); break; } } } static void agent_trace_token(agent_worker *w, int token, const char *text, size_t text_len, int index) { if (!w || !w->trace) return; pthread_mutex_lock(&w->mu); agent_trace_time(w->trace); fprintf(w->trace, " token index=%d id=%d bytes=%zu text=\"", index, token, text_len); agent_trace_escaped(w->trace, text ? text : "", text_len); fputs("\" hex=", w->trace); for (size_t i = 0; i < text_len; i++) fprintf(w->trace, "%02x", (unsigned char)text[i]); fputc('\n', w->trace); fflush(w->trace); pthread_mutex_unlock(&w->mu); } static void agent_trace_tokens(agent_worker *w, const char *label, const ds4_tokens *tokens, int start) { if (!w || !w->trace || !tokens) return; if (start < 0) start = 0; if (start > tokens->len) start = tokens->len; agent_trace(w, "tokens label=%s start=%d len=%d", label ? label : "", start, tokens->len); for (int i = start; i < tokens->len; i++) { size_t text_len = 0; char *text = ds4_token_text(w->engine, tokens->v[i], &text_len); agent_trace_token(w, tokens->v[i], text, text_len, i); free(text); } } static void agent_trace_text(agent_worker *w, const char *label, const char *text, size_t len) { if (!w || !w->trace) return; pthread_mutex_lock(&w->mu); agent_trace_time(w->trace); fprintf(w->trace, " %s=\"", label ? label : "text"); agent_trace_escaped(w->trace, text ? text : "", len); fputs("\"\n", w->trace); fflush(w->trace); pthread_mutex_unlock(&w->mu); } /* ============================================================================ * DSML Tool-Call Parser * ============================================================================ * * The model streams raw text tokens. This parser recognizes completed DSML * tool stanzas and keeps a copy of the raw stanza for diagnostics. It is * deliberately strict after the opening marker: typo recovery belongs to the * streaming detector so the actual tool parser stays small and predictable. */ static bool bytes_has_prefix(const char *p, size_t n, const char *prefix) { size_t plen = strlen(prefix); return n >= plen && memcmp(p, prefix, plen) == 0; } static bool bytes_is_partial_prefix(const char *p, size_t n, const char *prefix) { size_t plen = strlen(prefix); return n < plen && memcmp(prefix, p, n) == 0; } static void agent_tool_call_free(agent_tool_call *c) { if (!c) return; free(c->name); for (int i = 0; i < c->argc; i++) { free(c->args[i].name); free(c->args[i].value); } free(c->args); memset(c, 0, sizeof(*c)); } static void agent_tool_calls_free(agent_tool_calls *calls) { if (!calls) return; for (int i = 0; i < calls->len; i++) agent_tool_call_free(&calls->v[i]); free(calls->v); memset(calls, 0, sizeof(*calls)); } static void agent_tool_call_add_arg(agent_tool_call *c, const char *name, const char *value, size_t value_len, bool is_string) { if (c->argc == c->argcap) { c->argcap = c->argcap ? c->argcap * 2 : 4; c->args = xrealloc(c->args, (size_t)c->argcap * sizeof(c->args[0])); } c->args[c->argc++] = (agent_tool_arg){ .name = xstrdup(name), .value = xstrndup(value, value_len), .is_string = is_string, }; } static void agent_tool_calls_push(agent_tool_calls *calls, agent_tool_call *call) { if (!call->name) return; if (calls->len == calls->cap) { calls->cap = calls->cap ? calls->cap * 2 : 2; calls->v = xrealloc(calls->v, (size_t)calls->cap * sizeof(calls->v[0])); } calls->v[calls->len++] = *call; memset(call, 0, sizeof(*call)); } static const char *agent_tool_arg_value(const agent_tool_call *call, const char *name) { for (int i = 0; i < call->argc; i++) { if (call->args[i].name && !strcmp(call->args[i].name, name)) return call->args[i].value ? call->args[i].value : ""; } return NULL; } static void agent_dsml_parser_free(agent_dsml_parser *p) { if (!p) return; free(p->raw); agent_tool_call_free(&p->current); free(p->param_name); agent_tool_calls_free(&p->calls); memset(p, 0, sizeof(*p)); } static void agent_dsml_parser_reset(agent_dsml_parser *p) { agent_dsml_parser_free(p); p->state = AGENT_DSML_SEARCH; } static void agent_dsml_raw_append(agent_dsml_parser *p, const char *s, size_t n) { if (!n) return; if (p->raw_len + n + 1 > p->raw_cap) { size_t cap = p->raw_cap ? p->raw_cap * 2 : 512; while (cap < p->raw_len + n + 1) cap *= 2; p->raw = xrealloc(p->raw, cap); p->raw_cap = cap; } memcpy(p->raw + p->raw_len, s, n); p->raw_len += n; p->raw[p->raw_len] = '\0'; } static char *agent_parse_attr(const char *tag, const char *name) { char pat[64]; snprintf(pat, sizeof(pat), "%s=\"", name); const char *p = strstr(tag, pat); if (!p) return NULL; p += strlen(pat); const char *end = strchr(p, '"'); if (!end) return NULL; return xstrndup(p, (size_t)(end - p)); } static void agent_dsml_set_error(agent_dsml_parser *p, const char *msg) { p->state = AGENT_DSML_ERROR; snprintf(p->error, sizeof(p->error), "%s", msg); } static bool agent_dsml_open_tag_is(const char *tag, const char *name) { char prefix[64]; snprintf(prefix, sizeof(prefix), "<|DSML|%s", name); size_t prefix_len = strlen(prefix); if (strncmp(tag, prefix, prefix_len) != 0) return false; char c = tag[prefix_len]; return c == '>' || c == ' ' || c == '\t' || c == '\r' || c == '\n'; } static bool agent_dsml_close_tag_at(const char *s, const char *name, size_t *tag_len) { char prefix[64]; static const char dsml_bar[] = "|"; snprintf(prefix, sizeof(prefix), "') return false; if (tag_len) *tag_len = (size_t)(p - s) + 1; return true; } /* Recognize a streamed parameter close tag prefix. Full close detection is * handled by agent_dsml_close_tag_at(); this helper exists for online behavior: * terminal rendering must hide partial close tags without waiting for the whole * parameter to finish. */ static bool agent_dsml_parameter_close_tail(const char *tail, size_t len, bool *complete) { static const char prefix[] = "') { *complete = i == len - 1; return *complete; } if (tail[i] != ' ' && tail[i] != '\t' && tail[i] != '\r' && tail[i] != '\n') return false; } return true; } static void agent_dsml_update_param_close_prefix(agent_dsml_parser *p) { p->param_close_prefix = false; if (p->state != AGENT_DSML_PARAM_VALUE || p->raw_len <= p->param_value_start) return; const char *value = p->raw + p->param_value_start; const char *end = p->raw + p->raw_len; const char *lt = end; while (lt > value) { lt--; if (*lt == '<') break; } if (lt < value || *lt != '<') return; size_t tail_len = (size_t)(end - lt); if (tail_len > 64) return; bool complete = false; static const char dsml_marker[] = "param_close_prefix = tail_len >= sizeof(dsml_marker) - 1 && memcmp(lt, dsml_marker, sizeof(dsml_marker) - 1) == 0 && agent_dsml_parameter_close_tail(lt, tail_len, &complete) && !complete; } /* Find a DSML closing tag while accepting the few harmless closing-tag variants * the model has been observed to emit. Opening tags stay strict so accidental * prose does not become a tool call. */ static char *agent_dsml_find_close_tag(const char *s, const char *name, size_t *tag_len) { const char *p = s; while ((p = strstr(p, "state == AGENT_DSML_STRUCTURAL || p->state == AGENT_DSML_PARAM_VALUE) { if (p->state == AGENT_DSML_PARAM_VALUE) { size_t end_tag_len = 0; char *end = agent_dsml_find_close_tag(p->raw + p->param_value_start, "parameter", &end_tag_len); if (!end) return; agent_tool_call_add_arg(&p->current, p->param_name ? p->param_name : "", p->raw + p->param_value_start, (size_t)(end - (p->raw + p->param_value_start)), p->param_is_string); p->param_close_prefix = false; free(p->param_name); p->param_name = NULL; p->parse_pos = (size_t)(end - p->raw) + end_tag_len; p->state = AGENT_DSML_STRUCTURAL; continue; } while (p->parse_pos < p->raw_len && (p->raw[p->parse_pos] == ' ' || p->raw[p->parse_pos] == '\t' || p->raw[p->parse_pos] == '\r' || p->raw[p->parse_pos] == '\n')) p->parse_pos++; if (p->parse_pos >= p->raw_len) return; size_t close_len = 0; if (agent_dsml_close_tag_at(p->raw + p->parse_pos, "tool_calls", &close_len)) { agent_tool_calls_push(&p->calls, &p->current); p->parse_pos += close_len; p->state = AGENT_DSML_DONE; return; } if (agent_dsml_close_tag_at(p->raw + p->parse_pos, "invoke", &close_len)) { agent_tool_calls_push(&p->calls, &p->current); p->parse_pos += close_len; continue; } char *tag_end = strchr(p->raw + p->parse_pos, '>'); if (!tag_end) return; size_t tag_len = (size_t)(tag_end - (p->raw + p->parse_pos)) + 1; char *tag = xstrndup(p->raw + p->parse_pos, tag_len); if (agent_dsml_open_tag_is(tag, "invoke")) { agent_tool_call_free(&p->current); p->current.name = agent_parse_attr(tag, "name"); if (!p->current.name) { free(tag); agent_dsml_set_error(p, "tool invoke without name"); return; } p->parse_pos += tag_len; } else if (agent_dsml_open_tag_is(tag, "parameter")) { free(p->param_name); p->param_name = agent_parse_attr(tag, "name"); char *is_string = agent_parse_attr(tag, "string"); p->param_is_string = is_string && !strcmp(is_string, "true"); free(is_string); if (!p->param_name) { free(tag); agent_dsml_set_error(p, "tool parameter without name"); return; } p->parse_pos += tag_len; p->param_value_start = p->parse_pos; p->param_close_prefix = false; p->state = AGENT_DSML_PARAM_VALUE; } else { snprintf(p->error, sizeof(p->error), "unexpected DSML tag: %.*s", (int)(tag_len > 80 ? 80 : tag_len), tag); free(tag); p->state = AGENT_DSML_ERROR; return; } free(tag); } } static void agent_dsml_start(agent_dsml_parser *p) { static const char start[] = "<|DSML|tool_calls>"; p->state = AGENT_DSML_STRUCTURAL; p->search_len = 0; agent_dsml_raw_append(p, start, strlen(start)); p->parse_pos = strlen(start); } static void agent_dsml_feed(agent_dsml_parser *p, const char *s, size_t n) { static const char start[] = "<|DSML|tool_calls>"; const size_t start_len = sizeof(start) - 1; if (p->state == AGENT_DSML_DONE || p->state == AGENT_DSML_ERROR) return; for (size_t i = 0; i < n; i++) { char c = s[i]; if (p->state == AGENT_DSML_SEARCH) { if (p->search_len == sizeof(p->search_tail)) { memmove(p->search_tail, p->search_tail + 1, --p->search_len); } p->search_tail[p->search_len++] = c; if (p->search_len >= start_len && memcmp(p->search_tail + p->search_len - start_len, start, start_len) == 0) agent_dsml_start(p); continue; } agent_dsml_raw_append(p, &c, 1); agent_dsml_parse(p); if (p->state == AGENT_DSML_PARAM_VALUE) agent_dsml_update_param_close_prefix(p); else p->param_close_prefix = false; } } /* ============================================================================ * Assistant Markdown Rendering * ============================================================================ * * This renderer handles only the cheap markdown cues that make terminal output * readable: **bold**, *italic*, inline code, and fenced code blocks. It is a * streaming parser, so it buffers only ambiguous marker bytes long enough to * decide whether they are formatting or literal text. */ static void agent_tail_capture_append(agent_tail_capture *t, const char *s, size_t n) { if (!t || !n) return; if (!t->cap) return; if (!t->buf) t->buf = xmalloc(t->cap); t->total += n; if (n >= t->cap) { memcpy(t->buf, s + n - t->cap, t->cap); t->start = 0; t->len = t->cap; return; } if (t->len < t->cap) { size_t free_tail = t->cap - t->len; size_t first = n < free_tail ? n : free_tail; size_t pos = (t->start + t->len) % t->cap; size_t right = t->cap - pos; size_t chunk = first < right ? first : right; memcpy(t->buf + pos, s, chunk); if (first > chunk) memcpy(t->buf, s + chunk, first - chunk); t->len += first; s += first; n -= first; } while (n) { size_t pos = (t->start + t->len) % t->cap; size_t right = t->cap - pos; size_t chunk = n < right ? n : right; memcpy(t->buf + pos, s, chunk); t->start = (t->start + chunk) % t->cap; s += chunk; n -= chunk; } } static char *agent_tail_capture_take(agent_tail_capture *t, size_t *len) { size_t n = t ? t->len : 0; char *out = xmalloc(n + 1); if (n) { size_t right = t->cap - t->start; size_t first = n < right ? n : right; memcpy(out, t->buf + t->start, first); if (n > first) memcpy(out + first, t->buf, n - first); } out[n] = '\0'; if (len) *len = n; free(t->buf); memset(t, 0, sizeof(*t)); return out; } static void renderer_write(agent_token_renderer *r, const char *s, size_t n) { if (r->capture) agent_tail_capture_append(r->capture, s, n); else agent_publish(r->worker, s, n); } static void renderer_set_grey(agent_token_renderer *r) { if (r->use_color) renderer_write(r, "\x1b[38;5;245m", 11); } static void renderer_reset_color(agent_token_renderer *r) { if (r->use_color) renderer_write(r, "\x1b[0m", 4); r->color_open = false; } static size_t renderer_utf8_need(unsigned char c) { if (c < 0x80) return 1; if (c >= 0xc2 && c <= 0xdf) return 2; if (c >= 0xe0 && c <= 0xef) return 3; if (c >= 0xf0 && c <= 0xf4) return 4; return 1; } static bool renderer_has_text_attrs(agent_token_renderer *r) { return r->in_think || r->md_bold || r->md_italic || r->md_inline_code || r->md_code_block; } static void renderer_set_text_attrs(agent_token_renderer *r) { if (!r->use_color) return; if (r->in_think) { renderer_set_grey(r); return; } if (r->md_code_block) { renderer_write(r, "\x1b[38;5;75m", 10); return; } else if (r->md_inline_code) { renderer_write(r, "\x1b[36m", 5); } if (r->md_bold) renderer_write(r, "\x1b[1m", 4); if (r->md_italic) renderer_write(r, "\x1b[3m", 4); } static void renderer_restore_text_attrs(agent_token_renderer *r) { if (!r->use_color || !r->color_open || !renderer_has_text_attrs(r)) return; renderer_set_text_attrs(r); } static void renderer_write_complete_char_raw(agent_token_renderer *r, const char *s, size_t n) { bool styled = r->use_color && renderer_has_text_attrs(r); if (styled && !r->color_open) { renderer_set_text_attrs(r); r->color_open = true; } else if (!styled && r->color_open) { renderer_reset_color(r); } renderer_write(r, s, n); if (n) r->wrote_visible_output = true; r->last_output_newline = n == 1 && s[0] == '\n'; } static void renderer_flush_utf8(agent_token_renderer *r) { if (!r->utf8_pending_len) return; renderer_write_complete_char_raw(r, r->utf8_pending, r->utf8_pending_len); r->utf8_pending_len = 0; r->utf8_pending_need = 0; } static void renderer_write_char_raw(agent_token_renderer *r, char c) { unsigned char uc = (unsigned char)c; if (r->utf8_pending_len) { if ((uc & 0xc0) == 0x80 && r->utf8_pending_len < sizeof(r->utf8_pending)) { r->utf8_pending[r->utf8_pending_len++] = c; if (r->utf8_pending_len == r->utf8_pending_need) renderer_flush_utf8(r); return; } renderer_flush_utf8(r); } size_t need = renderer_utf8_need(uc); if (need == 1) { renderer_write_complete_char_raw(r, &c, 1); return; } r->utf8_pending[0] = c; r->utf8_pending_len = 1; r->utf8_pending_need = need; } static void renderer_write_plain_byte(agent_token_renderer *r, char c) { bool old_bold = r->md_bold; bool old_italic = r->md_italic; bool old_inline_code = r->md_inline_code; bool old_code_block = r->md_code_block; /* Code blocks are streamed immediately in plain text, then repainted with * syntax colors when a complete terminal-safe line is available. Disable * markdown attributes only for this byte; renderer_write_char_raw() will * reset any tracked manual color once if needed. */ r->md_bold = false; r->md_italic = false; r->md_inline_code = false; r->md_code_block = false; renderer_write_char_raw(r, c); r->md_bold = old_bold; r->md_italic = old_italic; r->md_inline_code = old_inline_code; r->md_code_block = old_code_block; } /* Poor man's code highlighter inspired by antirez/kilo: a tiny language table * plus one line-oriented tokenizer for comments, strings, numbers, and * separator-bounded keywords. This is deliberately not a full parser; it is * only for making fenced Markdown code readable in the terminal. */ #define AGENT_HL_NORMAL 0 #define AGENT_HL_COMMENT 1 #define AGENT_HL_KEYWORD1 2 #define AGENT_HL_KEYWORD2 3 #define AGENT_HL_STRING 4 #define AGENT_HL_NUMBER 5 #define AGENT_SYNTAX_NUMBERS (1u<<0) #define AGENT_SYNTAX_STRINGS (1u<<1) #define AGENT_SYNTAX_BACKTICK_STRINGS (1u<<2) #define AGENT_SYNTAX_CASE_INSENSITIVE (1u<<3) struct agent_syntax { const char *name; const char *aliases; const char **keywords; const char *singleline_comments[3]; const char *multiline_start; const char *multiline_end; unsigned flags; }; static const char *agent_kw_generic[] = { "if","else","for","while","do","switch","case","default","break", "continue","return","try","catch","finally","throw","throws","class", "struct","enum","interface","trait","impl","fn","func","function", "def","lambda","let","var","const","static","public","private", "protected","import","include","from","export","package","module", "namespace","new","delete","async","await","yield","match","type", "true|","false|","null|","nil|","none|","None|","NULL|","void|", "int|","long|","float|","double|","char|","bool|","string|", "String|","usize|","isize|","u8|","u16|","u32|","u64|","i8|", "i16|","i32|","i64|",NULL }; static const char *agent_kw_c[] = { "auto","break","case","continue","default","do","else","enum", "extern","for","goto","if","register","return","sizeof","static", "struct","switch","typedef","union","volatile","while", "alignas","alignof","and","and_eq","asm","bitand","bitor","class", "compl","constexpr","const_cast","decltype","delete","dynamic_cast", "explicit","export","false","friend","inline","mutable","namespace", "new","noexcept","not","not_eq","nullptr","operator","or","or_eq", "private","protected","public","reinterpret_cast","static_assert", "static_cast","template","this","thread_local","throw","true","try", "typeid","typename","virtual","xor","xor_eq", "NULL|","bool|","char|","const|","double|","float|","int|","long|", "short|","signed|","size_t|","ssize_t|","uint8_t|","uint16_t|", "uint32_t|","uint64_t|","unsigned|","void|",NULL }; static const char *agent_kw_python[] = { "and","as","assert","async","await","break","case","class","continue", "def","del","elif","else","except","finally","for","from","global", "if","import","in","is","lambda","match","nonlocal","not","or","pass", "raise","return","try","while","with","yield", "False|","None|","True|","bool|","bytes|","dict|","float|","int|", "list|","object|","set|","str|","tuple|",NULL }; static const char *agent_kw_js[] = { "async","await","break","case","catch","class","const","continue", "debugger","default","delete","do","else","export","extends", "finally","for","from","function","get","if","import","in", "instanceof","let","new","of","return","set","static","super", "switch","this","throw","try","typeof","var","void","while","with", "yield","abstract","as","declare","enum","implements","interface", "keyof","namespace","private","protected","public","readonly","type", "any|","boolean|","false|","never|","null|","number|","string|", "symbol|","true|","undefined|","unknown|","void|",NULL }; static const char *agent_kw_java[] = { "abstract","assert","break","case","catch","class","const","continue", "default","do","else","enum","extends","final","finally","for","goto", "if","implements","import","instanceof","interface","native","new", "package","private","protected","public","return","static","strictfp", "super","switch","synchronized","this","throw","throws","transient", "try","volatile","while", "boolean|","byte|","char|","double|","false|","float|","int|","long|", "null|","short|","true|","void|",NULL }; static const char *agent_kw_csharp[] = { "abstract","as","base","break","case","catch","checked","class","const", "continue","default","delegate","do","else","enum","event","explicit", "extern","finally","fixed","for","foreach","goto","if","implicit","in", "interface","internal","is","lock","namespace","new","operator","out", "override","params","private","protected","public","readonly","ref", "return","sealed","sizeof","stackalloc","static","struct","switch", "this","throw","try","typeof","unchecked","unsafe","using","virtual", "volatile","while","async","await","get","init","record","set","var", "bool|","byte|","char|","decimal|","double|","false|","float|","int|", "long|","null|","object|","sbyte|","short|","string|","true|","uint|", "ulong|","ushort|","void|",NULL }; static const char *agent_kw_go[] = { "break","case","chan","const","continue","default","defer","else", "fallthrough","for","func","go","goto","if","import","interface", "map","package","range","return","select","struct","switch","type", "var","bool|","byte|","complex64|","complex128|","error|","false|", "float32|","float64|","int|","int8|","int16|","int32|","int64|", "nil|","rune|","string|","true|","uint|","uint8|","uint16|", "uint32|","uint64|","uintptr|",NULL }; static const char *agent_kw_rust[] = { "as","async","await","break","const","continue","crate","dyn","else", "enum","extern","fn","for","if","impl","in","let","loop","match", "mod","move","mut","pub","ref","return","self","Self","static", "struct","super","trait","type","unsafe","use","where","while", "bool|","char|","false|","f32|","f64|","i8|","i16|","i32|","i64|", "i128|","isize|","str|","String|","true|","u8|","u16|","u32|", "u64|","u128|","usize|",NULL }; static const char *agent_kw_shell[] = { "case","do","done","elif","else","esac","fi","for","function","if", "in","select","then","time","until","while","break","continue", "return","export","local","readonly","source","test","true|","false|", "echo|","printf|","cd|","pwd|","read|","set|","unset|","shift|",NULL }; static const char *agent_kw_sql[] = { "add","alter","and","as","asc","between","by","case","check","column", "constraint","create","delete","desc","distinct","drop","else","end", "exists","foreign","from","group","having","in","index","insert", "into","is","join","key","left","like","limit","not","null","on", "or","order","outer","primary","references","right","select","set", "table","then","union","unique","update","values","view","where", "bigint|","boolean|","date|","decimal|","false|","int|","integer|", "numeric|","real|","text|","timestamp|","true|","varchar|",NULL }; static const char *agent_kw_ruby[] = { "BEGIN","END","alias","and","begin","break","case","class","def", "defined?","do","else","elsif","end","ensure","for","if","in", "module","next","not","or","redo","rescue","retry","return","self", "super","then","undef","unless","until","when","while","yield", "false|","nil|","true|",NULL }; static const char *agent_kw_php[] = { "abstract","and","array","as","break","callable","case","catch","class", "clone","const","continue","declare","default","die","do","echo","else", "elseif","empty","enddeclare","endfor","endforeach","endif","endswitch", "endwhile","eval","exit","extends","final","finally","fn","for", "foreach","function","global","goto","if","implements","include", "include_once","instanceof","insteadof","interface","isset","list", "match","namespace","new","or","print","private","protected","public", "readonly","require","require_once","return","static","switch","throw", "trait","try","unset","use","var","while","xor","bool|","false|", "float|","int|","null|","string|","true|","void|",NULL }; static const char *agent_kw_swift[] = { "actor","as","associatedtype","async","await","break","case","catch", "class","continue","default","defer","do","else","enum","extension", "fallthrough","for","func","guard","if","import","in","init","inout", "is","let","nonisolated","operator","private","protocol","public", "repeat","return","self","Self","static","struct","subscript","super", "switch","throw","throws","try","typealias","var","where","while", "Any|","Bool|","Double|","false|","Float|","Int|","nil|","String|", "true|","Void|",NULL }; static const char *agent_kw_kotlin[] = { "as","break","class","continue","do","else","false","for","fun","if", "in","interface","is","null","object","package","return","super", "this","throw","true","try","typealias","typeof","val","var","when", "while","actual","annotation","by","catch","companion","const", "constructor","crossinline","data","enum","expect","external","final", "finally","import","infix","init","inline","inner","internal","lateinit", "noinline","open","operator","out","override","private","protected", "public","reified","sealed","suspend","tailrec","vararg", "Any|","Boolean|","Byte|","Char|","Double|","Float|","Int|","Long|", "Short|","String|","Unit|",NULL }; static const char *agent_kw_zig[] = { "addrspace","align","allowzero","and","anyframe","anytype","asm", "async","await","break","callconv","catch","comptime","const", "continue","defer","else","enum","errdefer","error","export","extern", "fn","for","if","inline","linksection","noalias","noinline","nosuspend", "opaque","or","orelse","packed","pub","resume","return","struct", "suspend","switch","test","threadlocal","try","union","unreachable", "usingnamespace","var","volatile","while", "bool|","false|","f32|","f64|","i32|","i64|","null|","true|","u8|", "u16|","u32|","u64|","usize|","void|",NULL }; static const char *agent_kw_lua[] = { "and","break","do","else","elseif","end","false","for","function", "goto","if","in","local","nil","not","or","repeat","return","then", "true","until","while",NULL }; static const char *agent_kw_html[] = { "a","body","button","div","doctype","form","h1","h2","h3","head", "html","input","label","li","link","main","meta","ol","option","p", "script","section","select","span","style","table","tbody","td","th", "thead","title","tr","ul","class|","href|","id|","name|","rel|", "src|","type|","value|",NULL }; static const char *agent_kw_css[] = { "align-items","background","border","bottom","color","display","flex", "font","font-size","gap","grid","height","justify-content","left", "margin","max-width","min-width","padding","position","right","top", "transform","width","z-index","absolute|","auto|","block|","flex|", "grid|","hidden|","inline|","none|","relative|","solid|",NULL }; static const agent_syntax agent_syntaxes[] = { {"generic", " text txt", agent_kw_generic, {"//","#",NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS | AGENT_SYNTAX_BACKTICK_STRINGS}, {"c", " c h cpp c++ cc cxx hpp hxx objc objective-c", agent_kw_c, {"//",NULL,NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"python", " py python py3", agent_kw_python, {"#",NULL,NULL}, NULL, NULL, AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"javascript", " js jsx javascript typescript ts tsx node mjs cjs", agent_kw_js, {"//",NULL,NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS | AGENT_SYNTAX_BACKTICK_STRINGS}, {"java", " java", agent_kw_java, {"//",NULL,NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"csharp", " cs c# csharp dotnet", agent_kw_csharp, {"//",NULL,NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"go", " go golang", agent_kw_go, {"//",NULL,NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS | AGENT_SYNTAX_BACKTICK_STRINGS}, {"rust", " rs rust", agent_kw_rust, {"//",NULL,NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"shell", " sh bash zsh shell fish ksh", agent_kw_shell, {"#",NULL,NULL}, NULL, NULL, AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS | AGENT_SYNTAX_BACKTICK_STRINGS}, {"sql", " sql postgres mysql sqlite", agent_kw_sql, {"--",NULL,NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS | AGENT_SYNTAX_CASE_INSENSITIVE}, {"ruby", " rb ruby", agent_kw_ruby, {"#",NULL,NULL}, NULL, NULL, AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"php", " php", agent_kw_php, {"//","#",NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"swift", " swift", agent_kw_swift, {"//",NULL,NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"kotlin", " kt kts kotlin", agent_kw_kotlin, {"//",NULL,NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"zig", " zig", agent_kw_zig, {"//",NULL,NULL}, NULL, NULL, AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"lua", " lua", agent_kw_lua, {"--",NULL,NULL}, NULL, NULL, AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"html", " html htm xml svg", agent_kw_html, {NULL,NULL,NULL}, "", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"css", " css scss sass", agent_kw_css, {NULL,NULL,NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"json", " json jsonc", NULL, {"//",NULL,NULL}, "/*", "*/", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"yaml", " yaml yml toml ini", NULL, {"#",NULL,NULL}, NULL, NULL, AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {"markdown", " md markdown", agent_kw_generic, {NULL,NULL,NULL}, "", AGENT_SYNTAX_NUMBERS | AGENT_SYNTAX_STRINGS}, {NULL, NULL, NULL, {NULL,NULL,NULL}, NULL, NULL, 0} }; static bool agent_syntax_alias_match(const char *aliases, const char *lang) { if (!aliases || !lang || !lang[0]) return false; size_t llen = strlen(lang); const char *p = aliases; while (*p) { while (*p == ' ') p++; const char *start = p; while (*p && *p != ' ') p++; if ((size_t)(p - start) == llen && !strncasecmp(start, lang, llen)) return true; } return false; } static const agent_syntax *agent_syntax_for_lang(const char *lang) { if (lang && lang[0]) { for (const agent_syntax *s = agent_syntaxes; s->name; s++) { if (!strcasecmp(s->name, lang) || agent_syntax_alias_match(s->aliases, lang)) return s; } } return &agent_syntaxes[0]; } static const agent_syntax *agent_syntax_for_path(const char *path) { if (!path || !path[0]) return agent_syntax_for_lang(NULL); const char *base = strrchr(path, '/'); base = base ? base + 1 : path; if (!strcasecmp(base, "Dockerfile")) return agent_syntax_for_lang("sh"); if (!strcasecmp(base, "Makefile") || !strcasecmp(base, "makefile")) return agent_syntax_for_lang("sh"); const char *dot = strrchr(base, '.'); if (!dot || !dot[1]) return agent_syntax_for_lang(NULL); return agent_syntax_for_lang(dot + 1); } static bool agent_syntax_separator(char c) { unsigned char uc = (unsigned char)c; return c == '\0' || isspace(uc) || strchr(",.()+-/*=~%[]{}<>:;!&|^?", c) != NULL; } static const char *agent_syntax_line_comment(const agent_syntax *syn, const char *p) { if (!syn) return NULL; for (int i = 0; i < 3 && syn->singleline_comments[i]; i++) { const char *m = syn->singleline_comments[i]; size_t mlen = strlen(m); if (mlen && !strncmp(p, m, mlen)) return m; } return NULL; } static int agent_syntax_color(int hl) { switch (hl) { case AGENT_HL_COMMENT: return 244; case AGENT_HL_KEYWORD1: return 214; case AGENT_HL_KEYWORD2: return 81; case AGENT_HL_STRING: return 150; case AGENT_HL_NUMBER: return 203; default: return 252; } } static void renderer_syntax_write(agent_token_renderer *r, int hl, const char *s, size_t n) { if (!n) return; if (hl != AGENT_HL_NORMAL) r->md_syntax_has_highlight = true; if (r->md_syntax_silent) return; if (r->use_color && hl != AGENT_HL_NORMAL) { char seq[32]; snprintf(seq, sizeof(seq), "\x1b[38;5;%dm", agent_syntax_color(hl)); renderer_write(r, seq, strlen(seq)); } renderer_write(r, s, n); if (r->use_color && hl != AGENT_HL_NORMAL) renderer_write(r, "\x1b[0m", 4); r->wrote_visible_output = true; r->last_output_newline = false; } static void renderer_syntax_write_upto_marker(agent_token_renderer *r) { static const char marker[] = "[upto]"; r->md_syntax_has_highlight = true; if (r->md_syntax_silent) return; if (r->use_color) { renderer_write(r, "\x1b[38;5;244m[", strlen("\x1b[38;5;244m[")); renderer_write(r, "\x1b[1;38;5;177mupto", strlen("\x1b[1;38;5;177mupto")); renderer_write(r, "\x1b[38;5;244m]\x1b[0m", strlen("\x1b[38;5;244m]\x1b[0m")); } else { renderer_write(r, marker, sizeof(marker) - 1); } r->wrote_visible_output = true; r->last_output_newline = false; } static size_t agent_syntax_keyword_len(const char *kw, bool *secondary) { size_t len = strlen(kw); *secondary = len && kw[len - 1] == '|'; return *secondary ? len - 1 : len; } static bool agent_syntax_match_keyword(const agent_syntax *syn, const char *p, const char *line_end, size_t *out_len, int *out_hl) { if (!syn || !syn->keywords) return false; for (int i = 0; syn->keywords[i]; i++) { bool secondary = false; size_t klen = agent_syntax_keyword_len(syn->keywords[i], &secondary); if ((size_t)(line_end - p) < klen) continue; bool match = (syn->flags & AGENT_SYNTAX_CASE_INSENSITIVE) ? !strncasecmp(p, syn->keywords[i], klen) : !strncmp(p, syn->keywords[i], klen); if (!match) continue; if (!agent_syntax_separator(p[klen])) continue; *out_len = klen; *out_hl = secondary ? AGENT_HL_KEYWORD2 : AGENT_HL_KEYWORD1; return true; } return false; } static bool agent_syntax_number_start(const char *p, const char *line, bool prev_sep, int prev_hl) { unsigned char c = (unsigned char)*p; if (isdigit(c) && (prev_sep || prev_hl == AGENT_HL_NUMBER)) return true; if (*p == '.' && p > line && prev_hl == AGENT_HL_NUMBER) return true; return false; } static size_t agent_syntax_number_len(const char *p, const char *line_end) { const char *q = p; while (q < line_end) { unsigned char c = (unsigned char)*q; if (isalnum(c) || *q == '_' || *q == '.' || *q == '+' || *q == '-') q++; else break; } return (size_t)(q - p); } static void renderer_syntax_emit_line(agent_token_renderer *r, const char *line, size_t len) { const agent_syntax *syn = r->md_syntax ? r->md_syntax : agent_syntax_for_lang(NULL); const char *p = line; const char *end = line + len; bool prev_sep = true; int prev_hl = AGENT_HL_NORMAL; int in_string = 0; while (p < end) { if (r->md_code_highlight_upto && (size_t)(end - p) >= strlen("[upto]") && !strncmp(p, "[upto]", strlen("[upto]"))) { renderer_syntax_write_upto_marker(r); p += strlen("[upto]"); prev_sep = true; prev_hl = AGENT_HL_NORMAL; continue; } if (r->md_code_in_ml_comment) { const char *mce = syn->multiline_end; if (mce && *mce) { size_t mlen = strlen(mce); const char *q = p; while (q < end && ((size_t)(end - q) < mlen || strncmp(q, mce, mlen))) q++; if (q < end) { q += mlen; renderer_syntax_write(r, AGENT_HL_COMMENT, p, (size_t)(q - p)); p = q; r->md_code_in_ml_comment = false; prev_sep = true; prev_hl = AGENT_HL_COMMENT; continue; } } renderer_syntax_write(r, AGENT_HL_COMMENT, p, (size_t)(end - p)); return; } const char *scs = agent_syntax_line_comment(syn, p); if (!in_string && scs) { renderer_syntax_write(r, AGENT_HL_COMMENT, p, (size_t)(end - p)); return; } if (!in_string && syn->multiline_start && syn->multiline_end && !strncmp(p, syn->multiline_start, strlen(syn->multiline_start))) { size_t mlen = strlen(syn->multiline_start); const char *q = p + mlen; size_t elen = strlen(syn->multiline_end); while (q < end && ((size_t)(end - q) < elen || strncmp(q, syn->multiline_end, elen))) q++; if (q < end) q += elen; else r->md_code_in_ml_comment = true; renderer_syntax_write(r, AGENT_HL_COMMENT, p, (size_t)(q - p)); p = q; prev_sep = false; prev_hl = AGENT_HL_COMMENT; continue; } if ((syn->flags & AGENT_SYNTAX_STRINGS) && in_string) { const char *q = p; while (q < end) { if (*q == '\\' && q + 1 < end) { q += 2; continue; } q++; if (q[-1] == in_string) { in_string = 0; break; } } renderer_syntax_write(r, AGENT_HL_STRING, p, (size_t)(q - p)); p = q; prev_sep = false; prev_hl = AGENT_HL_STRING; continue; } if ((syn->flags & AGENT_SYNTAX_STRINGS) && (*p == '"' || *p == '\'' || ((syn->flags & AGENT_SYNTAX_BACKTICK_STRINGS) && *p == '`'))) { int quote = *p; const char *q = p + 1; while (q < end) { if (*q == '\\' && q + 1 < end) { q += 2; continue; } q++; if (q[-1] == quote) { break; } } renderer_syntax_write(r, AGENT_HL_STRING, p, (size_t)(q - p)); p = q; prev_sep = false; prev_hl = AGENT_HL_STRING; continue; } if ((syn->flags & AGENT_SYNTAX_NUMBERS) && agent_syntax_number_start(p, line, prev_sep, prev_hl)) { size_t nlen = agent_syntax_number_len(p, end); renderer_syntax_write(r, AGENT_HL_NUMBER, p, nlen); p += nlen; prev_sep = false; prev_hl = AGENT_HL_NUMBER; continue; } if (prev_sep) { size_t klen = 0; int khl = AGENT_HL_NORMAL; if (agent_syntax_match_keyword(syn, p, end, &klen, &khl)) { renderer_syntax_write(r, khl, p, klen); p += klen; prev_sep = false; prev_hl = khl; continue; } } renderer_syntax_write(r, AGENT_HL_NORMAL, p, 1); prev_sep = agent_syntax_separator(*p); prev_hl = AGENT_HL_NORMAL; p++; } } static void renderer_code_line_append(agent_token_renderer *r, const char *s, size_t n) { if (!n) return; if (r->md_code_line_len + n + 1 > r->md_code_line_cap) { size_t cap = r->md_code_line_cap ? r->md_code_line_cap * 2 : 256; while (cap < r->md_code_line_len + n + 1) cap *= 2; r->md_code_line = xrealloc(r->md_code_line, cap); r->md_code_line_cap = cap; } memcpy(r->md_code_line + r->md_code_line_len, s, n); r->md_code_line_len += n; r->md_code_line[r->md_code_line_len] = '\0'; } static int renderer_terminal_cols(void) { struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return ws.ws_col; return 80; } static bool renderer_code_line_can_repaint(agent_token_renderer *r) { if (!r->use_color || r->capture || r->md_code_line_len == 0) return false; int cols = renderer_terminal_cols(); size_t prefix_len = r->md_code_line_prefix ? strlen(r->md_code_line_prefix) : 0; if (cols <= 1 || prefix_len + r->md_code_line_len >= (size_t)cols) return false; for (size_t i = 0; i < r->md_code_line_len; i++) { unsigned char c = (unsigned char)r->md_code_line[i]; if (c == '\t' || c == 0x1b || c >= 0x80 || (c < 0x20 && c != '\r')) return false; } return true; } static void renderer_code_write_line_prefix(agent_token_renderer *r) { if (!r->md_code_line_prefix) return; if (r->use_color && r->md_code_line_prefix_color) renderer_write(r, r->md_code_line_prefix_color, strlen(r->md_code_line_prefix_color)); renderer_write(r, r->md_code_line_prefix, strlen(r->md_code_line_prefix)); if (r->use_color && r->md_code_line_prefix_color) renderer_write(r, "\x1b[0m", 4); r->color_open = false; } /* Run the syntax highlighter in silent mode to learn whether the already * streamed line would change if repainted, while preserving the multiline * comment state until the caller decides whether repaint is safe. */ static bool renderer_code_scan_line(agent_token_renderer *r, bool *final_ml_comment) { bool old_silent = r->md_syntax_silent; bool old_highlight = r->md_syntax_has_highlight; bool old_ml_comment = r->md_code_in_ml_comment; r->md_syntax_silent = true; r->md_syntax_has_highlight = false; renderer_syntax_emit_line(r, r->md_code_line, r->md_code_line_len); bool changed = r->md_syntax_has_highlight; *final_ml_comment = r->md_code_in_ml_comment; r->md_code_in_ml_comment = old_ml_comment; r->md_syntax_silent = old_silent; r->md_syntax_has_highlight = old_highlight; return changed; } /* Code is shown as soon as bytes arrive. At end-of-line we can cheaply * replace only that terminal row with syntax-highlighted text, but only for * simple one-row ASCII lines; long, tabbed, escaped, or UTF-8 lines are left * as streamed and only advance the highlighter state. */ static void renderer_code_emit_buffered_line(agent_token_renderer *r, bool with_newline) { bool final_ml_comment = r->md_code_in_ml_comment; bool changed = renderer_code_scan_line(r, &final_ml_comment); bool repaint = changed && renderer_code_line_can_repaint(r); if (repaint) { renderer_reset_color(r); renderer_write(r, "\r\x1b[0K", 5); renderer_code_write_line_prefix(r); renderer_syntax_emit_line(r, r->md_code_line, r->md_code_line_len); } else { r->md_code_in_ml_comment = final_ml_comment; } r->md_code_line_len = 0; if (with_newline) { renderer_write_plain_byte(r, '\n'); r->wrote_visible_output = true; r->last_output_newline = true; r->md_code_line_start = true; } } static void renderer_code_byte(agent_token_renderer *r, char c) { if (c == '\n') { renderer_code_emit_buffered_line(r, true); return; } renderer_code_line_append(r, &c, 1); renderer_write_plain_byte(r, c); if (c != ' ' && c != '\t' && c != '\r') r->md_code_line_start = false; } static void renderer_code_emit_backtick_literals(agent_token_renderer *r, size_t count) { for (size_t i = 0; i < count; i++) renderer_code_byte(r, '`'); } static void renderer_code_begin(agent_token_renderer *r) { renderer_reset_color(r); r->md_code_block = true; r->md_inline_code = false; r->md_fence_info = true; r->md_code_line_start = true; r->md_code_in_ml_comment = false; r->md_syntax = agent_syntax_for_lang(NULL); r->md_fence_lang_len = 0; r->md_fence_lang[0] = '\0'; r->md_code_line_prefix = NULL; r->md_code_line_prefix_color = NULL; r->md_code_highlight_upto = false; r->md_code_line_len = 0; } static void renderer_code_stream_begin(agent_token_renderer *r, const agent_syntax *syntax) { renderer_reset_color(r); r->md_code_block = true; r->md_inline_code = false; r->md_fence_info = false; r->md_code_line_start = true; r->md_code_in_ml_comment = false; r->md_syntax = syntax ? syntax : agent_syntax_for_lang(NULL); r->md_fence_lang_len = 0; r->md_fence_lang[0] = '\0'; r->md_code_line_prefix = NULL; r->md_code_line_prefix_color = NULL; r->md_code_highlight_upto = false; r->md_code_line_len = 0; } static void renderer_code_stream_set_prefix(agent_token_renderer *r, const char *prefix, const char *color) { r->md_code_line_prefix = prefix; r->md_code_line_prefix_color = color; } static void renderer_code_stream_set_upto_marker(agent_token_renderer *r, bool enabled) { r->md_code_highlight_upto = enabled; } static void renderer_code_end(agent_token_renderer *r) { bool only_space = true; for (size_t i = 0; i < r->md_code_line_len; i++) { if (r->md_code_line[i] != ' ' && r->md_code_line[i] != '\t' && r->md_code_line[i] != '\r') { only_space = false; break; } } if (r->md_code_line_len && !only_space) renderer_code_emit_buffered_line(r, false); else r->md_code_line_len = 0; r->md_code_block = false; r->md_inline_code = false; r->md_fence_info = false; r->md_code_line_start = true; r->md_code_in_ml_comment = false; r->md_syntax = NULL; r->md_fence_lang_len = 0; r->md_fence_lang[0] = '\0'; r->md_code_line_prefix = NULL; r->md_code_line_prefix_color = NULL; } /* Tiny streaming Markdown highlighter for assistant prose. It deliberately * recognizes only delimiters that the model commonly emits in short answers: * **bold**, *italic*, `inline code`, ``inline code`` and fenced code blocks. * The state machine holds only possible delimiter bytes; once a byte is known * to be ordinary text it is sent to the raw UTF-8 writer above. Tool * visualization and redirected output bypass this layer. */ static void renderer_markdown_clear_pending(agent_token_renderer *r) { r->md_pending = AGENT_MD_PENDING_NONE; r->md_pending_len = 0; } static void renderer_markdown_emit_pending_literals(agent_token_renderer *r) { char c; if (r->md_pending == AGENT_MD_PENDING_STAR) { c = '*'; } else if (r->md_pending == AGENT_MD_PENDING_BACKTICK) { c = '`'; } else { return; } size_t count = r->md_pending_len; renderer_markdown_clear_pending(r); if (r->md_code_block) { if (c == '`') renderer_code_emit_backtick_literals(r, count); else for (size_t i = 0; i < count; i++) renderer_code_byte(r, c); return; } for (size_t i = 0; i < count; i++) renderer_write_char_raw(r, c); } static void renderer_markdown_commit_backticks(agent_token_renderer *r) { size_t count = r->md_pending_len; renderer_markdown_clear_pending(r); if (count >= 3) { for (size_t i = 0; i < count; i++) renderer_write_plain_byte(r, '`'); if (r->md_code_block) renderer_code_end(r); else renderer_code_begin(r); return; } if (r->md_code_block) { renderer_code_emit_backtick_literals(r, count); return; } /* Support both `code` and ``code``. The latter is uncommon in model * replies, but accepting it costs nothing and avoids leaking delimiters. */ r->md_inline_code = !r->md_inline_code; } static bool renderer_space_byte(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } /* Consume one byte of markdown-aware assistant output. Backticks and stars * are held in r->pending until the parser knows whether they form a marker; * all ordinary text is emitted with the current terminal attributes. */ static void renderer_markdown_feed(agent_token_renderer *r, char c) { if (r->md_fence_info) { if (c == '\n') { if (r->md_code_block) { r->md_fence_lang[r->md_fence_lang_len] = '\0'; r->md_syntax = agent_syntax_for_lang(r->md_fence_lang); } renderer_write_plain_byte(r, '\n'); r->md_fence_info = false; } else if (r->md_code_block) { unsigned char uc = (unsigned char)c; if (r->md_fence_lang_len + 1 < sizeof(r->md_fence_lang) && (isalnum(uc) || c == '_' || c == '-' || c == '+' || c == '#')) { r->md_fence_lang[r->md_fence_lang_len++] = c; } renderer_write_plain_byte(r, c); } return; } if (r->md_pending == AGENT_MD_PENDING_BACKTICK) { if (c == '`') { r->md_pending_len++; return; } renderer_markdown_commit_backticks(r); renderer_markdown_feed(r, c); return; } if (r->md_pending == AGENT_MD_PENDING_STAR) { renderer_markdown_clear_pending(r); if (!r->md_inline_code && !r->md_code_block && c == '*') { r->md_bold = !r->md_bold; return; } if (!r->md_inline_code && !r->md_code_block && (r->md_italic || !renderer_space_byte(c))) { r->md_italic = !r->md_italic; renderer_markdown_feed(r, c); return; } renderer_write_char_raw(r, '*'); renderer_markdown_feed(r, c); return; } if (c == '`' && (!r->md_code_block || r->md_code_line_start)) { r->md_pending = AGENT_MD_PENDING_BACKTICK; r->md_pending_len = 1; return; } if (r->md_code_block) { renderer_code_byte(r, c); return; } if (!r->md_inline_code && !r->md_code_block && c == '*') { r->md_pending = AGENT_MD_PENDING_STAR; r->md_pending_len = 1; return; } renderer_write_char_raw(r, c); } static void renderer_markdown_finish(agent_token_renderer *r) { /* A closing code fence can be the final bytes of the assistant reply. In * that case no following character arrives to force the pending backticks * through the normal streaming path, so commit a full fence here instead of * leaking the literal ``` marker to the terminal. */ if (r->md_pending == AGENT_MD_PENDING_BACKTICK && r->md_pending_len >= 3) renderer_markdown_commit_backticks(r); else renderer_markdown_emit_pending_literals(r); if (r->md_code_block && r->md_code_line_len) renderer_code_emit_buffered_line(r, false); r->md_bold = false; r->md_italic = false; r->md_inline_code = false; r->md_code_block = false; r->md_fence_info = false; r->md_code_line_start = false; r->md_code_in_ml_comment = false; r->md_syntax = NULL; r->md_fence_lang_len = 0; r->md_fence_lang[0] = '\0'; r->md_code_line_prefix = NULL; r->md_code_line_prefix_color = NULL; r->md_code_highlight_upto = false; free(r->md_code_line); r->md_code_line = NULL; r->md_code_line_len = 0; r->md_code_line_cap = 0; } static void renderer_write_char(agent_token_renderer *r, char c) { if (!r->format_markdown || r->in_think) { renderer_markdown_emit_pending_literals(r); renderer_write_char_raw(r, c); return; } renderer_markdown_feed(r, c); } /* Render assistant text while hiding tags and dimming thinking text. * The function is also responsible for not prematurely emitting a partial * control tag split across model tokens. */ static void renderer_process(agent_token_renderer *r, const char *text, size_t len, bool finish) { const char *think_open = ""; const char *think_close = ""; size_t total = r->pending_len + len; char *buf = xmalloc(total ? total : 1); if (r->pending_len) memcpy(buf, r->pending, r->pending_len); if (len) memcpy(buf + r->pending_len, text, len); r->pending_len = 0; size_t i = 0; while (i < total) { const char *cur = buf + i; size_t rem = total - i; if (bytes_has_prefix(cur, rem, think_open)) { r->in_think = true; i += strlen(think_open); continue; } if (bytes_has_prefix(cur, rem, think_close)) { r->in_think = false; renderer_reset_color(r); if (!r->last_output_newline) renderer_write(r, "\n", 1); renderer_write(r, "\n", 1); r->last_output_newline = true; i += strlen(think_close); continue; } if (!finish && cur[0] == '<' && (bytes_is_partial_prefix(cur, rem, think_open) || bytes_is_partial_prefix(cur, rem, think_close))) { if (rem < sizeof(r->pending)) { memcpy(r->pending, cur, rem); r->pending_len = rem; } break; } renderer_write_char(r, cur[0]); i++; } free(buf); } static void renderer_finish(agent_token_renderer *r) { if (r->format_thinking) { renderer_process(r, NULL, 0, true); } renderer_markdown_finish(r); renderer_flush_utf8(r); renderer_reset_color(r); if (r->wrote_visible_output) { if (!r->last_output_newline) renderer_write(r, "\n", 1); renderer_write(r, "\n", 1); r->last_output_newline = true; } } static void renderer_color(agent_token_renderer *r, const char *seq) { renderer_markdown_emit_pending_literals(r); renderer_flush_utf8(r); bool reset = !seq || !seq[0] || !strcmp(seq, "\x1b[0m"); if (r->use_color && seq && seq[0]) renderer_write(r, seq, strlen(seq)); r->color_open = r->use_color && !reset; } static void renderer_plain(agent_token_renderer *r, const char *s, size_t n) { renderer_markdown_emit_pending_literals(r); renderer_flush_utf8(r); renderer_write(r, s, n); if (n) r->wrote_visible_output = true; if (n) r->last_output_newline = s[n - 1] == '\n'; } /* ============================================================================ * Streaming Tool Visualization * ============================================================================ * * Tool calls are parsed for execution later, but they are also visualized while * the model is still sampling. This state machine suppresses raw DSML and * prints compact, tool-specific progress such as "$ command" or * "Reading file 1:500...". */ static bool streq_any(const char *s, const char *a, const char *b, const char *c, const char *d) { return (a && !strcmp(s, a)) || (b && !strcmp(s, b)) || (c && !strcmp(s, c)) || (d && !strcmp(s, d)); } static agent_tool_param_kind agent_tool_param_kind_for(const char *tool, const char *param) { if (!tool) tool = ""; if (!param) param = ""; if (!strcmp(tool, "bash") && !strcmp(param, "command")) return AGENT_TOOL_PARAM_BASH_COMMAND; if (!strcmp(tool, "edit") && !strcmp(param, "old")) return AGENT_TOOL_PARAM_DIFF_OLD; if (!strcmp(tool, "edit") && !strcmp(param, "new")) return AGENT_TOOL_PARAM_DIFF_NEW; if (streq_any(param, "path", "file", "filename", NULL)) return AGENT_TOOL_PARAM_PATH; if (streq_any(param, "line", "start_line", "end_line", "offset") || streq_any(param, "start", "end", "count", "max_lines") || streq_any(param, "timeout_sec", "refresh_sec", NULL, NULL)) return AGENT_TOOL_PARAM_OFFSET; if (streq_any(param, "content", "text", NULL, NULL)) return AGENT_TOOL_PARAM_CONTENT; return AGENT_TOOL_PARAM_NORMAL; } static const char *agent_tool_param_color(agent_tool_param_kind kind) { switch (kind) { case AGENT_TOOL_PARAM_PATH: return "\x1b[32m"; case AGENT_TOOL_PARAM_OFFSET: return "\x1b[33m"; case AGENT_TOOL_PARAM_CONTENT: return "\x1b[34m"; case AGENT_TOOL_PARAM_DIFF_OLD: return "\x1b[31m"; case AGENT_TOOL_PARAM_DIFF_NEW: return "\x1b[32m"; case AGENT_TOOL_PARAM_BASH_COMMAND: return "\x1b[1;36m"; default: return "\x1b[37m"; } } static void agent_tool_viz_write(agent_stream_renderer *sr, const char *s, size_t n) { renderer_plain(sr->renderer, s, n); for (size_t i = 0; i < n; i++) sr->viz.last_output_newline = s[i] == '\n'; } static void agent_tool_viz_puts(agent_stream_renderer *sr, const char *s) { agent_tool_viz_write(sr, s, strlen(s)); } static void agent_tool_viz_start(agent_stream_renderer *sr) { agent_tool_visualizer *v = &sr->viz; bool line_open = !sr->renderer->last_output_newline; memset(v, 0, sizeof(*v)); v->active = true; v->at_line_start = true; v->last_output_newline = true; if (sr->replay) { if (line_open) agent_tool_viz_puts(sr, "\n"); } else if (sr->renderer->use_color) { /* The raw DSML start marker may arrive after ordinary text on the * current row. Clear that row only for the live terminal UI; plain * stdout mode must never leak cursor-control escapes into pipes. */ agent_tool_viz_puts(sr, "\r\x1b[2K"); } else if (line_open) { agent_tool_viz_puts(sr, "\n"); } v->last_output_newline = true; } static void agent_tool_viz_line_prefix(agent_stream_renderer *sr) { agent_tool_visualizer *v = &sr->viz; if (!v->last_output_newline) agent_tool_viz_puts(sr, "\n"); agent_tool_viz_puts(sr, "🛠️ "); v->at_line_start = false; } static const char *agent_tool_viz_prefix(const char *name) { if (!strcmp(name, "bash")) return "$ "; if (!strcmp(name, "read")) return "read "; if (!strcmp(name, "write")) return "write "; if (!strcmp(name, "edit")) return "edit "; if (!strcmp(name, "search")) return "search "; if (!strcmp(name, "google_search")) return "google "; if (!strcmp(name, "visit_page")) return "visit "; return NULL; } static void agent_tool_viz_tool(agent_stream_renderer *sr, const char *name) { agent_tool_visualizer *v = &sr->viz; if (v->tool_announced && !strcmp(v->tool_name, name)) return; if (v->tool_announced && !v->last_output_newline) agent_tool_viz_puts(sr, "\n"); snprintf(v->tool_name, sizeof(v->tool_name), "%s", name ? name : "tool"); v->tool_announced = true; v->read_style = !strcmp(v->tool_name, "read"); agent_tool_viz_line_prefix(sr); if (v->read_style) { renderer_color(sr->renderer, "\x1b[1;37m"); agent_tool_viz_puts(sr, "Reading "); renderer_color(sr->renderer, "\x1b[32m"); v->read_prefix_rendered = true; return; } renderer_color(sr->renderer, !strcmp(v->tool_name, "bash") ? "\x1b[1;36m" : "\x1b[1;37m"); const char *prefix = agent_tool_viz_prefix(v->tool_name); if (prefix) { agent_tool_viz_puts(sr, prefix); } else { agent_tool_viz_puts(sr, v->tool_name); agent_tool_viz_puts(sr, " "); } renderer_color(sr->renderer, "\x1b[0m"); } static void agent_tool_viz_append(char *dst, size_t cap, char c) { size_t len = strlen(dst); if (len + 1 >= cap) return; dst[len] = c; dst[len + 1] = '\0'; } static void agent_tool_viz_read_value_byte(agent_stream_renderer *sr, char c) { agent_tool_visualizer *v = &sr->viz; if (!strcmp(v->param_name, "path")) { agent_tool_viz_append(v->read_path, sizeof(v->read_path), c); if (v->read_prefix_rendered) agent_tool_viz_write(sr, &c, 1); } else if (!strcmp(v->param_name, "start_line")) { agent_tool_viz_append(v->read_start, sizeof(v->read_start), c); } else if (!strcmp(v->param_name, "max_lines")) { agent_tool_viz_append(v->read_max, sizeof(v->read_max), c); } else if (!strcmp(v->param_name, "whole")) { agent_tool_viz_append(v->read_whole, sizeof(v->read_whole), c); } } static void agent_tool_viz_render_read(agent_stream_renderer *sr) { agent_tool_visualizer *v = &sr->viz; if (!v->read_style || v->read_line_rendered) return; if (!v->read_prefix_rendered) { agent_tool_viz_line_prefix(sr); renderer_color(sr->renderer, "\x1b[1;37m"); agent_tool_viz_puts(sr, "Reading "); renderer_color(sr->renderer, "\x1b[32m"); agent_tool_viz_puts(sr, v->read_path[0] ? v->read_path : ""); } else if (!v->read_path[0]) { renderer_color(sr->renderer, "\x1b[32m"); agent_tool_viz_puts(sr, ""); } renderer_color(sr->renderer, "\x1b[33m"); bool whole = agent_parse_bool_default(v->read_whole, false); if (whole && (!v->read_start[0] || !strcmp(v->read_start, "1"))) { agent_tool_viz_puts(sr, " (whole file)"); } else if (whole) { agent_tool_viz_puts(sr, " "); agent_tool_viz_puts(sr, v->read_start); agent_tool_viz_puts(sr, ":EOF"); } else { agent_tool_viz_puts(sr, " "); agent_tool_viz_puts(sr, v->read_start[0] ? v->read_start : "1"); agent_tool_viz_puts(sr, ":"); agent_tool_viz_puts(sr, v->read_max[0] ? v->read_max : "500"); } renderer_color(sr->renderer, "\x1b[1;37m"); agent_tool_viz_puts(sr, "..."); renderer_color(sr->renderer, "\x1b[0m"); agent_tool_viz_puts(sr, "\n"); v->read_line_rendered = true; } static bool agent_tool_viz_param_is_code_body(agent_tool_visualizer *v) { if (!strcmp(v->tool_name, "write") && v->param_kind == AGENT_TOOL_PARAM_CONTENT) return true; if (!strcmp(v->tool_name, "edit") && (v->param_kind == AGENT_TOOL_PARAM_DIFF_OLD || v->param_kind == AGENT_TOOL_PARAM_DIFF_NEW || v->param_kind == AGENT_TOOL_PARAM_CONTENT)) return true; return false; } static const char *agent_tool_viz_diff_prefix(agent_tool_param_kind kind, const char **color) { if (color) *color = NULL; const char *prefix = NULL; if (kind == AGENT_TOOL_PARAM_DIFF_OLD) { prefix = "- "; if (color) *color = "\x1b[31m"; } else if (kind == AGENT_TOOL_PARAM_DIFF_NEW) { prefix = "+ "; if (color) *color = "\x1b[32m"; } return prefix; } static void agent_tool_viz_code_prefix(agent_stream_renderer *sr) { agent_tool_visualizer *v = &sr->viz; if (!v->at_line_start) return; const char *color = NULL; const char *prefix = agent_tool_viz_diff_prefix(v->param_kind, &color); if (!prefix) return; renderer_color(sr->renderer, color); renderer_write(sr->renderer, prefix, strlen(prefix)); renderer_color(sr->renderer, "\x1b[0m"); sr->renderer->wrote_visible_output = true; sr->renderer->last_output_newline = false; v->last_output_newline = false; v->at_line_start = false; } static void agent_tool_viz_code_begin(agent_stream_renderer *sr) { agent_tool_visualizer *v = &sr->viz; const agent_syntax *syntax = agent_syntax_for_path(v->tool_path); renderer_code_stream_begin(sr->renderer, syntax); renderer_code_stream_set_upto_marker(sr->renderer, !strcmp(v->tool_name, "edit") && v->param_kind == AGENT_TOOL_PARAM_DIFF_OLD); v->code_param_active = true; if (v->param_kind == AGENT_TOOL_PARAM_DIFF_OLD || v->param_kind == AGENT_TOOL_PARAM_DIFF_NEW) { const char *color = NULL; const char *prefix = agent_tool_viz_diff_prefix(v->param_kind, &color); /* Diff prefixes are terminal UI, not code. Keep them outside the * syntax buffer so a later row repaint preserves their red/green color * while highlighting only the actual edited line. */ renderer_code_stream_set_prefix(sr->renderer, prefix, color); agent_tool_viz_code_prefix(sr); } } static void agent_tool_viz_code_end(agent_stream_renderer *sr) { agent_tool_visualizer *v = &sr->viz; if (!v->code_param_active) return; renderer_code_end(sr->renderer); v->code_param_active = false; v->at_line_start = true; v->last_output_newline = sr->renderer->last_output_newline; } static void agent_tool_viz_code_byte(agent_stream_renderer *sr, char c) { agent_tool_visualizer *v = &sr->viz; agent_tool_viz_code_prefix(sr); renderer_code_byte(sr->renderer, c); v->last_output_newline = c == '\n'; v->at_line_start = c == '\n'; } static void agent_tool_viz_param_begin(agent_stream_renderer *sr, const char *name) { agent_tool_visualizer *v = &sr->viz; if (!v->tool_announced && sr->parser->current.name) agent_tool_viz_tool(sr, sr->parser->current.name); snprintf(v->param_name, sizeof(v->param_name), "%s", name ? name : ""); v->param_kind = agent_tool_param_kind_for(v->tool_name, v->param_name); v->param_active = true; v->param_end_len = 0; if (v->read_style) return; if (v->param_kind == AGENT_TOOL_PARAM_DIFF_OLD || v->param_kind == AGENT_TOOL_PARAM_DIFF_NEW) { if (!v->last_output_newline) agent_tool_viz_puts(sr, "\n"); v->at_line_start = true; agent_tool_viz_code_begin(sr); return; } if (v->param_kind == AGENT_TOOL_PARAM_CONTENT) { if (!v->last_output_newline) agent_tool_viz_puts(sr, "\n"); if (strcmp(v->tool_name, "write")) { renderer_color(sr->renderer, "\x1b[1;37m"); agent_tool_viz_puts(sr, v->param_name); agent_tool_viz_puts(sr, ":\n"); } v->at_line_start = true; if (agent_tool_viz_param_is_code_body(v)) { agent_tool_viz_code_begin(sr); } else { renderer_color(sr->renderer, "\x1b[34m"); } return; } if (v->param_kind != AGENT_TOOL_PARAM_BASH_COMMAND) { if (!v->at_line_start) agent_tool_viz_puts(sr, " "); renderer_color(sr->renderer, "\x1b[1;37m"); agent_tool_viz_puts(sr, v->param_name); agent_tool_viz_puts(sr, "="); } else { renderer_color(sr->renderer, agent_tool_param_color(AGENT_TOOL_PARAM_BASH_COMMAND)); return; } renderer_color(sr->renderer, agent_tool_param_color(v->param_kind)); } static void agent_tool_viz_param_end(agent_stream_renderer *sr) { agent_tool_visualizer *v = &sr->viz; v->param_end_len = 0; if (v->code_param_active) agent_tool_viz_code_end(sr); if (!v->read_style) renderer_color(sr->renderer, "\x1b[0m"); v->param_active = false; v->param_name[0] = '\0'; } static void agent_tool_viz_param_raw_byte(agent_stream_renderer *sr, char c) { agent_tool_visualizer *v = &sr->viz; if (v->read_style) { agent_tool_viz_read_value_byte(sr, c); return; } if (v->param_kind == AGENT_TOOL_PARAM_PATH) { agent_tool_viz_append(v->tool_path, sizeof(v->tool_path), c); } if (v->code_param_active) { agent_tool_viz_code_byte(sr, c); return; } if (v->param_kind == AGENT_TOOL_PARAM_BASH_COMMAND) { agent_tool_viz_write(sr, &c, 1); v->at_line_start = c == '\n'; return; } if (v->param_kind == AGENT_TOOL_PARAM_DIFF_OLD || v->param_kind == AGENT_TOOL_PARAM_DIFF_NEW) { agent_tool_viz_code_begin(sr); agent_tool_viz_code_byte(sr, c); return; } agent_tool_viz_write(sr, &c, 1); v->at_line_start = c == '\n'; } static void agent_tool_viz_restore_param_color(agent_stream_renderer *sr) { agent_tool_visualizer *v = &sr->viz; if (!v->active || !v->param_active || v->read_style) return; renderer_color(sr->renderer, agent_tool_param_color(v->param_kind)); } /* Stream one DSML parameter byte into the visualizer. The visualizer must not * wait for the whole parameter: large write/edit contents should show progress * as the model emits them, while still detecting the closing parameter tag. */ static void agent_tool_viz_param_value_byte(agent_stream_renderer *sr, char c) { agent_tool_visualizer *v = &sr->viz; if (v->param_end_len || c == '<') { if (v->param_end_len == sizeof(v->param_end_tail)) { size_t keep = v->param_end_len; v->param_end_len = 0; for (size_t i = 0; i < keep; i++) agent_tool_viz_param_raw_byte(sr, v->param_end_tail[i]); if (c != '<') { agent_tool_viz_param_raw_byte(sr, c); return; } } if (v->param_end_len < sizeof(v->param_end_tail)) v->param_end_tail[v->param_end_len++] = c; bool complete = false; if (agent_dsml_parameter_close_tail(v->param_end_tail, v->param_end_len, &complete)) { if (complete) agent_tool_viz_param_end(sr); return; } size_t keep = v->param_end_len; v->param_end_len = 0; for (size_t i = 0; i < keep; i++) agent_tool_viz_param_raw_byte(sr, v->param_end_tail[i]); return; } agent_tool_viz_param_raw_byte(sr, c); } static void agent_tool_viz_finish(agent_stream_renderer *sr, const char *status) { agent_tool_visualizer *v = &sr->viz; if (!v->active) return; if (v->param_active) agent_tool_viz_param_end(sr); if (!status || !status[0]) agent_tool_viz_render_read(sr); if (status && status[0]) { if (!v->last_output_newline) agent_tool_viz_puts(sr, "\n"); renderer_color(sr->renderer, "\x1b[90m"); agent_tool_viz_puts(sr, status); renderer_color(sr->renderer, "\x1b[0m"); } if (!v->last_output_newline) agent_tool_viz_puts(sr, "\n"); v->active = false; } static void agent_tool_viz_dump_invalid_dsml(agent_stream_renderer *sr) { agent_tool_visualizer *v = &sr->viz; if (!v->active) return; /* The normal path hides DSML and paints a friendly semantic projection. If * parsing fails, show the exact bytes we rejected so the next fix is based * on evidence instead of guessing from the projection. */ if (v->param_active) { v->param_active = false; v->param_end_len = 0; v->param_name[0] = '\0'; } if (!v->last_output_newline) agent_tool_viz_puts(sr, "\n"); renderer_color(sr->renderer, "\x1b[1;31m"); if (sr->parser->raw && sr->parser->raw_len) { agent_tool_viz_write(sr, sr->parser->raw, sr->parser->raw_len); } else { agent_tool_viz_puts(sr, ""); } renderer_color(sr->renderer, "\x1b[0m"); if (!v->last_output_newline) agent_tool_viz_puts(sr, "\n"); } static void agent_stream_finish_ignored_dsml(agent_stream_renderer *sr, const char *detail) { const char *msg = detail && detail[0] ? detail : "tool calling is not allowed inside "; sr->dsml_in_think = true; sr->dsml_in_think_reported = true; agent_trace(sr->renderer->worker, "dsml ignored inside thinking: %s", msg); if (!sr->renderer->last_output_newline) renderer_plain(sr->renderer, "\n", 1); renderer_color(sr->renderer, "\x1b[1;31m"); renderer_plain(sr->renderer, "[tool call ignored: ", 20); renderer_plain(sr->renderer, msg, strlen(msg)); renderer_plain(sr->renderer, "]\n", 2); renderer_color(sr->renderer, "\x1b[0m"); agent_dsml_parser_reset(sr->parser); sr->dsml_active = false; sr->dsml_ignored = false; } static void agent_stream_malformed_dsml(agent_stream_renderer *sr, const char *detail) { const char *msg = detail && detail[0] ? detail : "DSML markup outside a valid tool_calls block"; if (sr->parser->state == AGENT_DSML_ERROR) return; agent_dsml_set_error(sr->parser, msg); agent_trace(sr->renderer->worker, "malformed dsml in assistant output: %s", msg); if (!sr->renderer->last_output_newline) renderer_plain(sr->renderer, "\n", 1); renderer_color(sr->renderer, "\x1b[1;31m"); renderer_plain(sr->renderer, "[invalid tool call: ", 20); renderer_plain(sr->renderer, msg, strlen(msg)); renderer_plain(sr->renderer, "]\n", 2); renderer_color(sr->renderer, "\x1b[0m"); } /* Mirror parser progress into the terminal visualizer. Parser state is the * source of truth; this function only decides what the user should see. */ static void agent_stream_tool_events(agent_stream_renderer *sr) { agent_dsml_parser *p = sr->parser; agent_tool_visualizer *v = &sr->viz; if (!v->tool_announced && p->current.name) agent_tool_viz_tool(sr, p->current.name); if (v->tool_announced && !p->current.name && !v->param_active) { agent_tool_viz_render_read(sr); if (!v->last_output_newline) agent_tool_viz_puts(sr, "\n"); v->read_style = false; v->read_prefix_rendered = false; v->read_line_rendered = false; v->read_path[0] = '\0'; v->read_start[0] = '\0'; v->read_max[0] = '\0'; v->read_whole[0] = '\0'; v->tool_announced = false; } if (!v->param_active && p->state == AGENT_DSML_PARAM_VALUE && p->param_name) agent_tool_viz_param_begin(sr, p->param_name); } static void agent_stream_preflight_closed_param(agent_stream_renderer *sr) { if (!sr || sr->replay || sr->dsml_ignored || sr->tool_preflight_error) return; agent_dsml_parser *p = sr->parser; agent_tool_visualizer *v = &sr->viz; if (!p || !v->param_active || strcmp(v->param_name, "old") != 0) return; if (!p->current.name || strcmp(p->current.name, "edit") != 0) return; char err[256] = {0}; if (agent_preflight_edit_old(sr->renderer->worker, &p->current, err, sizeof(err))) return; sr->tool_preflight_error = true; snprintf(sr->tool_preflight_error_msg, sizeof(sr->tool_preflight_error_msg), "edit old selector failed before new was generated: %s", err[0] ? err : "old text is not a unique match"); agent_trace(sr->renderer->worker, "edit old preflight failed: %s", sr->tool_preflight_error_msg); } static void agent_stream_feed_dsml_byte(agent_stream_renderer *sr, char c) { bool was_param = !sr->dsml_ignored && sr->viz.param_active; agent_dsml_feed(sr->parser, &c, 1); if (!sr->dsml_ignored) { agent_stream_tool_events(sr); if (was_param) agent_tool_viz_param_value_byte(sr, c); if (was_param && sr->parser->state != AGENT_DSML_PARAM_VALUE && sr->viz.param_active) { agent_stream_preflight_closed_param(sr); agent_tool_viz_param_end(sr); } } if (sr->parser->state == AGENT_DSML_DONE) { if (sr->dsml_ignored) { agent_stream_finish_ignored_dsml( sr, "tool calling is not allowed inside "); } else { agent_trace(sr->renderer->worker, "dsml done calls=%d", sr->parser->calls.len); agent_tool_viz_finish(sr, NULL); sr->dsml_active = false; } } else if (sr->parser->state == AGENT_DSML_ERROR) { if (sr->dsml_ignored) { agent_stream_finish_ignored_dsml( sr, "malformed tool call inside "); } else { char status[220]; snprintf(status, sizeof(status), "[invalid tool call: %s]\n", sr->parser->error[0] ? sr->parser->error : "parse error"); agent_trace(sr->renderer->worker, "dsml error %s", sr->parser->error[0] ? sr->parser->error : "parse error"); agent_tool_viz_dump_invalid_dsml(sr); agent_tool_viz_finish(sr, status); sr->dsml_active = false; } } } /* Start a DSML block from the streaming detector. The detector may accept a * known malformed opening form for robustness, but the parser is seeded with * canonical bytes so all later parsing remains strict. */ static void agent_stream_start_dsml(agent_stream_renderer *sr, bool ignored) { sr->dsml_active = true; sr->dsml_ignored = ignored; if (ignored) sr->dsml_in_think = true; sr->dsml_start_len = 0; sr->post_think_gap = false; agent_trace(sr->renderer->worker, "dsml start detected%s", ignored ? " inside thinking" : ""); agent_dsml_start(sr->parser); if (!ignored) { agent_tool_viz_start(sr); agent_stream_tool_events(sr); } } static void agent_stream_note_plain_dsml_byte(agent_stream_renderer *sr, char c); static void agent_stream_flush_start_tail(agent_stream_renderer *sr) { if (!sr->dsml_start_len) return; sr->post_think_gap = false; for (size_t i = 0; i < sr->dsml_start_len; i++) { renderer_write_char(sr->renderer, sr->dsml_start_tail[i]); agent_stream_note_plain_dsml_byte(sr, sr->dsml_start_tail[i]); if (sr->parser->state == AGENT_DSML_ERROR) break; } sr->dsml_start_len = 0; } static bool agent_stream_dsml_start_match(const char *tail, size_t len, bool *complete, bool *implicit_invoke) { static const char canonical[] = "<|DSML|tool_calls>"; static const char missing_bar[] = ""; static const char invoke[] = "<|DSML|invoke"; static const char invoke_missing_bar[] = "= needle_len && memcmp(tail + len - needle_len, needle, needle_len) == 0; } /* Detect DSML-looking control markers in text that is not currently owned by * the executable DSML parser. This helper intentionally has no policy: inside * the marker means "tool call attempted too early", while in normal * assistant output it means malformed DSML that the model should see as a tool * error. */ static bool agent_dsml_marker_detector_feed(agent_dsml_marker_detector *d, char c) { if (d->len == sizeof(d->tail)) { memmove(d->tail, d->tail + 1, sizeof(d->tail) - 1); d->len--; } d->tail[d->len++] = c; static const char fullwidth_marker[] = "|DSML|"; static const char ascii_marker[] = "|DSML|"; static const char missing_open[] = "tail, d->len, fullwidth_marker, sizeof(fullwidth_marker) - 1) || agent_tail_matches(d->tail, d->len, ascii_marker, sizeof(ascii_marker) - 1) || agent_tail_matches(d->tail, d->len, missing_open, sizeof(missing_open) - 1) || agent_tail_matches(d->tail, d->len, missing_close, sizeof(missing_close) - 1); } static void agent_stream_note_thinking_dsml_byte(agent_stream_renderer *sr, char c) { if (!sr->in_think || sr->dsml_in_think) return; if (agent_dsml_marker_detector_feed(&sr->think_dsml, c)) sr->dsml_in_think = true; } static void agent_stream_note_plain_dsml_byte(agent_stream_renderer *sr, char c) { if (sr->parser->state == AGENT_DSML_ERROR) return; if (sr->dsml_active || sr->in_think || sr->dsml_in_think) return; if (agent_dsml_marker_detector_feed(&sr->plain_dsml, c)) { agent_stream_malformed_dsml( sr, "DSML markup outside a valid tool_calls block"); } } /* Route ordinary assistant bytes either to normal markdown rendering or into * the DSML detector. The detector must hold short prefixes because the model * can split "<|DSML|tool_calls>" across arbitrary tokens. */ static void agent_stream_normal_byte(agent_stream_renderer *sr, char c) { static const char start[] = "<|DSML|tool_calls>"; static const char canonical_invoke[] = "<|DSML|invoke"; if (sr->parser->state == AGENT_DSML_ERROR) return; agent_stream_note_thinking_dsml_byte(sr, c); /* DeepSeek usually emits one or more blank lines after before * either prose or a DSML tool stanza. At that point the bytes are just a * visual gap between the hidden thinking phase and the real answer, and * printing them makes tool calls appear after odd empty lines. We only * suppress whitespace in this very narrow post-thinking window; once the * first non-space byte arrives, normal rendering resumes. */ if (sr->post_think_gap && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) { return; } if (sr->dsml_start_len || c == start[0]) { if (sr->dsml_start_len < sizeof(sr->dsml_start_tail)) sr->dsml_start_tail[sr->dsml_start_len++] = c; bool complete = false, implicit_invoke = false; if (agent_stream_dsml_start_match(sr->dsml_start_tail, sr->dsml_start_len, &complete, &implicit_invoke)) { if (complete) { /* Accept the common missing-leading-bar typo * "" here, but seed the parser with the * canonical marker so the rest of the DSML parser stays * strict and simple. Also accept a direct invoke opener as an * implicit tool_calls block; the model often knows it wants a * tool but forgets the outer wrapper. */ agent_stream_start_dsml(sr, sr->in_think); if (implicit_invoke) { for (size_t i = 0; i < sizeof(canonical_invoke) - 1; i++) agent_stream_feed_dsml_byte(sr, canonical_invoke[i]); } } return; } if (sr->dsml_start_len > 1 && sr->dsml_start_tail[sr->dsml_start_len - 1] == start[0]) { sr->post_think_gap = false; size_t flush = sr->dsml_start_len - 1; for (size_t i = 0; i < flush; i++) { renderer_write_char(sr->renderer, sr->dsml_start_tail[i]); agent_stream_note_plain_dsml_byte(sr, sr->dsml_start_tail[i]); if (sr->parser->state == AGENT_DSML_ERROR) break; } if (sr->parser->state == AGENT_DSML_ERROR) { sr->dsml_start_len = 0; return; } sr->dsml_start_tail[0] = start[0]; sr->dsml_start_len = 1; return; } agent_stream_flush_start_tail(sr); return; } sr->post_think_gap = false; renderer_write_char(sr->renderer, c); agent_stream_note_plain_dsml_byte(sr, c); } /* This is the single streaming display state machine for assistant output. It * hides raw DSML as soon as the tool_calls marker is complete, lets the DSML * parser continue building executable calls, and paints semantic tool output * from parser state changes. The sampled transcript remains unchanged: only * the terminal projection is rewritten. */ static void agent_stream_text(agent_stream_renderer *sr, const char *text, size_t len, bool finish) { const char *think_open = ""; const char *think_close = ""; size_t total = sr->pending_len + len; char *buf = xmalloc(total ? total : 1); if (sr->pending_len) memcpy(buf, sr->pending, sr->pending_len); if (len) memcpy(buf + sr->pending_len, text, len); sr->pending_len = 0; /* The UI may reset terminal attributes while redrawing the editable prompt * between generated chunks. If a DSML parameter is still streaming, make * each new token fragment self-contained by restoring the active parameter * color before visible bytes are projected. This keeps the prompt normal * without sacrificing long write/edit content coloring. */ if (len) agent_tool_viz_restore_param_color(sr); if (len && !sr->dsml_active) renderer_restore_text_attrs(sr->renderer); size_t i = 0; while (i < total) { char *cur = buf + i; size_t rem = total - i; if (!sr->dsml_active && bytes_has_prefix(cur, rem, think_open)) { agent_stream_flush_start_tail(sr); sr->post_think_gap = false; sr->in_think = true; sr->renderer->in_think = true; i += strlen(think_open); continue; } if (!sr->dsml_active && bytes_has_prefix(cur, rem, think_close)) { agent_stream_flush_start_tail(sr); sr->in_think = false; sr->renderer->in_think = false; renderer_reset_color(sr->renderer); if (!sr->renderer->last_output_newline) renderer_write(sr->renderer, "\n", 1); renderer_write(sr->renderer, "\n", 1); sr->renderer->last_output_newline = true; sr->post_think_gap = true; i += strlen(think_close); continue; } if (!finish && !sr->dsml_active && cur[0] == '<' && (bytes_is_partial_prefix(cur, rem, think_open) || bytes_is_partial_prefix(cur, rem, think_close))) { if (rem < sizeof(sr->pending)) { memcpy(sr->pending, cur, rem); sr->pending_len = rem; } break; } if (sr->dsml_active) { agent_stream_feed_dsml_byte(sr, cur[0]); } else if (sr->in_think) { /* Tool calls are executable only after thinking has closed. Still * route thinking bytes through the DSML start detector so an * accidental in-think tool stanza can be suppressed cleanly instead * of being shown as raw markup or, worse, executed. */ agent_stream_normal_byte(sr, cur[0]); } else { agent_stream_normal_byte(sr, cur[0]); } i++; } free(buf); if (finish) { agent_stream_flush_start_tail(sr); sr->post_think_gap = false; if (sr->dsml_active) { if (sr->dsml_ignored) { agent_stream_finish_ignored_dsml( sr, "tool calling is not allowed inside "); } else { agent_tool_viz_finish(sr, sr->tool_preflight_error ? "[tool call stopped: edit old selector failed]\n" : "[tool call interrupted]\n"); sr->dsml_active = false; } } if (sr->dsml_in_think && !sr->dsml_in_think_reported) { agent_stream_finish_ignored_dsml( sr, "tool calling is not allowed inside "); } } } /* ============================================================================ * Worker Progress And Generic Buffers * ============================================================================ */ static void worker_progress_cb(void *ud, const char *event, int current, int total) { (void)total; agent_worker *w = ud; if (!w || !event) return; if (strcmp(event, "prefill_chunk") && strcmp(event, "prefill_display")) return; worker_apply_pending_power(w); pthread_mutex_lock(&w->mu); int done = current - w->progress_base; if (done < 0) done = 0; if (done > w->status.prefill_total) done = w->status.prefill_total; w->status.prefill_done = done; double elapsed = now_sec() - w->progress_started_at; w->status.prefill_tps = done > 0 && elapsed > 0.0 ? (double)done / elapsed : 0.0; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } static bool worker_should_interrupt(agent_worker *w) { pthread_mutex_lock(&w->mu); bool interrupt = w->interrupt || w->stop; pthread_mutex_unlock(&w->mu); return interrupt; } /* Ctrl+C is a latched request consumed by the worker. Once an interrupted * operation has reached a stable append-only boundary and is about to publish * IDLE, the request must be acknowledged; otherwise the editor can observe an * idle worker with a stale interrupt still pending. */ static void worker_clear_interrupt(agent_worker *w) { pthread_mutex_lock(&w->mu); w->interrupt = false; pthread_mutex_unlock(&w->mu); } static bool agent_err_is_interrupted(const char *err) { return err && !strcmp(err, "interrupted"); } static bool worker_cancel_session_cb(void *ud) { return worker_should_interrupt(ud); } typedef struct { char *ptr; size_t len; size_t cap; bool truncated; } agent_buf; static void agent_buf_append(agent_buf *b, const char *s, size_t n) { if (!n || b->truncated) return; const size_t max = 128 * 1024; if (b->len + n > max) { n = max > b->len ? max - b->len : 0; b->truncated = true; } if (!n) return; if (b->len + n + 1 > b->cap) { size_t cap = b->cap ? b->cap * 2 : 4096; while (cap < b->len + n + 1) cap *= 2; b->ptr = xrealloc(b->ptr, cap); b->cap = cap; } memcpy(b->ptr + b->len, s, n); b->len += n; b->ptr[b->len] = '\0'; } static void agent_buf_puts(agent_buf *b, const char *s) { agent_buf_append(b, s, strlen(s)); } static char *agent_buf_take(agent_buf *b) { if (!b->ptr) return xstrdup(""); char *p = b->ptr; memset(b, 0, sizeof(*b)); return p; } static bool agent_tokens_equal(const ds4_tokens *a, const ds4_tokens *b) { if (!a || !b || a->len != b->len) return false; for (int i = 0; i < a->len; i++) { if (a->v[i] != b->v[i]) return false; } return true; } static bool agent_mkdir_p(const char *path) { if (!path || !path[0]) return false; char *tmp = xstrdup(path); for (char *p = tmp + 1; *p; p++) { if (*p != '/') continue; *p = '\0'; if (mkdir(tmp, 0700) != 0 && errno != EEXIST) { free(tmp); return false; } *p = '/'; } bool ok = mkdir(tmp, 0700) == 0 || errno == EEXIST; free(tmp); return ok; } static char *agent_default_cache_dir(void) { const char *home = getenv("HOME"); if (!home || !home[0]) home = "."; agent_buf b = {0}; agent_buf_puts(&b, home); if (b.len == 0 || b.ptr[b.len - 1] != '/') agent_buf_puts(&b, "/"); agent_buf_puts(&b, ".ds4/kvcache"); return agent_buf_take(&b); } static char *agent_kv_path_for_sha(const char *dir, const char sha[41]) { char name[44]; memcpy(name, sha, 40); memcpy(name + 40, ".kv", 4); return ds4_kvstore_path_join(dir, name); } static void agent_le_put64(uint8_t *p, uint64_t v) { for (int i = 0; i < 8; i++) p[i] = (uint8_t)(v >> (8 * i)); } /* Agent session IDs are intentionally independent from the rendered transcript: * once a session has a title and creation time, resaving it keeps the same file * name while the transcript and KV payload evolve. */ static void agent_session_identity_sha(const char *title, uint64_t created_at, char sha_out[41]) { size_t title_len = title ? strlen(title) : 0; agent_buf b = {0}; agent_buf_append(&b, title ? title : "", title_len); uint8_t ts[8]; agent_le_put64(ts, created_at); agent_buf_append(&b, (const char *)ts, sizeof(ts)); ds4_kvstore_sha1_bytes_hex(b.ptr ? b.ptr : "", b.len, sha_out); free(b.ptr); } static void agent_worker_clear_session_identity(agent_worker *w) { w->session_sha[0] = '\0'; free(w->session_title); w->session_title = NULL; w->session_created_at = 0; free(w->legacy_session_path_to_delete); w->legacy_session_path_to_delete = NULL; } typedef struct { bool has_title_trailer; bool legacy_identity; char *title; uint64_t created_at; char sha[41]; } agent_kv_session_meta; static void agent_kv_session_meta_free(agent_kv_session_meta *m) { free(m->title); memset(m, 0, sizeof(*m)); } /* ============================================================================ * Agent KV Store And Session Persistence * ============================================================================ */ static char *agent_session_title_from_text(const char *text, size_t text_len, size_t max_bytes); /* Agent sessions deliberately use a different policy from ds4-server: * * - sysprompt.kv is a fixed bootstrap checkpoint for the current tool/system * prompt. Because its name is fixed, the current rendered text is compared * with the text stored in the file before loading. A mismatch simply rebuilds * and overwrites the file. * - conversation sessions are explicit saves only. Their stable file name is * SHA1(title || created_at_le64).kv, where title is the first user prompt and * created_at is preserved across future saves. The title is stored in an * agent-only trailer after the KV payload. * * The DS4 payload stores the exact token sequence and graph state. The rendered * text is retained for listing, history rendering, and stripped-session rebuilds. */ static bool agent_kv_read_text(FILE *fp, uint32_t text_bytes, char **text_out, char *err, size_t err_len) { char *text = xmalloc((size_t)text_bytes + 1); if (fread(text, 1, text_bytes, fp) != text_bytes) { if (err && err_len) snprintf(err, err_len, "truncated cached text"); free(text); return false; } text[text_bytes] = '\0'; *text_out = text; return true; } static bool agent_kv_write_title_trailer(FILE *fp, const char *title, char *err, size_t err_len) { size_t title_len = title ? strlen(title) : 0; if (title_len > UINT32_MAX) { snprintf(err, err_len, "agent session title is too large"); return false; } uint8_t tb[4]; ds4_kvstore_le_put32(tb, (uint32_t)title_len); return fwrite(tb, 1, sizeof(tb), fp) == sizeof(tb) && fwrite(title ? title : "", 1, title_len, fp) == title_len; } /* Read the optional agent title trailer without disturbing the payload cursor. * The caller is positioned just after rendered text, which is also the payload * start expected by ds4_session_load_payload(). */ static bool agent_kv_read_title_trailer(FILE *fp, const ds4_kvstore_entry *hdr, char **title_out, char *err, size_t err_len) { off_t payload_pos = ftello(fp); if (payload_pos < 0) { if (err && err_len) snprintf(err, err_len, "%s", strerror(errno)); return false; } if (hdr->payload_bytes > (uint64_t)LLONG_MAX || fseeko(fp, (off_t)hdr->payload_bytes, SEEK_CUR) != 0) { if (err && err_len) snprintf(err, err_len, "%s", strerror(errno)); return false; } uint8_t tb[4]; if (fread(tb, 1, sizeof(tb), fp) != sizeof(tb)) { if (err && err_len) snprintf(err, err_len, "missing agent session title trailer"); fseeko(fp, payload_pos, SEEK_SET); return false; } uint32_t title_bytes = ds4_kvstore_le_get32(tb); char *title = xmalloc((size_t)title_bytes + 1); if (fread(title, 1, title_bytes, fp) != title_bytes) { if (err && err_len) snprintf(err, err_len, "truncated agent session title trailer"); free(title); fseeko(fp, payload_pos, SEEK_SET); return false; } title[title_bytes] = '\0'; if (fseeko(fp, payload_pos, SEEK_SET) != 0) { if (err && err_len) snprintf(err, err_len, "%s", strerror(errno)); free(title); return false; } *title_out = title; return true; } static void agent_kv_identity_sha(const ds4_kvstore_entry *hdr, const char *text, uint32_t text_bytes, const char *title, char sha_out[41]) { if (hdr->ext_flags & DS4_KVSTORE_EXT_SESSION_TITLE) { agent_session_identity_sha(title ? title : "", hdr->created_at, sha_out); } else { ds4_kvstore_sha1_bytes_hex(text, text_bytes, sha_out); } } /* Load a KV file and optionally verify either its session identity or exact * rendered text. sysprompt.kv uses exact text because the file name is fixed; * saved sessions use their filename SHA: modern agent sessions hash the title * trailer plus created_at, while legacy sessions still hash rendered text. */ static bool agent_kv_load_path(agent_worker *w, const char *path, const char *expected_sha, const char *expected_text, size_t expected_text_len, ds4_tokens *loaded_tokens, agent_kv_session_meta *meta_out, char *err, size_t err_len) { FILE *fp = fopen(path, "rb"); if (!fp) { snprintf(err, err_len, "%s", strerror(errno)); return false; } ds4_kvstore_entry hdr = {0}; uint32_t text_bytes = 0; bool ok = ds4_kvstore_read_header(fp, &hdr, &text_bytes); if (!ok) snprintf(err, err_len, "invalid KV header"); char *text = NULL; if (ok) ok = agent_kv_read_text(fp, text_bytes, &text, err, err_len); char *title = NULL; bool has_title = ok && (hdr.ext_flags & DS4_KVSTORE_EXT_SESSION_TITLE); if (has_title) ok = agent_kv_read_title_trailer(fp, &hdr, &title, err, err_len); uint32_t expected_tokens = hdr.tokens; if (ok && hdr.payload_bytes != 0 && hdr.model_id != (uint8_t)ds4_engine_model_id(w->engine)) { snprintf(err, err_len, "KV checkpoint was written for a different model"); ok = false; } if (ok && hdr.payload_bytes != 0 && hdr.quant_bits != (uint8_t)ds4_engine_routed_quant_bits(w->engine)) { snprintf(err, err_len, "KV checkpoint was written for a different quantization"); ok = false; } if (ok && expected_text) { if ((size_t)text_bytes != expected_text_len || memcmp(text, expected_text, expected_text_len) != 0) { snprintf(err, err_len, "cached text does not match current system prompt"); ok = false; } } if (ok && expected_sha) { char actual_sha[41]; agent_kv_identity_sha(&hdr, text, text_bytes, title, actual_sha); if (strcmp(actual_sha, expected_sha)) { snprintf(err, err_len, "cached session identity does not match file name"); ok = false; } } char load_err[160] = {0}; if (ok && hdr.payload_bytes == 0) { ds4_tokens rebuilt = {0}; ds4_tokenize_rendered_chat(w->engine, text, &rebuilt); expected_tokens = (uint32_t)rebuilt.len; if (agent_worker_sync_tokens(w, &rebuilt, true, err, err_len) != 0) { ds4_session_invalidate(w->session); ok = false; } ds4_tokens_free(&rebuilt); } else if (ok && ds4_session_load_payload(w->session, fp, hdr.payload_bytes, load_err, sizeof(load_err)) != 0) { snprintf(err, err_len, "%s", load_err[0] ? load_err : "failed to load KV payload"); ds4_session_invalidate(w->session); ok = false; } fclose(fp); if (ok) { const ds4_tokens *live = ds4_session_tokens(w->session); if (!live || live->len != (int)expected_tokens) { snprintf(err, err_len, "KV payload token count mismatch"); ds4_session_invalidate(w->session); ok = false; } else if (loaded_tokens) { ds4_tokens_free(loaded_tokens); ds4_tokens_copy(loaded_tokens, live); } if (meta_out) { agent_kv_session_meta_free(meta_out); meta_out->has_title_trailer = has_title; meta_out->legacy_identity = !has_title; meta_out->created_at = hdr.created_at; agent_kv_identity_sha(&hdr, text, text_bytes, title, meta_out->sha); meta_out->title = has_title ? xstrdup(title) : agent_session_title_from_text(text, text_bytes, 0); } } free(title); free(text); return ok; } /* Save the current live KV under the rendered transcript identity. The caller * decides the policy: fixed sysprompt path or SHA-named session path. */ static bool agent_kv_save_path(agent_worker *w, const char *path, const ds4_tokens *tokens, const char *reason, char sha_out[41], const char *session_title, uint64_t session_created_at, char *err, size_t err_len) { const ds4_tokens *live = ds4_session_tokens(w->session); if (!agent_tokens_equal(live, tokens)) { snprintf(err, err_len, "live KV state does not match session transcript"); return false; } const int quant_bits = ds4_engine_routed_quant_bits(w->engine); if (quant_bits != 2 && quant_bits != 4) { snprintf(err, err_len, "unsupported routed quantization for KV save"); return false; } const int model_id = ds4_engine_model_id(w->engine); size_t text_len = 0; char *text = ds4_kvstore_render_tokens_text(w->engine, tokens, &text_len); if (!text) { snprintf(err, err_len, "failed to render KV text key"); return false; } if (text_len > UINT32_MAX) { snprintf(err, err_len, "rendered KV text key is too large"); free(text); return false; } const bool session_identity = session_title != NULL; uint64_t now = (uint64_t)time(NULL); uint64_t created_at = session_identity && session_created_at ? session_created_at : now; char sha[41]; if (session_identity) agent_session_identity_sha(session_title, created_at, sha); else ds4_kvstore_sha1_bytes_hex(text, text_len, sha); if (sha_out) memcpy(sha_out, sha, sizeof(sha)); ds4_session_payload_file staged = {0}; char save_err[160] = {0}; if (ds4_session_stage_payload(w->session, &staged, save_err, sizeof(save_err)) != 0) { snprintf(err, err_len, "%s", save_err[0] ? save_err : "session has no valid KV payload"); free(text); return false; } uint64_t payload_bytes = staged.bytes; agent_buf tmpl = {0}; agent_buf_puts(&tmpl, path); agent_buf_puts(&tmpl, ".tmp.XXXXXX"); char *tmp = agent_buf_take(&tmpl); int fd = mkstemp(tmp); if (fd < 0) { snprintf(err, err_len, "%s", strerror(errno)); ds4_session_payload_file_free(&staged); free(tmp); free(text); return false; } FILE *fp = fdopen(fd, "wb"); if (!fp) { snprintf(err, err_len, "%s", strerror(errno)); close(fd); unlink(tmp); ds4_session_payload_file_free(&staged); free(tmp); free(text); return false; } uint8_t h[DS4_KVSTORE_FIXED_HEADER]; ds4_kvstore_fill_header(h, (uint8_t)model_id, (uint8_t)quant_bits, ds4_kvstore_reason_code(reason), session_identity ? DS4_KVSTORE_EXT_SESSION_TITLE : 0, (uint32_t)tokens->len, 0, (uint32_t)ds4_session_ctx(w->session), created_at, now, payload_bytes); uint8_t tb[4]; ds4_kvstore_le_put32(tb, (uint32_t)text_len); errno = 0; bool ok = fwrite(h, 1, sizeof(h), fp) == sizeof(h) && fwrite(tb, 1, sizeof(tb), fp) == sizeof(tb) && fwrite(text, 1, text_len, fp) == text_len && ds4_session_write_staged_payload(&staged, fp, save_err, sizeof(save_err)) == 0 && (!session_identity || agent_kv_write_title_trailer(fp, session_title, save_err, sizeof(save_err))) && fflush(fp) == 0; int saved_errno = errno; if (fclose(fp) != 0) { if (!saved_errno) saved_errno = errno; ok = false; } if (ok && rename(tmp, path) != 0) { saved_errno = errno; ok = false; } if (!ok) { snprintf(err, err_len, "%s", saved_errno ? strerror(saved_errno) : (save_err[0] ? save_err : "failed to write KV file")); unlink(tmp); } ds4_session_payload_file_free(&staged); free(tmp); free(text); return ok; } static void agent_worker_build_system_tokens(agent_worker *w, ds4_tokens *out) { ds4_chat_begin(w->engine, out); if (w->cfg->gen.think_mode == DS4_THINK_MAX && effective_think_mode(w->cfg) == DS4_THINK_MAX) ds4_chat_append_max_effort_prefix(w->engine, out); agent_append_system_prompt(w->engine, out, w->cfg->gen.system); } static void agent_publish_system_status(agent_worker *w, const char *msg) { if (w->cfg->non_interactive) return; if (isatty(STDOUT_FILENO)) { static const char marker[] = "\x1b[33m✦ \x1b[38;5;218m"; agent_publish(w, marker, sizeof(marker) - 1); agent_publish(w, msg, strlen(msg)); agent_publish(w, "\x1b[0m\n", strlen("\x1b[0m\n")); } else { agent_publish(w, "✦ ", strlen("✦ ")); agent_publish(w, msg, strlen(msg)); agent_publish(w, "\n", 1); } } static void agent_publishf_system_status(agent_worker *w, const char *fmt, ...) { char stack[1024]; va_list ap; va_start(ap, fmt); int n = vsnprintf(stack, sizeof(stack), fmt, ap); va_end(ap); if (n <= 0) return; if ((size_t)n < sizeof(stack)) { agent_publish_system_status(w, stack); return; } char *heap = xmalloc((size_t)n + 1); va_start(ap, fmt); vsnprintf(heap, (size_t)n + 1, fmt, ap); va_end(ap); agent_publish_system_status(w, heap); free(heap); } static int agent_web_confirm(void *privdata, const char *message, char *err, size_t err_len) { agent_worker *w = privdata; if (!w || w->cfg->non_interactive) { snprintf(err, err_len, "visible Chrome browser startup requires interactive approval"); return 0; } pthread_mutex_lock(&w->mu); w->web_approval_pending = true; w->web_approval_answered = false; w->web_approval_result = false; w->web_approval_error[0] = '\0'; snprintf(w->web_approval_message, sizeof(w->web_approval_message), "%s", message ? message : "Start visible Chrome browser? (y/n) "); agent_wake_locked(w); while (!w->stop && !w->interrupt && !w->web_approval_answered) pthread_cond_wait(&w->cond, &w->mu); bool ok = w->web_approval_result; if (!w->web_approval_answered && (w->stop || w->interrupt)) { ok = false; w->web_approval_pending = false; snprintf(w->web_approval_error, sizeof(w->web_approval_error), "interrupted"); } if (!ok) { snprintf(err, err_len, "%s", w->web_approval_error[0] ? w->web_approval_error : "user denied Chrome browser start"); } pthread_mutex_unlock(&w->mu); return ok ? 1 : 0; } static void agent_web_log(void *privdata, const char *message) { agent_worker *w = privdata; if (!w || !message || !message[0]) return; agent_trace(w, "web: %s", message); } static bool agent_web_cancel(void *privdata) { return worker_should_interrupt(privdata); } static bool worker_take_web_approval_request(agent_worker *w, char *message, size_t message_len) { pthread_mutex_lock(&w->mu); bool pending = w->web_approval_pending; if (pending) { snprintf(message, message_len, "%s", w->web_approval_message); w->web_approval_pending = false; } pthread_mutex_unlock(&w->mu); return pending; } static void worker_answer_web_approval(agent_worker *w, bool allow, const char *deny_error) { pthread_mutex_lock(&w->mu); w->web_approval_result = allow; w->web_approval_answered = true; if (!allow) snprintf(w->web_approval_error, sizeof(w->web_approval_error), "%s", deny_error && deny_error[0] ? deny_error : "user denied Chrome browser start"); pthread_cond_signal(&w->cond); agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } /* When a model turn finishes with a tool call, queued user messages should not * preempt that tool. The worker asks the UI thread for the queue contents only * after the tool result is appended, so the next model input can contain both * the tool observation and the user's pending correction. */ static char *worker_request_queued_user_drain(agent_worker *w) { pthread_mutex_lock(&w->mu); w->queued_user_drain_pending = true; w->queued_user_drain_answered = false; free(w->queued_user_drain_text); w->queued_user_drain_text = NULL; agent_wake_locked(w); pthread_cond_signal(&w->cond); while (!w->stop && !w->queued_user_drain_answered) pthread_cond_wait(&w->cond, &w->mu); char *text = w->queued_user_drain_text; w->queued_user_drain_text = NULL; w->queued_user_drain_pending = false; w->queued_user_drain_answered = false; pthread_mutex_unlock(&w->mu); return text; } static bool worker_take_queued_user_drain_request(agent_worker *w) { pthread_mutex_lock(&w->mu); bool pending = w->queued_user_drain_pending; if (pending) w->queued_user_drain_pending = false; pthread_mutex_unlock(&w->mu); return pending; } static void worker_answer_queued_user_drain(agent_worker *w, char *text) { pthread_mutex_lock(&w->mu); free(w->queued_user_drain_text); w->queued_user_drain_text = text; w->queued_user_drain_answered = true; pthread_cond_signal(&w->cond); agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } /* Synchronize the live DS4 session to a transcript. This is the agent's main * cache-saving operation: if the requested transcript extends the live session, * only the suffix is prefetched; otherwise the DS4 session rebuilds from the * longest common prefix it can retain. */ static int agent_worker_sync_tokens(agent_worker *w, const ds4_tokens *tokens, bool publish_progress, char *err, size_t err_len) { int old_pos = ds4_session_pos(w->session); int common = ds4_session_common_prefix(w->session, tokens); int cached = common == old_pos && tokens->len >= old_pos ? common : 0; int suffix = tokens->len - cached; if (suffix < 0) suffix = tokens->len; if (publish_progress) { pthread_mutex_lock(&w->mu); unsigned prefill_label = w->status.state == AGENT_WORKER_PREFILL ? w->status.prefill_label : agent_next_prefill_label(); w->status.state = AGENT_WORKER_PREFILL; w->progress_base = cached; w->progress_started_at = now_sec(); w->status.prefill_done = 0; w->status.prefill_total = suffix; w->status.prefill_label = prefill_label; w->status.prefill_tps = 0.0; w->status.generated = 0; w->status.gen_tps = 0.0; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } ds4_session_set_progress(w->session, publish_progress ? worker_progress_cb : NULL, publish_progress ? w : NULL); ds4_session_set_display_progress(w->session, publish_progress ? worker_progress_cb : NULL, publish_progress ? w : NULL); ds4_session_set_cancel(w->session, worker_cancel_session_cb, w); int rc = ds4_session_sync(w->session, tokens, err, err_len); ds4_session_set_cancel(w->session, NULL, NULL); ds4_session_set_progress(w->session, NULL, NULL); ds4_session_set_display_progress(w->session, NULL, NULL); return rc; } /* Start a new session at the system/tool prompt. A fixed sysprompt.kv * checkpoint avoids paying this prefill cost repeatedly, but only when the * rendered prompt text still matches the file. The same fixed path is shared * by Flash and Pro; agent_kv_load_path() checks the model id, so switching * model families rebuilds this cache instead of restoring incompatible KV. */ static bool agent_worker_reset_to_sysprompt(agent_worker *w, char *err, size_t err_len) { ds4_tokens sys = {0}; agent_worker_build_system_tokens(w, &sys); size_t text_len = 0; char *text = ds4_kvstore_render_tokens_text(w->engine, &sys, &text_len); if (!text) { snprintf(err, err_len, "failed to render system prompt"); ds4_tokens_free(&sys); return false; } bool loaded = false; char load_err[160] = {0}; if (w->sysprompt_path) { loaded = agent_kv_load_path(w, w->sysprompt_path, NULL, text, text_len, &w->transcript, NULL, load_err, sizeof(load_err)); if (loaded) { agent_trace(w, "sysprompt kv hit file=%s tokens=%d", w->sysprompt_path, w->transcript.len); } } if (!loaded) { if (w->sysprompt_path) agent_publish_system_status(w, "Updating system prompt cache..."); ds4_tokens_free(&w->transcript); ds4_tokens_copy(&w->transcript, &sys); if (agent_worker_sync_tokens(w, &w->transcript, true, err, err_len) != 0) { free(text); ds4_tokens_free(&sys); return false; } if (w->sysprompt_path) { char save_err[160] = {0}; char ignored_sha[41]; if (!agent_kv_save_path(w, w->sysprompt_path, &w->transcript, "agent-system", ignored_sha, NULL, 0, save_err, sizeof(save_err))) { if (w->cfg->non_interactive) { fprintf(stderr, "ds4-agent: failed to save system prompt KV: %s\n", save_err); } else { agent_buf b = {0}; agent_buf_puts(&b, "\nds4-agent: failed to save system prompt KV: "); agent_buf_puts(&b, save_err); agent_buf_puts(&b, "\n"); char *msg = agent_buf_take(&b); agent_publish(w, msg, strlen(msg)); free(msg); } } else { agent_trace(w, "sysprompt kv stored file=%s tokens=%d", w->sysprompt_path, w->transcript.len); } } } agent_worker_note_system_prompt_seen(w); pthread_mutex_lock(&w->mu); w->user_activity = false; w->session_dirty = false; w->status.state = AGENT_WORKER_IDLE; w->status.prefill_done = 0; w->status.prefill_total = 0; w->status.prefill_tps = 0.0; w->status.generated = 0; w->status.gen_tps = 0.0; w->status.greedy_sampling = false; w->status.error[0] = '\0'; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); w->datetime_context_injected = false; agent_worker_clear_session_identity(w); free(text); ds4_tokens_free(&sys); return true; } static bool agent_worker_should_stop(agent_worker *w) { pthread_mutex_lock(&w->mu); bool stop = w->stop; pthread_mutex_unlock(&w->mu); return stop; } static bool agent_worker_wait_distributed_route(agent_worker *w, char *err, size_t err_len) { if (!w || !w->cfg || w->cfg->engine.distributed.role != DS4_DISTRIBUTED_COORDINATOR) return true; char last[160] = {0}; unsigned ticks = 0; const struct timespec delay = {0, 250000000L}; for (;;) { int ready = ds4_session_distributed_route_ready(w->session, err, err_len); if (ready > 0) { if (ticks != 0) { if (w->cfg->non_interactive) fprintf(stderr, "ds4-agent: distributed route ready\n"); else agent_publish_system_status(w, "Distributed route ready."); } if (err_len) err[0] = '\0'; return true; } if (ready < 0) return false; const char *why = err && err[0] ? err : "route incomplete"; if (strcmp(last, why) != 0 || (ticks % 20u) == 0) { if (w->cfg->non_interactive) { fprintf(stderr, "ds4-agent: waiting for distributed route: %s\n", why); } else { char msg[224]; snprintf(msg, sizeof(msg), "Waiting for distributed route: %s", why); agent_publish_system_status(w, msg); } snprintf(last, sizeof(last), "%s", why); } if (agent_worker_should_stop(w)) { snprintf(err, err_len, "agent stopped while waiting for distributed route"); return false; } nanosleep(&delay, NULL); ticks++; } } static bool agent_worker_has_user_session(agent_worker *w) { pthread_mutex_lock(&w->mu); bool yes = w->user_activity; pthread_mutex_unlock(&w->mu); return yes; } static bool agent_worker_needs_save(agent_worker *w) { pthread_mutex_lock(&w->mu); bool yes = w->user_activity && w->session_dirty; pthread_mutex_unlock(&w->mu); return yes; } /* Save the current session under its stable agent identity. The worker owns * the live KV, so busy /save requests are deferred until a stable append-only * point and then executed by the worker thread. */ static bool agent_worker_save_session_now(agent_worker *w, char sha_out[41], int *tokens_out, char *err, size_t err_len) { if (!agent_worker_has_user_session(w)) { snprintf(err, err_len, "nothing to save"); return false; } if (agent_worker_sync_tokens(w, &w->transcript, false, err, err_len) != 0) return false; if (!agent_mkdir_p(w->cache_dir)) { snprintf(err, err_len, "failed to create %s", w->cache_dir); return false; } size_t text_len = 0; char *text = ds4_kvstore_render_tokens_text(w->engine, &w->transcript, &text_len); if (!text) { snprintf(err, err_len, "failed to render session text"); return false; } if (!w->session_title) { w->session_title = agent_session_title_from_text(text, text_len, 0); } if (w->session_created_at == 0) w->session_created_at = (uint64_t)time(NULL); char sha[41]; agent_session_identity_sha(w->session_title, w->session_created_at, sha); char *path = agent_kv_path_for_sha(w->cache_dir, sha); bool ok = agent_kv_save_path(w, path, &w->transcript, "agent-session", sha_out, w->session_title, w->session_created_at, err, err_len); if (ok) { memcpy(w->session_sha, sha, sizeof(w->session_sha)); if (w->legacy_session_path_to_delete && strcmp(w->legacy_session_path_to_delete, path) != 0) { unlink(w->legacy_session_path_to_delete); } free(w->legacy_session_path_to_delete); w->legacy_session_path_to_delete = NULL; pthread_mutex_lock(&w->mu); w->session_dirty = false; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); if (tokens_out) *tokens_out = w->transcript.len; } free(path); free(text); return ok; } static bool agent_worker_save_session(agent_worker *w, char *err, size_t err_len) { if (!worker_is_idle(w)) { snprintf(err, err_len, "model is busy"); return false; } char sha[41]; int tokens = 0; bool ok = agent_worker_save_session_now(w, sha, &tokens, err, err_len); if (ok) printf("saved session %.8s (%d tokens)\n", sha, tokens); return ok; } /* ============================================================================ * Session Listing, History Rendering, And Completion * ============================================================================ */ static void agent_format_age(uint64_t when, char *buf, size_t len) { uint64_t now = (uint64_t)time(NULL); uint64_t age = when && now > when ? now - when : 0; if (age < 60) snprintf(buf, len, "%llus ago", (unsigned long long)age); else if (age < 3600) snprintf(buf, len, "%llum ago", (unsigned long long)(age / 60)); else if (age < 86400) snprintf(buf, len, "%lluh ago", (unsigned long long)(age / 3600)); else snprintf(buf, len, "%llud ago", (unsigned long long)(age / 86400)); } static char *agent_session_title_from_span(const char *p, const char *end, size_t max_bytes, const char *empty_title) { bool limited = max_bytes != 0; if (limited && max_bytes < 4) max_bytes = 4; while (p < end && isspace((unsigned char)*p)) p++; while (end > p && isspace((unsigned char)end[-1])) end--; agent_buf b = {0}; bool space = false; bool truncated = false; for (const char *s = p; s < end; s++) { unsigned char c = (unsigned char)*s; if (isspace(c)) { space = b.len != 0; continue; } if (space && (!limited || b.len + 4 < max_bytes)) { agent_buf_puts(&b, " "); space = false; } if (limited && b.len + 4 > max_bytes) { truncated = true; break; } agent_buf_append(&b, s, 1); } if (truncated) agent_buf_puts(&b, "..."); if (!b.ptr || !b.len) { free(b.ptr); return xstrdup(empty_title); } return agent_buf_take(&b); } static char *agent_session_title_from_prompt(const char *prompt, size_t max_bytes) { const char *p = prompt ? prompt : ""; return agent_session_title_from_span(p, p + strlen(p), max_bytes, "(empty user prompt)"); } /* Extract a human-readable title from the first user turn stored in the * rendered transcript. max_bytes==0 means "full normalized title"; callers * that render to the terminal pass an explicit display budget. */ static char *agent_session_title_from_text(const char *text, size_t text_len, size_t max_bytes) { static const char user_mark[] = "<|User|>"; static const char assistant_mark[] = "<|Assistant|>"; const char *p = text ? strstr(text, user_mark) : NULL; if (!p) return xstrdup("(no user prompt)"); p += strlen(user_mark); const char *end = text + text_len; const char *assistant = strstr(p, assistant_mark); const char *next_user = strstr(p, user_mark); if (assistant && assistant < end) end = assistant; if (next_user && next_user < end) end = next_user; return agent_session_title_from_span(p, end, max_bytes, "(empty user prompt)"); } static char *agent_session_title_clip(const char *title, size_t max_bytes) { if (!title) return xstrdup("(no user prompt)"); size_t len = strlen(title); if (max_bytes == 0 || len <= max_bytes) return xstrdup(title); if (max_bytes < 4) max_bytes = 4; agent_buf b = {0}; agent_buf_append(&b, title, max_bytes - 3); agent_buf_puts(&b, "..."); return agent_buf_take(&b); } static char *agent_session_title_from_file(const char *path, size_t max_bytes) { FILE *fp = fopen(path, "rb"); if (!fp) return xstrdup("(unreadable session)"); ds4_kvstore_entry hdr = {0}; uint32_t text_bytes = 0; char *text = NULL; char *trailer_title = NULL; bool ok = ds4_kvstore_read_header(fp, &hdr, &text_bytes) && agent_kv_read_text(fp, text_bytes, &text, NULL, 0); if (ok && (hdr.ext_flags & DS4_KVSTORE_EXT_SESSION_TITLE)) ok = agent_kv_read_title_trailer(fp, &hdr, &trailer_title, NULL, 0); fclose(fp); char *title = ok ? (trailer_title ? agent_session_title_clip(trailer_title, max_bytes) : agent_session_title_from_text(text, text_bytes, max_bytes)) : xstrdup("(unreadable session)"); free(trailer_title); free(text); return title; } #define AGENT_HISTORY_DEFAULT_TURNS 3 #define AGENT_HISTORY_MAX_TURNS 200 #define AGENT_HISTORY_ASSISTANT_MAX_LINES 80 #define AGENT_HISTORY_ASSISTANT_MAX_BYTES 12000 typedef enum { AGENT_HISTORY_MARK_NONE, AGENT_HISTORY_MARK_USER, AGENT_HISTORY_MARK_ASSISTANT, AGENT_HISTORY_MARK_EOS, } agent_history_mark; typedef struct { const char **v; agent_history_mark *mark; int len; int cap; } agent_history_ptrs; static void agent_history_ptrs_push(agent_history_ptrs *p, const char *s, agent_history_mark mark) { if (p->len == p->cap) { p->cap = p->cap ? p->cap * 2 : 16; p->v = xrealloc(p->v, (size_t)p->cap * sizeof(p->v[0])); p->mark = xrealloc(p->mark, (size_t)p->cap * sizeof(p->mark[0])); } p->v[p->len] = s; p->mark[p->len] = mark; p->len++; } static const char *agent_memmem(const char *hay, size_t hay_len, const char *needle, size_t needle_len) { if (!needle_len) return hay; if (needle_len > hay_len) return NULL; const char first = needle[0]; const char *end = hay + hay_len - needle_len + 1; for (const char *p = hay; p < end; p++) { if (*p == first && memcmp(p, needle, needle_len) == 0) return p; } return NULL; } static const char *agent_history_next_marker(const char *p, const char *end, agent_history_mark *mark, size_t *mark_len) { static const char user_mark[] = "<|User|>"; static const char assistant_mark[] = "<|Assistant|>"; static const char eos_mark[] = "<|end▁of▁sentence|>"; const char *u = agent_memmem(p, (size_t)(end - p), user_mark, sizeof(user_mark) - 1); const char *a = agent_memmem(p, (size_t)(end - p), assistant_mark, sizeof(assistant_mark) - 1); const char *e = agent_memmem(p, (size_t)(end - p), eos_mark, sizeof(eos_mark) - 1); if (!u && !a && !e) return NULL; if (u && (!a || u < a) && (!e || u < e)) { if (mark) *mark = AGENT_HISTORY_MARK_USER; if (mark_len) *mark_len = sizeof(user_mark) - 1; return u; } if (a && (!e || a < e)) { if (mark) *mark = AGENT_HISTORY_MARK_ASSISTANT; if (mark_len) *mark_len = sizeof(assistant_mark) - 1; return a; } if (mark) *mark = AGENT_HISTORY_MARK_EOS; if (mark_len) *mark_len = sizeof(eos_mark) - 1; return e; } static void agent_history_trim(const char **p, const char **end) { while (*p < *end && isspace((unsigned char)**p)) (*p)++; while (*end > *p && isspace((unsigned char)(*end)[-1])) (*end)--; } static bool agent_history_has_prefix(const char *p, const char *end, const char *prefix) { size_t n = strlen(prefix); return (size_t)(end - p) >= n && memcmp(p, prefix, n) == 0; } /* Tool messages are rendered as user turns in the transcript. Return the * inner payload for the current wrapper so /history skips these * pseudo-user turns and displays their content without leaking the wrapper. */ static bool agent_history_tool_result_payload(const char **p, const char **end) { const char *s = *p, *e = *end; agent_history_trim(&s, &e); const char *open = ""; const char *close = ""; const size_t open_len = strlen(open); const size_t close_len = strlen(close); if (!agent_history_has_prefix(s, e, open)) return false; s += open_len; if ((size_t)(e - s) >= close_len && memcmp(e - close_len, close, close_len) == 0) { e -= close_len; } *p = s; *end = e; return true; } static bool agent_history_is_tool_user(const char *p, const char *end) { agent_history_trim(&p, &end); return agent_history_tool_result_payload(&p, &end) || agent_history_has_prefix(p, end, "Tool:") || agent_history_has_prefix(p, end, "Tool result"); } static void agent_history_ptrs_free(agent_history_ptrs *p) { free(p->v); free(p->mark); memset(p, 0, sizeof(*p)); } /* Find the oldest rendered-chat marker needed to show the last N user turns. * Tool-result pseudo-user turns are skipped while human turns exist, so * /history stays centered on the human conversation. Compacted sessions can * legitimately have a tail made only of tool result turns; in that case we * fall back to recent tool/assistant events instead of showing an empty * history. */ static const char *agent_history_start_for_turns(const char *text, size_t len, int user_turns, bool *tool_only) { const char *end = text + len; agent_history_ptrs marks = {0}; agent_history_ptrs users = {0}; agent_history_ptrs all_users = {0}; const char *p = text; while (p < end) { agent_history_mark mark = AGENT_HISTORY_MARK_NONE; size_t mark_len = 0; const char *m = agent_history_next_marker(p, end, &mark, &mark_len); if (!m) break; agent_history_ptrs_push(&marks, m, mark); const char *content = m + mark_len; agent_history_mark next_mark = AGENT_HISTORY_MARK_NONE; size_t next_len = 0; const char *next = agent_history_next_marker(content, end, &next_mark, &next_len); const char *content_end = next ? next : end; if (mark == AGENT_HISTORY_MARK_USER) { agent_history_ptrs_push(&all_users, m, mark); if (!agent_history_is_tool_user(content, content_end)) agent_history_ptrs_push(&users, m, mark); } p = content_end; } const char *start = end; if (tool_only) *tool_only = false; if (users.len > 0) { int idx = users.len - user_turns; if (idx < 0) idx = 0; start = users.v[idx]; } else if (all_users.len > 0) { int idx = all_users.len - user_turns; if (idx < 0) idx = 0; start = all_users.v[idx]; if (tool_only) *tool_only = true; /* Tool result messages are stored as user-role turns after the * assistant DSML stanza that produced them. Include that preceding * assistant marker when it is still in the retained tail, otherwise * replay shows the result but hides the call that caused it. */ for (int i = marks.len - 1; i >= 0; i--) { if (marks.v[i] >= start) continue; if (marks.mark[i] == AGENT_HISTORY_MARK_USER) break; if (marks.mark[i] == AGENT_HISTORY_MARK_ASSISTANT) { start = marks.v[i]; break; } } } agent_history_ptrs_free(&marks); agent_history_ptrs_free(&users); agent_history_ptrs_free(&all_users); return start; } static bool agent_history_latest_compaction_summary(const char *text, size_t len, const char **sum_start, const char **sum_end) { static const char start_mark[] = "[ds4-agent compacted earlier conversation. Durable task-state summary follows.]"; static const char end_mark[] = "[End compacted summary. Recent conversation continues verbatim below.]"; const char *end = text + len; const char *scan = text; const char *best_start = NULL; const char *best_end = NULL; while (scan < end) { const char *s = agent_memmem(scan, (size_t)(end - scan), start_mark, sizeof(start_mark) - 1); if (!s) break; const char *content = s + sizeof(start_mark) - 1; const char *e = agent_memmem(content, (size_t)(end - content), end_mark, sizeof(end_mark) - 1); if (!e) break; best_start = content; best_end = e; scan = e + sizeof(end_mark) - 1; } if (!best_start || !best_end) return false; agent_history_trim(&best_start, &best_end); if (best_start >= best_end) return false; if (sum_start) *sum_start = best_start; if (sum_end) *sum_end = best_end; return true; } static void agent_history_publish_limited(agent_worker *w, const char *p, const char *end, int max_lines, size_t max_bytes); static void agent_history_render_compaction_summary(agent_worker *w, const char *text, size_t len) { const char *p = NULL, *end = NULL; if (!agent_history_latest_compaction_summary(text, len, &p, &end)) return; bool color = isatty(STDOUT_FILENO) != 0; if (color) { const char *s = "\n\x1b[1;95mCompacted Summary:\x1b[0m\n"; agent_publish(w, s, strlen(s)); } else { agent_publish(w, "\nCompacted Summary:\n", strlen("\nCompacted Summary:\n")); } agent_history_publish_limited(w, p, end, 80, 12000); } static const char *agent_history_skip_utf8_continuation(const char *p, const char *end) { while (p < end && (((unsigned char)*p) & 0xc0) == 0x80) p++; return p; } static const char *agent_history_tail_start(const char *p, const char *end, int max_lines, size_t max_bytes, bool *truncated) { *truncated = false; if (p >= end) return p; const char *start = p; size_t len = (size_t)(end - p); if (max_bytes && len > max_bytes) { start = end - max_bytes; *truncated = true; } if (max_lines > 0) { const char *scan = end; if (scan > p && scan[-1] == '\n') scan--; const char *line_start = p; int lines = 0; while (scan > p) { scan--; if (*scan == '\n' && ++lines == max_lines) { line_start = scan + 1; break; } } if (line_start > p) *truncated = true; if (line_start > start) start = line_start; } return agent_history_skip_utf8_continuation(start, end); } static void agent_history_publish_limited(agent_worker *w, const char *p, const char *end, int max_lines, size_t max_bytes) { bool truncated = false; const char *start = agent_history_tail_start(p, end, max_lines, max_bytes, &truncated); if (truncated) agent_publish(w, "\n... earlier history truncated; showing tail ...\n", strlen("\n... earlier history truncated; showing tail ...\n")); agent_publish(w, start, (size_t)(end - start)); if (end > start && end[-1] != '\n') agent_publish(w, "\n", 1); } static void agent_history_render_assistant(agent_worker *w, const char *p, const char *end) { agent_history_trim(&p, &end); if (p >= end) return; bool source_truncated = false; (void)agent_history_tail_start(p, end, AGENT_HISTORY_ASSISTANT_MAX_LINES, AGENT_HISTORY_ASSISTANT_MAX_BYTES, &source_truncated); bool use_color = isatty(STDOUT_FILENO) != 0; agent_tail_capture tail = { .cap = source_truncated ? AGENT_HISTORY_ASSISTANT_MAX_BYTES : 0, }; agent_token_renderer renderer = { .engine = w->engine, .worker = w, .format_thinking = true, /* History replay should look like the original live output: the user is * switching back to a session, not reading a different transcript * format. Tool calls are still dry-rendered below, so replay never * executes tools or mutates transcript state. */ .format_markdown = true, .use_color = use_color && !source_truncated, .last_output_newline = true, .capture = source_truncated ? &tail : NULL, }; agent_dsml_parser dsml = {.state = AGENT_DSML_SEARCH}; agent_stream_renderer stream = { .renderer = &renderer, .parser = &dsml, .replay = true, }; /* Dry-run replay: the same streaming projection hides DSML and renders * semantic tool lines, but no tool is executed and no transcript state is * changed. The saved KV payload remains the only authority for resume. */ agent_stream_text(&stream, p, (size_t)(end - p), true); renderer_finish(&renderer); agent_dsml_parser_free(&dsml); if (source_truncated) { size_t tail_len = 0; char *tail_text = agent_tail_capture_take(&tail, &tail_len); bool rendered_truncated = tail.total > tail_len; bool line_truncated = false; const char *tail_start = agent_history_tail_start(tail_text, tail_text + tail_len, AGENT_HISTORY_ASSISTANT_MAX_LINES, AGENT_HISTORY_ASSISTANT_MAX_BYTES, &line_truncated); if (use_color) agent_publish(w, "\x1b[90m", 5); agent_publish(w, "\n... earlier assistant history truncated; showing tail ...\n", strlen("\n... earlier assistant history truncated; showing tail ...\n")); (void)rendered_truncated; agent_publish(w, tail_start, (size_t)(tail_text + tail_len - tail_start)); if (tail_len && tail_text[tail_len - 1] != '\n') agent_publish(w, "\n", 1); if (use_color) agent_publish(w, "\x1b[0m", 4); free(tail_text); } } /* Re-render saved transcript text for /history and /switch. It intentionally * uses the same assistant/token renderer as live output, so restored history * looks like the original terminal stream instead of raw rendered-chat text. */ static void agent_history_render_text(agent_worker *w, const char *text, size_t len, int user_turns) { if (user_turns <= 0) return; if (user_turns > AGENT_HISTORY_MAX_TURNS) user_turns = AGENT_HISTORY_MAX_TURNS; const char *end = text + len; agent_history_render_compaction_summary(w, text, len); bool tool_only = false; const char *p = agent_history_start_for_turns(text, len, user_turns, &tool_only); if (p >= end) { agent_publish(w, "\n(no user history)\n", strlen("\n(no user history)\n")); return; } bool color = isatty(STDOUT_FILENO) != 0; if (color) agent_publish(w, "\n\x1b[90m", strlen("\n\x1b[90m")); else agent_publish(w, "\n", 1); if (tool_only) { agent_publishf(w, "--- session history: recent tool/assistant events ---\n"); } else { agent_publishf(w, "--- session history: last %d user turn%s ---\n", user_turns, user_turns == 1 ? "" : "s"); } if (color) agent_publish(w, "\x1b[0m", 4); while (p < end) { agent_history_mark mark = AGENT_HISTORY_MARK_NONE; size_t mark_len = 0; const char *m = agent_history_next_marker(p, end, &mark, &mark_len); if (!m) break; const char *content = m + mark_len; agent_history_mark next_mark = AGENT_HISTORY_MARK_NONE; size_t next_len = 0; const char *next = agent_history_next_marker(content, end, &next_mark, &next_len); const char *content_end = next ? next : end; const char *tp = content, *te = content_end; agent_history_trim(&tp, &te); if (mark == AGENT_HISTORY_MARK_USER) { if (agent_history_is_tool_user(tp, te)) { const char *payload_start = tp; const char *payload_end = te; (void)agent_history_tool_result_payload(&payload_start, &payload_end); if (color) { const char *s = "\x1b[90mTool result:\n"; agent_publish(w, s, strlen(s)); } else { agent_publish(w, "Tool result:\n", strlen("Tool result:\n")); } agent_history_publish_limited(w, payload_start, payload_end, 12, 3000); if (color) agent_publish(w, "\x1b[0m", 4); } else { if (color) { const char *s = "\x1b[1;32mUser:\x1b[0m\n"; agent_publish(w, s, strlen(s)); } else { agent_publish(w, "User:\n", strlen("User:\n")); } agent_history_publish_limited(w, tp, te, 24, 6000); } } else if (mark == AGENT_HISTORY_MARK_ASSISTANT) { if (color) { const char *s = "\x1b[1;37mAssistant:\x1b[0m\n"; agent_publish(w, s, strlen(s)); } else { agent_publish(w, "Assistant:\n", strlen("Assistant:\n")); } agent_history_render_assistant(w, tp, te); } p = content_end; } if (color) { const char *s = "\x1b[90m--- end history ---\x1b[0m\n"; agent_publish(w, s, strlen(s)); } else { agent_publish(w, "--- end history ---\n", strlen("--- end history ---\n")); } } /* Render recent saved transcript text without mutating the live session. */ static bool agent_worker_show_history(agent_worker *w, int user_turns, char *err, size_t err_len) { if (!worker_is_idle(w)) { snprintf(err, err_len, "model is busy"); return false; } size_t text_len = 0; char *text = ds4_kvstore_render_tokens_text(w->engine, &w->transcript, &text_len); if (!text) { snprintf(err, err_len, "failed to render session text"); return false; } agent_history_render_text(w, text, text_len, user_turns); free(text); return true; } typedef struct { ds4_kvstore_entry entry; char *title; } agent_session_list_item; static int agent_session_list_cmp_recent(const void *a, const void *b) { const agent_session_list_item *sa = a, *sb = b; uint64_t ta = sa->entry.last_used ? sa->entry.last_used : sa->entry.created_at; uint64_t tb = sb->entry.last_used ? sb->entry.last_used : sb->entry.created_at; if (ta < tb) return 1; if (ta > tb) return -1; return strcmp(sa->entry.sha, sb->entry.sha); } static void agent_session_list_free(agent_session_list_item *v, int n) { for (int i = 0; i < n; i++) { ds4_kvstore_entry_free(&v[i].entry); free(v[i].title); } free(v); } static void agent_session_list_push(agent_session_list_item **v, int *len, int *cap, ds4_kvstore_entry entry, char *title) { if (*len == *cap) { *cap = *cap ? *cap * 2 : 16; *v = xrealloc(*v, (size_t)*cap * sizeof((*v)[0])); } (*v)[(*len)++] = (agent_session_list_item){ .entry = entry, .title = title, }; } /* Print resumable sessions from ~/.ds4/kvcache. sysprompt.kv is intentionally * ignored because it is an implementation cache, not a user session. */ static void agent_worker_list_sessions(agent_worker *w) { DIR *d = opendir(w->cache_dir); if (!d) { printf("no sessions: %s\n", strerror(errno)); return; } int cols = renderer_terminal_cols(); size_t title_budget = cols > 16 ? (size_t)(cols - 12) : 20; if (title_budget > 160) title_budget = 160; agent_session_list_item *sessions = NULL; int sessions_len = 0, sessions_cap = 0; const uint8_t model_id = (uint8_t)ds4_engine_model_id(w->engine); struct dirent *de; while ((de = readdir(d)) != NULL) { char sha[41]; if (!ds4_kvstore_sha_hex_name(de->d_name, sha)) continue; char *path = ds4_kvstore_path_join(w->cache_dir, de->d_name); ds4_kvstore_entry e = {0}; if (ds4_kvstore_read_entry_file(path, sha, &e)) { if (e.model_id == model_id) { char *title = agent_session_title_from_file(path, title_budget); agent_session_list_push(&sessions, &sessions_len, &sessions_cap, e, title); } else { ds4_kvstore_entry_free(&e); } } free(path); } closedir(d); if (!sessions_len) { printf("no saved sessions\n"); return; } qsort(sessions, (size_t)sessions_len, sizeof(sessions[0]), agent_session_list_cmp_recent); bool color = isatty(STDOUT_FILENO) != 0; const char *sha_on = color ? "\x1b[1;96m" : ""; const char *title_on = color ? "\x1b[1;97m" : ""; const char *help_on = color ? "\x1b[97m" : ""; const char *dim = color ? "\x1b[90m" : ""; const char *reset = color ? "\x1b[0m" : ""; for (int i = 0; i < sessions_len; i++) { ds4_kvstore_entry *e = &sessions[i].entry; char age[32]; agent_format_age(e->last_used ? e->last_used : e->created_at, age, sizeof(age)); printf("%s%.8s%s %s>%s %s%s%s\n", sha_on, e->sha, reset, dim, reset, title_on, sessions[i].title, reset); printf(" %s> %s, %u tokens, %.2f MB%s%s\n\n", dim, age, e->tokens, (double)e->file_size / (1024.0 * 1024.0), e->payload_bytes == 0 ? ", stripped" : "", reset); } printf("%sUse /switch to select a session, /del to remove, " "/strip to strip KV cache.%s\n", help_on, reset); agent_session_list_free(sessions, sessions_len); } typedef struct { char sha[41]; uint64_t last_used; } agent_completion_session; typedef struct { agent_completion_session *v; int len; int cap; } agent_completion_sessions; static void agent_completion_sessions_push(agent_completion_sessions *s, const char sha[41], uint64_t last_used) { if (s->len == s->cap) { s->cap = s->cap ? s->cap * 2 : 16; s->v = xrealloc(s->v, (size_t)s->cap * sizeof(s->v[0])); } memcpy(s->v[s->len].sha, sha, 41); s->v[s->len].last_used = last_used; s->len++; } static int agent_completion_session_cmp(const void *a, const void *b) { const agent_completion_session *sa = a, *sb = b; if (sa->last_used < sb->last_used) return 1; if (sa->last_used > sb->last_used) return -1; return strcmp(sa->sha, sb->sha); } /* Tab completion for /switch. Suggestions are sorted by recent use and accept * either an empty prefix or any unambiguous hex prefix. */ static void agent_switch_completion_callback(const char *buf, linenoiseCompletions *lc) { agent_worker *w = agent_completion_worker; static const char cmd[] = "/switch"; const size_t cmd_len = sizeof(cmd) - 1; if (!w || !buf || strncmp(buf, cmd, cmd_len) != 0) return; const char *p = buf + cmd_len; if (*p && *p != ' ' && *p != '\t') return; while (*p == ' ' || *p == '\t') p++; const char *prefix = p; size_t prefix_len = strlen(prefix); for (size_t i = 0; i < prefix_len; i++) { if (!isxdigit((unsigned char)prefix[i])) return; } if (prefix_len > 40) return; DIR *d = opendir(w->cache_dir); if (!d) return; agent_completion_sessions sessions = {0}; const uint8_t model_id = (uint8_t)ds4_engine_model_id(w->engine); struct dirent *de; while ((de = readdir(d)) != NULL) { char sha[41]; if (!ds4_kvstore_sha_hex_name(de->d_name, sha)) continue; if (prefix_len && strncasecmp(sha, prefix, prefix_len) != 0) continue; uint64_t last_used = 0; char *path = ds4_kvstore_path_join(w->cache_dir, de->d_name); ds4_kvstore_entry e = {0}; if (ds4_kvstore_read_entry_file(path, sha, &e)) { if (e.model_id == model_id) last_used = e.last_used; else last_used = UINT64_MAX; ds4_kvstore_entry_free(&e); } else { last_used = UINT64_MAX; } free(path); if (last_used == UINT64_MAX) continue; agent_completion_sessions_push(&sessions, sha, last_used); } closedir(d); qsort(sessions.v, (size_t)sessions.len, sizeof(sessions.v[0]), agent_completion_session_cmp); for (int i = 0; i < sessions.len; i++) { char line[64]; int sha_chars = prefix_len > 8 ? 40 : 8; snprintf(line, sizeof(line), "/switch %.*s", sha_chars, sessions.v[i].sha); linenoiseAddCompletion(lc, line); } free(sessions.v); } /* Resolve a user-provided SHA prefix to exactly one saved session file. */ static bool agent_worker_find_session(agent_worker *w, const char *prefix, char sha_out[41], char **path_out, char *err, size_t err_len) { size_t plen = strlen(prefix); if (plen == 0 || plen > 40) { snprintf(err, err_len, "invalid session SHA prefix"); return false; } for (size_t i = 0; i < plen; i++) { if (!isxdigit((unsigned char)prefix[i])) { snprintf(err, err_len, "invalid session SHA prefix"); return false; } } DIR *d = opendir(w->cache_dir); if (!d) { snprintf(err, err_len, "%s", strerror(errno)); return false; } int matches = 0; char match_sha[41] = {0}; char *match_path = NULL; const uint8_t model_id = (uint8_t)ds4_engine_model_id(w->engine); struct dirent *de; while ((de = readdir(d)) != NULL) { char sha[41]; if (!ds4_kvstore_sha_hex_name(de->d_name, sha)) continue; if (strncasecmp(sha, prefix, plen) != 0) continue; char *path = ds4_kvstore_path_join(w->cache_dir, de->d_name); ds4_kvstore_entry e = {0}; bool same_model = ds4_kvstore_read_entry_file(path, sha, &e) && e.model_id == model_id; ds4_kvstore_entry_free(&e); if (!same_model) { free(path); continue; } matches++; if (matches == 1) { memcpy(match_sha, sha, sizeof(match_sha)); match_path = path; } else { free(path); } } closedir(d); if (matches == 0) { snprintf(err, err_len, "no saved session matches %.40s", prefix); return false; } if (matches > 1) { snprintf(err, err_len, "session prefix %.40s is ambiguous", prefix); free(match_path); return false; } memcpy(sha_out, match_sha, 41); *path_out = match_path; return true; } static bool agent_worker_delete_session(agent_worker *w, const char *prefix, char sha_out[41], char *err, size_t err_len) { char sha[41]; char *path = NULL; if (!agent_worker_find_session(w, prefix, sha, &path, err, err_len)) return false; if (unlink(path) != 0) { snprintf(err, err_len, "%s", strerror(errno)); free(path); return false; } if (sha_out) memcpy(sha_out, sha, 41); free(path); return true; } /* Strip the heavy backend payload from a saved session while preserving its * rendered transcript. Loading such a file later tokenizes the text and * rebuilds the live KV with a full prefill. */ static bool agent_worker_strip_session(agent_worker *w, const char *prefix, char sha_out[41], uint32_t *tokens_out, char *err, size_t err_len) { if (err && err_len) err[0] = '\0'; char sha[41]; char *path = NULL; if (!agent_worker_find_session(w, prefix, sha, &path, err, err_len)) return false; FILE *fp = fopen(path, "rb"); if (!fp) { snprintf(err, err_len, "%s", strerror(errno)); free(path); return false; } ds4_kvstore_entry hdr = {0}; uint32_t text_bytes = 0; char *text = NULL; char *title = NULL; bool ok = ds4_kvstore_read_header(fp, &hdr, &text_bytes) && agent_kv_read_text(fp, text_bytes, &text, err, err_len); if (ok && (hdr.ext_flags & DS4_KVSTORE_EXT_SESSION_TITLE)) ok = agent_kv_read_title_trailer(fp, &hdr, &title, err, err_len); fclose(fp); if (!ok) { if (!err[0]) snprintf(err, err_len, "failed to read session"); free(title); free(text); free(path); return false; } char actual_sha[41]; agent_kv_identity_sha(&hdr, text, text_bytes, title, actual_sha); if (strcmp(actual_sha, sha)) { snprintf(err, err_len, "cached session identity does not match file name"); free(title); free(text); free(path); return false; } ds4_tokens stripped_tokens = {0}; ds4_tokenize_rendered_chat(w->engine, text, &stripped_tokens); uint32_t stripped_token_count = (uint32_t)stripped_tokens.len; ds4_tokens_free(&stripped_tokens); agent_buf tmpl = {0}; agent_buf_puts(&tmpl, path); agent_buf_puts(&tmpl, ".tmp.XXXXXX"); char *tmp = agent_buf_take(&tmpl); int fd = mkstemp(tmp); if (fd < 0) { snprintf(err, err_len, "%s", strerror(errno)); free(tmp); free(text); free(path); return false; } fp = fdopen(fd, "wb"); if (!fp) { snprintf(err, err_len, "%s", strerror(errno)); close(fd); unlink(tmp); free(tmp); free(text); free(path); return false; } uint8_t h[DS4_KVSTORE_FIXED_HEADER]; uint64_t now = (uint64_t)time(NULL); ds4_kvstore_fill_header(h, hdr.model_id, hdr.quant_bits, hdr.reason, hdr.ext_flags, stripped_token_count, hdr.hits, hdr.ctx_size, hdr.created_at, now, 0); uint8_t tb[4]; ds4_kvstore_le_put32(tb, text_bytes); errno = 0; ok = fwrite(h, 1, sizeof(h), fp) == sizeof(h) && fwrite(tb, 1, sizeof(tb), fp) == sizeof(tb) && fwrite(text, 1, text_bytes, fp) == text_bytes && (!(hdr.ext_flags & DS4_KVSTORE_EXT_SESSION_TITLE) || agent_kv_write_title_trailer(fp, title, err, err_len)) && fflush(fp) == 0; int saved_errno = errno; if (fclose(fp) != 0) { if (!saved_errno) saved_errno = errno; ok = false; } if (ok && rename(tmp, path) != 0) { saved_errno = errno; ok = false; } if (!ok) { snprintf(err, err_len, "%s", saved_errno ? strerror(saved_errno) : "failed to write stripped session"); unlink(tmp); } else { if (sha_out) memcpy(sha_out, sha, 41); if (tokens_out) *tokens_out = stripped_token_count; } free(tmp); free(title); free(text); free(path); return ok; } /* Load a saved session KV into the live transcript and optionally replay recent * history for the human. */ static bool agent_worker_switch_session(agent_worker *w, const char *prefix, int history_turns, char *err, size_t err_len) { if (!worker_is_idle(w)) { snprintf(err, err_len, "model is busy"); return false; } char sha[41]; char *path = NULL; if (!agent_worker_find_session(w, prefix, sha, &path, err, err_len)) return false; bool stripped = false; ds4_kvstore_entry entry = {0}; if (ds4_kvstore_read_entry_file(path, sha, &entry)) { stripped = entry.payload_bytes == 0; ds4_kvstore_entry_free(&entry); } if (stripped) { printf("rebuilding stripped session %.8s from rendered text...\n", sha); fflush(stdout); } ds4_tokens loaded = {0}; agent_kv_session_meta meta = {0}; bool ok = agent_kv_load_path(w, path, sha, NULL, 0, &loaded, &meta, err, err_len); if (ok) { ds4_tokens_free(&w->transcript); w->transcript = loaded; free(w->session_title); w->session_title = meta.title ? xstrdup(meta.title) : xstrdup("(no user prompt)"); w->session_created_at = meta.created_at ? meta.created_at : (uint64_t)time(NULL); memcpy(w->session_sha, sha, sizeof(w->session_sha)); free(w->legacy_session_path_to_delete); w->legacy_session_path_to_delete = meta.legacy_identity ? xstrdup(path) : NULL; agent_worker_note_system_prompt_seen(w); w->datetime_context_injected = true; pthread_mutex_lock(&w->mu); w->user_activity = true; w->session_dirty = false; w->status.state = AGENT_WORKER_IDLE; w->status.ctx_used = w->transcript.len; w->status.ctx_size = w->cfg->gen.ctx_size; w->status.prefill_tps = 0.0; w->status.greedy_sampling = false; w->status.error[0] = '\0'; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); printf("switched to session %.8s (%d tokens%s)\n", sha, w->transcript.len, stripped ? ", rebuilt from text" : ""); if (history_turns > 0) (void)agent_worker_show_history(w, history_turns, err, err_len); } else { ds4_tokens_free(&loaded); } agent_kv_session_meta_free(&meta); free(path); return ok; } /* ============================================================================ * Tool Argument Parsing And File Tool Helpers * ============================================================================ */ static int agent_parse_timeout(const char *s) { if (!s || !s[0]) return 3600; char *end = NULL; double v = strtod(s, &end); if (end == s || v <= 0.0 || !isfinite(v)) return 3600; if (v < 1.0) v = 1.0; if (v > 24.0 * 3600.0) v = 24.0 * 3600.0; return (int)v; } static int agent_parse_int_default(const char *s, int def, int min, int max) { if (!s || !s[0]) return def; char *end = NULL; long v = strtol(s, &end, 10); if (end == s) return def; while (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n') end++; if (*end) return def; if (v < min) v = min; if (v > max) v = max; return (int)v; } static bool agent_parse_bool_default(const char *s, bool def) { if (!s || !s[0]) return def; if (!strcasecmp(s, "true") || !strcasecmp(s, "yes") || !strcmp(s, "1")) return true; if (!strcasecmp(s, "false") || !strcasecmp(s, "no") || !strcmp(s, "0")) return false; return def; } #define AGENT_FILE_MAX_BYTES (16*1024*1024) #define AGENT_READ_DEFAULT_LINES 500 #define AGENT_TOOL_RESULT_RESERVE_TOKENS 1024 #define AGENT_EDIT_UPTO_MIN_PREFIX_BYTES 64 #define AGENT_EDIT_UPTO_MIN_PREFIX_LINES 2 #define AGENT_COMPACT_SOFT_PERCENT 85 #define AGENT_COMPACT_MIN_FREE_TOKENS 8192 #define AGENT_COMPACT_TAIL_DIVISOR 10 #define AGENT_COMPACT_TAIL_CAP_TOKENS 50000 #define AGENT_COMPACT_SUMMARY_MAX_TOKENS 4096 typedef struct { size_t start; size_t content_end; size_t end; } agent_line_span; typedef struct { agent_line_span *v; int len; int cap; } agent_line_spans; static void agent_line_spans_free(agent_line_spans *spans) { free(spans->v); memset(spans, 0, sizeof(*spans)); } static void agent_line_spans_push(agent_line_spans *spans, agent_line_span span) { if (spans->len == spans->cap) { spans->cap = spans->cap ? spans->cap * 2 : 128; spans->v = xrealloc(spans->v, (size_t)spans->cap * sizeof(spans->v[0])); } spans->v[spans->len++] = span; } /* Split a text buffer into line spans. content_end excludes CR/LF so callers * can print or compare line content without newline spelling differences. */ static void agent_split_lines(const char *data, size_t len, agent_line_spans *spans) { size_t pos = 0; while (pos < len) { size_t start = pos; while (pos < len && data[pos] != '\n' && data[pos] != '\r') pos++; size_t content_end = pos; if (pos < len) { if (data[pos] == '\r' && pos + 1 < len && data[pos + 1] == '\n') pos += 2; else pos++; } agent_line_spans_push(spans, (agent_line_span){ .start = start, .content_end = content_end, .end = pos, }); } } static int agent_read_file_bytes(const char *path, char **data, size_t *len, char *err, size_t errlen) { FILE *fp = fopen(path, "rb"); if (!fp) { snprintf(err, errlen, "open %s: %s", path, strerror(errno)); return -1; } char *buf = NULL; size_t used = 0, cap = 0; char tmp[8192]; while (true) { size_t n = fread(tmp, 1, sizeof(tmp), fp); if (n) { if (used + n > AGENT_FILE_MAX_BYTES) { fclose(fp); free(buf); snprintf(err, errlen, "file too large: %s exceeds %d bytes", path, AGENT_FILE_MAX_BYTES); return -1; } if (used + n + 1 > cap) { cap = cap ? cap * 2 : 8192; while (cap < used + n + 1) cap *= 2; buf = xrealloc(buf, cap); } memcpy(buf + used, tmp, n); used += n; buf[used] = '\0'; } if (n < sizeof(tmp)) { if (ferror(fp)) { snprintf(err, errlen, "read %s: %s", path, strerror(errno)); fclose(fp); free(buf); return -1; } break; } } fclose(fp); if (!buf) buf = xstrdup(""); *data = buf; *len = used; return 0; } static int agent_line_for_offset(const agent_line_spans *spans, size_t offset) { if (!spans || spans->len <= 0) return 1; for (int i = 0; i < spans->len; i++) { if (offset < spans->v[i].end) return i + 1; } return spans->len; } static bool agent_old_new_line_effect(const char *old_data, size_t old_len, const char *new_data, size_t new_len, size_t edit_offset, size_t replaced_len, int *start_line, int *end_line, int *delta) { agent_line_spans old_spans = {0}; agent_line_spans new_spans = {0}; agent_split_lines(old_data, old_len, &old_spans); agent_split_lines(new_data, new_len, &new_spans); bool ok = old_spans.len > 0; if (ok) { size_t old_last = edit_offset; if (replaced_len > 0) old_last = edit_offset + replaced_len - 1; if (old_last >= old_len) old_last = old_len ? old_len - 1 : 0; if (start_line) *start_line = agent_line_for_offset(&old_spans, edit_offset); if (end_line) *end_line = agent_line_for_offset(&old_spans, old_last); if (delta) *delta = new_spans.len - old_spans.len; } agent_line_spans_free(&old_spans); agent_line_spans_free(&new_spans); return ok; } static void agent_edit_result_append_context(agent_buf *b, const char *path, const char *data, size_t len, int anchor_start, int anchor_end); static char *agent_edit_result(const char *path, int start_line, int end_line, int delta, const char *new_data, size_t new_len, const char *kind) { agent_buf b = {0}; char msg[PATH_MAX + 180]; snprintf(msg, sizeof(msg), "Edited %s using %s\n", path, kind); agent_buf_puts(&b, msg); if (start_line > 0 && end_line >= start_line) { snprintf(msg, sizeof(msg), "Touched old lines %d-%d; current post-edit context follows.\n", start_line, end_line); agent_buf_puts(&b, msg); if (delta != 0) { snprintf(msg, sizeof(msg), "Line shift: old lines after %d moved by %+d (old line %d is now line %d). Re-read before relying on old line numbers there.\n", end_line, delta, end_line + 1, end_line + 1 + delta); agent_buf_puts(&b, msg); } } if (start_line > 0 && end_line >= start_line) { int new_anchor_end = end_line + delta; if (new_anchor_end < start_line) new_anchor_end = start_line; agent_edit_result_append_context(&b, path, new_data, new_len, start_line, new_anchor_end); } return agent_buf_take(&b); } static void agent_worker_set_more(agent_worker *w, const char *path, int next_line, bool bare) { snprintf(w->more_path, sizeof(w->more_path), "%s", path ? path : ""); w->more_next_line = next_line; w->more_bare = bare; w->more_valid = path && path[0] && next_line > 0; } static bool agent_tool_result_fits_context(agent_worker *w, const char *result, int reserve_tokens, int *tokens_out) { ds4_tokens tmp = {0}; ds4_tokens_copy(&tmp, &w->transcript); ds4_chat_append_message(w->engine, &tmp, "tool", result ? result : ""); int tokens = tmp.len; ds4_tokens_free(&tmp); if (tokens_out) *tokens_out = tokens; return tokens + reserve_tokens < w->cfg->gen.ctx_size; } /* Read file text for the model. Normal mode shows plain line numbers. Raw * mode is reserved for cases where line decoration would corrupt the payload * being inspected. */ static char *agent_read_range(agent_worker *w, const char *path, int start_line, int max_lines, bool whole_file, bool bare, bool set_more) { char err[256]; char *data = NULL; size_t len = 0; if (!path || !path[0]) return xstrdup("Tool error: read requires path\n"); if (agent_read_file_bytes(path, &data, &len, err, sizeof(err)) != 0) { agent_buf b = {0}; agent_buf_puts(&b, "Tool error: "); agent_buf_puts(&b, err); agent_buf_puts(&b, "\n"); return agent_buf_take(&b); } agent_line_spans spans = {0}; agent_split_lines(data, len, &spans); if (start_line < 1) start_line = 1; int start_idx = start_line - 1; if (start_idx > spans.len) start_idx = spans.len; if (whole_file) { max_lines = spans.len - start_idx; } else { if (max_lines <= 0) max_lines = AGENT_READ_DEFAULT_LINES; } int end_idx = start_idx + max_lines; if (end_idx > spans.len) end_idx = spans.len; agent_buf out = {0}; if (bare) { size_t start = start_idx < spans.len ? spans.v[start_idx].start : len; size_t end = end_idx > start_idx ? spans.v[end_idx - 1].end : start; agent_buf_append(&out, data + start, end - start); if (end > start && out.ptr[out.len - 1] != '\n') agent_buf_puts(&out, "\n"); if (end_idx < spans.len) { char note[160]; snprintf(note, sizeof(note), "[Read truncated at line %d of %d. continue_offset=%d. " "Call more with count=%d to read the next chunk.]\n", end_idx, spans.len, end_idx + 1, max_lines > 0 ? max_lines : AGENT_READ_DEFAULT_LINES); agent_buf_puts(&out, note); } } else { char hdr[PATH_MAX + 160]; if (end_idx < spans.len) { snprintf(hdr, sizeof(hdr), "%s: lines %d-%d of %d; continue_offset=%d; " "call more with count=%d to read the next chunk\n", path, spans.len ? start_idx + 1 : 0, end_idx, spans.len, end_idx + 1, max_lines > 0 ? max_lines : AGENT_READ_DEFAULT_LINES); } else { snprintf(hdr, sizeof(hdr), "%s: lines %d-%d of %d\n", path, spans.len ? start_idx + 1 : 0, end_idx, spans.len); } agent_buf_puts(&out, hdr); for (int i = start_idx; i < end_idx; i++) { agent_line_span sp = spans.v[i]; char prefix[64]; snprintf(prefix, sizeof(prefix), "%d ", i + 1); agent_buf_puts(&out, prefix); agent_buf_append(&out, data + sp.start, sp.content_end - sp.start); agent_buf_puts(&out, "\n"); } } if (set_more) { if (end_idx < spans.len) agent_worker_set_more(w, path, end_idx + 1, bare); else agent_worker_set_more(w, NULL, 0, false); } agent_line_spans_free(&spans); free(data); return agent_buf_take(&out); } static char *agent_tool_read(agent_worker *w, const agent_tool_call *call) { const char *path = agent_tool_arg_value(call, "path"); bool whole = agent_parse_bool_default(agent_tool_arg_value(call, "whole"), false); int start = agent_parse_int_default(agent_tool_arg_value(call, "start_line"), 1, 1, INT_MAX); int count = agent_parse_int_default(agent_tool_arg_value(call, "max_lines"), AGENT_READ_DEFAULT_LINES, 1, INT_MAX); bool raw = agent_parse_bool_default(agent_tool_arg_value(call, "raw"), false); return agent_read_range(w, path, start, count, whole, raw, true); } static char *agent_tool_more(agent_worker *w, const agent_tool_call *call) { int count = agent_parse_int_default(agent_tool_arg_value(call, "count"), AGENT_READ_DEFAULT_LINES, 1, INT_MAX); if (!w->more_valid) return xstrdup("Tool error: no previous output to continue\n"); return agent_read_range(w, w->more_path, w->more_next_line, count, false, w->more_bare, true); } static char *agent_tool_write(agent_worker *w, const agent_tool_call *call) { (void)w; const char *path = agent_tool_arg_value(call, "path"); const char *content = agent_tool_arg_value(call, "content"); if (!path || !path[0]) return xstrdup("Tool error: write requires path\n"); if (!content) return xstrdup("Tool error: write requires content\n"); FILE *fp = fopen(path, "wb"); if (!fp) { agent_buf b = {0}; agent_buf_puts(&b, "Tool error: open for write failed: "); agent_buf_puts(&b, strerror(errno)); agent_buf_puts(&b, "\n"); return agent_buf_take(&b); } size_t len = strlen(content); size_t wr = fwrite(content, 1, len, fp); int close_rc = fclose(fp); if (wr != len || close_rc != 0) { agent_buf b = {0}; agent_buf_puts(&b, "Tool error: write failed: "); agent_buf_puts(&b, strerror(errno)); agent_buf_puts(&b, "\n"); return agent_buf_take(&b); } char msg[PATH_MAX + 160]; snprintf(msg, sizeof(msg), "Wrote %zu bytes to %s\n", len, path); return xstrdup(msg); } static char *agent_tool_list(const agent_tool_call *call) { const char *path = agent_tool_arg_value(call, "path"); if (!path || !path[0]) path = "."; DIR *dir = opendir(path); if (!dir) { agent_buf b = {0}; agent_buf_puts(&b, "Tool error: opendir failed: "); agent_buf_puts(&b, strerror(errno)); agent_buf_puts(&b, "\n"); return agent_buf_take(&b); } agent_buf out = {0}; char hdr[PATH_MAX + 64]; snprintf(hdr, sizeof(hdr), "%s:\n", path); agent_buf_puts(&out, hdr); struct dirent *de; int shown = 0; while ((de = readdir(dir)) != NULL && shown < 300) { if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; char full[PATH_MAX]; snprintf(full, sizeof(full), "%s/%s", path, de->d_name); struct stat st; if (lstat(full, &st) != 0) continue; char type = S_ISDIR(st.st_mode) ? 'd' : S_ISLNK(st.st_mode) ? 'l' : S_ISREG(st.st_mode) ? '-' : '?'; char line[PATH_MAX + 96]; snprintf(line, sizeof(line), "%c %10lld %s%s\n", type, (long long)st.st_size, de->d_name, S_ISDIR(st.st_mode) ? "/" : ""); agent_buf_puts(&out, line); shown++; } if (de) agent_buf_puts(&out, "... more entries omitted ...\n"); closedir(dir); return agent_buf_take(&out); } /* ============================================================================ * Edit And Search Tools * ============================================================================ */ static int agent_write_file_bytes(const char *path, const char *data, size_t len, char *err, size_t errlen) { FILE *fp = fopen(path, "wb"); if (!fp) { snprintf(err, errlen, "open %s: %s", path, strerror(errno)); return -1; } size_t wr = fwrite(data, 1, len, fp); if (wr != len) { snprintf(err, errlen, "write %s: %s", path, strerror(errno)); fclose(fp); return -1; } if (fclose(fp) != 0) { snprintf(err, errlen, "close %s: %s", path, strerror(errno)); return -1; } return 0; } static void agent_edit_result_append_line(agent_buf *b, const char *data, const agent_line_span *sp, int line) { char prefix[64]; snprintf(prefix, sizeof(prefix), "%d ", line); agent_buf_puts(b, prefix); agent_buf_append(b, data + sp->start, sp->content_end - sp->start); agent_buf_puts(b, "\n"); } /* Successful edits return the nearby post-edit file shape. This spends cheap * prefill tokens to save expensive model retries: the model immediately sees * shifted line numbers, braces, semicolons, and accidental duplication. */ static void agent_edit_result_append_context(agent_buf *b, const char *path, const char *data, size_t len, int anchor_start, int anchor_end) { enum { CONTEXT_BEFORE = 5, CONTEXT_AFTER = 8, EDITED_CONTEXT_HEAD = 18, EDITED_CONTEXT_TAIL = 18 }; agent_line_spans spans = {0}; agent_split_lines(data, len, &spans); if (spans.len <= 0) { agent_line_spans_free(&spans); return; } if (anchor_start < 1) anchor_start = 1; if (anchor_start > spans.len) anchor_start = spans.len; if (anchor_end < anchor_start) anchor_end = anchor_start; if (anchor_end > spans.len) anchor_end = spans.len; int ctx_start = anchor_start - CONTEXT_BEFORE; if (ctx_start < 1) ctx_start = 1; int ctx_end = anchor_end + CONTEXT_AFTER; if (ctx_end > spans.len) ctx_end = spans.len; char hdr[PATH_MAX + 160]; snprintf(hdr, sizeof(hdr), "Current file around edit: %s lines %d-%d of %d\n", path, ctx_start, ctx_end, spans.len); agent_buf_puts(b, hdr); int edited_lines = anchor_end - anchor_start + 1; if (edited_lines <= EDITED_CONTEXT_HEAD + EDITED_CONTEXT_TAIL) { for (int line = ctx_start; line <= ctx_end; line++) agent_edit_result_append_line(b, data, &spans.v[line - 1], line); } else { int head_end = anchor_start + EDITED_CONTEXT_HEAD - 1; int tail_start = anchor_end - EDITED_CONTEXT_TAIL + 1; for (int line = ctx_start; line <= head_end; line++) agent_edit_result_append_line(b, data, &spans.v[line - 1], line); snprintf(hdr, sizeof(hdr), "... %d edited lines omitted ...\n", tail_start - head_end - 1); agent_buf_puts(b, hdr); for (int line = tail_start; line <= ctx_end; line++) agent_edit_result_append_line(b, data, &spans.v[line - 1], line); } agent_line_spans_free(&spans); } static const char *agent_memmem_simple(const char *hay, size_t hay_len, const char *needle, size_t needle_len) { if (!needle_len) return hay; if (needle_len > hay_len) return NULL; size_t last = hay_len - needle_len; for (size_t i = 0; i <= last; i++) { if (hay[i] == needle[0] && !memcmp(hay + i, needle, needle_len)) return hay + i; } return NULL; } static bool agent_find_unique(const char *data, size_t len, const char *needle, size_t needle_len, const char **match, const char *label, char *err, size_t err_len) { if (!needle || needle_len == 0) { snprintf(err, err_len, "%s anchor is empty", label); return false; } const char *first = agent_memmem_simple(data, len, needle, needle_len); if (!first) { snprintf(err, err_len, "%s anchor not found", label); return false; } size_t after_first = (size_t)(first - data) + 1; const char *second = after_first <= len ? agent_memmem_simple(data + after_first, len - after_first, needle, needle_len) : NULL; if (second) { snprintf(err, err_len, "%s anchor is not unique", label); return false; } *match = first; return true; } /* Find an anchor only in the suffix after start. * * Anchored edits use "head [upto] tail": the head fixes the edit start, and * the tail should delimit the first unique end point after that start. A tail * may legitimately appear earlier in the file, so checking global uniqueness * would reject valid edits. */ static bool agent_find_unique_after(const char *data, size_t len, const char *start, const char *needle, size_t needle_len, const char **match, const char *label, char *err, size_t err_len) { if (!needle || needle_len == 0) { snprintf(err, err_len, "%s anchor is empty", label); return false; } if (start < data || start > data + len) { snprintf(err, err_len, "%s search starts outside file", label); return false; } size_t off = (size_t)(start - data); const char *first = agent_memmem_simple(data + off, len - off, needle, needle_len); if (!first) { snprintf(err, err_len, "%s anchor not found after old head", label); return false; } size_t after_first = (size_t)(first - data) + 1; const char *second = after_first <= len ? agent_memmem_simple(data + after_first, len - after_first, needle, needle_len) : NULL; if (second) { snprintf(err, err_len, "%s anchor is not unique after old head", label); return false; } *match = first; return true; } static bool agent_edit_old_may_be_closing_tag(const char *text, size_t len) { size_t i = 0; while (i < len && (text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n')) i++; return i < len && text[i] == '<'; } static bool agent_edit_old_is_only_space(const char *text, size_t len) { for (size_t i = 0; i < len; i++) { if (text[i] != ' ' && text[i] != '\t' && text[i] != '\r' && text[i] != '\n') return false; } return true; } static bool agent_span_has_nonspace(const char *s, size_t len) { for (size_t i = 0; i < len; i++) { if (!isspace((unsigned char)s[i])) return true; } return false; } static bool agent_edit_old_prefix_mature_for_upto(const char *old, size_t old_len) { if (old_len < AGENT_EDIT_UPTO_MIN_PREFIX_BYTES) return false; int nonempty_lines = 0; bool line_has_text = false; for (size_t i = 0; i < old_len; i++) { if (old[i] == '\n') { if (line_has_text) nonempty_lines++; line_has_text = false; } else if (!isspace((unsigned char)old[i])) { line_has_text = true; } } return nonempty_lines >= AGENT_EDIT_UPTO_MIN_PREFIX_LINES; } static bool agent_edit_old_ready_for_upto(const char *old, size_t old_len) { if (!old_len || strstr(old, "[upto]")) return false; if (!agent_edit_old_prefix_mature_for_upto(old, old_len)) return false; size_t end = old_len; while (end > 0 && (old[end - 1] == ' ' || old[end - 1] == '\t' || old[end - 1] == '\r')) end--; return end > 0 && old[end - 1] == '\n'; } /* While the model streams an edit old=... argument, stop it from retyping a * large exact old block once the emitted prefix is already a unique file * anchor. The next sampled token is inspected before eval: if it would keep * writing old text rather than close the parameter, the caller evaluates a * complete "[upto]" marker line instead. */ static bool agent_edit_upto_forcer_should_replace(agent_edit_upto_forcer *forcer, agent_dsml_parser *p, const char *next_text, size_t next_len) { if (!forcer || !p) return false; bool in_edit_old = p->state == AGENT_DSML_PARAM_VALUE && p->current.name && strcmp(p->current.name, "edit") == 0 && p->param_name && strcmp(p->param_name, "old") == 0; if (!in_edit_old) { forcer->active = false; forcer->done = false; return false; } if (!forcer->active) { forcer->active = true; forcer->done = false; } if (forcer->done) return false; if (agent_edit_old_may_be_closing_tag(next_text, next_len) || agent_edit_old_is_only_space(next_text, next_len)) return false; const char *path = agent_tool_arg_value(&p->current, "path"); if (!path || !path[0] || p->param_value_start > p->raw_len) return false; const char *old = p->raw + p->param_value_start; size_t old_len = p->raw_len - p->param_value_start; if (!agent_edit_old_ready_for_upto(old, old_len)) return false; char err[256]; char *data = NULL; size_t len = 0; if (agent_read_file_bytes(path, &data, &len, err, sizeof(err)) != 0) return false; const char *match = NULL; bool unique = agent_find_unique(data, len, old, old_len, &match, "old prefix", err, sizeof(err)); free(data); if (unique) { forcer->done = true; return true; } return false; } static bool agent_edit_find_old_span(const char *data, size_t len, const char *old, const char **match, size_t *match_len, bool *anchored, char *err, size_t err_len) { static const char marker[] = "[upto]"; size_t old_len = strlen(old); const char *upto = strstr(old, marker); if (!upto) { *anchored = false; if (!agent_find_unique(data, len, old, old_len, match, "old text", err, err_len)) return false; *match_len = old_len; return true; } if (strstr(upto + strlen(marker), marker)) { snprintf(err, err_len, "old text contains more than one [upto] marker"); return false; } size_t head_len = (size_t)(upto - old); const char *tail = upto + strlen(marker); size_t tail_len = old_len - head_len - strlen(marker); /* Strip leading newline/CR from tail before searching. The head already * includes the newline at its end, so the extra \n that follows [upto] in * the old text (whether injected by the forcer or written by the model) * must not be part of the tail needle -- the file after the head has no * duplicate newline. */ while (tail_len > 0 && (*tail == '\n' || *tail == '\r')) { tail++; tail_len--; } if (!agent_span_has_nonspace(tail, tail_len)) { snprintf(err, err_len, "old text after [upto] must include a unique tail anchor"); return false; } const char *head_pos = NULL; const char *tail_pos = NULL; if (!agent_find_unique(data, len, old, head_len, &head_pos, "old head", err, err_len)) return false; if (!agent_find_unique_after(data, len, head_pos + head_len, tail, tail_len, &tail_pos, "old tail", err, err_len)) return false; *anchored = true; *match = head_pos; *match_len = (size_t)(tail_pos - head_pos) + tail_len; return true; } #ifdef DS4_AGENT_TEST static int agent_test_failures; static void agent_test_assert(bool cond, const char *expr, const char *file, int line) { if (cond) return; fprintf(stderr, "%s:%d: assertion failed: %s\n", file, line, expr); agent_test_failures++; } #define AGENT_TEST_ASSERT(expr) \ agent_test_assert((expr), #expr, __FILE__, __LINE__) static void test_agent_edit_upto_tail_newline_is_not_part_of_anchor(void) { const char *data = "CFLAGS = -Wall -Wextra -g\n" "LDFLAGS =\n" "\n" "all: bc\n" "\n" "bc: main.c\n" "\t$(CC) $(CFLAGS) -o bc main.c $(LDFLAGS)\n" "\n" "clean:\n" "\trm -f bc\n"; const char *old = "CFLAGS = -Wall -Wextra -g\n" "LDFLAGS =\n" "\n" "all: bc\n" "\n" "bc: main.c\n" "\t$(CC) $(CFLAGS) -o bc main.c $(LDFLAGS)\n" "\n" "[upto]\n" "clean:\n"; const char *match = NULL; size_t match_len = 0; bool anchored = false; char err[128] = {0}; AGENT_TEST_ASSERT(agent_edit_find_old_span(data, strlen(data), old, &match, &match_len, &anchored, err, sizeof(err))); AGENT_TEST_ASSERT(anchored); AGENT_TEST_ASSERT(match == data); AGENT_TEST_ASSERT(match_len == strlen(data) - strlen("\trm -f bc\n")); } static void test_agent_edit_upto_requires_tail_after_newline_strip(void) { const char *data = "head\nbody\ntail\n"; const char *old = "head\n[upto]\n"; const char *match = NULL; size_t match_len = 0; bool anchored = false; char err[128] = {0}; AGENT_TEST_ASSERT(!agent_edit_find_old_span(data, strlen(data), old, &match, &match_len, &anchored, err, sizeof(err))); AGENT_TEST_ASSERT(strstr(err, "must include a unique tail anchor") != NULL); } static void ds4_agent_unit_tests_run(void) { test_agent_edit_upto_tail_newline_is_not_part_of_anchor(); test_agent_edit_upto_requires_tail_after_newline_strip(); } #endif static bool agent_preflight_edit_old(agent_worker *w, const agent_tool_call *call, char *err, size_t err_len) { (void)w; const char *path = agent_tool_arg_value(call, "path"); if (!path || !path[0]) return true; /* Cannot preflight until path is known. */ const char *old = agent_tool_arg_value(call, "old"); if (!old || !old[0]) { snprintf(err, err_len, "edit requires non-empty old text"); return false; } char *data = NULL; size_t len = 0; if (agent_read_file_bytes(path, &data, &len, err, err_len) != 0) return false; const char *match = NULL; size_t match_len = 0; bool anchored = false; bool ok = agent_edit_find_old_span(data, len, old, &match, &match_len, &anchored, err, err_len); free(data); return ok; } static char *agent_apply_file_splice(const char *path, const char *data, size_t len, size_t offset, size_t remove_len, const char *insert, const char *kind) { char err[256]; if (!insert) insert = ""; size_t insert_len = strlen(insert); size_t out_len = offset + insert_len + (len - offset - remove_len); char *out = xmalloc(out_len + 1); memcpy(out, data, offset); memcpy(out + offset, insert, insert_len); memcpy(out + offset + insert_len, data + offset + remove_len, len - offset - remove_len); out[out_len] = '\0'; int rc = agent_write_file_bytes(path, out, out_len, err, sizeof(err)); if (rc != 0) { free(out); agent_buf b = {0}; agent_buf_puts(&b, "Tool error: "); agent_buf_puts(&b, err); agent_buf_puts(&b, "\n"); return agent_buf_take(&b); } int start_line = 0, end_line = 0, delta = 0; agent_old_new_line_effect(data, len, out, out_len, offset, remove_len, &start_line, &end_line, &delta); char *result = agent_edit_result(path, start_line, end_line, delta, out, out_len, kind); free(out); return result; } /* Old/new editing is intentionally conservative: exact old text must be unique. * For large replacements, old may contain one [upto] marker: the head must be * unique, and the tail must be unique after that head before the whole span is * replaced. */ static char *agent_tool_edit(agent_worker *w, const agent_tool_call *call) { (void)w; const char *path = agent_tool_arg_value(call, "path"); if (!path || !path[0]) return xstrdup("Tool error: edit requires path\n"); const char *old = agent_tool_arg_value(call, "old"); const char *new_text = agent_tool_arg_value(call, "new"); if (!old || !old[0]) return xstrdup("Tool error: edit requires non-empty old text\n"); if (!new_text) return xstrdup("Tool error: edit requires new text\n"); char err[256]; char *data = NULL; size_t len = 0; if (agent_read_file_bytes(path, &data, &len, err, sizeof(err)) != 0) { agent_buf b = {0}; agent_buf_puts(&b, "Tool error: "); agent_buf_puts(&b, err); agent_buf_puts(&b, "\n"); return agent_buf_take(&b); } const char *match = NULL; size_t match_len = 0; bool anchored = false; if (!agent_edit_find_old_span(data, len, old, &match, &match_len, &anchored, err, sizeof(err))) { free(data); agent_buf b = {0}; agent_buf_puts(&b, "Tool error: "); agent_buf_puts(&b, err); agent_buf_puts(&b, "\n"); return agent_buf_take(&b); } char *result = agent_apply_file_splice(path, data, len, (size_t)(match - data), match_len, new_text, anchored ? "anchored old/new replacement" : "old/new replacement"); free(data); return result; } typedef struct { const char *query; const char *glob; regex_t regex; bool use_regex; bool regex_ready; bool case_sensitive; int context; int max_results; int results; agent_buf out; } agent_search_ctx; static bool agent_literal_match(const char *s, size_t n, const char *q, bool case_sensitive) { size_t qn = strlen(q); if (!qn) return true; if (qn > n) return false; for (size_t i = 0; i + qn <= n; i++) { bool ok = true; for (size_t j = 0; j < qn; j++) { unsigned char a = (unsigned char)s[i + j]; unsigned char b = (unsigned char)q[j]; if (!case_sensitive) { a = (unsigned char)tolower(a); b = (unsigned char)tolower(b); } if (a != b) { ok = false; break; } } if (ok) return true; } return false; } static bool agent_search_line_matches(agent_search_ctx *ctx, const char *s, size_t n) { if (ctx->use_regex) { char *line = xstrndup(s, n); int rc = regexec(&ctx->regex, line, 0, NULL, 0); free(line); return rc == 0; } return agent_literal_match(s, n, ctx->query, ctx->case_sensitive); } static void agent_search_emit_line(agent_search_ctx *ctx, const char *data, agent_line_span sp, int line_no) { char prefix[64]; snprintf(prefix, sizeof(prefix), " %d ", line_no); agent_buf_puts(&ctx->out, prefix); agent_buf_append(&ctx->out, data + sp.start, sp.content_end - sp.start); agent_buf_puts(&ctx->out, "\n"); } /* Search one text file and emit matching lines with plain line numbers. */ static void agent_search_file(agent_search_ctx *ctx, const char *path) { if (ctx->results >= ctx->max_results) return; if (ctx->glob && ctx->glob[0]) { const char *base = strrchr(path, '/'); base = base ? base + 1 : path; if (fnmatch(ctx->glob, base, 0) != 0 && fnmatch(ctx->glob, path, 0) != 0) return; } char err[256]; char *data = NULL; size_t len = 0; if (agent_read_file_bytes(path, &data, &len, err, sizeof(err)) != 0) return; if (memchr(data, '\0', len)) { free(data); return; } agent_line_spans spans = {0}; agent_split_lines(data, len, &spans); bool printed_file = false; int last_context_line = -1; for (int i = 0; i < spans.len && ctx->results < ctx->max_results; i++) { agent_line_span sp = spans.v[i]; if (!agent_search_line_matches(ctx, data + sp.start, sp.content_end - sp.start)) continue; if (!printed_file) { agent_buf_puts(&ctx->out, path); agent_buf_puts(&ctx->out, "\n"); printed_file = true; } int from = i - ctx->context; int to = i + ctx->context; if (from < 0) from = 0; if (to >= spans.len) to = spans.len - 1; if (from <= last_context_line) from = last_context_line + 1; for (int j = from; j <= to; j++) { agent_search_emit_line(ctx, data, spans.v[j], j + 1); last_context_line = j; } ctx->results++; } if (printed_file) agent_buf_puts(&ctx->out, "\n"); agent_line_spans_free(&spans); free(data); } /* Recursively search a file or directory, avoiding .git and stopping once the * result cap is reached. */ static void agent_search_path(agent_search_ctx *ctx, const char *path, int depth) { if (ctx->results >= ctx->max_results || depth > 24) return; struct stat st; if (lstat(path, &st) != 0) return; if (S_ISREG(st.st_mode)) { agent_search_file(ctx, path); return; } if (!S_ISDIR(st.st_mode)) return; DIR *dir = opendir(path); if (!dir) return; struct dirent *de; while ((de = readdir(dir)) != NULL && ctx->results < ctx->max_results) { if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; if (!strcmp(de->d_name, ".git")) continue; char child[PATH_MAX]; snprintf(child, sizeof(child), "%s/%s", path, de->d_name); agent_search_path(ctx, child, depth + 1); } closedir(dir); } /* Implement the search tool using either literal matching or POSIX regex. */ static char *agent_tool_search(agent_worker *w, const agent_tool_call *call) { (void)w; const char *query = agent_tool_arg_value(call, "query"); if (!query || !query[0]) return xstrdup("Tool error: search requires query\n"); const char *path = agent_tool_arg_value(call, "path"); if (!path || !path[0]) path = "."; const char *mode = agent_tool_arg_value(call, "mode"); agent_search_ctx ctx = { .query = query, .glob = agent_tool_arg_value(call, "glob"), .use_regex = mode && !strcmp(mode, "regex"), .case_sensitive = agent_parse_bool_default(agent_tool_arg_value(call, "case_sensitive"), true), .context = agent_parse_int_default(agent_tool_arg_value(call, "context"), 0, 0, 5), .max_results = agent_parse_int_default(agent_tool_arg_value(call, "max_results"), 50, 1, 500), }; if (ctx.use_regex) { int flags = REG_EXTENDED | REG_NOSUB; if (!ctx.case_sensitive) flags |= REG_ICASE; int rc = regcomp(&ctx.regex, query, flags); if (rc != 0) { char msg[256]; regerror(rc, &ctx.regex, msg, sizeof(msg)); agent_buf b = {0}; agent_buf_puts(&b, "Tool error: invalid regex: "); agent_buf_puts(&b, msg); agent_buf_puts(&b, "\n"); return agent_buf_take(&b); } ctx.regex_ready = true; } agent_search_path(&ctx, path, 0); if (ctx.regex_ready) regfree(&ctx.regex); if (!ctx.out.ptr) agent_buf_puts(&ctx.out, "No matches\n"); else { char hdr[96]; snprintf(hdr, sizeof(hdr), "%d match%s shown\n\n", ctx.results, ctx.results == 1 ? "" : "es"); size_t hdr_len = strlen(hdr); if (ctx.out.len + hdr_len + 1 > ctx.out.cap) { ctx.out.cap = ctx.out.len + hdr_len + 1; ctx.out.ptr = xrealloc(ctx.out.ptr, ctx.out.cap); } memmove(ctx.out.ptr + hdr_len, ctx.out.ptr, ctx.out.len + 1); memcpy(ctx.out.ptr, hdr, hdr_len); ctx.out.len += hdr_len; } return agent_buf_take(&ctx.out); } /* ============================================================================ * Browser Web Tools * ============================================================================ * * The browser subsystem lives in ds4_web.c: it owns visible Chrome and CDP. The * agent side only asks for permission, dispatches tools, and caps visit_page * output using the same "head plus temp file" shape as bash. */ #define AGENT_WEB_HEAD_BYTES (8*1024) #define AGENT_WEB_HEAD_LINES 100 static int agent_count_lines(const char *s) { if (!s || !s[0]) return 0; int lines = 0; for (const char *p = s; *p; p++) { if (*p == '\n') lines++; } if (s[strlen(s) - 1] != '\n') lines++; return lines; } static char *agent_string_head(const char *s, int max_lines, size_t max_bytes, int *lines_read, bool *byte_limited) { if (lines_read) *lines_read = 0; if (byte_limited) *byte_limited = false; if (!s) return xstrdup(""); size_t used = 0; int lines = 0; while (s[used] && used < max_bytes && lines < max_lines) { if (s[used++] == '\n') lines++; } if (s[used] && used >= max_bytes && byte_limited) *byte_limited = true; if (used && s[used - 1] != '\n' && lines < max_lines) lines++; if (lines_read) *lines_read = lines; return xstrndup(s, used); } static bool agent_write_temp_text(const char *prefix, const char *text, char *path, size_t path_len, char *err, size_t err_len) { char tmpl[PATH_MAX]; snprintf(tmpl, sizeof(tmpl), "/tmp/%s_XXXXXX", prefix); int fd = mkstemp(tmpl); if (fd < 0) { snprintf(err, err_len, "failed to create temporary file: %s", strerror(errno)); return false; } size_t len = text ? strlen(text) : 0; const char *p = text ? text : ""; size_t left = len; while (left) { ssize_t n = write(fd, p, left); if (n < 0 && errno == EINTR) continue; if (n <= 0) { snprintf(err, err_len, "failed to write temporary file: %s", strerror(errno)); close(fd); unlink(tmpl); return false; } p += n; left -= (size_t)n; } if (close(fd) != 0) { snprintf(err, err_len, "failed to close temporary file: %s", strerror(errno)); unlink(tmpl); return false; } snprintf(path, path_len, "%s", tmpl); return true; } static char *agent_tool_google_search(agent_worker *w, const agent_tool_call *call) { const char *query = agent_tool_arg_value(call, "query"); if (!query || !query[0]) return xstrdup("Tool error: google_search requires query\n"); char err[256] = {0}; agent_publishf_system_status(w, "Searching Google for %s...", query); char *md = ds4_web_google_search(w->web, query, err, sizeof(err)); if (!md) { agent_buf b = {0}; agent_buf_puts(&b, "Tool error: google_search failed: "); agent_buf_puts(&b, err[0] ? err : "unknown error"); agent_buf_puts(&b, "\n"); return agent_buf_take(&b); } return md; } static char *agent_tool_visit_page(agent_worker *w, const agent_tool_call *call) { const char *url = agent_tool_arg_value(call, "url"); if (!url || !url[0]) return xstrdup("Tool error: visit_page requires url\n"); char err[256] = {0}; agent_publishf_system_status(w, "Opening page %s...", url); char *md = ds4_web_visit_page(w->web, url, err, sizeof(err)); if (!md) { agent_buf b = {0}; agent_buf_puts(&b, "Tool error: visit_page failed: "); agent_buf_puts(&b, err[0] ? err : "unknown error"); agent_buf_puts(&b, "\n"); return agent_buf_take(&b); } char path[PATH_MAX]; if (!agent_write_temp_text("ds4_agent_web", md, path, sizeof(path), err, sizeof(err))) { free(md); agent_buf b = {0}; agent_buf_puts(&b, "Tool error: visit_page failed: "); agent_buf_puts(&b, err[0] ? err : "could not store rendered page"); agent_buf_puts(&b, "\n"); return agent_buf_take(&b); } int total_lines = agent_count_lines(md); int shown_lines = 0; bool byte_limited = false; char *head = agent_string_head(md, AGENT_WEB_HEAD_LINES, AGENT_WEB_HEAD_BYTES, &shown_lines, &byte_limited); bool truncated = byte_limited || shown_lines < total_lines; agent_buf out = {0}; char line[PATH_MAX + 256]; snprintf(line, sizeof(line), "visit_page url=%s\noutput_path=%s (%zu bytes, %d lines)\n", url, path, strlen(md), total_lines); agent_buf_puts(&out, line); if (truncated) { snprintf(line, sizeof(line), "\n", AGENT_WEB_HEAD_LINES, path); agent_buf_puts(&out, line); agent_buf_puts(&out, head); if (head[0] && head[strlen(head) - 1] != '\n') agent_buf_puts(&out, "\n"); agent_buf_puts(&out, "\n"); agent_buf_puts(&out, "Use read path= start_line= max_lines= raw=true to inspect more rendered Markdown.\n"); } else { agent_buf_puts(&out, "\n"); agent_buf_puts(&out, head); if (head[0] && head[strlen(head) - 1] != '\n') agent_buf_puts(&out, "\n"); agent_buf_puts(&out, "\n"); } free(head); free(md); return agent_buf_take(&out); } /* ============================================================================ * Asynchronous Bash Jobs * ============================================================================ * * Bash commands are tracked jobs, not blocking one-shot calls. Each job owns a * process, a pipe, and a secure /tmp output file. The first observation is * head-biased so headers and early errors are visible; later progress updates * are tail-biased and report how much output was added since the previous * observation. */ #define AGENT_BASH_HEAD_BYTES (8*1024) #define AGENT_BASH_HEAD_LINES 100 #define AGENT_BASH_TAIL_BYTES (32*1024) #define AGENT_BASH_PROGRESS_TAIL_LINES 4 #define AGENT_BASH_FINAL_TAIL_LINES 20 struct agent_bash_job { int id; pid_t pid; int pipe_fd; int tmp_fd; char path[PATH_MAX]; char *cmd; double start_time; double timeout_sec; size_t bytes; int newline_count; char last_byte; size_t observed_bytes; int observed_display_lines; bool observed_once; int exit_status; bool running; bool timed_out; struct agent_bash_job *next; agent_worker *worker; /* back-pointer for terminal state restoration */ }; static int agent_bash_display_lines(const agent_bash_job *job) { if (!job || job->bytes == 0) return 0; return job->newline_count + (job->last_byte != '\n'); } static void agent_bash_note_output(agent_bash_job *job, const char *s, size_t n) { for (size_t i = 0; i < n; i++) { if (s[i] == '\n') job->newline_count++; } if (n) job->last_byte = s[n - 1]; job->bytes += n; } static void agent_bash_job_free(agent_bash_job *job) { if (!job) return; if (job->running && job->pid > 0) { kill(-job->pid, SIGKILL); kill(job->pid, SIGKILL); waitpid(job->pid, NULL, 0); } if (job->pipe_fd >= 0) close(job->pipe_fd); if (job->tmp_fd >= 0) close(job->tmp_fd); free(job->cmd); free(job); } static void agent_bash_jobs_free(agent_worker *w) { agent_bash_job *job = w->bash_jobs; while (job) { agent_bash_job *next = job->next; agent_bash_job_free(job); job = next; } w->bash_jobs = NULL; } static agent_bash_job *agent_bash_find_job(agent_worker *w, int id, pid_t pid) { for (agent_bash_job *job = w->bash_jobs; job; job = job->next) { if ((id > 0 && job->id == id) || (id <= 0 && pid > 0 && job->pid == pid)) return job; } return NULL; } static void agent_bash_remove_job(agent_worker *w, agent_bash_job *target) { agent_bash_job **link = &w->bash_jobs; while (*link) { if (*link == target) { *link = target->next; target->next = NULL; agent_bash_job_free(target); return; } link = &(*link)->next; } } static void agent_bash_drain(agent_bash_job *job) { if (!job || job->pipe_fd < 0) return; char tmp[4096]; for (;;) { ssize_t n = read(job->pipe_fd, tmp, sizeof(tmp)); if (n > 0) { agent_bash_note_output(job, tmp, (size_t)n); if (job->tmp_fd >= 0) write_all(job->tmp_fd, tmp, (size_t)n); continue; } if (n < 0 && errno == EINTR) continue; break; } } static void agent_worker_note_terminal_mode_may_have_changed(agent_worker *w) { if (!w) return; pthread_mutex_lock(&w->mu); w->raw_mode_needs_restore = true; pthread_mutex_unlock(&w->mu); } static void agent_bash_finalize(agent_bash_job *job, int status) { agent_bash_drain(job); if (job->pipe_fd >= 0) { close(job->pipe_fd); job->pipe_fd = -1; } if (job->tmp_fd >= 0) { close(job->tmp_fd); job->tmp_fd = -1; } if (WIFEXITED(status)) job->exit_status = WEXITSTATUS(status); else if (WIFSIGNALED(status)) job->exit_status = 128 + WTERMSIG(status); else job->exit_status = -1; job->running = false; /* A child can still open /dev/tty directly and alter terminal state even * though its stdin is /dev/null. Ask the UI thread to verify raw mode at * a safe point instead of touching linenoise from the worker path. */ agent_worker_note_terminal_mode_may_have_changed(job->worker); } /* Drain available output, notice process exit, and enforce timeout. This is * called opportunistically by status/wait/compaction instead of a background * reaper thread, keeping all bash job state owned by the agent worker. */ static void agent_bash_poll(agent_bash_job *job) { if (!job || !job->running) return; agent_bash_drain(job); int status = 0; pid_t rc = waitpid(job->pid, &status, WNOHANG); if (rc == job->pid) { agent_bash_finalize(job, status); return; } if (rc < 0 && errno != EINTR) { job->exit_status = -1; job->running = false; if (job->pipe_fd >= 0) { close(job->pipe_fd); job->pipe_fd = -1; } if (job->tmp_fd >= 0) { close(job->tmp_fd); job->tmp_fd = -1; } agent_worker_note_terminal_mode_may_have_changed(job->worker); return; } if (now_sec() - job->start_time >= job->timeout_sec) { job->timed_out = true; kill(-job->pid, SIGKILL); kill(job->pid, SIGKILL); while (waitpid(job->pid, &status, 0) < 0 && errno == EINTR) {} agent_bash_finalize(job, status); } } /* Spawn a shell command into its own process group so bash_stop/timeout can * kill grandchildren created by the shell, not just the /bin/sh wrapper. */ static agent_bash_job *agent_bash_start(agent_worker *w, const char *cmd, int timeout_sec, char *err, size_t err_len) { char tmp_path[] = "/tmp/ds4_agent_output_XXXXXX"; int tmpfd = mkstemp(tmp_path); if (tmpfd < 0) { snprintf(err, err_len, "failed to create temporary output file: %s", strerror(errno)); return NULL; } int pipefd[2]; if (pipe(pipefd) != 0) { snprintf(err, err_len, "failed to create pipe: %s", strerror(errno)); close(tmpfd); unlink(tmp_path); return NULL; } pid_t pid = fork(); if (pid < 0) { snprintf(err, err_len, "failed to fork: %s", strerror(errno)); close(pipefd[0]); close(pipefd[1]); close(tmpfd); unlink(tmp_path); return NULL; } if (pid == 0) { setpgid(0, 0); close(tmpfd); /* The bash tool is not interactive. Give the shell /dev/null as * stdin so it does not inherit the live linenoise terminal and reset * it from raw mode to cooked mode behind the agent's back. */ int null_fd = open("/dev/null", O_RDONLY); if (null_fd >= 0) { if (dup2(null_fd, STDIN_FILENO) < 0) close(STDIN_FILENO); if (null_fd != STDIN_FILENO) close(null_fd); } else { close(STDIN_FILENO); } close(pipefd[0]); dup2(pipefd[1], STDOUT_FILENO); dup2(pipefd[1], STDERR_FILENO); close(pipefd[1]); execl("/bin/sh", "sh", "-c", cmd ? cmd : "", (char *)NULL); _exit(127); } close(pipefd[1]); setpgid(pid, pid); int old_flags; set_nonblock(pipefd[0], true, &old_flags); agent_bash_job *job = xmalloc(sizeof(*job)); memset(job, 0, sizeof(*job)); if (w->next_bash_job_id <= 0) w->next_bash_job_id = 1; job->id = w->next_bash_job_id++; job->pid = pid; job->pipe_fd = pipefd[0]; job->tmp_fd = tmpfd; snprintf(job->path, sizeof(job->path), "%s", tmp_path); job->cmd = xstrdup(cmd); job->start_time = now_sec(); job->timeout_sec = timeout_sec; job->exit_status = -1; job->running = true; job->worker = w; job->next = w->bash_jobs; w->bash_jobs = job; return job; } static void agent_tail_append(agent_buf *b, const char *s, size_t n, size_t max) { if (!n) return; agent_buf_append(b, s, n); if (b->len > max) { size_t drop = b->len - max; memmove(b->ptr, b->ptr + drop, b->len - drop + 1); b->len -= drop; } } /* Read the first max_lines from the output file, with a byte cap to avoid a * pathological single long line flooding the next model turn. */ static char *agent_bash_read_head(const agent_bash_job *job, int max_lines, size_t max_bytes, int *lines_read, bool *byte_limited) { if (lines_read) *lines_read = 0; if (byte_limited) *byte_limited = false; if (!job || !job->path[0] || job->bytes == 0) return xstrdup(""); FILE *fp = fopen(job->path, "rb"); if (!fp) return xstrdup("\n"); agent_buf out = {0}; int lines = 0; while (lines < max_lines && out.len < max_bytes) { int c = fgetc(fp); if (c == EOF) { if (ferror(fp) && errno == EINTR) { clearerr(fp); continue; } break; } char ch = (char)c; agent_buf_append(&out, &ch, 1); if (ch == '\n') lines++; } if (out.len >= max_bytes && !feof(fp) && byte_limited) *byte_limited = true; fclose(fp); if (lines_read) *lines_read = lines + (out.len && out.ptr[out.len - 1] != '\n'); if (!out.ptr) return xstrdup(""); return agent_buf_take(&out); } /* Read the last max_lines from the full output file. The model-visible label * says "tail -N " so it is clear this is not the complete output. */ static char *agent_bash_read_tail_lines(const agent_bash_job *job, int max_lines) { if (!job || !job->path[0] || job->bytes == 0) return xstrdup(""); FILE *fp = fopen(job->path, "rb"); if (!fp) return xstrdup("\n"); agent_buf tail = {0}; char tmp[2048]; for (;;) { size_t n = fread(tmp, 1, sizeof(tmp), fp); if (n) agent_tail_append(&tail, tmp, n, AGENT_BASH_TAIL_BYTES); if (n < sizeof(tmp)) { if (ferror(fp) && errno == EINTR) { clearerr(fp); continue; } break; } } fclose(fp); if (!tail.ptr) return xstrdup(""); char *start = tail.ptr; int newlines = 0; for (char *p = tail.ptr + tail.len; p > tail.ptr; p--) { if (p[-1] == '\n' && ++newlines > max_lines) { start = p; break; } } char *out = xstrdup(start); free(tail.ptr); return out; } /* Build the tool result for a bash job. mark_observed advances the per-job * cursor so the next status reports only fresh output. */ static char *agent_bash_observation(agent_bash_job *job, bool mark_observed) { agent_bash_poll(job); bool first_observation = !job->observed_once; int display_lines = agent_bash_display_lines(job); double elapsed = now_sec() - job->start_time; agent_buf out = {0}; char line[PATH_MAX + 256]; if (job->running) { snprintf(line, sizeof(line), "bash job=%d pid=%ld status=running elapsed_sec=%.1f timeout_sec=%.0f\n", job->id, (long)job->pid, elapsed, job->timeout_sec); } else { snprintf(line, sizeof(line), "bash job=%d pid=%ld status=done elapsed_sec=%.1f timed_out=%d\n", job->id, (long)job->pid, elapsed, job->timed_out ? 1 : 0); } agent_buf_puts(&out, line); if (!job->running) { snprintf(line, sizeof(line), "exit_status=%d\n", job->exit_status); agent_buf_puts(&out, line); } if (job->bytes == 0) { agent_buf_puts(&out, "\n\n"); } else if (first_observation) { int shown_lines = 0; bool byte_limited = false; char *head = agent_bash_read_head(job, AGENT_BASH_HEAD_LINES, AGENT_BASH_HEAD_BYTES, &shown_lines, &byte_limited); bool truncated = byte_limited || display_lines > shown_lines; if (!job->running && !truncated) { agent_buf_puts(&out, "\n"); agent_buf_puts(&out, head); if (head[0] && head[strlen(head) - 1] != '\n') agent_buf_puts(&out, "\n"); agent_buf_puts(&out, "\n"); } else { snprintf(line, sizeof(line), "output_path=%s (%zu bytes, %d lines)\n", job->path[0] ? job->path : "", job->bytes, display_lines); agent_buf_puts(&out, line); snprintf(line, sizeof(line), "\n", AGENT_BASH_HEAD_LINES, job->path); agent_buf_puts(&out, line); agent_buf_puts(&out, head); if (head[0] && head[strlen(head) - 1] != '\n') agent_buf_puts(&out, "\n"); agent_buf_puts(&out, "\n"); } free(head); } else { int tail_lines = job->running ? AGENT_BASH_PROGRESS_TAIL_LINES : AGENT_BASH_FINAL_TAIL_LINES; char *tail = agent_bash_read_tail_lines(job, tail_lines); snprintf(line, sizeof(line), "output_path=%s (%zu bytes, %d lines)\n", job->path[0] ? job->path : "", job->bytes, display_lines); agent_buf_puts(&out, line); snprintf(line, sizeof(line), "\n", tail_lines, job->path); agent_buf_puts(&out, line); agent_buf_puts(&out, tail); if (tail[0] && tail[strlen(tail) - 1] != '\n') agent_buf_puts(&out, "\n"); snprintf(line, sizeof(line), "\n"); agent_buf_puts(&out, line); free(tail); } if (job->running) { snprintf(line, sizeof(line), "\nUse bash_status job=%d to get info before refresh time; use bash_stop job=%d to stop execution\n", job->id, job->id); agent_buf_puts(&out, line); } if (mark_observed) { job->observed_bytes = job->bytes; job->observed_display_lines = display_lines; job->observed_once = true; } return agent_buf_take(&out); } static void agent_bash_publish_observation(agent_worker *w, const char *obs) { if (!obs || !obs[0]) return; const char *body = NULL; const char *label = strstr(obs, "\n\n"); if (tag_end) { agent_publish(w, "\x1b[90m", 5); if (strstr(label, "\n\n"); if (label) { body = label + strlen("\n\n"); close = ""; } } if (!body || !body[0]) return; const char *end = close ? strstr(body, close) : NULL; size_t n = end ? (size_t)(end - body) : strlen(body); if (n) { bool failed = strstr(obs, "status=done") && !strstr(obs, "exit_status=0\n"); if (failed) agent_publish(w, "\x1b[38;5;208m", 11); agent_publish(w, body, n); if (body[n - 1] != '\n') agent_publish(w, "\n", 1); if (failed) agent_publish(w, "\x1b[0m", 4); } } static void agent_bash_refresh_for(agent_worker *w, agent_bash_job *job, int refresh_sec) { double start = now_sec(); while (job->running && now_sec() - start < refresh_sec) { if (worker_should_interrupt(w)) break; agent_bash_poll(job); if (!job->running) break; struct pollfd pfd = {.fd = job->pipe_fd, .events = POLLIN}; poll(&pfd, 1, 100); } agent_bash_poll(job); } /* Common implementation for bash, bash_status, and bash_stop. */ static char *agent_bash_job_tool_result(agent_worker *w, agent_bash_job *job, bool wait, int refresh_sec, bool stop, bool remove_if_done) { if (stop && job->running) { kill(-job->pid, SIGTERM); kill(job->pid, SIGTERM); double start = now_sec(); while (job->running && now_sec() - start < 1.0) { agent_bash_poll(job); if (!job->running) break; usleep(20000); } if (job->running) { kill(-job->pid, SIGKILL); kill(job->pid, SIGKILL); } } if (wait || stop) agent_bash_refresh_for(w, job, refresh_sec); else agent_bash_poll(job); char *obs = agent_bash_observation(job, true); agent_bash_publish_observation(w, obs); if (remove_if_done && !job->running) agent_bash_remove_job(w, job); return obs; } static int agent_tool_job_id(const agent_tool_call *call) { return agent_parse_int_default(agent_tool_arg_value(call, "job"), 0, 0, INT_MAX); } static pid_t agent_tool_pid(const agent_tool_call *call) { return (pid_t)agent_parse_int_default(agent_tool_arg_value(call, "pid"), 0, 0, INT_MAX); } /* ============================================================================ * Tool Dispatch * ============================================================================ */ /* Execute one parsed DSML tool call and return the text that will be appended as * the tool-role result. UI visualization already happened while streaming; this * function is only about side effects and the model-visible observation. */ static char *agent_execute_tool_call(agent_worker *w, const agent_tool_call *call) { agent_buf result = {0}; if (!call->name) return xstrdup("Tool error: missing tool name\n"); if (!strcmp(call->name, "read")) return agent_tool_read(w, call); if (!strcmp(call->name, "more")) return agent_tool_more(w, call); if (!strcmp(call->name, "write")) return agent_tool_write(w, call); if (!strcmp(call->name, "list")) return agent_tool_list(call); if (!strcmp(call->name, "edit")) return agent_tool_edit(w, call); if (!strcmp(call->name, "search")) return agent_tool_search(w, call); if (!strcmp(call->name, "google_search")) return agent_tool_google_search(w, call); if (!strcmp(call->name, "visit_page")) return agent_tool_visit_page(w, call); if (!strcmp(call->name, "bash")) { const char *cmd = agent_tool_arg_value(call, "command"); if (!cmd || !cmd[0]) return xstrdup("Tool error: bash requires command\n"); int timeout = agent_parse_timeout(agent_tool_arg_value(call, "timeout_sec")); int refresh = agent_parse_int_default(agent_tool_arg_value(call, "refresh_sec"), 60, 1, 3600); char err[160] = {0}; agent_bash_job *job = agent_bash_start(w, cmd, timeout, err, sizeof(err)); if (!job) { agent_buf_puts(&result, "Tool error: bash failed to start: "); agent_buf_puts(&result, err[0] ? err : "unknown error"); agent_buf_puts(&result, "\n"); return agent_buf_take(&result); } return agent_bash_job_tool_result(w, job, true, refresh, false, true); } if (!strcmp(call->name, "bash_status") || !strcmp(call->name, "bash_stop")) { int job_id = agent_tool_job_id(call); pid_t pid = agent_tool_pid(call); agent_bash_job *job = agent_bash_find_job(w, job_id, pid); if (!job) { char msg[128]; snprintf(msg, sizeof(msg), "Tool error: bash job not found: job=%d pid=%ld\n", job_id, (long)pid); return xstrdup(msg); } int refresh = agent_parse_int_default(agent_tool_arg_value(call, "refresh_sec"), 60, 1, 3600); bool stop = !strcmp(call->name, "bash_stop"); bool wait = stop; return agent_bash_job_tool_result(w, job, wait, refresh, stop, true); } { char header[256]; snprintf(header, sizeof(header), "\n[tool:%s] unknown tool\n", call->name); agent_publish(w, header, strlen(header)); agent_buf_puts(&result, "Tool error: unknown tool: "); agent_buf_puts(&result, call->name); agent_buf_puts(&result, "\n"); return agent_buf_take(&result); } } /* Execute all tool calls from one DSML block, preserving per-call labels in the * combined result so the model can associate observations with calls. */ static char *agent_execute_tool_calls(agent_worker *w, const agent_tool_calls *calls) { agent_buf all = {0}; for (int i = 0; i < calls->len; i++) { char *res = agent_execute_tool_call(w, &calls->v[i]); char hdr[128]; snprintf(hdr, sizeof(hdr), "Tool result %d (%s):\n", i + 1, calls->v[i].name ? calls->v[i].name : "unknown"); agent_buf_puts(&all, hdr); agent_buf_puts(&all, res); if (res[0] && res[strlen(res) - 1] != '\n') agent_buf_puts(&all, "\n"); free(res); } if (calls->len == 0) agent_buf_puts(&all, "Tool error: empty tool call block\n"); return agent_buf_take(&all); } /* If compaction happens while a bash process is still alive, inject a small * tool-role reminder into the rebuilt transcript. Otherwise the summary could * preserve the user's task but lose the fact that an external process still * needs status/wait/stop handling. */ static char *agent_bash_jobs_compaction_observation(agent_worker *w) { if (!w->bash_jobs) return NULL; agent_buf out = {0}; agent_buf_puts(&out, "Bash job update after context compaction. Running jobs still need explicit bash_status or bash_stop if relevant.\n"); for (agent_bash_job *job = w->bash_jobs, *next = NULL; job; job = next) { next = job->next; char *obs = agent_bash_observation(job, true); char hdr[64]; snprintf(hdr, sizeof(hdr), "\nJob %d:\n", job->id); agent_buf_puts(&out, hdr); agent_buf_puts(&out, obs); free(obs); if (!job->running) agent_bash_remove_job(w, job); } return agent_buf_take(&out); } /* ============================================================================ * Context Compaction * ============================================================================ * * Compaction asks the model for durable task state, then rebuilds the live * transcript as: system prompt + summary + recent verbatim tail. This keeps * the active KV usable while avoiding unbounded transcript growth. */ /* Decide when to compact before an ordinary turn or before appending a large * tool result. The fixed free-token threshold is capped proportionally for * smaller contexts so tests with tiny contexts still compact rather than fail. */ static bool agent_worker_should_compact(agent_worker *w) { int ctx = w->cfg->gen.ctx_size; int used = w->transcript.len; if (ctx <= 0 || used <= 0) return false; if (used >= (ctx * AGENT_COMPACT_SOFT_PERCENT) / 100) return true; int free_threshold = AGENT_COMPACT_MIN_FREE_TOKENS; int proportional = ctx / 4; if (free_threshold > proportional) free_threshold = proportional; return ctx - used <= free_threshold; } static int agent_special_token_id(ds4_engine *engine, const char *rendered) { ds4_tokens t = {0}; ds4_tokenize_rendered_chat(engine, rendered, &t); int id = t.len == 1 ? t.v[0] : -1; ds4_tokens_free(&t); return id; } /* Pick a recent verbatim tail for the compacted transcript. Prefer a user * boundary inside the budget so the rebuilt context starts at a natural turn. */ static int agent_compact_tail_start(agent_worker *w, int bottom, int sys_len) { int tail_budget = w->cfg->gen.ctx_size / AGENT_COMPACT_TAIL_DIVISOR; if (tail_budget > AGENT_COMPACT_TAIL_CAP_TOKENS) tail_budget = AGENT_COMPACT_TAIL_CAP_TOKENS; if (tail_budget < 1) tail_budget = 1; int target = bottom - tail_budget; if (target < sys_len) target = sys_len; int user_id = agent_special_token_id(w->engine, "<|User|>"); if (user_id < 0) return target; for (int i = target; i < bottom; i++) { if (w->transcript.v[i] == user_id) return i; } return target; } static void agent_tokens_append_range(ds4_tokens *dst, const ds4_tokens *src, int start, int end) { if (start < 0) start = 0; if (end > src->len) end = src->len; for (int i = start; i < end; i++) ds4_tokens_push(dst, src->v[i]); } /* Build the private prompt used to ask the model for durable state. The prompt * explicitly forbids tool calls because the result is consumed internally, not * delivered as an assistant turn. */ static char *agent_compact_make_prompt(const char *reason) { agent_buf b = {0}; agent_buf_puts(&b, "Internal ds4-agent context compaction request. This is not a user request.\n" "Write a durable task-state summary of the conversation so far. Preserve only facts that matter for continuing the work:\n" "- user goals, constraints, and preferences\n" "- files inspected or edited\n" "- commands run and important results\n" "- decisions, rejected approaches, known bugs, and pending next steps\n" "- reloadable bulky data with exact paths/ranges/commands when available\n\n" "Do not invent facts. Do not include generic narration. Do not include raw file contents unless they were essential to a conclusion.\n" "After the summary, stop. Do not continue the user task, do not call tools, and do not output thinking tags or DSML markup.\n" "Output only the compact summary.\n"); if (reason && reason[0]) { agent_buf_puts(&b, "\nCompaction reason: "); agent_buf_puts(&b, reason); agent_buf_puts(&b, "\n"); } return agent_buf_take(&b); } /* Perform the full compaction exchange and rebuild the live DS4 session from * the compacted transcript. Any failure invalidates live KV because the model * may have just seen private compaction instructions that are not part of the * real conversation. */ static bool agent_worker_compact(agent_worker *w, const char *reason, char *err, size_t err_len) { const int bottom = w->transcript.len; if (bottom <= 0) return true; ds4_tokens sys = {0}; agent_worker_build_system_tokens(w, &sys); if (bottom <= sys.len) { ds4_tokens_free(&sys); return true; } agent_publishf(w, "\n\x1b[1;95mCOMPACTING\x1b[0m %s: summarizing durable task state\n\x1b[38;5;245m", reason && reason[0] ? reason : "context"); char *prompt_text = agent_compact_make_prompt(reason); ds4_tokens prompt = {0}; ds4_tokens_copy(&prompt, &w->transcript); ds4_chat_append_message(w->engine, &prompt, "user", prompt_text); free(prompt_text); ds4_chat_append_assistant_prefix(w->engine, &prompt, DS4_THINK_NONE); pthread_mutex_lock(&w->mu); w->status.state = AGENT_WORKER_COMPACTING; w->progress_started_at = now_sec(); w->status.prefill_done = 0; w->status.prefill_total = 0; w->status.prefill_tps = 0.0; w->status.generated = 0; w->status.gen_tps = 0.0; w->status.greedy_sampling = false; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); int summary_room = w->cfg->gen.ctx_size - prompt.len - 1; if (summary_room < 256) { snprintf(err, err_len, "not enough context left to request compaction summary"); ds4_tokens_free(&prompt); ds4_tokens_free(&sys); agent_publish(w, "\x1b[0m\n", 5); return false; } int summary_max = summary_room < AGENT_COMPACT_SUMMARY_MAX_TOKENS ? summary_room : AGENT_COMPACT_SUMMARY_MAX_TOKENS; ds4_session_set_progress(w->session, worker_progress_cb, w); ds4_session_set_display_progress(w->session, worker_progress_cb, w); ds4_session_set_cancel(w->session, worker_cancel_session_cb, w); int sync_rc = ds4_session_sync(w->session, &prompt, err, err_len); ds4_session_set_cancel(w->session, NULL, NULL); ds4_session_set_progress(w->session, NULL, NULL); ds4_session_set_display_progress(w->session, NULL, NULL); if (sync_rc == DS4_SESSION_SYNC_INTERRUPTED) { ds4_session_invalidate(w->session); snprintf(err, err_len, "interrupted"); agent_publish_system_status( w, "Compaction interrupted; keeping the previous conversation state."); ds4_tokens_free(&prompt); ds4_tokens_free(&sys); agent_publish(w, "\x1b[0m\n", 5); worker_clear_interrupt(w); return false; } if (sync_rc != 0) { ds4_session_invalidate(w->session); ds4_tokens_free(&prompt); ds4_tokens_free(&sys); agent_publish(w, "\x1b[0m\n", 5); return false; } /* From here until the final rebuild, the live KV contains the internal * compaction prompt/summary, while w->transcript still contains the real * conversation. If anything fails, invalidate live KV so the next turn * cannot accidentally continue from the private compaction exchange. */ agent_buf summary = {0}; char eval_err[160] = {0}; int think_end_id = agent_special_token_id(w->engine, ""); int dsml_id = agent_special_token_id(w->engine, "|DSML|"); double t0 = now_sec(); for (int i = 0; i < summary_max; i++) { if (worker_should_interrupt(w)) { snprintf(err, err_len, "interrupted"); ds4_session_invalidate(w->session); ds4_tokens_free(&prompt); ds4_tokens_free(&sys); free(summary.ptr); agent_publish(w, "\x1b[0m\n", 5); agent_publish_system_status( w, "Compaction interrupted; keeping the previous conversation state."); worker_clear_interrupt(w); return false; } int token = ds4_session_argmax(w->session); if (token == ds4_token_eos(w->engine)) break; if (token == think_end_id || token == dsml_id) { if (token == dsml_id && summary.len && summary.ptr[summary.len - 1] == '<') { summary.ptr[--summary.len] = '\0'; } agent_trace(w, "compaction summary stopped before control token id=%d", token); break; } if (ds4_session_eval(w->session, token, eval_err, sizeof(eval_err)) != 0) { snprintf(err, err_len, "%s", eval_err); ds4_session_invalidate(w->session); ds4_tokens_free(&prompt); ds4_tokens_free(&sys); free(summary.ptr); agent_publish(w, "\x1b[0m\n", 5); return false; } size_t text_len = 0; char *text = ds4_token_text(w->engine, token, &text_len); agent_buf_append(&summary, text, text_len); agent_publish(w, text, text_len); free(text); double dt = now_sec() - t0; pthread_mutex_lock(&w->mu); w->status.generated = i + 1; w->status.gen_tps = dt > 0.0 ? (double)(i + 1) / dt : 0.0; w->status.greedy_sampling = false; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } agent_publish(w, "\x1b[0m\n", 5); ds4_tokens_free(&prompt); if (!summary.ptr || !summary.ptr[0]) { snprintf(err, err_len, "compaction summary was empty"); ds4_session_invalidate(w->session); ds4_tokens_free(&sys); free(summary.ptr); return false; } int tail_start = agent_compact_tail_start(w, bottom, sys.len); ds4_tokens compacted = {0}; ds4_tokens_copy(&compacted, &sys); agent_buf summary_msg = {0}; agent_buf_puts(&summary_msg, "\n\n[ds4-agent compacted earlier conversation. Durable task-state summary follows.]\n"); agent_buf_puts(&summary_msg, summary.ptr); if (summary_msg.len && summary_msg.ptr[summary_msg.len - 1] != '\n') agent_buf_puts(&summary_msg, "\n"); agent_buf_puts(&summary_msg, "[End compacted summary. Recent conversation continues verbatim below.]\n\n"); ds4_chat_append_message(w->engine, &compacted, "system", summary_msg.ptr); free(summary_msg.ptr); free(summary.ptr); agent_tokens_append_range(&compacted, &w->transcript, tail_start, bottom); agent_publishf(w, "\x1b[1;95mCOMPACTING\x1b[0m rebuilding context: old=%d summary+tail=%d tail=%d\n", bottom, compacted.len, bottom - tail_start); ds4_tokens old_transcript = {0}; ds4_tokens_copy(&old_transcript, &w->transcript); ds4_tokens_free(&w->transcript); w->transcript = compacted; if (agent_worker_sync_tokens(w, &w->transcript, true, err, err_len) != 0) { ds4_session_invalidate(w->session); ds4_tokens_free(&w->transcript); w->transcript = old_transcript; ds4_tokens_free(&sys); return false; } agent_worker_note_system_prompt_seen(w); ds4_tokens_free(&old_transcript); ds4_tokens_free(&sys); char *bash_update = agent_bash_jobs_compaction_observation(w); if (bash_update) { ds4_chat_append_message(w->engine, &w->transcript, "tool", bash_update); w->session_dirty = true; agent_trace_text(w, "tool-after-compaction", bash_update, strlen(bash_update)); agent_publish(w, "\x1b[90mCOMPACTING added bash job update after rebuild\x1b[0m\n", strlen("\x1b[90mCOMPACTING added bash job update after rebuild\x1b[0m\n")); free(bash_update); } agent_trace(w, "compacted reason=\"%s\" old=%d new=%d tail_start=%d tail=%d", reason ? reason : "", bottom, w->transcript.len, tail_start, bottom - tail_start); return true; } static bool agent_worker_compact_if_needed(agent_worker *w, const char *reason, char *err, size_t err_len) { if (!agent_worker_should_compact(w)) return true; return agent_worker_compact(w, reason, err, err_len); } static int worker_accept_generated_token(agent_worker *w, int token, int *generated, double t0, agent_stream_renderer *stream, char *err, size_t err_len) { if (ds4_session_eval(w->session, token, err, err_len) != 0) return 1; ds4_tokens_push(&w->transcript, token); size_t text_len = 0; char *text = ds4_token_text(w->engine, token, &text_len); agent_trace_token(w, token, text, text_len, *generated + 1); agent_stream_text(stream, text, text_len, false); free(text); (*generated)++; double dt = now_sec() - t0; pthread_mutex_lock(&w->mu); w->status.generated = *generated; w->status.gen_tps = dt > 0.0 ? (double)*generated / dt : 0.0; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); return 0; } static int worker_force_generated_text(agent_worker *w, const char *text, int max_tokens, int *generated, double t0, agent_stream_renderer *stream, char *err, size_t err_len) { ds4_tokens tokens = {0}; ds4_tokenize_text(w->engine, text, &tokens); if (tokens.len > max_tokens - *generated) { snprintf(err, err_len, "not enough generation room to force %s", text); ds4_tokens_free(&tokens); return 1; } for (int i = 0; i < tokens.len && *generated < max_tokens; i++) { if (worker_accept_generated_token(w, tokens.v[i], generated, t0, stream, err, err_len) != 0) { ds4_tokens_free(&tokens); return 1; } } ds4_tokens_free(&tokens); return 0; } /* ============================================================================ * Model Worker Thread * ============================================================================ */ /* DSML structure is a machine-readable grammar, so once the model has clearly * started a tool stanza we decode grammar bytes greedily. Parameter values are * different: they can be shell commands, code, file contents, or edit bodies, * and should keep the configured sampling behavior. The only exception is a * parameter closing tag once it is clearly DSML syntax, not ordinary text such * as HTML/XML/code containing "parser) return false; if (sr->parser->state == AGENT_DSML_ERROR || sr->parser->state == AGENT_DSML_DONE) return false; /* A possible opening marker is being held back by the start detector. A * single '<' is too common in prose/code to justify forcing argmax; after * the second byte, the buffered prefix still matching here is specifically * DSML-shaped ("<|..." or the tolerated "dsml_start_len > 1) return true; if (!sr->dsml_active) return false; if (sr->parser->state == AGENT_DSML_STRUCTURAL) return true; if (sr->parser->state != AGENT_DSML_PARAM_VALUE) return false; return sr->parser->param_close_prefix; } static int worker_sample_with_mode(agent_worker *w, const agent_config *cfg, bool greedy, uint64_t *rng) { return ds4_session_sample(w->session, greedy ? 0.0f : cfg->gen.temperature, 0, greedy ? 1.0f : cfg->gen.top_p, greedy ? 0.0f : cfg->gen.min_p, rng); } static void worker_set_greedy_sampling(agent_worker *w, bool greedy) { pthread_mutex_lock(&w->mu); if (w->status.greedy_sampling != greedy) { w->status.greedy_sampling = greedy; agent_wake_locked(w); } pthread_mutex_unlock(&w->mu); } /* Run one user turn until the assistant stops or returns a tool call. Tool * results are appended to the transcript and the loop continues, which gives * the model native DSML tool iteration without a client/server protocol. */ static int worker_run_turn(agent_worker *w, const char *user_text) { agent_config *cfg = w->cfg; ds4_think_mode think_mode = effective_think_mode(cfg); pthread_mutex_lock(&w->mu); w->interrupt = false; w->status.error[0] = '\0'; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); char compact_err[160] = {0}; if (!agent_worker_compact_if_needed(w, "soft limit before user turn", compact_err, sizeof(compact_err))) { if (agent_err_is_interrupted(compact_err)) { worker_clear_interrupt(w); agent_set_status(w, AGENT_WORKER_IDLE); return 0; } agent_set_error(w, compact_err[0] ? compact_err : "context compaction failed"); return 1; } agent_worker_maybe_append_datetime_context(w); agent_trace_text(w, "user", user_text ? user_text : "", user_text ? strlen(user_text) : 0); if (!w->session_title) { w->session_title = agent_session_title_from_prompt(user_text, 0); w->session_created_at = (uint64_t)time(NULL); agent_session_identity_sha(w->session_title, w->session_created_at, w->session_sha); } ds4_chat_append_message(w->engine, &w->transcript, "user", user_text); uint64_t rng = cfg->gen.seed ? cfg->gen.seed : ((uint64_t)time(NULL) ^ ((uint64_t)getpid() << 32) ^ (uint64_t)clock()); pthread_mutex_lock(&w->mu); w->user_activity = true; w->session_dirty = true; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); /* A user turn may contain any number of assistant/tool/assistant rounds. * Coding agents naturally perform long read/edit/test loops, so there is * deliberately no artificial "too many tool calls" ceiling here: context * pressure, compaction, user Ctrl+C, and the model's final answer are the * real stopping conditions. The transcript is the single source of truth: * after a DSML stanza completes we terminate that assistant message, append * the tool result as a tool message, then ask the model to continue. */ for (int tool_round = 0; ; tool_round++) { if (tool_round > 0 && !agent_worker_compact_if_needed(w, "soft limit before tool continuation", compact_err, sizeof(compact_err))) { if (agent_err_is_interrupted(compact_err)) { worker_clear_interrupt(w); agent_set_status(w, AGENT_WORKER_IDLE); return 0; } agent_set_error(w, compact_err[0] ? compact_err : "context compaction failed"); return 1; } agent_worker_maybe_append_system_prompt_reminder(w); ds4_chat_append_assistant_prefix(w->engine, &w->transcript, think_mode); const ds4_tokens *prompt_for_sync = &w->transcript; int old_pos = ds4_session_pos(w->session); int common = ds4_session_common_prefix(w->session, &w->transcript); int cached = common == old_pos && w->transcript.len >= old_pos ? common : 0; int suffix = prompt_for_sync->len - cached; agent_trace(w, "prefill tool_round=%d transcript=%d prompt=%d cached=%d suffix=%d think=%s", tool_round, w->transcript.len, prompt_for_sync->len, cached, suffix, ds4_think_mode_name(think_mode)); agent_trace_tokens(w, "prefill_suffix", prompt_for_sync, cached); pthread_mutex_lock(&w->mu); unsigned prefill_label = w->status.state == AGENT_WORKER_PREFILL ? w->status.prefill_label : agent_next_prefill_label(); w->status.state = AGENT_WORKER_PREFILL; w->progress_base = cached; w->progress_started_at = now_sec(); w->status.prefill_done = 0; w->status.prefill_total = suffix; w->status.prefill_label = prefill_label; w->status.prefill_tps = 0.0; w->status.generated = 0; w->status.gen_tps = 0.0; w->status.greedy_sampling = false; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); char err[160]; ds4_session_set_progress(w->session, worker_progress_cb, w); ds4_session_set_display_progress(w->session, worker_progress_cb, w); ds4_session_set_cancel(w->session, worker_cancel_session_cb, w); int sync_rc = ds4_session_sync(w->session, prompt_for_sync, err, sizeof(err)); ds4_session_set_cancel(w->session, NULL, NULL); ds4_session_set_progress(w->session, NULL, NULL); ds4_session_set_display_progress(w->session, NULL, NULL); if (sync_rc == DS4_SESSION_SYNC_INTERRUPTED) { agent_publish_system_status( w, "Model reading interrupted; the model may only be aware of the prefix processed so far."); ds4_tokens_push(&w->transcript, ds4_token_eos(w->engine)); worker_clear_interrupt(w); agent_set_status(w, AGENT_WORKER_IDLE); return 0; } if (sync_rc != 0) { agent_set_error(w, err); return 1; } int max_tokens = cfg->gen.n_predict; int room = ds4_session_ctx(w->session) - ds4_session_pos(w->session); if (room <= 1) max_tokens = 0; else if (max_tokens > room - 1) max_tokens = room - 1; bool use_color = isatty(STDOUT_FILENO) != 0; agent_token_renderer renderer = { .engine = w->engine, .worker = w, .format_thinking = ds4_think_mode_enabled(think_mode), .format_markdown = use_color, .in_think = ds4_think_mode_enabled(think_mode), .use_color = use_color, .last_output_newline = true, }; agent_dsml_parser dsml = {.state = AGENT_DSML_SEARCH}; agent_stream_renderer stream = { .renderer = &renderer, .parser = &dsml, .in_think = ds4_think_mode_enabled(think_mode), }; agent_edit_upto_forcer upto_forcer = {0}; bool got_tool = false; bool malformed_tool = false; bool early_tool_error = false; int generated = 0; double t0 = now_sec(); pthread_mutex_lock(&w->mu); w->status.state = AGENT_WORKER_GENERATING; w->status.prefill_tps = 0.0; w->status.greedy_sampling = false; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); bool status_greedy_sampling = false; while (generated < max_tokens && !worker_should_interrupt(w)) { worker_apply_pending_power(w); bool greedy_sampling = agent_stream_wants_greedy_sampling(&stream); if (greedy_sampling != status_greedy_sampling) { worker_set_greedy_sampling(w, greedy_sampling); status_greedy_sampling = greedy_sampling; } int token = worker_sample_with_mode(w, cfg, greedy_sampling, &rng); if (token == ds4_token_eos(w->engine)) break; size_t text_len = 0; char *text = ds4_token_text(w->engine, token, &text_len); if (agent_edit_upto_forcer_should_replace(&upto_forcer, &dsml, text, text_len)) { agent_trace(w, "edit old auto-upto replaced token=%d text=%.*s", token, (int)(text_len > 80 ? 80 : text_len), text); free(text); if (worker_force_generated_text(w, "[upto]\n", max_tokens, &generated, t0, &stream, err, sizeof(err)) != 0) { agent_dsml_parser_free(&dsml); agent_set_error(w, err); return 1; } } else { free(text); if (worker_accept_generated_token(w, token, &generated, t0, &stream, err, sizeof(err)) != 0) { agent_dsml_parser_free(&dsml); agent_set_error(w, err); return 1; } } greedy_sampling = agent_stream_wants_greedy_sampling(&stream); if (greedy_sampling != status_greedy_sampling) { worker_set_greedy_sampling(w, greedy_sampling); status_greedy_sampling = greedy_sampling; } if (dsml.state == AGENT_DSML_DONE) { got_tool = true; break; } if (stream.tool_preflight_error) { early_tool_error = true; break; } if (dsml.state == AGENT_DSML_ERROR) { malformed_tool = true; break; } if (stream.dsml_in_think) { malformed_tool = true; break; } } bool interrupted = worker_should_interrupt(w); agent_stream_text(&stream, NULL, 0, true); renderer_finish(&renderer); worker_set_greedy_sampling(w, false); if (interrupted) { ds4_tokens_push(&w->transcript, ds4_token_eos(w->engine)); agent_dsml_parser_free(&dsml); agent_publish_system_status(w, "Stopped by user"); worker_clear_interrupt(w); agent_set_status(w, AGENT_WORKER_IDLE); return 0; } if (stream.dsml_in_think) { got_tool = false; malformed_tool = true; early_tool_error = false; snprintf(dsml.error, sizeof(dsml.error), "tool calling is not allowed inside "); } else if (!malformed_tool && dsml.state == AGENT_DSML_ERROR) { malformed_tool = true; } else if (!got_tool && !malformed_tool && !early_tool_error && !interrupted && (dsml.state == AGENT_DSML_STRUCTURAL || dsml.state == AGENT_DSML_PARAM_VALUE)) { malformed_tool = true; snprintf(dsml.error, sizeof(dsml.error), "incomplete DSML tool call"); } ds4_tokens_push(&w->transcript, ds4_token_eos(w->engine)); if (!got_tool && !malformed_tool && !early_tool_error) { agent_dsml_parser_free(&dsml); agent_set_status(w, AGENT_WORKER_IDLE); return 0; } char *tool_result; if (early_tool_error) { agent_buf b = {0}; agent_buf_puts(&b, "Tool error: "); agent_buf_puts(&b, stream.tool_preflight_error_msg[0] ? stream.tool_preflight_error_msg : "edit old selector failed before new was generated"); agent_buf_puts(&b, "\n"); tool_result = agent_buf_take(&b); } else if (malformed_tool) { agent_buf b = {0}; agent_buf_puts(&b, "Tool error: invalid DSML tool call: "); agent_buf_puts(&b, dsml.error[0] ? dsml.error : "parse error"); agent_buf_puts(&b, "\n"); agent_buf_puts(&b, agent_dsml_syntax_reminder); tool_result = agent_buf_take(&b); } else { tool_result = agent_execute_tool_calls(w, &dsml.calls); } int projected_tokens = 0; if (!agent_tool_result_fits_context(w, tool_result, AGENT_TOOL_RESULT_RESERVE_TOKENS, &projected_tokens)) { if (!agent_worker_compact(w, "tool result would exceed context", compact_err, sizeof(compact_err))) { free(tool_result); agent_dsml_parser_free(&dsml); if (agent_err_is_interrupted(compact_err)) { worker_clear_interrupt(w); agent_set_status(w, AGENT_WORKER_IDLE); return 0; } agent_set_error(w, compact_err[0] ? compact_err : "context compaction failed"); return 1; } if (!agent_tool_result_fits_context(w, tool_result, AGENT_TOOL_RESULT_RESERVE_TOKENS, &projected_tokens)) { free(tool_result); agent_buf b = {0}; char msg[256]; snprintf(msg, sizeof(msg), "Tool error: tool result still does not fit after context compaction " "(projected_prompt=%d tokens, ctx=%d, reserve=%d). " "Retry with a smaller read/search/bash output.\n", projected_tokens, w->cfg->gen.ctx_size, AGENT_TOOL_RESULT_RESERVE_TOKENS); agent_buf_puts(&b, msg); tool_result = agent_buf_take(&b); if (!agent_tool_result_fits_context(w, tool_result, 16, NULL)) { free(tool_result); agent_dsml_parser_free(&dsml); agent_set_error(w, "context full after compaction"); return 1; } } } ds4_chat_append_message(w->engine, &w->transcript, "tool", tool_result); free(tool_result); agent_dsml_parser_free(&dsml); char *queued_user = worker_request_queued_user_drain(w); if (queued_user && queued_user[0]) { agent_trace_text(w, "queued_user", queued_user, strlen(queued_user)); ds4_chat_append_message(w->engine, &w->transcript, "user", queued_user); pthread_mutex_lock(&w->mu); w->user_activity = true; w->session_dirty = true; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } free(queued_user); } } static void worker_request_save(agent_worker *w) { pthread_mutex_lock(&w->mu); w->save_requested = true; pthread_cond_signal(&w->cond); agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } static void worker_request_compact(agent_worker *w) { pthread_mutex_lock(&w->mu); w->compact_requested = true; pthread_cond_signal(&w->cond); agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } static void worker_request_power(agent_worker *w, int power) { pthread_mutex_lock(&w->mu); w->requested_power = power; w->power_requested = true; w->status.power_percent = power; pthread_cond_signal(&w->cond); agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } static bool worker_take_save_requested(agent_worker *w) { pthread_mutex_lock(&w->mu); bool requested = w->save_requested; w->save_requested = false; pthread_mutex_unlock(&w->mu); return requested; } static bool worker_take_compact_requested(agent_worker *w) { pthread_mutex_lock(&w->mu); bool requested = w->compact_requested; w->compact_requested = false; pthread_mutex_unlock(&w->mu); return requested; } static bool worker_take_power_requested(agent_worker *w, int *power) { pthread_mutex_lock(&w->mu); bool requested = w->power_requested; if (requested) { if (power) *power = w->requested_power; w->power_requested = false; } pthread_mutex_unlock(&w->mu); return requested; } static void worker_apply_pending_power(agent_worker *w) { int power = 0; if (!worker_take_power_requested(w, &power)) return; if (ds4_session_set_power(w->session, power) != 0) { agent_publishf(w, "\npower change failed\n"); return; } pthread_mutex_lock(&w->mu); w->cfg->engine.power_percent = power; w->status.power_percent = power; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } static void worker_run_deferred_save(agent_worker *w) { if (!worker_take_save_requested(w)) return; agent_set_status(w, AGENT_WORKER_SAVING); char err[160] = {0}; char sha[41]; int tokens = 0; if (agent_worker_save_session_now(w, sha, &tokens, err, sizeof(err))) agent_publishf(w, "\nsaved session %.8s (%d tokens)\n", sha, tokens); else agent_publishf(w, "\nsave failed: %s\n", err[0] ? err : "unknown error"); agent_set_status(w, AGENT_WORKER_IDLE); } static void worker_run_deferred_compact(agent_worker *w) { if (!worker_take_compact_requested(w)) return; if (!agent_worker_has_user_session(w)) { agent_publishf(w, "\ncompact skipped: nothing to compact\n"); return; } int before = w->transcript.len; char err[160] = {0}; if (agent_worker_compact(w, "user requested compaction", err, sizeof(err))) { if (w->transcript.len != before) { pthread_mutex_lock(&w->mu); w->session_dirty = true; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); } else { agent_publishf(w, "\ncompact skipped: nothing to compact\n"); } agent_set_status(w, AGENT_WORKER_IDLE); } else { if (agent_err_is_interrupted(err)) { worker_clear_interrupt(w); agent_set_status(w, AGENT_WORKER_IDLE); return; } agent_set_error(w, err[0] ? err : "context compaction failed"); } } /* Worker thread entry point. The UI thread submits plain user text; this * thread owns all DS4 session mutation, tool execution, and compaction. */ static void *worker_main(void *arg) { agent_worker *w = arg; agent_trace(w, "agent worker start ctx=%d backend=%s model=%s trace=%s", w->cfg->gen.ctx_size, ds4_backend_name(w->cfg->engine.backend), w->cfg->engine.model_path ? w->cfg->engine.model_path : "", w->cfg->gen.trace_path ? w->cfg->gen.trace_path : ""); char init_err[160] = {0}; if (!agent_worker_wait_distributed_route(w, init_err, sizeof(init_err)) || !agent_worker_reset_to_sysprompt(w, init_err, sizeof(init_err))) { agent_set_error(w, init_err[0] ? init_err : "failed to initialize system prompt"); } agent_trace_tokens(w, "initial_system_prompt", &w->transcript, 0); pthread_mutex_lock(&w->mu); w->initialized = true; agent_wake_locked(w); pthread_mutex_unlock(&w->mu); while (true) { pthread_mutex_lock(&w->mu); while (!w->stop && !w->cmd_text && !w->save_requested && !w->compact_requested && !w->power_requested) pthread_cond_wait(&w->cond, &w->mu); if (w->stop) { pthread_mutex_unlock(&w->mu); break; } if (w->power_requested) { pthread_mutex_unlock(&w->mu); worker_apply_pending_power(w); continue; } if (!w->cmd_text && w->save_requested) { pthread_mutex_unlock(&w->mu); worker_run_deferred_save(w); continue; } if (!w->cmd_text && w->compact_requested) { pthread_mutex_unlock(&w->mu); worker_run_deferred_compact(w); continue; } char *cmd = w->cmd_text; w->cmd_text = NULL; pthread_mutex_unlock(&w->mu); worker_run_turn(w, cmd); free(cmd); worker_apply_pending_power(w); worker_run_deferred_compact(w); worker_run_deferred_save(w); } agent_set_status(w, AGENT_WORKER_STOPPED); return NULL; } /* ============================================================================ * Worker/UI Synchronization Helpers * ============================================================================ */ static int set_nonblock(int fd, bool on, int *old_flags) { int flags = fcntl(fd, F_GETFL, 0); if (flags < 0) return -1; if (old_flags) *old_flags = flags; int next = on ? (flags | O_NONBLOCK) : (flags & ~O_NONBLOCK); return fcntl(fd, F_SETFL, next); } /* Check and clear the raw_mode_needs_restore flag under the worker mutex. * Returns true if the UI thread should verify/reapply linenoise raw mode. */ static bool worker_check_raw_mode_restore(agent_worker *w) { bool needs = false; pthread_mutex_lock(&w->mu); if (w->raw_mode_needs_restore) { w->raw_mode_needs_restore = false; needs = true; } pthread_mutex_unlock(&w->mu); return needs; } static void drain_wake_fd(int fd) { char buf[128]; for (;;) { ssize_t n = read(fd, buf, sizeof(buf)); if (n > 0) continue; if (n < 0 && errno == EINTR) continue; break; } } /* Submit one user turn if the worker is idle. Busy submissions are rejected so * the UI can keep the typed text editable instead of silently queueing it. */ static bool worker_submit(agent_worker *w, const char *text) { pthread_mutex_lock(&w->mu); bool ok = w->initialized && w->status.state == AGENT_WORKER_IDLE && !w->cmd_text; if (ok) { w->cmd_text = xstrdup(text); /* A submitted turn is no longer idle, even if the worker thread has * not yet reached its real prefill accounting. Non-interactive mode * depends on this to avoid exiting in the small handoff window between * accepting stdin and starting generation. */ w->status.state = AGENT_WORKER_PREFILL; w->status.prefill_done = 0; w->status.prefill_total = 0; w->status.prefill_label = agent_next_prefill_label(); w->status.prefill_tps = 0.0; w->status.generated = 0; w->status.gen_tps = 0.0; w->status.greedy_sampling = false; pthread_cond_signal(&w->cond); } pthread_mutex_unlock(&w->mu); return ok; } static int worker_status_power_locked(agent_worker *w) { if (w->power_requested) return w->requested_power; int power = w->cfg->engine.power_percent; return power > 0 ? power : 100; } /* Request interruption at the next model/tool polling point. */ static void worker_interrupt(agent_worker *w) { pthread_mutex_lock(&w->mu); w->interrupt = true; if (w->cfg && w->cfg->engine.distributed.role == DS4_DISTRIBUTED_COORDINATOR && (w->status.state == AGENT_WORKER_PREFILL || w->status.state == AGENT_WORKER_GENERATING || w->status.state == AGENT_WORKER_COMPACTING)) { w->status.state = AGENT_WORKER_DRAINING; w->status.prefill_tps = 0.0; w->status.greedy_sampling = false; agent_wake_locked(w); } pthread_mutex_unlock(&w->mu); } /* Stop the worker thread. */ static void worker_stop(agent_worker *w) { pthread_mutex_lock(&w->mu); w->stop = true; pthread_cond_signal(&w->cond); pthread_mutex_unlock(&w->mu); } /* The UI thread consumes output in batches. Taking ownership of w->out under * the mutex keeps terminal writes outside the lock while preserving order. */ static void worker_consume(agent_worker *w, char **out, size_t *out_len, agent_status *status) { pthread_mutex_lock(&w->mu); if (out) { *out = w->out; *out_len = w->out_len; w->out = NULL; w->out_len = 0; w->out_cap = 0; } w->status.ctx_used = w->transcript.len; w->status.ctx_size = w->cfg->gen.ctx_size; w->status.power_percent = worker_status_power_locked(w); if (status) *status = w->status; w->wake_pending = false; pthread_mutex_unlock(&w->mu); } static void worker_get_status(agent_worker *w, agent_status *status) { pthread_mutex_lock(&w->mu); w->status.ctx_used = w->transcript.len; w->status.ctx_size = w->cfg->gen.ctx_size; w->status.power_percent = worker_status_power_locked(w); *status = w->status; pthread_mutex_unlock(&w->mu); } static bool worker_is_idle(agent_worker *w) { pthread_mutex_lock(&w->mu); bool idle = w->initialized && (w->status.state == AGENT_WORKER_IDLE || w->status.state == AGENT_WORKER_ERROR); pthread_mutex_unlock(&w->mu); return idle; } static bool worker_is_initialized(agent_worker *w, agent_status *status) { pthread_mutex_lock(&w->mu); w->status.ctx_used = w->transcript.len; w->status.ctx_size = w->cfg->gen.ctx_size; w->status.power_percent = worker_status_power_locked(w); if (status) *status = w->status; bool initialized = w->initialized; pthread_mutex_unlock(&w->mu); return initialized; } static bool stdout_is_tty(void) { return isatty(STDOUT_FILENO) != 0; } static char *agent_format_user_prompt_echo(const char *text) { agent_buf b = {0}; if (stdout_is_tty()) { agent_buf_puts(&b, "\x1b[1;91m*\x1b[1;97m "); agent_buf_puts(&b, text); agent_buf_puts(&b, "\x1b[0m\n\n"); } else { agent_buf_puts(&b, "* "); agent_buf_puts(&b, text); agent_buf_puts(&b, "\n\n"); } return agent_buf_take(&b); } static void agent_echo_user_prompt(const char *text) { char *msg = agent_format_user_prompt_echo(text); printf("%s", msg); fflush(stdout); free(msg); } /* ============================================================================ * Terminal Prompt, Status Footer, And Async Output Rendering * ============================================================================ */ static void agent_format_ctx_size(int ctx_size, char *buf, size_t len); #define AGENT_INPUT_INITIAL_BUFLEN 4096 #define AGENT_INPUT_MAX_BUFLEN (1024*1024) #define AGENT_STATUS_STYLE_START "\x1b[48;5;238;38;5;252m" #define AGENT_STATUS_STYLE_END "\x1b[0m" #define AGENT_STATUS_BAR_FILL "\x1b[48;5;238;38;5;201;1m" #define AGENT_QUEUE_STYLE "\x1b[38;5;87;1m" #define AGENT_STATUS_REDRAW_INTERVAL_SEC 0.20 #define AGENT_PROGRESS_BAR_WIDTH 32 #define AGENT_PROGRESS_BAR_MAX_BYTES 256 static void agent_progress_append(char *buf, size_t len, size_t *pos, const char *s) { if (len == 0 || *pos >= len - 1) return; size_t avail = len - *pos; int n = snprintf(buf + *pos, avail, "%s", s); if (n <= 0) return; if ((size_t)n >= avail) *pos = len - 1; else *pos += (size_t)n; } static void build_prompt_text(const agent_status *st, char *buf, size_t len) { (void)st; snprintf(buf, len, "ds4-agent> "); } static void agent_progress_bar(int done, int total, double tps, char *buf, size_t len, bool color) { if (len == 0) return; if (total <= 0) total = 1; if (done < 0) done = 0; if (done > total) done = total; int filled = (int)(((long long)done * AGENT_PROGRESS_BAR_WIDTH) / total); if (filled < 0) filled = 0; if (filled > AGENT_PROGRESS_BAR_WIDTH) filled = AGENT_PROGRESS_BAR_WIDTH; if (color && filled == 0 && done < total) filled = 1; char rate[32] = {0}; size_t rate_len = 0; if (tps > 0.0 && filled < AGENT_PROGRESS_BAR_WIDTH) { snprintf(rate, sizeof(rate), " %.0ft/s", tps); rate_len = strlen(rate); } size_t pos = 0; agent_progress_append(buf, len, &pos, "["); if (color) agent_progress_append(buf, len, &pos, AGENT_STATUS_BAR_FILL); for (int i = 0; i < AGENT_PROGRESS_BAR_WIDTH && pos + 1 < len; i++) { if (color && i == filled) { agent_progress_append(buf, len, &pos, AGENT_STATUS_STYLE_START); } if (i >= filled && rate_len > 0 && (size_t)(i - filled) < rate_len) { char ch[2] = {rate[i - filled], '\0'}; agent_progress_append(buf, len, &pos, ch); } else { agent_progress_append(buf, len, &pos, i < filled ? "▶" : "·"); } } if (color) agent_progress_append(buf, len, &pos, AGENT_STATUS_STYLE_START); agent_progress_append(buf, len, &pos, "]"); buf[pos < len ? pos : len - 1] = '\0'; } static void agent_power_status_suffix(const agent_status *st, char *buf, size_t len) { if (len == 0) return; if (st->power_percent > 0 && st->power_percent < 100) snprintf(buf, len, " | ⚡ %d%%", st->power_percent); else buf[0] = '\0'; } static unsigned agent_next_prefill_label(void) { static unsigned next; return next++; } /* Keep each prefill operation on a single playful label so the footer does not * visually churn while progress updates stream in. */ static const char *agent_prefill_label(const agent_status *st) { static const char *labels[] = { "reading", "absorbing", "studying", "gathering", "crunching", "scrutinizing", }; size_t n = sizeof(labels) / sizeof(labels[0]); return labels[(st ? st->prefill_label : 0u) % n]; } /* Build the one-line footer shown below the prompt. It is intentionally compact * because linenoise redraws it on every progress update. */ static void build_status_text(const agent_status *st, char *buf, size_t len) { char used[32], total_ctx[32]; char power[32]; agent_format_ctx_size(st->ctx_used, used, sizeof(used)); agent_format_ctx_size(st->ctx_size, total_ctx, sizeof(total_ctx)); agent_power_status_suffix(st, power, sizeof(power)); switch (st->state) { case AGENT_WORKER_PREFILL: { int done = st->prefill_done; int total = st->prefill_total > 0 ? st->prefill_total : 1; if (done > total) done = total; double pct = 100.0 * (double)done / (double)total; char bar[AGENT_PROGRESS_BAR_MAX_BYTES]; agent_progress_bar(done, total, st->prefill_tps, bar, sizeof(bar), stdout_is_tty()); snprintf(buf, len, "ctx %s/%s | %s %s %d/%d %.1f%%%s", used, total_ctx, agent_prefill_label(st), bar, done, total, pct, power); break; } case AGENT_WORKER_GENERATING: snprintf(buf, len, "ctx %s/%s | generation %d tokens%s %.1f t/s%s", used, total_ctx, st->generated, st->greedy_sampling ? " ❄️" : "", st->gen_tps, power); break; case AGENT_WORKER_COMPACTING: snprintf(buf, len, "ctx %s/%s | COMPACTING summary %d tokens %.1f t/s%s", used, total_ctx, st->generated, st->gen_tps, power); break; case AGENT_WORKER_DRAINING: snprintf(buf, len, "ctx %s/%s | stopping after distributed cluster drains%s", used, total_ctx, power); break; case AGENT_WORKER_SAVING: snprintf(buf, len, "ctx %s/%s | saving session%s", used, total_ctx, power); break; case AGENT_WORKER_ERROR: snprintf(buf, len, "ctx %s/%s | error: %s%s", used, total_ctx, st->error[0] ? st->error : "unknown error", power); break; case AGENT_WORKER_STOPPED: snprintf(buf, len, "ctx %s/%s | interrupted%s", used, total_ctx, power); break; default: snprintf(buf, len, "ctx %s/%s | idle%s", used, total_ctx, power); break; } } typedef struct { char **v; size_t len; size_t cap; } agent_prompt_queue; static void agent_prompt_queue_push(agent_prompt_queue *q, const char *text) { if (q->len == q->cap) { q->cap = q->cap ? q->cap * 2 : 4; q->v = xrealloc(q->v, q->cap * sizeof(q->v[0])); } q->v[q->len++] = xstrdup(text ? text : ""); } static char *agent_prompt_queue_pop(agent_prompt_queue *q) { if (!q->len) return NULL; char *text = q->v[0]; memmove(q->v, q->v + 1, (q->len - 1) * sizeof(q->v[0])); q->len--; return text; } static void agent_prompt_queue_push_front(agent_prompt_queue *q, char *text) { if (q->len == q->cap) { q->cap = q->cap ? q->cap * 2 : 4; q->v = xrealloc(q->v, q->cap * sizeof(q->v[0])); } memmove(q->v + 1, q->v, q->len * sizeof(q->v[0])); q->v[0] = text; q->len++; } static char *agent_prompt_queue_take_all(agent_prompt_queue *q) { if (!q->len) return NULL; if (q->len == 1) return agent_prompt_queue_pop(q); agent_buf b = {0}; for (size_t i = 0; i < q->len; i++) { char hdr[64]; if (i) agent_buf_puts(&b, "\n\n"); snprintf(hdr, sizeof(hdr), "Queued user message %zu:\n", i + 1); agent_buf_puts(&b, hdr); agent_buf_puts(&b, q->v[i]); free(q->v[i]); } q->len = 0; return agent_buf_take(&b); } static char *agent_prompt_queue_take_all_echo(agent_prompt_queue *q) { if (!q->len) return NULL; agent_buf b = {0}; for (size_t i = 0; i < q->len; i++) { char *echo = agent_format_user_prompt_echo(q->v[i]); agent_buf_puts(&b, echo); free(echo); } return agent_buf_take(&b); } static const char *agent_prompt_queue_peek(const agent_prompt_queue *q) { return q->len ? q->v[0] : NULL; } static void agent_prompt_queue_free(agent_prompt_queue *q) { for (size_t i = 0; i < q->len; i++) free(q->v[i]); free(q->v); memset(q, 0, sizeof(*q)); } static bool agent_footer_is_multiline(const char *status) { return status && strchr(status, '\n'); } /* Build the editable footer. With queued prompts, the footer becomes multiple * rows: a compact queue preview first, then the normal status row. */ static void build_footer_text(const agent_status *st, const agent_prompt_queue *queue, int cols, char *buf, size_t len) { char status[512]; build_status_text(st, status, sizeof(status)); if (!queue || !queue->len) { snprintf(buf, len, "%s", status); return; } const char *queued = agent_prompt_queue_peek(queue); if (cols < 40) cols = 40; int max_rows = 3; size_t budget = (size_t)cols * (size_t)max_rows; const char *plain_suffix = " (ctrl+x to edit, ESC to send ASAP)"; size_t queued_len = strlen(queued); char more_suffix[160]; const char *suffix = plain_suffix; size_t take = queued_len; if (queued_len + strlen(plain_suffix) > budget) { size_t reserve = 72; take = budget > reserve ? budget - reserve : budget / 2; snprintf(more_suffix, sizeof(more_suffix), "... %zu characters more ..., (ctrl+x to edit, ESC to send ASAP)", queued_len - take); suffix = more_suffix; } agent_buf msg = {0}; agent_buf_puts(&msg, "queued: "); for (size_t i = 0; i < take; i++) { char c = queued[i]; if (c == '\n' || c == '\r' || c == '\t') c = ' '; agent_buf_append(&msg, &c, 1); } agent_buf_puts(&msg, suffix); char *preview = agent_buf_take(&msg); agent_buf out = {0}; size_t pos = 0, preview_len = strlen(preview); for (int row = 0; row < max_rows && pos < preview_len; row++) { if (row) agent_buf_puts(&out, "\n"); if (stdout_is_tty()) agent_buf_puts(&out, AGENT_QUEUE_STYLE); size_t part = preview_len - pos; if (part > (size_t)cols) part = (size_t)cols; agent_buf_append(&out, preview + pos, part); if (stdout_is_tty()) agent_buf_puts(&out, "\x1b[0m"); pos += part; } agent_buf_puts(&out, "\n"); if (stdout_is_tty()) agent_buf_puts(&out, AGENT_STATUS_STYLE_START); agent_buf_puts(&out, status); snprintf(buf, len, "%s", out.ptr ? out.ptr : ""); free(preview); free(out.ptr); } typedef struct { struct linenoiseState edit; char *input; char prompt[160]; char status[4096]; int old_stdin_flags; bool active; bool hidden; bool output_line_open; bool prompt_below_output; int output_col; bool scroll_region; int term_rows; int term_cols; int output_bottom; int prompt_row; int reserved_rows; bool output_cursor_saved; bool output_at_scroll_boundary; double last_prompt_redraw_time; char cpr_buf[32]; size_t cpr_len; bool paste_open; bool paste_start_pending; char paste_tail[6]; size_t paste_tail_len; } agent_editor; static void editor_queue_bytes(agent_editor *ed, const char *buf, size_t len); static void editor_hide(agent_editor *ed); static void editor_show(agent_editor *ed); typedef enum { CPR_INVALID, CPR_PARTIAL, CPR_COMPLETE, } cpr_state; /* Classify a possible terminal cursor-position reply (ESC[row;colR). User * keystrokes can arrive interleaved with these replies, so we only swallow bytes * when they are definitely part of a complete CPR sequence. */ static cpr_state cpr_candidate_state(const char *buf, size_t len) { if (len == 0) return CPR_PARTIAL; if ((unsigned char)buf[0] != 0x1b) return CPR_INVALID; if (len == 1) return CPR_PARTIAL; if (buf[1] != '[') return CPR_INVALID; if (len == 2) return CPR_PARTIAL; size_t p = 2; if (buf[p] < '0' || buf[p] > '9') return CPR_INVALID; while (p < len && buf[p] >= '0' && buf[p] <= '9') p++; if (p == len) return CPR_PARTIAL; if (buf[p++] != ';') return CPR_INVALID; if (p == len) return CPR_PARTIAL; if (buf[p] < '0' || buf[p] > '9') return CPR_INVALID; while (p < len && buf[p] >= '0' && buf[p] <= '9') p++; if (p == len) return CPR_PARTIAL; return p + 1 == len && buf[p] == 'R' ? CPR_COMPLETE : CPR_INVALID; } static void editor_flush_cpr_candidate(agent_editor *ed) { if (!ed->cpr_len) return; linenoiseEditQueueInput(&ed->edit, ed->cpr_buf, ed->cpr_len); ed->cpr_len = 0; } static bool agent_tail_ends_with(const char *tail, size_t tail_len, const char *seq, size_t seq_len) { return tail_len >= seq_len && memcmp(tail + tail_len - seq_len, seq, seq_len) == 0; } static bool agent_tail_has_seq_prefix(const char *tail, size_t tail_len, const char *seq, size_t seq_len) { size_t max = tail_len < seq_len - 1 ? tail_len : seq_len - 1; for (size_t n = max; n > 0; n--) { if (memcmp(tail + tail_len - n, seq, n) == 0) return true; } return false; } /* Track bracketed paste markers outside linenoise. The nonblocking event loop * may receive a paste in chunks; pausing linenoiseEditFeed() until ESC[201~ * arrives prevents pasted newlines from being interpreted as Enter. */ static void editor_track_bracketed_paste(agent_editor *ed, char c) { static const char start[] = "\x1b[200~"; static const char end[] = "\x1b[201~"; if (ed->paste_tail_len == sizeof(ed->paste_tail)) { memmove(ed->paste_tail, ed->paste_tail + 1, sizeof(ed->paste_tail) - 1); ed->paste_tail_len--; } ed->paste_tail[ed->paste_tail_len++] = c; /* The blocking linenoise() path waits inside linenoiseEditPaste() until it * sees ESC[201~. In the agent the outer event loop reads stdin in * non-blocking chunks; if we let linenoise start parsing ESC[200~ before * the closing marker has arrived, pasted newlines can be interpreted as * Enter. Keep feeding bytes into linenoise's queue, but don't call * linenoiseEditFeed() while the terminal paste envelope is still open. */ if (agent_tail_ends_with(ed->paste_tail, ed->paste_tail_len, start, sizeof(start) - 1)) { ed->paste_open = true; ed->paste_start_pending = false; } else if (agent_tail_ends_with(ed->paste_tail, ed->paste_tail_len, end, sizeof(end) - 1)) { ed->paste_open = false; ed->paste_start_pending = false; } else { ed->paste_start_pending = !ed->paste_open && agent_tail_has_seq_prefix(ed->paste_tail, ed->paste_tail_len, start, sizeof(start) - 1); } } /* Separate late CPR replies from real user input before handing bytes to * linenoise. */ static void editor_filter_input_byte(agent_editor *ed, char c) { if (ed->cpr_len || (unsigned char)c == 0x1b) { if (ed->cpr_len == sizeof(ed->cpr_buf)) { editor_flush_cpr_candidate(ed); } ed->cpr_buf[ed->cpr_len++] = c; cpr_state st = cpr_candidate_state(ed->cpr_buf, ed->cpr_len); if (st == CPR_COMPLETE) { ed->cpr_len = 0; /* Late terminal cursor report: discard it. */ } else if (st == CPR_INVALID) { editor_flush_cpr_candidate(ed); } return; } linenoiseEditQueueInput(&ed->edit, &c, 1); } /* Queue raw terminal bytes into linenoise while preserving paste envelopes and * filtering cursor-position replies. */ static void editor_queue_bytes(agent_editor *ed, const char *buf, size_t len) { for (size_t i = 0; i < len; i++) { editor_track_bracketed_paste(ed, buf[i]); editor_filter_input_byte(ed, buf[i]); } } /* Drain stdin in nonblocking mode. The outer event loop decides when queued * bytes are fed to linenoiseEditFeed(). */ static void editor_read_stdin(agent_editor *ed) { char buf[256]; for (;;) { ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); if (n > 0) { editor_queue_bytes(ed, buf, (size_t)n); continue; } if (n < 0 && errno == EINTR) continue; break; } } static bool editor_take_queued_byte(agent_editor *ed, unsigned char byte) { struct linenoiseState *l = &ed->edit; for (size_t i = l->queued_input_pos; i < l->queued_input_len; i++) { if ((unsigned char)l->queued_input[i] != byte) continue; memmove(l->queued_input + i, l->queued_input + i + 1, l->queued_input_len - i - 1); l->queued_input_len--; if (l->queued_input_pos > l->queued_input_len) l->queued_input_pos = l->queued_input_len; return true; } return false; } static bool editor_take_bare_escape(agent_editor *ed) { if (ed->cpr_len == 1 && (unsigned char)ed->cpr_buf[0] == 0x1b) { ed->cpr_len = 0; return true; } return false; } static void editor_replace_input(agent_editor *ed, const char *text) { if (ed->hidden) editor_show(ed); linenoiseEditClear(&ed->edit); if (text && text[0]) linenoiseEditInsert(&ed->edit, text, strlen(text)); } /* Fallback cursor tracking for terminals that do not answer CPR quickly. It is * intentionally approximate for wide Unicode; the CPR path handles exact * positioning in normal interactive terminals. */ static void editor_note_output(agent_editor *ed, const char *text, size_t len) { int cols = ed->edit.cols > 0 ? (int)ed->edit.cols : 80; for (size_t i = 0; i < len; i++) { size_t start = i; unsigned char c = (unsigned char)text[i]; if (c == 0x1b && i + 1 < len && text[i + 1] == '[') { (void)start; i += 2; while (i < len) { unsigned char e = (unsigned char)text[i]; if (e >= 0x40 && e <= 0x7e) break; i++; } continue; } if (c == '\n') { ed->output_col = 0; ed->output_line_open = false; continue; } if (c == '\r') { ed->output_col = 0; continue; } if (c == '\b') { if (ed->output_col > 0) ed->output_col--; continue; } int width = 1; if (c == '\t') { width = 8 - (ed->output_col & 7); } else if (c < 0x20 || c == 0x7f) { width = 0; } else if (c >= 0xc0) { while (i + 1 < len && (((unsigned char)text[i + 1]) & 0xc0) == 0x80) i++; } else if ((c & 0xc0) == 0x80) { width = 0; } if (width > 0) { ed->output_col = (ed->output_col + width) % cols; ed->output_line_open = true; } } } /* Normalize generated LF to CRLF for terminal output without changing the text * stored in the transcript. */ static void editor_write_terminal_text(const char *text, size_t len) { size_t start = 0; for (size_t i = 0; i < len; i++) { if (text[i] != '\n') continue; if (i > start) write_all(STDOUT_FILENO, text + start, i - start); write_all(STDOUT_FILENO, "\r\n", 2); start = i + 1; } if (start < len) write_all(STDOUT_FILENO, text + start, len - start); } /* Locate a CPR reply inside a mixed stdin buffer. Bytes before/after the reply * are user input and must be queued back into linenoise. */ static bool find_cpr_reply(const char *buf, size_t len, size_t *start, size_t *end, int *row, int *col) { for (size_t i = 0; i + 5 < len; i++) { if ((unsigned char)buf[i] != 0x1b || buf[i + 1] != '[') continue; size_t p = i + 2; int r = 0, c = 0; if (p >= len || buf[p] < '0' || buf[p] > '9') continue; while (p < len && buf[p] >= '0' && buf[p] <= '9') { r = r * 10 + (buf[p++] - '0'); } if (p >= len || buf[p++] != ';') continue; if (p >= len || buf[p] < '0' || buf[p] > '9') continue; while (p < len && buf[p] >= '0' && buf[p] <= '9') { c = c * 10 + (buf[p++] - '0'); } if (p >= len || buf[p] != 'R') continue; *start = i; *end = p + 1; *row = r; *col = c; return true; } return false; } /* Ask the terminal for the cursor column after writing model output. Any user * bytes read while waiting for the CPR reply are queued back into linenoise so * typing during generation is not lost. */ static bool editor_query_cursor(agent_editor *ed, int *col_out) { if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) return false; char buf[512]; size_t len = 0, start = 0, end = 0; int row = 0, col = 0; write_all(STDOUT_FILENO, "\x1b[6n", 4); for (int attempt = 0; attempt < 8; attempt++) { struct pollfd pfd = {.fd = STDIN_FILENO, .events = POLLIN}; int rc = poll(&pfd, 1, 5); if (rc < 0 && errno == EINTR) continue; if (rc <= 0) continue; for (;;) { ssize_t n = read(STDIN_FILENO, buf + len, sizeof(buf) - len); if (n > 0) { len += (size_t)n; if (find_cpr_reply(buf, len, &start, &end, &row, &col)) { if (start) editor_queue_bytes(ed, buf, start); if (end < len) editor_queue_bytes(ed, buf + end, len - end); (void)row; *col_out = col; return col > 0; } if (len == sizeof(buf)) break; continue; } if (n < 0 && errno == EINTR) continue; break; } } if (len) editor_queue_bytes(ed, buf, len); return false; } static void editor_move_to_output_cursor(agent_editor *ed) { char seq[64]; write_all(STDOUT_FILENO, "\x1b[1A", 4); int n = snprintf(seq, sizeof(seq), "\x1b[%dG", ed->output_col + 1); if (n > 0) write_all(STDOUT_FILENO, seq, (size_t)n); } static bool editor_get_terminal_size(int *rows, int *cols) { struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != 0) return false; if (ws.ws_row < 1 || ws.ws_col < 1) return false; *rows = ws.ws_row; *cols = ws.ws_col; return true; } static void editor_csi_cursor(int row, int col) { char seq[64]; int n = snprintf(seq, sizeof(seq), "\x1b[%d;%dH", row, col); if (n > 0) write_all(STDOUT_FILENO, seq, (size_t)n); } static void editor_save_output_cursor(agent_editor *ed) { if (!ed->scroll_region) return; write_all(STDOUT_FILENO, "\0337", 2); ed->output_cursor_saved = true; } static void editor_restore_output_cursor(agent_editor *ed) { if (!ed->scroll_region) return; if (ed->output_cursor_saved) { write_all(STDOUT_FILENO, "\0338", 2); } else { editor_csi_cursor(ed->output_bottom, 1); } } static void editor_move_to_prompt_row(agent_editor *ed) { if (!ed->scroll_region) return; editor_csi_cursor(ed->prompt_row, 1); } static void editor_move_to_prompt_cursor(agent_editor *ed) { if (!ed->scroll_region) return; if (ed->edit.screen_cursor_row > 0 && ed->edit.screen_cursor_col > 0) { editor_csi_cursor(ed->edit.screen_cursor_row, ed->edit.screen_cursor_col); } else { editor_move_to_prompt_row(ed); } } static void editor_clear_row(int row) { editor_csi_cursor(row, 1); write_all(STDOUT_FILENO, "\r\x1b[0K", 5); } static void editor_clear_prompt_region(agent_editor *ed) { if (!ed->scroll_region) return; for (int row = ed->prompt_row; row <= ed->term_rows; row++) editor_clear_row(row); /* In scroll-region mode ds4-agent owns the absolute prompt/status rows. * Clearing them directly is more reliable than asking linenoise to clean * relative to whatever cursor position the last worker/status transition * left behind. Reset linenoise's render bookkeeping so the next show is a * pure write into the reserved rows. */ ed->edit.oldrows = 0; ed->edit.oldstatusrows = 0; ed->edit.oldrpos = 1; ed->edit.oldpos = ed->edit.pos; } static void editor_set_scroll_margin(int bottom) { char seq[96]; int n = snprintf(seq, sizeof(seq), "\x1b[1;%dr", bottom); if (n > 0) write_all(STDOUT_FILENO, seq, (size_t)n); } static void editor_scroll_output_up(int bottom, int lines) { if (lines <= 0) return; editor_set_scroll_margin(bottom); editor_csi_cursor(bottom, 1); for (int i = 0; i < lines; i++) write_all(STDOUT_FILENO, "\n", 1); } static bool editor_set_scroll_layout(agent_editor *ed, int reserved_rows, bool allow_shrink, bool scroll_on_grow) { if (!ed->scroll_region) return false; int rows = 0, cols = 0; if (!editor_get_terminal_size(&rows, &cols)) return false; if (rows < 8 || cols < 20) return false; if (reserved_rows < 2) reserved_rows = 2; if (reserved_rows > rows - 2) reserved_rows = rows - 2; if (!allow_shrink && ed->reserved_rows > 0 && ed->term_rows == rows && ed->term_cols == cols && reserved_rows < ed->reserved_rows) { reserved_rows = ed->reserved_rows; } int output_bottom = rows - reserved_rows; int prompt_row = output_bottom + 1; bool changed = ed->term_rows != rows || ed->term_cols != cols || ed->output_bottom != output_bottom || ed->prompt_row != prompt_row || ed->reserved_rows != reserved_rows; if (!changed) return true; /* If the prompt grows, rows that were output rows become prompt rows. Do * not simply clear them: first scroll the old output region upward by the * number of newly reserved rows, exactly as if the model had printed more * lines. If the prompt shrinks, no output is restored; the output region * simply grows downward and the prompt/status block remains bottom * anchored. */ bool scrolled_output = false; if (scroll_on_grow && ed->term_rows == rows && ed->term_cols == cols && ed->output_bottom > 0 && output_bottom < ed->output_bottom) { editor_scroll_output_up(ed->output_bottom, ed->output_bottom - output_bottom); scrolled_output = true; } editor_set_scroll_margin(output_bottom); ed->term_rows = rows; ed->term_cols = cols; ed->output_bottom = output_bottom; ed->prompt_row = prompt_row; ed->reserved_rows = reserved_rows; ed->output_cursor_saved = false; ed->output_at_scroll_boundary = scrolled_output; for (int row = prompt_row; row <= rows; row++) editor_clear_row(row); /* If the prompt grew while generated output was in the middle of a line, * the scroll above moved that partial line up with its column intact. * Preserve that column when saving the new output cursor; otherwise the * next token resumes at column 1 and overwrites the line it was extending. */ int output_col = ed->output_line_open ? ed->output_col + 1 : 1; if (output_col < 1) output_col = 1; if (output_col > cols) output_col = cols; editor_csi_cursor(output_bottom, output_col); editor_save_output_cursor(ed); editor_move_to_prompt_row(ed); return true; } static int editor_linenoise_layout_changed(struct linenoiseState *l, size_t prompt_rows, size_t status_rows, void *privdata) { (void)l; agent_editor *ed = privdata; if (!ed || !ed->scroll_region) return 0; if (prompt_rows < 1) prompt_rows = 1; int reserved = (int)(prompt_rows + status_rows); if (!editor_set_scroll_layout(ed, reserved, true, true)) return 0; return ed->prompt_row; } /* Keep generated output inside a scroll region that excludes the live prompt * and status footer. This lets terminals scroll model/tool output naturally * without rewriting the prompt on every streamed token, which is especially * important over SSH where full redraws are visibly expensive. */ static bool editor_configure_scroll_region(agent_editor *ed) { if (ed->scroll_region) return true; if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) return false; int rows = 0, cols = 0; if (!editor_get_terminal_size(&rows, &cols)) return false; if (rows < 8 || cols < 20) return false; ed->term_rows = 0; ed->term_cols = 0; ed->output_bottom = 0; ed->prompt_row = 0; ed->reserved_rows = 0; ed->output_cursor_saved = false; ed->output_at_scroll_boundary = false; ed->scroll_region = true; if (!editor_set_scroll_layout(ed, 2, true, false)) return false; /* The agent prints backend startup lines before the editor exists. Once * the scroll region is installed, create an append line at the bottom of * that region instead of guessing that the old terminal cursor was already * there. Without this first scroll, the first agent/model output can * overwrite the last visible startup line. */ editor_scroll_output_up(ed->output_bottom, 1); ed->output_cursor_saved = false; editor_csi_cursor(ed->output_bottom, 1); editor_save_output_cursor(ed); editor_move_to_prompt_row(ed); return true; } static void editor_restore_terminal_layout(agent_editor *ed) { if (!ed->scroll_region) return; write_all(STDOUT_FILENO, "\x1b[0m", 4); write_all(STDOUT_FILENO, "\x1b[r", 3); editor_csi_cursor(ed->term_rows, 1); write_all(STDOUT_FILENO, "\r\x1b[0K\r\n", 7); ed->scroll_region = false; ed->output_cursor_saved = false; ed->term_rows = ed->term_cols = 0; ed->output_bottom = ed->prompt_row = 0; ed->reserved_rows = 0; ed->output_at_scroll_boundary = false; } /* Start linenoise in nonblocking mode and install the status footer. */ static int editor_start(agent_editor *ed, const char *prompt, const char *status, const char *initial) { char *input = xmalloc(AGENT_INPUT_INITIAL_BUFLEN); snprintf(ed->prompt, sizeof(ed->prompt), "%s", prompt); snprintf(ed->status, sizeof(ed->status), "%s", status ? status : ""); bool had_scroll_region = ed->scroll_region; bool use_scroll_region = editor_configure_scroll_region(ed); if (use_scroll_region) { if (had_scroll_region) editor_set_scroll_layout(ed, 2, true, false); editor_move_to_prompt_row(ed); } if (linenoiseEditStart(&ed->edit, STDIN_FILENO, STDOUT_FILENO, input, AGENT_INPUT_INITIAL_BUFLEN, ed->prompt) != 0) { editor_restore_terminal_layout(ed); free(input); return -1; } bool embedded_status = agent_footer_is_multiline(ed->status); const char *status_start = stdout_is_tty() && !embedded_status ? AGENT_STATUS_STYLE_START : ""; const char *status_end = stdout_is_tty() && ed->status[0] ? AGENT_STATUS_STYLE_END : ""; linenoiseEditSetStatus(&ed->edit, ed->status, status_start, status_end); linenoiseEditSetLayoutCallback(&ed->edit, editor_linenoise_layout_changed, ed); if (isatty(ed->edit.ifd) || getenv("LINENOISE_ASSUME_TTY")) { linenoiseHide(&ed->edit); linenoiseShow(&ed->edit); } ed->input = input; ed->edit.buflen_max = AGENT_INPUT_MAX_BUFLEN; ed->active = true; if (set_nonblock(STDIN_FILENO, true, &ed->old_stdin_flags) != 0) ed->old_stdin_flags = -1; if (initial && initial[0]) linenoiseEditInsert(&ed->edit, initial, strlen(initial)); ed->hidden = false; ed->output_line_open = false; ed->prompt_below_output = false; ed->output_col = 0; ed->cpr_len = 0; ed->paste_open = false; ed->paste_start_pending = false; ed->paste_tail_len = 0; return 0; } /* Stop the live editor and restore stdin flags. */ static void editor_stop(agent_editor *ed) { if (!ed->active) return; /* ds4-agent treats linenoise as a live input widget, not as persistent * command scrollback. Clear it before shutdown so submitting a line and * immediately reopening the editor does not leave the accepted * prompt+input duplicated above the fresh prompt. */ if (!ed->hidden && (isatty(ed->edit.ifd) || getenv("LINENOISE_ASSUME_TTY"))) editor_hide(ed); linenoiseEditStop(&ed->edit); if (ed->old_stdin_flags >= 0) fcntl(STDIN_FILENO, F_SETFL, ed->old_stdin_flags); free(ed->edit.buf); ed->input = NULL; ed->active = false; ed->hidden = false; ed->output_line_open = false; ed->prompt_below_output = false; ed->output_col = 0; ed->cpr_len = 0; ed->paste_open = false; ed->paste_start_pending = false; ed->paste_tail_len = 0; } /* Hide the live prompt before model output is written. In scroll-region mode * the output cursor was saved before the prompt was drawn, so restoring it is * enough to append more model/tool bytes without touching the prompt rows. */ static void editor_hide(agent_editor *ed) { if (!ed->active || ed->hidden) return; if (ed->scroll_region) { editor_clear_prompt_region(ed); editor_restore_output_cursor(ed); ed->hidden = true; return; } linenoiseHide(&ed->edit); if (ed->prompt_below_output) { editor_move_to_output_cursor(ed); ed->prompt_below_output = false; } ed->hidden = true; } /* Restore the live prompt after output. The primary path draws it in the * reserved bottom rows; the fallback path keeps the older one-row-below-output * trick for terminals where scroll regions are unavailable. */ static void editor_show(agent_editor *ed) { if (!ed->active || !ed->hidden) return; if (ed->scroll_region) { editor_save_output_cursor(ed); editor_move_to_prompt_row(ed); write_all(STDOUT_FILENO, "\x1b[0m", 4); linenoiseShow(&ed->edit); ed->hidden = false; return; } if (ed->output_line_open) { write_all(STDOUT_FILENO, "\r\n", 2); ed->prompt_below_output = true; } else { ed->prompt_below_output = false; } /* Model/tool output can leave SGR attributes active while it streams. * Redrawing linenoise always starts from normal attributes; tool rendering * re-emits its own color on the next streamed byte if it is still inside a * colored parameter. */ write_all(STDOUT_FILENO, "\x1b[0m", 4); linenoiseShow(&ed->edit); ed->hidden = false; } static void editor_update_prompt(agent_editor *ed, const char *prompt) { snprintf(ed->prompt, sizeof(ed->prompt), "%s", prompt); ed->edit.prompt = ed->prompt; ed->edit.plen = strlen(ed->prompt); } static void editor_update_status(agent_editor *ed, const char *status) { snprintf(ed->status, sizeof(ed->status), "%s", status ? status : ""); bool embedded_status = agent_footer_is_multiline(ed->status); const char *status_start = stdout_is_tty() && !embedded_status ? AGENT_STATUS_STYLE_START : ""; const char *status_end = stdout_is_tty() && ed->status[0] ? AGENT_STATUS_STYLE_END : ""; linenoiseEditSetStatus(&ed->edit, ed->status, status_start, status_end); } static void editor_set_prompt_status(agent_editor *ed, const char *prompt, const char *status) { bool prompt_changed = strcmp(ed->prompt, prompt) != 0; bool status_changed = strcmp(ed->status, status ? status : "") != 0; if (!ed->active || (!prompt_changed && !status_changed)) return; if (ed->hidden) { if (prompt_changed) editor_update_prompt(ed, prompt); if (status_changed) editor_update_status(ed, status); return; } editor_hide(ed); if (prompt_changed) editor_update_prompt(ed, prompt); if (status_changed) editor_update_status(ed, status); editor_show(ed); } static void editor_redraw_visible_prompt(agent_editor *ed) { if (!ed->active || !ed->scroll_region) return; editor_clear_prompt_region(ed); editor_move_to_prompt_row(ed); write_all(STDOUT_FILENO, "\x1b[0m", 4); linenoiseShow(&ed->edit); ed->last_prompt_redraw_time = now_sec(); } static bool editor_prompt_redraw_due(agent_editor *ed) { double now = now_sec(); if (ed->last_prompt_redraw_time <= 0.0 || now - ed->last_prompt_redraw_time >= AGENT_STATUS_REDRAW_INTERVAL_SEC) { return true; } return false; } static void editor_write_scroll_output_preserve_prompt(agent_editor *ed, const char *text, size_t len) { static const char sync_start[] = "\x1b[?2026h"; static const char sync_end[] = "\x1b[?2026l"; if (!len) return; write_all(STDOUT_FILENO, sync_start, sizeof(sync_start) - 1); editor_restore_output_cursor(ed); editor_write_terminal_text(text, len); editor_note_output(ed, text, len); editor_save_output_cursor(ed); write_all(STDOUT_FILENO, "\x1b[0m", 4); editor_move_to_prompt_cursor(ed); write_all(STDOUT_FILENO, sync_end, sizeof(sync_end) - 1); ed->output_at_scroll_boundary = true; } /* Serialize async model/tool output with linenoise. This is the central * terminal contract. In scroll-region mode the live prompt stays painted: * output is appended in the upper scroll area, then the cursor is returned to * linenoise's remembered prompt position. The fallback path still hides and * redraws because it has no protected prompt rows. */ static void editor_write_async(agent_editor *ed, const char *text, size_t len, const char *prompt, const char *status, bool force_show) { if (ed->scroll_region && ed->active && !ed->hidden && len) { bool prompt_changed = strcmp(ed->prompt, prompt) != 0; bool status_changed = strcmp(ed->status, status ? status : "") != 0; editor_write_scroll_output_preserve_prompt(ed, text, len); if (prompt_changed) editor_update_prompt(ed, prompt); if (status_changed) editor_update_status(ed, status); if ((force_show || editor_prompt_redraw_due(ed)) && (prompt_changed || status_changed)) { editor_redraw_visible_prompt(ed); } return; } editor_hide(ed); if (len) { editor_write_terminal_text(text, len); if (ed->scroll_region) ed->output_at_scroll_boundary = true; if (!ed->scroll_region) { if (text[len - 1] == '\n' || text[len - 1] == '\r') { ed->output_col = 0; ed->output_line_open = false; } else { int col = 0; if (editor_query_cursor(ed, &col)) { int cols = ed->edit.cols > 0 ? (int)ed->edit.cols : 80; ed->output_col = col > 0 ? col - 1 : 0; ed->output_line_open = true; if (ed->output_col + 1 >= cols) { write_all(STDOUT_FILENO, "\r\n", 2); ed->output_col = 0; } } else { editor_note_output(ed, text, len); } } } } if (ed->active) { editor_update_prompt(ed, prompt); editor_update_status(ed, status); /* In scroll-region mode this saves the current output cursor and * redraws linenoise in the fixed prompt rows. In fallback mode it may * put the prompt below an unfinished generated line. */ if (force_show || len) editor_show(ed); } } /* Ctrl+C while idle is an edit-cancel key, not an exit key. Clear the real * linenoise buffer so stale text cannot be submitted later, then leave a short * visible hint about the explicit EOF exit path. */ static void editor_cancel_input_with_hint(agent_editor *ed, const char *prompt, const char *status) { if (!ed->active) return; if (ed->hidden) editor_show(ed); linenoiseEditClear(&ed->edit); const char *msg = stdout_is_tty() ? "\x1b[1;33mpress Ctrl+D to exit\x1b[0m\n" : "press Ctrl+D to exit\n"; editor_write_async(ed, msg, strlen(msg), prompt, status, true); } static void runtime_help(void) { puts("Commands:"); puts(" /help Show this help."); puts(" /save Save the current session."); puts(" /compact Compact the current session context now."); puts(" /list List saved sessions."); puts(" /switch SHA Load a saved session and show recent history."); puts(" /del SHA Delete a saved session."); puts(" /strip SHA Strip KV payload; /switch rebuilds it by prefill."); puts(" /history [N] Show N recent user turns from the current session."); puts(" /power N Set GPU duty cycle percentage, 1..100."); puts(" /new Start a fresh session from the system prompt."); puts(" /quit, /exit Exit."); puts(" Ctrl+C Interrupt generation; clear edited text."); puts(" Enter Queue text while the agent is busy."); puts(" Ctrl+X Edit the first queued prompt."); puts(" ESC Interrupt and send queued prompt immediately."); puts(" Ctrl+D Exit from an empty prompt."); } static void agent_format_ctx_size(int ctx_size, char *buf, size_t len) { if (ctx_size >= 1000) { if (ctx_size % 1000 == 0) snprintf(buf, len, "%dk", ctx_size / 1000); else snprintf(buf, len, "%.1fk", (double)ctx_size / 1000.0); } else { snprintf(buf, len, "%d", ctx_size); } } static void agent_format_welcome_banner(const agent_config *cfg, char *buf, size_t len) { char ctx[32]; agent_format_ctx_size(cfg->gen.ctx_size, ctx, sizeof(ctx)); if (stdout_is_tty()) { snprintf(buf, len, "\x1b[1;97mDwarf\x1b[1;94mStar\x1b[0m 🐋 Agent, context %s tokens\n\n", ctx); } else { snprintf(buf, len, "DwarfStar Agent, context %s tokens\n\n", ctx); } } static void editor_write_welcome_banner(agent_editor *editor, const agent_config *cfg, const char *prompt, const char *statusline) { char banner[256]; agent_format_welcome_banner(cfg, banner, sizeof(banner)); editor_write_async(editor, banner, strlen(banner), prompt, statusline, true); } /* Initialize the worker, cache directory, sysprompt checkpoint path, trace file, * and model thread. After this returns, all DS4 session mutation happens on * the worker thread. */ static int agent_worker_init(agent_worker *w, ds4_engine *engine, agent_config *cfg) { memset(w, 0, sizeof(*w)); w->engine = engine; w->cfg = cfg; w->wake_fd[0] = -1; w->wake_fd[1] = -1; pthread_mutex_init(&w->mu, NULL); pthread_cond_init(&w->cond, NULL); w->status.state = AGENT_WORKER_IDLE; if (pipe(w->wake_fd) != 0) return -1; int old_flags; set_nonblock(w->wake_fd[0], true, &old_flags); set_nonblock(w->wake_fd[1], true, &old_flags); if (ds4_session_create(&w->session, engine, cfg->gen.ctx_size) != 0) { fprintf(stderr, "ds4-agent: session backend is required\n"); return -1; } w->cache_dir = agent_default_cache_dir(); if (!agent_mkdir_p(w->cache_dir)) { fprintf(stderr, "ds4-agent: failed to create %s: %s\n", w->cache_dir, strerror(errno)); return -1; } ds4_web_config web_cfg = { .home_dir = getenv("HOME"), .port = 9333, .confirm = agent_web_confirm, .confirm_privdata = w, .log = agent_web_log, .log_privdata = w, .cancel = agent_web_cancel, .cancel_privdata = w, }; w->web = ds4_web_create(&web_cfg); w->sysprompt_path = ds4_kvstore_path_join(w->cache_dir, "sysprompt.kv"); if (cfg->gen.trace_path && cfg->gen.trace_path[0]) { w->trace = fopen(cfg->gen.trace_path, "ab"); if (!w->trace) { fprintf(stderr, "ds4-agent: failed to open trace %s: %s\n", cfg->gen.trace_path, strerror(errno)); return -1; } } if (pthread_create(&w->thread, NULL, worker_main, w) != 0) return -1; return 0; } /* Shut down the worker and release owned resources, including any live bash * process groups. */ static void agent_worker_free(agent_worker *w) { worker_stop(w); if (w->thread) pthread_join(w->thread, NULL); agent_bash_jobs_free(w); ds4_web_free(w->web); ds4_session_free(w->session); ds4_tokens_free(&w->transcript); free(w->cache_dir); free(w->sysprompt_path); free(w->session_title); free(w->legacy_session_path_to_delete); free(w->queued_user_drain_text); if (w->wake_fd[0] >= 0) close(w->wake_fd[0]); if (w->wake_fd[1] >= 0) close(w->wake_fd[1]); if (w->trace) fclose(w->trace); free(w->cmd_text); free(w->out); pthread_cond_destroy(&w->cond); pthread_mutex_destroy(&w->mu); } typedef enum { AGENT_YES_NO_AUTO_NONE, AGENT_YES_NO_AUTO_NO, AGENT_YES_NO_AUTO_YES, } agent_yes_no_auto; typedef struct { int timeout_sec; agent_yes_no_auto timeout_answer; } agent_yes_no_options; static const char *agent_yes_no_auto_name(agent_yes_no_auto answer) { switch (answer) { case AGENT_YES_NO_AUTO_NO: return "no"; case AGENT_YES_NO_AUTO_YES: return "yes"; default: return ""; } } /* Shared y/n prompt. By default it blocks forever like the historical helper; * callers that cannot safely stall the agent can request an automatic answer * after timeout_sec seconds. */ static bool agent_prompt_yes_no_ex(const char *prompt, const agent_yes_no_options *opts, bool *timed_out) { char buf[32]; int timeout_sec = opts ? opts->timeout_sec : 0; agent_yes_no_auto auto_answer = opts ? opts->timeout_answer : AGENT_YES_NO_AUTO_NONE; bool use_timeout = timeout_sec > 0 && auto_answer != AGENT_YES_NO_AUTO_NONE; double deadline = use_timeout ? now_sec() + timeout_sec : 0.0; if (timed_out) *timed_out = false; for (;;) { printf("%s", prompt); if (use_timeout) { int rem = (int)(deadline - now_sec() + 0.999); if (rem < 0) rem = 0; printf("[auto-%s in %ds] ", agent_yes_no_auto_name(auto_answer), rem); } fflush(stdout); if (use_timeout) { double rem_sec = deadline - now_sec(); if (rem_sec <= 0.0) { if (timed_out) *timed_out = true; printf("\n"); return auto_answer == AGENT_YES_NO_AUTO_YES; } struct pollfd pfd = {.fd = STDIN_FILENO, .events = POLLIN}; int timeout_ms = (int)(rem_sec * 1000.0) + 1; int rc; do { rc = poll(&pfd, 1, timeout_ms); } while (rc < 0 && errno == EINTR); if (rc == 0) { if (timed_out) *timed_out = true; printf("\n"); return auto_answer == AGENT_YES_NO_AUTO_YES; } if (rc < 0) return false; } /* stdin may be in non-blocking mode (set by editor_start). * Temporarily switch to blocking so fgets can wait for input. */ int saved_flags = fcntl(STDIN_FILENO, F_GETFL, 0); if (saved_flags >= 0 && (saved_flags & O_NONBLOCK)) { fcntl(STDIN_FILENO, F_SETFL, saved_flags & ~O_NONBLOCK); } bool got_line = fgets(buf, sizeof(buf), stdin) != NULL; if (saved_flags >= 0 && (saved_flags & O_NONBLOCK)) { fcntl(STDIN_FILENO, F_SETFL, saved_flags); } if (!got_line) return false; char *p = buf; while (*p == ' ' || *p == '\t') p++; if (*p == 'y' || *p == 'Y') return true; if (*p == 'n' || *p == 'N') return false; } } static bool agent_prompt_yes_no(const char *prompt) { return agent_prompt_yes_no_ex(prompt, NULL, NULL); } /* Ask before discarding a dirty user session. Fresh sessions that contain only * the system prompt are deliberately ignored. */ static bool agent_maybe_save_before_leaving_session(agent_worker *w) { if (!agent_worker_needs_save(w)) return true; if (!agent_prompt_yes_no("Save current session? (y/n) ")) return true; char err[160] = {0}; if (agent_worker_save_session(w, err, sizeof(err))) return true; printf("save failed: %s\n", err); return agent_prompt_yes_no("Continue anyway? (y/n) "); } typedef enum { AGENT_EXIT_CANCEL, AGENT_EXIT_CLEAN, AGENT_EXIT_NOW, } agent_exit_save_result; /* Process exit is different from /new or /switch: once the terminal is already * restored, declining the save can terminate immediately and let the OS reclaim * model/Metal resources instead of waiting for orderly teardown. */ static agent_exit_save_result agent_maybe_save_before_exiting(agent_worker *w) { if (!agent_worker_needs_save(w)) return AGENT_EXIT_CLEAN; if (!agent_prompt_yes_no("Save current session? (y/n) ")) return AGENT_EXIT_NOW; char err[160] = {0}; if (agent_worker_save_session(w, err, sizeof(err))) return AGENT_EXIT_CLEAN; printf("save failed: %s\n", err); return agent_prompt_yes_no("Continue anyway? (y/n) ") ? AGENT_EXIT_NOW : AGENT_EXIT_CANCEL; } /* ============================================================================ * Interactive Runtime Loop * ============================================================================ */ static void agent_noninteractive_marker(const char *msg) { write_all(STDERR_FILENO, msg, strlen(msg)); write_all(STDERR_FILENO, "\n", 1); } static int agent_read_stdin_available(agent_input_buf *in, bool *eof) { char buf[4096]; for (;;) { ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); if (n > 0) { agent_input_buf_append(in, buf, (size_t)n); continue; } if (n == 0) { *eof = true; return 0; } if (errno == EINTR) continue; if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; perror("ds4-agent: read stdin"); return -1; } } /* Headless mode is intentionally just another front-end for the same worker. * With -p/--prompt it is a one-shot execution. Without -p it becomes a small * stdin protocol: announce readiness on stderr, collect bytes until stdin has * been quiet for 200 ms, submit that buffer as one prompt, and keep reading so * later input can be queued while the model is still working. */ static int run_agent_non_interactive(ds4_engine *engine, agent_config *cfg) { agent_worker worker; if (agent_worker_init(&worker, engine, cfg) != 0) return 1; const bool one_shot = cfg->gen.prompt != NULL; bool one_shot_submitted = false; bool stdin_eof = false; bool waiting_announced = false; bool stdin_nonblock = false; int old_stdin_flags = 0; agent_input_buf input = {0}; agent_prompt_queue queue = {0}; double quiet_deadline = 0.0; int rc = 0; if (!one_shot) { if (set_nonblock(STDIN_FILENO, true, &old_stdin_flags) != 0) { perror("ds4-agent: nonblocking stdin"); agent_worker_free(&worker); return 1; } stdin_nonblock = true; } while (true) { bool initialized = worker_is_initialized(&worker, NULL); bool idle = worker_is_idle(&worker); if (one_shot && !one_shot_submitted && initialized) { if (worker_submit(&worker, cfg->gen.prompt)) one_shot_submitted = true; idle = false; } if (!one_shot && queue.len && idle) { char *queued = agent_prompt_queue_take_all(&queue); if (worker_submit(&worker, queued)) { idle = false; } else { agent_prompt_queue_push_front(&queue, queued); queued = NULL; } free(queued); } if (!one_shot && initialized && idle && !queue.len && input.len == 0 && !stdin_eof && !waiting_announced) { agent_noninteractive_marker("+DWARFSTAR_WAITING"); waiting_announced = true; } int timeout_ms = -1; if (!one_shot && input.len > 0) { double rem = quiet_deadline - now_sec(); timeout_ms = rem <= 0.0 ? 0 : (int)(rem * 1000.0) + 1; } struct pollfd pfd[2]; int nfds = 0; int wake_idx = nfds; pfd[nfds++] = (struct pollfd){.fd = worker.wake_fd[0], .events = POLLIN}; int stdin_idx = -1; if (!one_shot && initialized && !stdin_eof) { stdin_idx = nfds; pfd[nfds++] = (struct pollfd){.fd = STDIN_FILENO, .events = POLLIN}; } int prc = poll(pfd, (nfds_t)nfds, timeout_ms); if (prc < 0) { if (errno == EINTR) continue; perror("ds4-agent: poll"); rc = 1; break; } if (pfd[wake_idx].revents & POLLIN) drain_wake_fd(worker.wake_fd[0]); if (stdin_idx >= 0 && (pfd[stdin_idx].revents & (POLLIN | POLLHUP))) { size_t old_len = input.len; if (agent_read_stdin_available(&input, &stdin_eof) != 0) { rc = 1; break; } if (input.len != old_len) { quiet_deadline = now_sec() + 0.200; waiting_announced = false; } } char *out = NULL; size_t out_len = 0; agent_status st = {0}; worker_consume(&worker, &out, &out_len, &st); if (out && out_len) { write_all(STDOUT_FILENO, out, out_len); fflush(stdout); } free(out); if (worker_take_queued_user_drain_request(&worker)) { char *queued = agent_prompt_queue_take_all(&queue); worker_answer_queued_user_drain(&worker, queued); } if (st.state == AGENT_WORKER_ERROR) { fprintf(stderr, "ds4-agent: %s\n", st.error[0] ? st.error : "worker error"); rc = 1; break; } if (!one_shot && input.len > 0 && (stdin_eof || now_sec() >= quiet_deadline)) { char *prompt = agent_input_buf_take(&input); if (worker_is_idle(&worker) && queue.len == 0) { if (!worker_submit(&worker, prompt)) { agent_prompt_queue_push(&queue, prompt); agent_noninteractive_marker("+DWARFSTAR_QUEUED"); } } else { agent_prompt_queue_push(&queue, prompt); agent_noninteractive_marker("+DWARFSTAR_QUEUED"); } free(prompt); waiting_announced = false; } if (one_shot && one_shot_submitted && worker_is_idle(&worker)) break; if (!one_shot && stdin_eof && input.len == 0 && queue.len == 0 && worker_is_idle(&worker)) break; } /* Drain anything published between the final status transition and the * loop exit. This keeps stdout complete without adding another protocol. */ char *out = NULL; size_t out_len = 0; worker_consume(&worker, &out, &out_len, NULL); if (out && out_len) { write_all(STDOUT_FILENO, out, out_len); fflush(stdout); } free(out); if (stdin_nonblock) fcntl(STDIN_FILENO, F_SETFL, old_stdin_flags); agent_input_buf_free(&input); agent_prompt_queue_free(&queue); agent_worker_free(&worker); return rc; } /* Main UI loop. poll() multiplexes stdin with the worker wake pipe; all * terminal writes go through editor_write_async() so linenoise, status footer, * model output, and tool output never race each other. */ static int run_agent(ds4_engine *engine, agent_config *cfg) { agent_worker worker; if (agent_worker_init(&worker, engine, cfg) != 0) return 1; char hist[PATH_MAX]; const char *home = getenv("HOME"); if (!home || !home[0]) home = "."; snprintf(hist, sizeof(hist), "%s/.ds4_agent_history", home); /* The agent uses ANSI scroll regions when possible: model/tool output * scrolls above the live linenoise prompt and status footer, so streaming * tokens do not require repainting the bottom rows. Terminals without * scroll-region support fall back to the older prompt-below-output path. */ linenoiseSetMultiLine(1); linenoiseHistorySetMaxLen(512); linenoiseHistoryLoad(hist); agent_completion_worker = &worker; linenoiseSetCompletionCallback(agent_switch_completion_callback); agent_status st; worker_get_status(&worker, &st); char prompt[160]; char statusline[4096]; build_prompt_text(&st, prompt, sizeof(prompt)); build_footer_text(&st, NULL, 80, statusline, sizeof(statusline)); agent_editor editor = {0}; agent_prompt_queue queue = {0}; if (editor_start(&editor, prompt, statusline, NULL) != 0) { fprintf(stderr, "ds4-agent: failed to start line editor\n"); agent_worker_free(&worker); return 1; } editor_write_welcome_banner(&editor, cfg, prompt, statusline); char *initial_pending = cfg->gen.prompt && cfg->gen.prompt[0] ? xstrdup(cfg->gen.prompt) : NULL; bool running = true; bool exit_save_handled = false; bool show_welcome_after_restart = false; bool force_status_redraw_after_restart = false; char *restore_line = NULL; while (running) { /* If a bash child process changed the terminal mode (e.g., from raw * to cooked), restore raw mode so linenoise continues to work. */ if (worker_check_raw_mode_restore(&worker)) { linenoiseRestoreRawMode(); } struct pollfd pfd[2] = { {.fd = STDIN_FILENO, .events = POLLIN}, {.fd = worker.wake_fd[0], .events = POLLIN}, }; int timeout = (!editor.paste_open && !editor.paste_start_pending && linenoiseEditQueuedInput(&editor.edit) > 0) ? 0 : 100; int rc = poll(pfd, 2, timeout); if (rc < 0 && errno != EINTR) break; if (agent_sigint) { agent_sigint = 0; if (worker_is_idle(&worker)) { editor_cancel_input_with_hint(&editor, prompt, statusline); } else { worker_interrupt(&worker); } } if (rc > 0 && (pfd[0].revents & POLLIN)) editor_read_stdin(&editor); /* Linenoise runs the terminal in raw mode, so Ctrl+C normally arrives * as byte 3 instead of SIGINT. Handle it before worker output is * drained and repainted; otherwise a busy decoding stream can leave the * interrupt waiting behind a large terminal-output backlog. */ if (editor_take_queued_byte(&editor, 3)) { /* Ctrl+C */ if (!worker_is_idle(&worker)) { worker_interrupt(&worker); } else { editor_cancel_input_with_hint(&editor, prompt, statusline); } } if (rc > 0 && (pfd[1].revents & POLLIN)) drain_wake_fd(worker.wake_fd[0]); char *out = NULL; size_t out_len = 0; worker_consume(&worker, &out, &out_len, &st); build_prompt_text(&st, prompt, sizeof(prompt)); int footer_cols = editor.edit.cols > 0 ? (int)editor.edit.cols : 80; build_footer_text(&st, &queue, footer_cols, statusline, sizeof(statusline)); if (out && out_len) { bool force_show = st.state == AGENT_WORKER_IDLE || st.state == AGENT_WORKER_ERROR || st.state == AGENT_WORKER_STOPPED; editor_write_async(&editor, out, out_len, prompt, statusline, force_show); } else { editor_set_prompt_status(&editor, prompt, statusline); if (editor.hidden && (st.state == AGENT_WORKER_IDLE || st.state == AGENT_WORKER_ERROR || st.state == AGENT_WORKER_STOPPED)) editor_show(&editor); } if (st.state == AGENT_WORKER_ERROR && st.error[0]) { char msg[320]; int n = snprintf(msg, sizeof(msg), "\nds4-agent: %s\n", st.error); editor_write_async(&editor, msg, n > 0 ? (size_t)n : 0, prompt, statusline, true); pthread_mutex_lock(&worker.mu); worker.status.state = AGENT_WORKER_IDLE; worker.status.prefill_tps = 0.0; worker.status.greedy_sampling = false; worker.status.error[0] = '\0'; pthread_mutex_unlock(&worker.mu); } free(out); if (worker_take_queued_user_drain_request(&worker)) { char *echo = agent_prompt_queue_take_all_echo(&queue); char *queued = agent_prompt_queue_take_all(&queue); if (echo) { build_footer_text(&st, &queue, footer_cols, statusline, sizeof(statusline)); editor_write_async(&editor, echo, strlen(echo), prompt, statusline, true); free(echo); } worker_answer_queued_user_drain(&worker, queued); continue; } char web_approval_msg[256]; if (worker_take_web_approval_request(&worker, web_approval_msg, sizeof(web_approval_msg))) { char *saved_input = NULL; if (editor.active && editor.edit.buf && editor.edit.len) saved_input = xstrndup(editor.edit.buf, editor.edit.len); editor_stop(&editor); editor_restore_terminal_layout(&editor); agent_yes_no_options approval_opts = { .timeout_sec = 30, .timeout_answer = AGENT_YES_NO_AUTO_NO, }; bool approval_timed_out = false; bool allow = agent_prompt_yes_no_ex(web_approval_msg, &approval_opts, &approval_timed_out); worker_answer_web_approval(&worker, allow, approval_timed_out ? "Chrome browser start approval timed out" : NULL); worker_get_status(&worker, &st); build_prompt_text(&st, prompt, sizeof(prompt)); int restart_cols = editor.edit.cols > 0 ? (int)editor.edit.cols : 80; build_footer_text(&st, &queue, restart_cols, statusline, sizeof(statusline)); editor_start(&editor, prompt, statusline, saved_input); free(saved_input); continue; } if (initial_pending && worker_is_idle(&worker)) { if (worker_submit(&worker, initial_pending)) { free(initial_pending); initial_pending = NULL; } } if (!initial_pending && queue.len && worker_is_idle(&worker)) { char *echo = agent_prompt_queue_take_all_echo(&queue); char *queued = agent_prompt_queue_take_all(&queue); if (worker_submit(&worker, queued)) { linenoiseHistoryAdd(queued); linenoiseHistorySave(hist); build_footer_text(&st, &queue, footer_cols, statusline, sizeof(statusline)); if (echo) editor_write_async(&editor, echo, strlen(echo), prompt, statusline, true); } else { agent_prompt_queue_push_front(&queue, queued); queued = NULL; } free(echo); free(queued); } if (queue.len && editor_take_queued_byte(&editor, 24)) { /* Ctrl+X */ char *queued = agent_prompt_queue_pop(&queue); editor_replace_input(&editor, queued); worker_get_status(&worker, &st); build_prompt_text(&st, prompt, sizeof(prompt)); footer_cols = editor.edit.cols > 0 ? (int)editor.edit.cols : 80; build_footer_text(&st, &queue, footer_cols, statusline, sizeof(statusline)); editor_set_prompt_status(&editor, prompt, statusline); free(queued); } if (queue.len && !worker_is_idle(&worker) && editor_take_bare_escape(&editor)) { worker_interrupt(&worker); } if (!editor.paste_open && !editor.paste_start_pending && linenoiseEditQueuedInput(&editor.edit) > 0) { if (editor.hidden) { /* A user key while the model is in the middle of a partial * output line means the prompt must become visible again. End * the model line explicitly; otherwise linenoise would redraw * on top of generated text. */ editor_show(&editor); } errno = 0; char *line = linenoiseEditFeed(&editor.edit); if (line == linenoiseEditMore) { /* Still editing. */ } else if (!line) { if (errno == EAGAIN) { if (!worker_is_idle(&worker)) { worker_interrupt(&worker); } else { editor_cancel_input_with_hint(&editor, prompt, statusline); } } else { running = false; } } else { char *cmd = line; while (*cmd == ' ' || *cmd == '\t' || *cmd == '\r' || *cmd == '\n') cmd++; char *end = cmd + strlen(cmd); while (end > cmd && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\r' || end[-1] == '\n')) end--; *end = '\0'; bool was_below_output = editor.prompt_below_output; bool had_output_line_open = editor.output_line_open; int saved_output_col = editor.output_col; editor_stop(&editor); bool busy = !worker_is_idle(&worker); if (!cmd[0]) { /* Empty input: just reopen the editor. */ } else if (!strcmp(cmd, "/help")) { runtime_help(); } else if (!strcmp(cmd, "/save")) { if (busy) { worker_request_save(&worker); printf("save scheduled at next safe point\n"); } else { char err[160] = {0}; if (!agent_worker_save_session(&worker, err, sizeof(err))) printf("save failed: %s\n", err); } } else if (!strcmp(cmd, "/compact")) { worker_request_compact(&worker); if (busy) printf("compaction scheduled at next safe point\n"); } else if (!strcmp(cmd, "/list")) { agent_worker_list_sessions(&worker); } else if (!strncmp(cmd, "/power", 6) && (cmd[6] == '\0' || cmd[6] == ' ' || cmd[6] == '\t')) { char *arg = cmd + 6; while (*arg == ' ' || *arg == '\t') arg++; if (!arg[0]) { printf("usage: /power <1..100>\n"); } else { int power = 0; if (!parse_power_percent(arg, &power)) { printf("usage: /power <1..100>\n"); } else { worker_request_power(&worker, power); } } } else if (cmd[0] == '/' && !agent_slash_command_known(cmd)) { ssize_t ignored = write(STDOUT_FILENO, "\a", 1); (void)ignored; restore_line = xstrdup(cmd); } else if (cmd[0] == '/' && busy) { printf("command requires the model to be idle: %s\n", cmd); } else if (!strcmp(cmd, "/quit") || !strcmp(cmd, "/exit")) { /* Stop the editor so raw mode and non-blocking stdin are * disabled before we prompt the user. Then restore the * ANSI scroll region too; AGENT_EXIT_NOW exits directly. */ editor_stop(&editor); editor_restore_terminal_layout(&editor); agent_exit_save_result exit_save = agent_maybe_save_before_exiting(&worker); if (exit_save == AGENT_EXIT_NOW) { exit(0); } else if (exit_save == AGENT_EXIT_CLEAN) { exit_save_handled = true; running = false; } else { /* AGENT_EXIT_CANCEL: user declined to proceed after a * save failure. Reopen the editor and continue. */ editor_start(&editor, prompt, statusline, NULL); } } else if (!strcmp(cmd, "/new")) { editor_restore_terminal_layout(&editor); if (agent_maybe_save_before_leaving_session(&worker)) { char err[160] = {0}; if (!agent_worker_reset_to_sysprompt(&worker, err, sizeof(err))) { printf("new session failed: %s\n", err); } else { show_welcome_after_restart = true; } } } else if (!strncmp(cmd, "/switch", 7) && (cmd[7] == '\0' || cmd[7] == ' ' || cmd[7] == '\t')) { char *arg = cmd + 7; while (*arg == ' ' || *arg == '\t') arg++; if (!arg[0]) { printf("usage: /switch \n"); } else { editor_restore_terminal_layout(&editor); if (agent_maybe_save_before_leaving_session(&worker)) { char *sha = arg; while (*arg && *arg != ' ' && *arg != '\t') arg++; if (*arg) *arg = '\0'; char err[160] = {0}; if (!agent_worker_switch_session(&worker, sha, AGENT_HISTORY_DEFAULT_TURNS, err, sizeof(err))) printf("switch failed: %s\n", err); else force_status_redraw_after_restart = true; } } } else if (!strncmp(cmd, "/del", 4) && (cmd[4] == '\0' || cmd[4] == ' ' || cmd[4] == '\t')) { char *arg = cmd + 4; while (*arg == ' ' || *arg == '\t') arg++; if (!arg[0]) { printf("usage: /del \n"); } else { char *sha_arg = arg; while (*arg && *arg != ' ' && *arg != '\t') arg++; if (*arg) *arg = '\0'; char sha[41] = {0}; char err[160] = {0}; if (agent_worker_delete_session(&worker, sha_arg, sha, err, sizeof(err))) printf("deleted session %.8s\n", sha); else printf("delete failed: %s\n", err); } } else if (!strncmp(cmd, "/strip", 6) && (cmd[6] == '\0' || cmd[6] == ' ' || cmd[6] == '\t')) { char *arg = cmd + 6; while (*arg == ' ' || *arg == '\t') arg++; if (!arg[0]) { printf("usage: /strip \n"); } else { char *sha_arg = arg; while (*arg && *arg != ' ' && *arg != '\t') arg++; if (*arg) *arg = '\0'; char sha[41] = {0}; uint32_t tokens = 0; char err[160] = {0}; if (agent_worker_strip_session(&worker, sha_arg, sha, &tokens, err, sizeof(err))) printf("stripped session %.8s (%u tokens)\n", sha, tokens); else printf("strip failed: %s\n", err); } } else if (!strncmp(cmd, "/history", 8) && (cmd[8] == '\0' || cmd[8] == ' ' || cmd[8] == '\t')) { char *arg = cmd + 8; while (*arg == ' ' || *arg == '\t') arg++; int history_turns = arg[0] ? agent_parse_int_default(arg, AGENT_HISTORY_DEFAULT_TURNS, 1, AGENT_HISTORY_MAX_TURNS) : AGENT_HISTORY_DEFAULT_TURNS; char err[160] = {0}; if (!agent_worker_show_history(&worker, history_turns, err, sizeof(err))) printf("history failed: %s\n", err); } else if (busy) { agent_prompt_queue_push(&queue, cmd); } else { linenoiseHistoryAdd(cmd); linenoiseHistorySave(hist); if (worker_submit(&worker, cmd)) { agent_echo_user_prompt(cmd); } else { restore_line = xstrdup(cmd); } } linenoiseFree(line); if (running) { worker_get_status(&worker, &st); build_prompt_text(&st, prompt, sizeof(prompt)); int restart_cols = editor.edit.cols > 0 ? (int)editor.edit.cols : 80; build_footer_text(&st, &queue, restart_cols, statusline, sizeof(statusline)); editor_start(&editor, prompt, statusline, restore_line); if (!editor.scroll_region && was_below_output) { editor.output_line_open = had_output_line_open; editor.prompt_below_output = was_below_output; editor.output_col = saved_output_col; } if (show_welcome_after_restart) { editor_write_welcome_banner(&editor, cfg, prompt, statusline); show_welcome_after_restart = false; } if (force_status_redraw_after_restart) { editor_write_async(&editor, "", 0, prompt, statusline, true); force_status_redraw_after_restart = false; } free(restore_line); restore_line = NULL; } } } } free(initial_pending); free(restore_line); agent_prompt_queue_free(&queue); editor_stop(&editor); editor_restore_terminal_layout(&editor); linenoiseSetCompletionCallback(NULL); agent_completion_worker = NULL; if (!exit_save_handled) { agent_exit_save_result exit_save = agent_maybe_save_before_exiting(&worker); if (exit_save == AGENT_EXIT_NOW) exit(0); } agent_worker_free(&worker); return 0; } #ifndef DS4_AGENT_TEST_NO_MAIN int main(int argc, char **argv) { agent_config cfg = parse_options(argc, argv); if (cfg.chdir_path && chdir(cfg.chdir_path) != 0) { fprintf(stderr, "ds4-agent: failed to chdir to %s: %s\n", cfg.chdir_path, strerror(errno)); return 1; } ds4_engine *engine = NULL; if (ds4_engine_open(&engine, &cfg.engine) != 0) return 1; log_context_memory(cfg.engine.backend, cfg.gen.ctx_size, cfg.engine.prefill_chunk); struct sigaction old_int; struct sigaction sa; memset(&sa, 0, sizeof(sa)); sigemptyset(&sa.sa_mask); sa.sa_handler = agent_sigint_handler; bool sigint_installed = !cfg.non_interactive && sigaction(SIGINT, &sa, &old_int) == 0; int rc = cfg.non_interactive ? run_agent_non_interactive(engine, &cfg) : run_agent(engine, &cfg); if (sigint_installed) sigaction(SIGINT, &old_int, NULL); ds4_engine_close(engine); return rc; } #endif