From 016a45bc6ea1fc004d2799787f270f5971faec0d Mon Sep 17 00:00:00 2001 From: Wang Zhiyu Date: Sun, 4 Oct 2026 13:47:06 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E6=94=B9=E8=BF=9B=20SGV=20=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .clang-format | 4 + src/agent.c | 727 ++++++++++++++++++++++++++++--------------------- src/i18n.c | 8 +- src/i18n.h | 2 +- src/markdown.c | 43 +-- src/meta.c | 6 +- src/sgr.h | 8 - 7 files changed, 445 insertions(+), 353 deletions(-) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..0d57498 --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never \ No newline at end of file diff --git a/src/agent.c b/src/agent.c index b843ae7..c499e95 100644 --- a/src/agent.c +++ b/src/agent.c @@ -1,70 +1,77 @@ /* agent.c - agent 主循环 */ #include "agent.h" -#include "sgr.h" -#include "tools.h" -#include "i18n.h" -#include "util.h" -#include "http.h" -#include "spinner.h" -#include "confirm.h" -#include "image.h" +#include #include #include #include #include -#include + +#include "confirm.h" +#include "http.h" +#include "i18n.h" +#include "image.h" +#include "sgr.h" +#include "spinner.h" +#include "tools.h" +#include "util.h" /* ------------------------------------------------------------------ */ /* 颜色与打印 */ /* ------------------------------------------------------------------ */ /* 前置声明: 定义在文件靠后的上下文管理一节 */ -static int role_is(const JsonValue *m, const char *want); +static int role_is(const JsonValue* m, const char* want); -static const char *col(const Agent *a, const char *code) +static const char* +col(const Agent* a, const char* code) { return a->color ? code : ""; } /* 把多行文本缩进显示, 最多 max_lines 行 */ -static void print_indented(const char *text, const char *prefix, - int max_lines, const char *color_code, int use_color) +static void +print_indented(const char* text, const char* prefix, int max_lines, + const char* color_code, int use_color) { - if (!text) return; - const char *p = text; + if (!text) + return; + const char* p = text; int lines = 0; while (*p) { - const char *nl = strchr(p, '\n'); + const char* nl = strchr(p, '\n'); size_t n = nl ? (size_t)(nl - p) : strlen(p); if (lines >= max_lines) { printf("%s%s%s%s\n", use_color ? C_DIM : "", prefix, - t("agent.output.elided"), use_color ? C_OFF : ""); + t("agent.output.elided"), use_color ? C_OFF : ""); break; } printf("%s%s%.*s%s\n", use_color ? color_code : "", prefix, (int)n, p, - use_color ? C_OFF : ""); + use_color ? C_OFF : ""); lines++; - if (!nl) break; + if (!nl) + break; p = nl + 1; } if (lines == 0) printf("%s%s%s%s\n", use_color ? C_DIM : "", prefix, - t("agent.output.empty"), use_color ? C_OFF : ""); + t("agent.output.empty"), use_color ? C_OFF : ""); } /* ------------------------------------------------------------------ */ /* 流式回调 */ /* ------------------------------------------------------------------ */ -typedef struct { - int printed; - MdRenderer *md; +typedef struct +{ + int printed; + MdRenderer* md; } DeltaCtx; -static void on_delta(void *ud, const char *text) +static void +on_delta(void* ud, const char* text) { - DeltaCtx *c = ud; + DeltaCtx* c = ud; /* 一出正文就把等待动画收掉, 否则动画会和正文抢同一行 */ spinner_stop(); /* 走 Markdown 渲染器: 它会处理被 SSE 切断的标记 */ @@ -73,9 +80,10 @@ static void on_delta(void *ud, const char *text) c->printed = 1; } -/* 收到思维链: 说明请求早就发出去了, 模型确实在推理, 而不是卡在网络. +/* 收到思维链: 说明请求早就发出去了, 模型确实在推理, 而不是卡在网络. * 只换标签, 不打印内容 —— 思维链不显示. */ -static void on_think(void *ud, const char *text) +static void +on_think(void* ud, const char* text) { (void)ud; (void)text; @@ -86,36 +94,49 @@ static void on_think(void *ud, const char *text) /* 生命周期 */ /* ------------------------------------------------------------------ */ -char *agent_default_system_prompt(void) +char* agent_default_system_prompt(void) { return xstrdup( "你是用户的 AI 助手\n" "\n" "你可以调用工具来读写文件和执行命令. \n" "工作原则: \n" - "1. 先观察再动手: 不确定文件内容或目录结构时, 先用 list_dir / read_file 查看, 不要凭猜测写代码. 搜内容用 run_command (如 grep -rn 模式 目录). \n" - "2. 修改已有文件优先使用 edit_file: 改一处就带上 read_file 显示的行号 line (集合型: 给一个整数, 或给一个整数数组把 old_string 覆盖到的那几行都填进来), 替换只发生在这些行里. 省略 line 等于全文件替换所有匹配, 只在你确实要改全文件时才省. 只有创建新文件或需要整体重写时才用 write_file. \n" - "3. 工具报错时, 先读错误信息定位原因, 再决定换一种方式; 不要重复执行同一个失败的命令. \n" - "4. 回答简洁直接, 不要把大段代码重复贴回来. 完成任务后用一两句话说明改了什么、验证结果如何. \n" - "5. 不要执行破坏性的操作 (例如删除用户数据、强制推送), 除非用户明确要求. \n" - "6. read_file / write_file / edit_file 只处理文本: 二进制文件会被拒绝. 看图片用 read_image, 看其它二进制用 run_command (file / xxd / strings). \n" + "1. 先观察再动手: 不确定文件内容或目录结构时, 先用 list_dir / read_file " + "查看, 不要凭猜测写代码. 搜内容用 run_command (如 grep -rn 模式 目录). " + "\n" + "2. 修改已有文件优先使用 edit_file: 改一处就带上 read_file 显示的行号 " + "line (集合型: 给一个整数, 或给一个整数数组把 old_string " + "覆盖到的那几行都填进来), 替换只发生在这些行里. 省略 line " + "等于全文件替换所有匹配, 只在你确实要改全文件时才省. " + "只有创建新文件或需要整体重写时才用 write_file. \n" + "3. 工具报错时, 先读错误信息定位原因, 再决定换一种方式; " + "不要重复执行同一个失败的命令. \n" + "4. 回答简洁直接, 不要把大段代码重复贴回来. " + "完成任务后用一两句话说明改了什么、验证结果如何. \n" + "5. 不要执行破坏性的操作 (例如删除用户数据、强制推送), " + "除非用户明确要求. " + "\n" + "6. read_file / write_file / edit_file 只处理文本: 二进制文件会被拒绝. " + "看图片用 read_image, 看其它二进制用 run_command (file / xxd / " + "strings). " + "\n" "\n" "除非必要, 否则不要执行命令.\n" "除非用户说明, 不得直接或间接(如使用curl)获取任何在线资源.\n"); } -void agent_init(Agent *a) +void agent_init(Agent* a) { memset(a, 0, sizeof *a); a->messages = json_new_array(); - a->max_steps = 0; /* 0 = 不限往返次数 (默认, 由配置覆盖) */ + a->max_steps = 0; /* 0 = 不限往返次数 (默认, 由配置覆盖) */ a->use_tools = 1; a->color = 0; a->show_tool_output = 1; a->confirm = 1; a->md = md_new(0); a->started_at = time(NULL); - a->exec_tool = tool_execute; /* 默认就是普通工具集 */ + a->exec_tool = tool_execute; /* 默认就是普通工具集 */ a->is_mutating = tool_mutating; a->probe_caps = 0; /* 必须显式初始化成"未测". CAP_NO 正好是 0, 而 memset 出来的零值就是它 —— @@ -125,15 +146,16 @@ void agent_init(Agent *a) a->caps.tools = CAP_UNKNOWN; } -void agent_set_color(Agent *a, int enabled) +void agent_set_color(Agent* a, int enabled) { a->color = enabled ? 1 : 0; md_set_enabled(a->md, a->color); } -void agent_free(Agent *a) +void agent_free(Agent* a) { - if (!a) return; + if (!a) + return; json_free(a->messages); json_free(a->tools); md_free(a->md); @@ -142,34 +164,36 @@ void agent_free(Agent *a) a->md = NULL; } -void agent_add_system(Agent *a, const char *prompt) +void agent_add_system(Agent* a, const char* prompt) { - if (!prompt) return; - JsonValue *m = json_new_object(); + if (!prompt) + return; + JsonValue* m = json_new_object(); json_object_set_string(m, "role", "system"); json_object_set_string(m, "content", prompt); /* system 永远是第一条; 历史里已有的旧 system 会被替换而不是叠加 */ - JsonValue *tmp = json_new_array(); + JsonValue* tmp = json_new_array(); json_array_push(tmp, m); for (size_t i = 0; i < json_array_size(a->messages); i++) { - JsonValue *old = json_array_get(a->messages, i); - const char *role = json_get_string(old, "role"); - if (i == 0 && role && strcmp(role, "system") == 0) continue; + JsonValue* old = json_array_get(a->messages, i); + const char* role = json_get_string(old, "role"); + if (i == 0 && role && strcmp(role, "system") == 0) + continue; json_array_push(tmp, json_clone(old)); } json_free(a->messages); a->messages = tmp; } -/* 加一条 user 消息: 文本 + 若干内容块 (parts 的所有权转移给这条消息, - * 可以为 NULL). 块排在文本之后. +/* 加一条 user 消息: 文本 + 若干内容块 (parts 的所有权转移给这条消息, + * 可以为 NULL). 块排在文本之后. * * 为什么需要它: /include 要把文件、图片跟着用户这一句话一起发出去, 而 * agent_add_user 只能放纯字符串. */ -void agent_add_user_parts(Agent *a, const char *text, JsonValue *parts) +void agent_add_user_parts(Agent* a, const char* text, JsonValue* parts) { - JsonValue *m = json_new_object(); + JsonValue* m = json_new_object(); json_object_set_string(m, "role", "user"); if (!parts || json_array_size(parts) == 0) { @@ -179,98 +203,114 @@ void agent_add_user_parts(Agent *a, const char *text, JsonValue *parts) return; } - JsonValue *content = json_new_array(); + JsonValue* content = json_new_array(); if (text && *text) { - JsonValue *tp = json_new_object(); + JsonValue* tp = json_new_object(); json_object_set_string(tp, "type", "text"); json_object_set_string(tp, "text", text); json_array_push(content, tp); } - json_array_steal(content, parts); /* 偷所有权 */ + json_array_steal(content, parts); /* 偷所有权 */ json_free(parts); json_object_set(m, "content", content); json_array_push(a->messages, m); } -void agent_add_user(Agent *a, const char *text) +void agent_add_user(Agent* a, const char* text) { - JsonValue *m = json_new_object(); + JsonValue* m = json_new_object(); json_object_set_string(m, "role", "user"); json_object_set_string(m, "content", text); json_array_push(a->messages, m); } -void agent_reset(Agent *a) +void agent_reset(Agent* a) { a->started_at = time(NULL); - JsonValue *keep = NULL; - JsonValue *first = json_array_get(a->messages, 0); - if (first && strcmp(json_get_string(first, "role") ? json_get_string(first, "role") : "", "system") == 0) + JsonValue* keep = NULL; + JsonValue* first = json_array_get(a->messages, 0); + if (first + && strcmp(json_get_string(first, "role") + ? json_get_string(first, "role") + : "", + "system") + == 0) keep = json_clone(first); json_free(a->messages); a->messages = json_new_array(); - if (keep) json_array_push(a->messages, keep); + if (keep) + json_array_push(a->messages, keep); } -size_t agent_history_len(const Agent *a) +size_t +agent_history_len(const Agent* a) { return json_array_size(a->messages); } -/* 面向用户的"历史消息数": **不含 system 提示词**. +/* 面向用户的"历史消息数": **不含 system 提示词**. * - * system 是配置不是对话. 删掉唯一一轮之后, 用户看到的应该是"历史剩 0 条", + * system 是配置不是对话. 删掉唯一一轮之后, 用户看到的应该是"历史剩 0 条", * 而不是"1 条" (那 1 条是提示词). /history、/back、/unback、/export、/import - * 统一用这个口径, 数字之间才对得上. + * 统一用这个口径, 数字之间才对得上. * * 内部判断"历史变了没有"仍然用 agent_history_len (数组真实长度)—— 那是在比 * 大小, 不是在报数. */ -size_t agent_conversation_len(const Agent *a) +size_t +agent_conversation_len(const Agent* a) { size_t n = json_array_size(a->messages); - if (n > 0 && role_is(json_array_get(a->messages, 0), "system")) n--; + if (n > 0 && role_is(json_array_get(a->messages, 0), "system")) + n--; return n; } -JsonValue *agent_take_last_round(Agent *a) +JsonValue* +agent_take_last_round(Agent* a) { - JsonValue *msgs = a->messages; + JsonValue* msgs = a->messages; size_t n = json_array_size(msgs); size_t start = 0; - if (n > 0 && role_is(json_array_get(msgs, 0), "system")) start = 1; + if (n > 0 && role_is(json_array_get(msgs, 0), "system")) + start = 1; /* 最后一条"开启一轮"的 user 是本轮的起点 */ size_t last_user = (size_t)-1; for (size_t i = start; i < n; i++) - if (agent_opens_round(json_array_get(msgs, i))) last_user = i; - if (last_user == (size_t)-1 || last_user < start) return NULL; + if (agent_opens_round(json_array_get(msgs, i))) + last_user = i; + if (last_user == (size_t)-1 || last_user < start) + return NULL; - JsonValue *removed = json_new_array(); - JsonValue **items = msgs->u.array.items; + JsonValue* removed = json_new_array(); + JsonValue** items = msgs->u.array.items; for (size_t i = last_user; i < n; i++) { - json_array_push(removed, items[i]); /* 转移所有权 */ + json_array_push(removed, items[i]); /* 转移所有权 */ } - msgs->u.array.count = last_user; /* 截断 */ + msgs->u.array.count = last_user; /* 截断 */ return removed; } -void agent_replace_history(Agent *a, JsonValue *messages) +void agent_replace_history(Agent* a, JsonValue* messages) { /* 当前生效的 system 提示词要保留 */ - JsonValue *sys = NULL; - JsonValue *first = json_array_get(a->messages, 0); - if (first && role_is(first, "system")) sys = json_clone(first); + JsonValue* sys = NULL; + JsonValue* first = json_array_get(a->messages, 0); + if (first && role_is(first, "system")) + sys = json_clone(first); json_free(a->messages); a->messages = json_new_array(); - if (sys) json_array_push(a->messages, sys); + if (sys) + json_array_push(a->messages, sys); if (messages && messages->type == JSON_ARRAY) { for (size_t i = 0; i < json_array_size(messages); i++) { - JsonValue *m = json_array_get(messages, i); - if (role_is(m, "system")) continue; /* 存档里的 system 不带进来 */ + JsonValue* m = json_array_get(messages, i); + if (role_is(m, "system")) + continue; /* 存档里的 system 不带进来 */ json_array_push(a->messages, json_clone(m)); } } @@ -282,37 +322,41 @@ void agent_replace_history(Agent *a, JsonValue *messages) /* 错误分类 */ /* ------------------------------------------------------------------ */ -/* 是不是 4xx. 图片引用失效没有稳定的措辞可认 (各家不同), 所以只认状态码, +/* 是不是 4xx. 图片引用失效没有稳定的措辞可认 (各家不同), 所以只认状态码, * 再配合"历史里确实有图片引用"来决定要不要把图摘掉重试. */ -int agent_is_http_4xx(const char *err) +int agent_is_http_4xx(const char* err) { return err && strstr(err, "HTTP 4") != NULL; } -static int role_is(const JsonValue *m, const char *want) +static int +role_is(const JsonValue* m, const char* want) { - const char *r = json_get_string(m, "role"); + const char* r = json_get_string(m, "role"); return r && strcmp(r, want) == 0; } -/* 打开新一轮的 user 消息 (agent.h 里有声明 —— meta.c 切轮时用的是同一个判据, - * 两边对"轮"的理解必须一致, 否则 /meta 报的轮号和主循环的口径对不上). +/* 打开新一轮的 user 消息 (agent.h 里有声明 —— meta.c 切轮时用的是同一个判据, + * 两边对"轮"的理解必须一致, 否则 /meta 报的轮号和主循环的口径对不上). * * read_image 之后会补一条"只有图片附件、没有文字"的 user 消息 (协议规定图片 * 只能出现在 user 消息里). 它不算新的一轮 —— 按它切轮会把紧邻的 - * assistant(tool_calls) + tool 那对切散, 下一轮直接 400. + * assistant(tool_calls) + tool 那对切散, 下一轮直接 400. * * 判据很直白: content 是数组、且里面一个 text 块都没有. */ -int agent_opens_round(const JsonValue *m) +int agent_opens_round(const JsonValue* m) { - if (!role_is(m, "user")) return 0; - JsonValue *c = json_object_get(m, "content"); - if (!c || c->type != JSON_ARRAY) return 1; /* 纯字符串 = 真的一轮 */ + if (!role_is(m, "user")) + return 0; + JsonValue* c = json_object_get(m, "content"); + if (!c || c->type != JSON_ARRAY) + return 1; /* 纯字符串 = 真的一轮 */ for (size_t i = 0; i < json_array_size(c); i++) { - const char *t = json_get_string(json_array_get(c, i), "type"); - if (t && strcmp(t, "text") == 0) return 1; + const char* t = json_get_string(json_array_get(c, i), "type"); + if (t && strcmp(t, "text") == 0) + return 1; } - return 0; /* 只有附件 */ + return 0; /* 只有附件 */ } /* ------------------------------------------------------------------ */ @@ -322,86 +366,92 @@ int agent_opens_round(const JsonValue *m) /* 调用摘要里, 兜底那些工具 (参数照原样看一眼)最多打多少字符 */ #define CALL_JSON_MAX 120 -/* 工具调用的一行摘要: **人话**, 不是原始 JSON. +/* 工具调用的一行摘要: **人话**, 不是原始 JSON. * * 以前这里把整个参数 JSON 原样打出来 (截到 256 字节). 对 edit_file 来说那是 * 一整段 old_string / new_string: 屏幕上是一坨转义过的引号, 既刷屏, 又完全看 - * 不出"要改哪里" —— 而人想知道的恰恰是"哪个文件的第几行、改成什么". + * 不出"要改哪里" —— 而人想知道的恰恰是"哪个文件的第几行、改成什么". * - * 现在这里只摆最要紧的那一两项 (改动的具体内容交给 show_edit_diff 去对照), + * 现在这里只摆最要紧的那一两项 (改动的具体内容交给 show_edit_diff 去对照), * 参数解析不出来 (模型给了非法 JSON)时就只有工具名 —— 原因在结果里说. */ -static void show_tool_call(const Agent *a, const char *name, const JsonValue *args) +static void +show_tool_call(const Agent* a, const char* name, const JsonValue* args) { char shown[400] = ""; - const char *path = args ? json_get_string(args, "path") : NULL; + const char* path = args ? json_get_string(args, "path") : NULL; if (args && strcmp(name, TOOL_EDIT_FILE) == 0) { char lines[128]; tool_lines_label(args, lines, sizeof lines); /* diff 只显示"改成什么", 看不出命中几处 —— 所以"全文件替换"和 * "所有匹配都换掉"这两种范围必须在摘要里说清楚. */ - const char *scope = lines[0] ? lines : t("tool.call.all_lines"); + const char* scope = lines[0] ? lines : t("tool.call.all_lines"); if (json_get_bool(args, "replace_all", 0)) scope = tf("tool.call.scope_all", "scope", scope, NULL); - /* 路径带引号、行号用方括号: `{/} edit_file "./Makefile" [280, 284]`. + /* 路径带引号、行号用方括号: `{/} edit_file "./Makefile" [280, 284]`. * 引号让路径从描述里"跳出来" (路径本身可能带空格), 方括号里的就是 * read_file 打出来的那些行号. */ snprintf(shown, sizeof shown, "\"%s\" [%s]", path ? path : "?", scope); } else if (args && strcmp(name, TOOL_WRITE_FILE) == 0) { - const char *content = json_get_string(args, "content"); + const char* content = json_get_string(args, "content"); snprintf(shown, sizeof shown, "%s", - tf("agent.call.write", "path", path ? path : "?", - "n", i18n_num((long long)(content ? strlen(content) : 0)), NULL)); + tf("agent.call.write", "path", path ? path : "?", "n", + i18n_num((long long)(content ? strlen(content) : 0)), + NULL)); } else if (args && strcmp(name, TOOL_RUN_COMMAND) == 0) { /* 提权必须一眼看见: 这条命令会以 root 跑 */ - const char *cmd = json_get_string(args, "command"); + const char* cmd = json_get_string(args, "command"); if (json_get_bool(args, "elevated", 0)) snprintf(shown, sizeof shown, "%s", - tf("agent.call.elevated", "cmd", cmd ? cmd : "", NULL)); + tf("agent.call.elevated", "cmd", cmd ? cmd : "", NULL)); else snprintf(shown, sizeof shown, "%s", cmd ? cmd : ""); } else if (path) { /* 路径一律带引号: 它可能含空格, 引号能把"这是一个路径"框出来 */ snprintf(shown, sizeof shown, "\"%s\"", path); } else if (args) { - /* 其它工具 (包括 meta 自己那一套: list_rounds / read_messages / …): + /* 其它工具 (包括 meta 自己那一套: list_rounds / read_messages / …): * 参数照原样看一眼, 但压到一行、截到 CALL_JSON_MAX 字符. 它们的参数里 * 没有上面那几种"整段正文", 短 JSON 反而是最省事也最准的摘要. */ - char *j = json_dump(args); + char* j = json_dump(args); size_t n = strlen(j); snprintf(shown, sizeof shown, "%.*s%s", CALL_JSON_MAX, j, - n > (size_t)CALL_JSON_MAX ? "…" : ""); + n > (size_t)CALL_JSON_MAX ? "…" : ""); free(j); } /* 压成一行 */ - for (char *p = shown; *p; p++) - if (*p == '\n' || *p == '\r' || *p == '\t') *p = ' '; + for (char* p = shown; *p; p++) + if (*p == '\n' || *p == '\r' || *p == '\t') + *p = ' '; - printf("%s%s{/} %s%s", col(a, C_DIM), col(a, C_CYAN), name, col(a, C_OFF)); - if (shown[0]) printf("%s %s%s", col(a, C_DIM), shown, col(a, C_OFF)); + printf("%s%s{/} %s%s", col(a, C_DIM), col(a, C_CYAN), name, + col(a, C_OFF)); + if (shown[0]) + printf("%s %s%s", col(a, C_DIM), shown, col(a, C_OFF)); putchar('\n'); fflush(stdout); } -static void show_tool_result(const Agent *a, const char *name, const JsonValue *args, - const char *result, int is_error, int diff_shown) +static void +show_tool_result(const Agent* a, const char* name, const JsonValue* args, + const char* result, int is_error, int diff_shown) { if (!a->show_tool_output) { printf("%s -> %s%s\n", col(a, is_error ? C_RED : C_DIM), - is_error ? t("agent.tool.failed") : t("agent.tool.done"), - col(a, C_OFF)); + is_error ? t("agent.tool.failed") : t("agent.tool.done"), + col(a, C_OFF)); fflush(stdout); return; } /* edit_file 改成功了: 屏幕上摆新旧对照 (带行号), 比那行"已在 X 第 N 行 - * 替换 1 处"有用得多 (那条回执仍会原样进历史 —— 它是给模型看的). - * --no-color 下也一样显示: - / + 前缀不依赖颜色. + * 替换 1 处"有用得多 (那条回执仍会原样进历史 —— 它是给模型看的). + * --no-color 下也一样显示: - / + 前缀不依赖颜色. * * 但**确认提示里已经摆过一次** (默认就开着确认), 就不再重复第二遍: 同一个 * 东西在一屏里出现两次只会让人怀疑"是不是要改两次". */ - if (!is_error && !diff_shown && name && strcmp(name, TOOL_EDIT_FILE) == 0 && - args && tool_render_diff(stdout, args, " │ ", a->color)) { + if (!is_error && !diff_shown && name && strcmp(name, TOOL_EDIT_FILE) == 0 + && args && tool_render_diff(stdout, args, " │ ", a->color)) { fflush(stdout); return; } @@ -411,41 +461,48 @@ static void show_tool_result(const Agent *a, const char *name, const JsonValue * } /* 执行一批 tool_calls, 把结果作为 tool 消息追加到历史 */ -static int execute_tool_calls(Agent *a, JsonValue *tool_calls) +static int +execute_tool_calls(Agent* a, JsonValue* tool_calls) { size_t count = json_array_size(tool_calls); - size_t interrupted_at = count; /* 中断发生在第几个调用之前 */ + size_t interrupted_at = count; /* 中断发生在第几个调用之前 */ for (size_t i = 0; i < count; i++) { - if (g_ligent_interrupt) { interrupted_at = i; break; } + if (g_ligent_interrupt) { + interrupted_at = i; + break; + } - JsonValue *tc = json_array_get(tool_calls, i); - const char *id = json_get_string(tc, "id"); - JsonValue *fn = json_object_get(tc, "function"); - const char *name = fn ? json_get_string(fn, "name") : NULL; - const char *args_raw = fn ? json_get_string(fn, "arguments") : NULL; - if (!args_raw) args_raw = "{}"; + JsonValue* tc = json_array_get(tool_calls, i); + const char* id = json_get_string(tc, "id"); + JsonValue* fn = json_object_get(tc, "function"); + const char* name = fn ? json_get_string(fn, "name") : NULL; + const char* args_raw = fn ? json_get_string(fn, "arguments") : NULL; + if (!args_raw) + args_raw = "{}"; char jerr[256]; - JsonValue *args = json_parse(args_raw, jerr, sizeof jerr); - /* 摘要打在**执行之前**: 命令可能跑很久, 用户得先看到要做什么. + JsonValue* args = json_parse(args_raw, jerr, sizeof jerr); + /* 摘要打在**执行之前**: 命令可能跑很久, 用户得先看到要做什么. * 参数解析不出来时摘要只有工具名, 原因会在结果里说. */ show_tool_call(a, name ? name : t("agent.tool.name_unknown"), args); int is_error = 0; - int diff_shown = 0; /* 确认提示里是不是已经摆过新旧对照 */ - char *result; + int diff_shown = 0; /* 确认提示里是不是已经摆过新旧对照 */ + char* result; if (!args) { result = xasprintf("错误: 工具参数不是合法 JSON (%s)", jerr); is_error = 1; - } else if (a->confirm && a->is_mutating(name) && !confirm_tool(name, args)) { - /* 拒绝不等于失败: 把结果回填给模型, 让它换个做法, + } else if (a->confirm && a->is_mutating(name) + && !confirm_tool(name, args)) { + /* 拒绝不等于失败: 把结果回填给模型, 让它换个做法, * 而不是让整轮对话就此中断 */ if (confirm_turn_mode() == CONFIRM_NEVER) - /* 用户在确认提示上选了 neVer = 本轮别再改了. + /* 用户在确认提示上选了 neVer = 本轮别再改了. * 话说死一点, 否则模型会一个接一个地试下去. */ - result = xstrdup("用户已表示这一轮不再执行任何改动. " - "不要再尝试改动, 直接说明你的结论或需要用户做什么. "); + result = xstrdup( + "用户已表示这一轮不再执行任何改动. " + "不要再尝试改动, 直接说明你的结论或需要用户做什么. "); else result = xstrdup("用户拒绝了这次操作. 请换一种不改动磁盘的做法, " "或先向用户说明你打算做什么并征得同意. "); @@ -457,11 +514,12 @@ static int execute_tool_calls(Agent *a, JsonValue *tool_calls) result = a->exec_tool(name, args, &is_error); } - /* args 要活到 show_tool_result 之后: edit_file 的新旧对照就是从这里取的 */ + /* args 要活到 show_tool_result 之后: edit_file 的新旧对照就是从这里取的 + */ show_tool_result(a, name, args, result, is_error, diff_shown); json_free(args); - JsonValue *tm = json_new_object(); + JsonValue* tm = json_new_object(); json_object_set_string(tm, "role", "tool"); json_object_set_string(tm, "tool_call_id", id ? id : ""); json_object_set_string(tm, "content", result); @@ -472,19 +530,19 @@ static int execute_tool_calls(Agent *a, JsonValue *tool_calls) /* ---- 被中断: 剩下的 tool_calls 也得补齐 ---- * * 这里以前直接 return 1, 于是 assistant(tool_calls) 后面少了几条 tool - * 消息 —— 和插错附件是同一个 400, 而且同样是"之后每轮都失败、历史却留着". - * 宁可给没跑成的调用补一句"没执行", 也不能把一段坏历史留在上下文里. + * 消息 —— 和插错附件是同一个 400, 而且同样是"之后每轮都失败、历史却留着". + * 宁可给没跑成的调用补一句"没执行", 也不能把一段坏历史留在上下文里. * * 顺序要紧: 占位 tool 消息必须在附件之前, 否则配对又断了. */ if (g_ligent_interrupt) { for (size_t i = interrupted_at; i < count; i++) { - JsonValue *tc = json_array_get(tool_calls, i); - const char *id = json_get_string(tc, "id"); - JsonValue *tm = json_new_object(); + JsonValue* tc = json_array_get(tool_calls, i); + const char* id = json_get_string(tc, "id"); + JsonValue* tm = json_new_object(); json_object_set_string(tm, "role", "tool"); json_object_set_string(tm, "tool_call_id", id ? id : ""); - json_object_set_string(tm, "content", - "错误: 用户中断了这一轮, 本次调用没有执行. "); + json_object_set_string( + tm, "content", "错误: 用户中断了这一轮, 本次调用没有执行. "); json_array_push(a->messages, tm); } } @@ -492,7 +550,7 @@ static int execute_tool_calls(Agent *a, JsonValue *tool_calls) /* ---- 附件: 必须等**所有** tool 消息都推完再推 ---- * * read_image 把图片挂在待发附件里, 而图片只能出现在 user 消息中 (tool - * 消息的内容只能是字符串), 所以要补一条 user 消息. + * 消息的内容只能是字符串), 所以要补一条 user 消息. * * 关键是补在哪儿: 服务端要求 assistant(tool_calls) 后面**紧跟**这一组 * tool 消息. 以前是在循环里每推完一条 tool 就补一次附件, 于是一条 @@ -506,11 +564,11 @@ static int execute_tool_calls(Agent *a, JsonValue *tool_calls) * 服务端回 400: An assistant message with 'tool_calls' must be followed by * tool messages responding to each 'tool_call_id'. 而且这段历史留在上下文 * 里, 之后**每一轮**都会 400. 攒到现在一次性推出去就没有这个问题. */ - JsonValue *attach = image_pending_take(); + JsonValue* attach = image_pending_take(); if (attach && json_array_size(attach) > 0) { - JsonValue *um = json_new_object(); + JsonValue* um = json_new_object(); json_object_set_string(um, "role", "user"); - json_object_set(um, "content", attach); /* 取得所有权 */ + json_object_set(um, "content", attach); /* 取得所有权 */ json_array_push(a->messages, um); } else { json_free(attach); @@ -523,68 +581,75 @@ static int execute_tool_calls(Agent *a, JsonValue *tool_calls) /* 主循环 */ /* ------------------------------------------------------------------ */ -/* 构造"这一轮请求"用的消息副本: 把历史里的图片引用展开成 image_url + base64. +/* 构造"这一轮请求"用的消息副本: 把历史里的图片引用展开成 image_url + base64. * * 历史里存的是对本地备份的引用 (几十字节), 图片字节在备份文件里; 展开只发生 - * 在请求这一层, 所以归档 / /history / /log 都不会被 base64 撑大. + * 在请求这一层, 所以归档 / /history / /log 都不会被 base64 撑大. * * 展开不出来的引用会被就地降级成文字说明 (image.c 里做的), 这里只负责提示 —— * 坏引用留在历史里的话, 每一轮都会重新失败一次. */ -/* 按探测到的能力收敛工具表. - * 模型明确不收图片时把 read_image 摘掉 —— 留着它只会让模型白调一次, - * 然后请求被 400 (虽然我们还有降级兜底, 但那是补救, 不如不给). +/* 按探测到的能力收敛工具表. + * 模型明确不收图片时把 read_image 摘掉 —— 留着它只会让模型白调一次, + * 然后请求被 400 (虽然我们还有降级兜底, 但那是补救, 不如不给). * 模型明确不收 tools 时整张表都不发: 否则每一轮都会以同样的 400 失败. */ -static JsonValue *tools_for_request(const Agent *a) +static JsonValue* +tools_for_request(const Agent* a) { - if (!a->tools || json_array_size(a->tools) == 0) return NULL; - if (a->caps.tools == CAP_NO) return NULL; - JsonValue *t = json_clone(a->tools); - if (a->caps.vision == CAP_NO) tools_drop(t, TOOL_READ_IMAGE); + if (!a->tools || json_array_size(a->tools) == 0) + return NULL; + if (a->caps.tools == CAP_NO) + return NULL; + JsonValue* t = json_clone(a->tools); + if (a->caps.vision == CAP_NO) + tools_drop(t, TOOL_READ_IMAGE); return t; } -/* 一次请求要用的一切: 消息副本 (图片引用已展开)+ 收敛过的工具表. +/* 一次请求要用的一切: 消息副本 (图片引用已展开)+ 收敛过的工具表. * 两者都是新分配的, 用完一起释放 —— 三处 llm_chat 调用点各来一份. */ -typedef struct { - JsonValue *messages; - JsonValue *tools; +typedef struct +{ + JsonValue* messages; + JsonValue* tools; } Req; -static Req req_prepare(Agent *a) +static Req +req_prepare(Agent* a) { Req r; /* 每次构造请求之前体检一遍结构: assistant(tool_calls) 后面必须紧跟对应的 - * tool 消息, 中间不能夹别的东西, 也不能少. + * tool 消息, 中间不能夹别的东西, 也不能少. * * 放在这里而不是 agent_run 开头一次 —— 历史在本轮之内也会变 (推 tool * 结果、补图片附件、中断), 而坏历史一旦发出去就是 400, 并且会一直留在 - * 上下文里, 之后每轮都失败. 放在这里才是"发出之前最后一道关". + * 上下文里, 之后每轮都失败. 放在这里才是"发出之前最后一道关". * 没问题时它只是一次只读扫描, 不分配任何东西. */ { int fixed = 0; - JsonValue *repaired = agent_repair_tool_pairing(a->messages, &fixed); + JsonValue* repaired = agent_repair_tool_pairing(a->messages, &fixed); if (repaired) { json_free(a->messages); a->messages = repaired; fprintf(stderr, "%s%s%s\n", col(a, C_DIM), - tf("agent.structure.fixed", "n", i18n_num(fixed), NULL), - col(a, C_OFF)); + tf("agent.structure.fixed", "n", i18n_num(fixed), NULL), + col(a, C_OFF)); } } int degraded = 0; - JsonValue *wire = image_expand_for_request(a->messages, °raded); + JsonValue* wire = image_expand_for_request(a->messages, °raded); if (degraded > 0) fprintf(stderr, "%s%s%s\n", col(a, C_DIM), - tf("agent.image.degraded", "n", i18n_num(degraded), NULL), - col(a, C_OFF)); + tf("agent.image.degraded", "n", i18n_num(degraded), NULL), + col(a, C_OFF)); r.messages = wire ? wire : json_clone(a->messages); r.tools = tools_for_request(a); return r; } -static void req_free(Req *r) +static void +req_free(Req* r) { json_free(r->messages); json_free(r->tools); @@ -596,54 +661,72 @@ static void req_free(Req *r) /* ------------------------------------------------------------------ */ /* 服务端要求得很死: 带 tool_calls 的 assistant 后面必须**紧跟**与每个 - * tool_call_id 一一对应的 tool 消息, 中间不能夹别的东西, 也不能少. + * tool_call_id 一一对应的 tool 消息, 中间不能夹别的东西, 也不能少. * * 我们的代码现在会维持这条不变量, 但历史还可能从别处被改坏: /meta 的编辑、 - * 手工改过的存档、以及早先版本的 bug (附件插错位置、中断时丢下没回应的调用). - * 一旦坏了, 症状是之后**每一轮**都 400 而历史原样保留 —— 用户只能 /reset. - * 所以发请求之前统一体检一次: 缺的补占位、孤儿 tool 消息丢掉, 并如实说明. + * 手工改过的存档、以及早先版本的 bug (附件插错位置、中断时丢下没回应的调用). + * 一旦坏了, 症状是之后**每一轮**都 400 而历史原样保留 —— 用户只能 /reset. + * 所以发请求之前统一体检一次: 缺的补占位、孤儿 tool 消息丢掉, 并如实说明. * * 没问题返回 NULL; 修过则返回重建好的数组 (调用方替换旧的). */ -JsonValue *agent_repair_tool_pairing(const JsonValue *messages, int *out_fixed) +JsonValue* +agent_repair_tool_pairing(const JsonValue* messages, int* out_fixed) { - if (out_fixed) *out_fixed = 0; - if (!messages || messages->type != JSON_ARRAY) return NULL; + if (out_fixed) + *out_fixed = 0; + if (!messages || messages->type != JSON_ARRAY) + return NULL; - typedef struct { char *id; int answered; } Pending; - Pending *pend = NULL; + typedef struct + { + char* id; + int answered; + } Pending; + Pending* pend = NULL; size_t npend = 0; - JsonValue *out = json_new_array(); + JsonValue* out = json_new_array(); int fixed = 0; - /* 给还没被回应的调用补一条占位 tool 消息 */ - #define FLUSH_PENDING(where) \ - do { \ - for (size_t k_ = 0; k_ < npend; k_++) { \ - if (pend[k_].answered) continue; \ - JsonValue *tm_ = json_new_object(); \ - json_object_set_string(tm_, "role", "tool"); \ - json_object_set_string(tm_, "tool_call_id", pend[k_].id); \ - json_object_set_string(tm_, "content", \ - "错误: 这条工具调用没有得到结果 (历史结构被改坏了, " \ - "这里补一条占位). 不要假设它执行过. "); \ - json_array_push(out, tm_); \ - fixed++; \ - } \ - for (size_t k_ = 0; k_ < npend; k_++) free(pend[k_].id); \ - free(pend); pend = NULL; npend = 0; \ - } while (0) +/* 给还没被回应的调用补一条占位 tool 消息 */ +#define FLUSH_PENDING(where) \ + do { \ + for (size_t k_ = 0; k_ < npend; k_++) { \ + if (pend[k_].answered) \ + continue; \ + JsonValue* tm_ = json_new_object(); \ + json_object_set_string(tm_, "role", "tool"); \ + json_object_set_string(tm_, "tool_call_id", pend[k_].id); \ + json_object_set_string( \ + tm_, "content", \ + "错误: 这条工具调用没有得到结果 (历史结构被改坏了, " \ + "这里补一条占位). 不要假设它执行过. "); \ + json_array_push(out, tm_); \ + fixed++; \ + } \ + for (size_t k_ = 0; k_ < npend; k_++) \ + free(pend[k_].id); \ + free(pend); \ + pend = NULL; \ + npend = 0; \ + } while (0) for (size_t i = 0; i < json_array_size(messages); i++) { - JsonValue *m = json_array_get(messages, i); - const char *role = json_get_string(m, "role"); + JsonValue* m = json_array_get(messages, i); + const char* role = json_get_string(m, "role"); /* tool 消息: 只留下"确实在等它"的那些, 孤儿丢掉 */ if (role && strcmp(role, "tool") == 0) { - const char *id = json_get_string(m, "tool_call_id"); + const char* id = json_get_string(m, "tool_call_id"); size_t hit = (size_t)-1; for (size_t k = 0; k < npend; k++) - if (!pend[k].answered && id && strcmp(pend[k].id, id) == 0) { hit = k; break; } - if (hit == (size_t)-1) { fixed++; continue; } + if (!pend[k].answered && id && strcmp(pend[k].id, id) == 0) { + hit = k; + break; + } + if (hit == (size_t)-1) { + fixed++; + continue; + } pend[hit].answered = 1; json_array_push(out, json_clone(m)); continue; @@ -654,12 +737,15 @@ JsonValue *agent_repair_tool_pairing(const JsonValue *messages, int *out_fixed) json_array_push(out, json_clone(m)); - if (!role || strcmp(role, "assistant") != 0) continue; - JsonValue *tcs = json_object_get(m, "tool_calls"); - if (!tcs || tcs->type != JSON_ARRAY) continue; + if (!role || strcmp(role, "assistant") != 0) + continue; + JsonValue* tcs = json_object_get(m, "tool_calls"); + if (!tcs || tcs->type != JSON_ARRAY) + continue; for (size_t k = 0; k < json_array_size(tcs); k++) { - const char *id = json_get_string(json_array_get(tcs, k), "id"); - if (!id || !*id) continue; + const char* id = json_get_string(json_array_get(tcs, k), "id"); + if (!id || !*id) + continue; pend = xrealloc(pend, (npend + 1) * sizeof *pend); pend[npend].id = xstrdup(id); pend[npend].answered = 0; @@ -667,120 +753,135 @@ JsonValue *agent_repair_tool_pairing(const JsonValue *messages, int *out_fixed) } } FLUSH_PENDING(); - #undef FLUSH_PENDING +#undef FLUSH_PENDING - if (fixed == 0) { json_free(out); return NULL; } - if (out_fixed) *out_fixed = fixed; + if (fixed == 0) { + json_free(out); + return NULL; + } + if (out_fixed) + *out_fixed = fixed; return out; } /* 这个 4xx 看起来是不是冲着图片来的? * * 降级会把历史里的图片**永久拿掉**, 所以不能见到 4xx 就动手: 一条 - * "tool_calls 后面缺少 tool 消息"的 400 也被当成图片问题处理的话, 图片白丢, - * 而且真正的错误被我们的提示盖住了 —— 排障方向直接被带偏. + * "tool_calls 后面缺少 tool 消息"的 400 也被当成图片问题处理的话, 图片白丢, + * 而且真正的错误被我们的提示盖住了 —— 排障方向直接被带偏. * - * 这里是关键词启发式, 不是精确判断, 所以: - * 1. 它是**兜底**, 主要防线是发送前就把格式和体积查掉; - * 2. 关键词只收那些几乎不可能出现在别处的词; + * 这里是关键词启发式, 不是精确判断, 所以: + * 1. 它是**兜底**, 主要防线是发送前就把格式和体积查掉; + * 2. 关键词只收那些几乎不可能出现在别处的词; * 3. 不确定时**不动手** —— 宁可让错误如实报出来, 也不误删用户的图. */ -static int err_mentions_image(const char *err) +static int +err_mentions_image(const char* err) { - static const char *kw[] = { - "image", "vision", "multimodal", "content type", "media type", - "图片", "图像", "模态", NULL - }; - if (!err) return 0; + static const char* kw[] = { "image", "vision", "multimodal", + "content type", "media type", "图片", + "图像", "模态", NULL }; + if (!err) + return 0; for (size_t i = 0; kw[i]; i++) - if (strcasestr(err, kw[i]) != NULL) return 1; + if (strcasestr(err, kw[i]) != NULL) + return 1; return 0; } -/* 能力探测序列. 独立于对话: 那几次试探请求不带任何历史, 也不进历史. - * 结果按 base_url+model 缓存, 所以只有本进程第一次 (或换了模型)才真发请求. +/* 能力探测序列. 独立于对话: 那几次试探请求不带任何历史, 也不进历史. + * 结果按 base_url+model 缓存, 所以只有本进程第一次 (或换了模型)才真发请求. * * 除了 agent_run 每轮开头, /include 要附带图片时也会调它 —— "确保模型支持" - * 这件事不能靠猜. 探测被关掉时它什么都不做 (结论保持"未测", 一切照旧放行). + * 这件事不能靠猜. 探测被关掉时它什么都不做 (结论保持"未测", 一切照旧放行). * - * force=1 是给 /probe 用的手动触发: 它绕开上面这两条 (开关与缓存). + * force=1 是给 /probe 用的手动触发: 它绕开上面这两条 (开关与缓存). * 两条理由不同: 开关说的是"启动时别自动探测", 不是"禁止探测"; 而缓存是 * "这次进程里已经测过" —— 用户显式敲了 /probe, 就该真的重测一遍, 否则 * 敲了等于没敲. */ -static void ensure_caps(Agent *a, int force) +static void +ensure_caps(Agent* a, int force) { - if (!force && !a->probe_caps) return; /* 关掉探测: 不下任何结论, 一切照旧放行 */ - if (force) cap_cache_clear(); /* 手动触发不吃缓存 */ + if (!force && !a->probe_caps) + return; /* 关掉探测: 不下任何结论, 一切照旧放行 */ + if (force) + cap_cache_clear(); /* 手动触发不吃缓存 */ char cerr[256] = ""; spinner_start(t("spinner.probe")); - int probed = cap_get(&a->llm, 1, &a->caps, a->llm.verbose, cerr, sizeof cerr); + int probed + = cap_get(&a->llm, 1, &a->caps, a->llm.verbose, cerr, sizeof cerr); spinner_stop(); - if (!probed) return; /* 命中缓存, 不用再啰嗦一遍 */ + if (!probed) + return; /* 命中缓存, 不用再啰嗦一遍 */ - /* 一行灰字把结论说清楚. 探测到"不支持"时把**因此做了什么**也写在同一行里, + /* 一行灰字把结论说清楚. 探测到"不支持"时把**因此做了什么**也写在同一行里, * 否则用户只看到"不支持", 不知道我们顺手把工具藏了. */ - const char *vis = - a->caps.vision == CAP_YES ? t("agent.caps.yes") : - a->caps.vision == CAP_NO ? t("agent.caps.no.vision") - : t("agent.caps.unknown"); - const char *tl = - a->caps.tools == CAP_YES ? t("agent.caps.yes") : - a->caps.tools == CAP_NO ? t("agent.caps.no.tools") - : t("agent.caps.unknown"); + const char* vis = a->caps.vision == CAP_YES ? t("agent.caps.yes") + : a->caps.vision == CAP_NO ? t("agent.caps.no.vision") + : t("agent.caps.unknown"); + const char* tl = a->caps.tools == CAP_YES ? t("agent.caps.yes") + : a->caps.tools == CAP_NO ? t("agent.caps.no.tools") + : t("agent.caps.unknown"); char note[320] = ""; if (a->caps.vision == CAP_UNKNOWN || a->caps.tools == CAP_UNKNOWN) snprintf(note, sizeof note, "%s", - tf("agent.caps.note", "why", - cerr[0] ? cerr : t("agent.caps.baseline_failed"), NULL)); + tf("agent.caps.note", "why", + cerr[0] ? cerr : t("agent.caps.baseline_failed"), NULL)); - fprintf(stderr, "%s%s%s\n", col(a, C_DIM), - tf("agent.caps.line", "vision", vis, "tools", tl, "note", note, NULL), - col(a, C_OFF)); + fprintf( + stderr, "%s%s%s\n", col(a, C_DIM), + tf("agent.caps.line", "vision", vis, "tools", tl, "note", note, NULL), + col(a, C_OFF)); } -void agent_ensure_caps(Agent *a) { ensure_caps(a, 0); } -void agent_probe_now(Agent *a) { ensure_caps(a, 1); } +void agent_ensure_caps(Agent* a) +{ + ensure_caps(a, 0); +} +void agent_probe_now(Agent* a) +{ + ensure_caps(a, 1); +} -int agent_run(Agent *a) +int agent_run(Agent* a) { char err[1024]; - /* "这一轮"的定义就在这儿: 一条用户消息引发的一整串工具调用. + /* "这一轮"的定义就在这儿: 一条用户消息引发的一整串工具调用. * 确认提示上的 Always/neVer 只活到这里, 下一条消息进来时复位. */ confirm_reset_turn(); - int img_dropped = 0; /* 图片被服务端拒了只降级一次, 以免来回打转 */ + int img_dropped = 0; /* 图片被服务端拒了只降级一次, 以免来回打转 */ agent_ensure_caps(a); - /* max_steps <= 0 时不限往返次数 (默认). 计数用 long long: 不限次数的会话 * 真的可能跑很多步, 而 int 溢出是 UB. */ - for (long long step = 0; ; step++) { + for (long long step = 0;; step++) { DeltaCtx dc; dc.printed = 0; dc.md = a->md; spinner_start(t("spinner.waiting")); Req req = req_prepare(a); - LlmResponse *resp = llm_chat(&a->llm, req.messages, req.tools, - a->llm.stream ? on_delta : NULL, &dc, - a->llm.stream ? on_think : NULL, NULL, - err, sizeof err); + LlmResponse* resp = llm_chat( + &a->llm, req.messages, req.tools, a->llm.stream ? on_delta : NULL, + &dc, a->llm.stream ? on_think : NULL, NULL, err, sizeof err); req_free(&req); spinner_stop(); - /* 上下文超限**不再自动裁剪历史**. + /* 上下文超限**不再自动裁剪历史**. * * 这里曾经有一整套"丢最老一轮 → 重试, 最多丢 5 轮"的机制 (配套 - * --no-auto-trim / auto_trim / LIGENT_AUTO_TRIM). 它被删掉的理由: + * --no-auto-trim / auto_trim / LIGENT_AUTO_TRIM). 它被删掉的理由: * - * 1. 判断依据只是错误文案里的关键词 ("too long"、"上下文"、"超出"…), + * 1. 判断依据只是错误文案里的关键词 ("too long"、"上下文"、"超出"…), * 任何 4xx 蹭到这些词都会触发裁剪 —— 而裁剪是把最老的几轮从历史里 - * **真的删掉**, 不可逆. 用一句猜出来的判据去毁掉对话记录, 代价不对等. + * **真的删掉**, 不可逆. 用一句猜出来的判据去毁掉对话记录, + * 代价不对等. * 2. 它把"历史太大"这个本该由用户决定的事 (/back 摘掉哪一轮、/reset * 重来、还是换个窗口更大的模型)自动化掉了, 用户看到的是对话莫名其妙 - * 少了几轮. + * 少了几轮. * * 现在超限就是如实报错, 历史一个字节都不动, 怎么处理由用户决定. */ @@ -788,30 +889,29 @@ int agent_run(Agent *a) * * 格式和体积本地已经拦了, 但服务端还可能因为别的原因不收 (图片损坏、 * 这个变体不支持……). 那条引用躺在历史里, 不修的话之后每一轮都会以 - * 同样的 4xx 失败、历史却原样保留 —— 用户只剩 /reset 一条路. + * 同样的 4xx 失败、历史却原样保留 —— 用户只剩 /reset 一条路. * 这里宁可丢掉这些图, 也不能让整段对话死在这儿. */ - if (!resp && !dc.printed && !img_dropped && - agent_is_http_4xx(err) && image_has_refs(a->messages) && - err_mentions_image(err)) { + if (!resp && !dc.printed && !img_dropped && agent_is_http_4xx(err) + && image_has_refs(a->messages) && err_mentions_image(err)) { int n = image_degrade_all(a->messages); if (n > 0) { img_dropped = 1; fprintf(stderr, "%s%s%s\n", col(a, C_DIM), - tf("agent.img.rejected", "n", i18n_num(n), NULL), - col(a, C_OFF)); - /* 模型那边只收到一句能照做的中文; 原始报错留在这里, + tf("agent.img.rejected", "n", i18n_num(n), NULL), + col(a, C_OFF)); + /* 模型那边只收到一句能照做的中文; 原始报错留在这里, * 以免"为什么被拒"这个信息彻底消失 */ fprintf(stderr, "%s%s%s\n", col(a, C_DIM), - tf("agent.img.server_said", "error", err, NULL), - col(a, C_OFF)); + tf("agent.img.server_said", "error", err, NULL), + col(a, C_OFF)); md_finish(a->md); dc.printed = 0; spinner_start(t("spinner.waiting")); Req req3 = req_prepare(a); resp = llm_chat(&a->llm, req3.messages, req3.tools, - a->llm.stream ? on_delta : NULL, &dc, - a->llm.stream ? on_think : NULL, NULL, - err, sizeof err); + a->llm.stream ? on_delta : NULL, &dc, + a->llm.stream ? on_think : NULL, NULL, err, + sizeof err); req_free(&req3); spinner_stop(); } @@ -820,88 +920,97 @@ int agent_run(Agent *a) if (!resp) { md_finish(a->md); fprintf(stderr, "%s%s%s\n", col(a, C_RED), - tf("agent.error", "error", err, NULL), col(a, C_OFF)); + tf("agent.error", "error", err, NULL), col(a, C_OFF)); return -1; } if (resp->interrupted) { md_finish(a->md); - if (dc.printed) fputc('\n', stdout); + if (dc.printed) + fputc('\n', stdout); printf("%s%s%s\n", col(a, C_DIM), t("agent.interrupted"), - col(a, C_OFF)); + col(a, C_OFF)); /* 把已经生成的部分内容留在历史里, 保持上下文连贯 */ json_array_push(a->messages, json_clone(resp->message)); llm_response_free(resp); return 1; } - /* 助手回复走 Markdown 渲染; 工具输出保持原样, 避免破坏代码/命令结果. + /* 助手回复走 Markdown 渲染; 工具输出保持原样, 避免破坏代码/命令结果. * - * 判据是"流式回调有没有真的打印过", 而不是"配置是不是流式模式": - * 服务端可能忽略 stream 参数直接返回整包 JSON (llm.c 会回退解析), - * 这种情况下不会有任何 delta 回调, 必须在这里补打印, + * 判据是"流式回调有没有真的打印过", 而不是"配置是不是流式模式": + * 服务端可能忽略 stream 参数直接返回整包 JSON (llm.c 会回退解析), + * 这种情况下不会有任何 delta 回调, 必须在这里补打印, * 否则内容被解析出来却永远不显示. */ int wrote_text = dc.printed; if (!wrote_text) { - const char *content = json_get_string(resp->message, "content"); + const char* content = json_get_string(resp->message, "content"); if (content && *content) { md_feed(a->md, content); wrote_text = 1; } } md_finish(a->md); - if (wrote_text) fputc('\n', stdout); + if (wrote_text) + fputc('\n', stdout); fflush(stdout); if (resp->incomplete) { fprintf(stderr, "%s%s%s\n", col(a, C_DIM), - tf("agent.stream.broken", "why", - resp->incomplete_reason ? resp->incomplete_reason : "", - NULL), - col(a, C_OFF)); + tf("agent.stream.broken", "why", + resp->incomplete_reason ? resp->incomplete_reason : "", + NULL), + col(a, C_OFF)); } if (resp->usage) { - a->tok_prompt += (long long)json_get_number(resp->usage, "prompt_tokens", 0); - a->tok_completion += (long long)json_get_number(resp->usage, "completion_tokens", 0); - a->tok_total += (long long)json_get_number(resp->usage, "total_tokens", 0); - a->tok_last_prompt = (long long)json_get_number(resp->usage, "prompt_tokens", 0); + a->tok_prompt + += (long long)json_get_number(resp->usage, "prompt_tokens", 0); + a->tok_completion += (long long)json_get_number( + resp->usage, "completion_tokens", 0); + a->tok_total + += (long long)json_get_number(resp->usage, "total_tokens", 0); + a->tok_last_prompt + = (long long)json_get_number(resp->usage, "prompt_tokens", 0); a->usage_reported = 1; } a->api_calls++; json_array_push(a->messages, json_clone(resp->message)); - JsonValue *tool_calls = json_object_get(resp->message, "tool_calls"); - int has_tools = a->use_tools && tool_calls && - json_array_size(tool_calls) > 0; - if (has_tools) tool_calls = json_clone(tool_calls); + JsonValue* tool_calls = json_object_get(resp->message, "tool_calls"); + int has_tools + = a->use_tools && tool_calls && json_array_size(tool_calls) > 0; + if (has_tools) + tool_calls = json_clone(tool_calls); llm_response_free(resp); if (!has_tools) { if (step == 0 && a->llm.stream && !dc.printed) { /* 既没输出也没调工具, 提示一下, 避免看起来像卡死 */ - const char *c = json_get_string( - json_array_get(a->messages, json_array_size(a->messages) - 1), + const char* c = json_get_string( + json_array_get(a->messages, + json_array_size(a->messages) - 1), "content"); if (!c || !*c) printf("%s%s%s\n", col(a, C_DIM), t("agent.empty_reply"), - col(a, C_OFF)); + col(a, C_OFF)); } return 0; } int rc = execute_tool_calls(a, tool_calls); json_free(tool_calls); - if (rc != 0) return 1; + if (rc != 0) + return 1; /* 只有设了正数上限才可能"到顶". 不限时这一轮一直跑到模型自己收尾 * (或用户 Ctrl-C)—— 每一次工具调用都会回到这个循环顶端, 中断检查 * 分别在 llm_chat 和 execute_tool_calls 里. */ if (a->max_steps > 0 && step + 1 >= a->max_steps) { fprintf(stderr, "%s%s%s\n", col(a, C_DIM), - tf("agent.max_steps", "n", i18n_num(a->max_steps), NULL), - col(a, C_OFF)); + tf("agent.max_steps", "n", i18n_num(a->max_steps), NULL), + col(a, C_OFF)); return 0; } } diff --git a/src/i18n.c b/src/i18n.c index 5963c1b..4972048 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -684,8 +684,8 @@ M("agent.caps.baseline_failed", "the baseline request did not go through", "基线请求未通过"), M("agent.caps.line", - "model capabilities: multimodal (image input) {vision} * tool calls {tools}{note}", - "模型能力: 多模态 (图片输入){vision} * 工具调用 {tools}{note}"), + "model capabilities: \n image input {vision} \n tool calls {tools}{note}", + "模型能力: \n 图片输入 {vision} \n 工具调用 {tools}{note}"), M("agent.caps.no.tools", "no (tool table no longer sent)", "不支持 (不再发送工具表)"), @@ -694,7 +694,7 @@ M("agent.caps.no.vision", "不支持 (已隐藏 read_image)"), M("agent.caps.note", " — anything untested is treated as supported ({why})", - " —— 没测出来的一律按「支持」处理 ({why})"), + " —- 没测出来的一律按 \"支持\" 处理 ({why})"), M("agent.caps.unknown", "untested", "未测"), @@ -1141,7 +1141,7 @@ M("meta.help", "/help 显示本帮助\n" "\n" "其余输入都是给 meta 模型的指令, 例如: \n" - " 把第 3 轮里我说的 "用 Python" 删掉\n" + " 把第 3 轮里我说的 \"用 Python\" 删掉\n" " 第 5 轮助手那个结论是错的, 改掉\n" " 第 2 到 4 轮在绕圈子, 压成一段\n"), M("meta.turn_failed", diff --git a/src/i18n.h b/src/i18n.h index 1d724c4..e617e76 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -45,7 +45,7 @@ const char *t(const char *key); /* 带占位符: 模板里写 {name}, 调用时按名字成对给值, 最后传 NULL. * tf("err.unknown_option", "option", arg, NULL) - * 按名字匹配 (不是按位置), 所以译文可以调整语序. */ + * 按名字而非位置匹配, 所以译文可以调整语序. */ const char *tf(const char *key, ...); /* 整数转字符串 (静态轮转缓冲), 专供 tf 的参数位置用: diff --git a/src/markdown.c b/src/markdown.c index 9254ceb..18588e8 100644 --- a/src/markdown.c +++ b/src/markdown.c @@ -6,47 +6,34 @@ */ #include "markdown.h" #include "util.h" - +#include "sgr.h" #include #include #include -/* 样式定义 (硬编码 SGR, 不依赖 terminfo) - * - * **只用 16 色范围内的码**: 前景 30-37, 加粗/暗淡/斜体用 1/2/3. - * 不用 90-97 那一段 (aixterm 的"亮色")—— 它虽然到处都是, 但严格讲不属于 - * 最窄的 16 色 ANSI, 而这段代码是硬编码 SGR、不做终端能力探测的 (连 terminfo - * 都不查). 既然不探测, 就该待在**所有终端都认**的那一档里. - * - * 原来链接用的是亮蓝 (aixterm 的 94), 是这里唯一一个越界的码. 改成 - * "\033[4;36m" (下划线 + 青): 链接在终端里的传统表示本来就是下划线, - * 而且青色和行内代码、列表符号同色系, 视觉上"这些是标记不是正文"是一致的. - * 更重要的是下划线是**属性** (SGR 4), 比颜色更可靠 —— 单色终端、色盲用户、 - * 以及把颜色重映射过的主题里, 下划线都还在. - * - * (这里不写出那个越界码的字面量, 以免以后 grep `\[9[0-7]` 做审计时被注释 - * 里的这一行报成假阳性. ) */ -#define MD_RESET "\033[0m" -#define MD_BOLD "\033[1m" -#define MD_DIM "\033[2m" -#define MD_ITALIC "\033[3m" -#define MD_CODE "\033[36m" /* 行内代码: 青 */ -#define MD_BULLET "\033[36m" -#define MD_LINK "\033[4;36m" /* 链接文字: 下划线 + 青 */ -#define MD_H1 "\033[1;36m" /* 粗体青 */ -#define MD_H2 "\033[1;35m" /* 粗体品红 */ -#define MD_H3 "\033[1;34m" /* 粗体蓝 */ -#define MD_H4 "\033[1m" /* 粗体 */ +#define MD_RESET C_OFF +#define MD_BOLD C_BOLD +#define MD_DIM C_DIM +#define MD_ITALIC C_ITALIC +#define MD_CODE C_CYAN +#define MD_BULLET C_CYAN +#define MD_LINK C_UNDERLINE C_CYAN +#define MD_H1 C_BOLD C_CYAN +#define MD_H2 C_BOLD C_MAGENTA +#define MD_H3 C_BOLD C_BLUE +#define MD_H4 C_BOLD /* 等待更多数据 (当前字节可能是一个被切断的标记) */ #define MD_NEED_MORE ((size_t)-1) /* 待处理缓冲上限: 遇到始终等不到结束标记的超长行时直接吐出, 保证内存有界 */ #define MD_MAX_PENDING (64 * 1024) + /* 候选表格行超过这个长度就不再当表格等下去, 避免长行被卡住 */ #define MD_MAX_TABLE_LINE 4096 + /* 表格累积多少行后强制渲染 (防止超大表格一直不输出) */ -#define MD_MAX_TABLE_ROWS 200 +#define MD_MAX_TABLE_ROWS 256 struct MdRenderer { int enabled; diff --git a/src/meta.c b/src/meta.c index c91da35..e0d45c3 100644 --- a/src/meta.c +++ b/src/meta.c @@ -31,7 +31,7 @@ static const char *META_SYSTEM_PROMPT = "\n" "## 为什么会有你\n" "用户想改这段历史, 通常是因为助手沿着一个他不满意的方向走下去了. 他要的\n" -"往往不是 "省 token" , 而是**让这段历史不再诱导助手重蹈覆辙**——\n" +"往往不是 \"省 token\" , 而是**让这段历史不再诱导助手重蹈覆辙**——\n" "删掉那句说错的前提、去掉那个被推翻的方案、删掉助手那次自信但错误的结论. \n" "另外 你拥有修改assistant的消息的能力, 这是常规的修改所不能完成的\n" "\n" @@ -41,7 +41,7 @@ static const char *META_SYSTEM_PROMPT = "## 工作方式\n" "1. 动手前先看清楚. 先 list_rounds 看目录、再 read_messages 读原文. \n" " 不要凭用户的转述猜内容. \n" -"2. 改最小必要范围. 用户说 "删掉那句用 Python" , 就删那一句, \n" +"2. 改最小必要范围. 用户说 \"删掉那句用 Python\" , 就删那一句, \n" " 不要顺手把整轮都删了. \n" "3. 用户没要求压缩就不要压缩 —— 压缩会丢证据. \n" "4. 每次改动后消息编号会重排. 要继续改, 先重新 list_rounds. \n" @@ -49,7 +49,7 @@ static const char *META_SYSTEM_PROMPT = "\n" "## 红线\n" "- 带 tool_calls 的 assistant 消息和紧随其后的 tool 结果是不可拆的整体, 否则会造成400错误. \n" -"- 不要为了 "整齐" 重排顺序. \n" +"- 不要为了 \"整齐\" 重排顺序. \n" "\n" "## 拿不准就问\n" "如果删掉某个点会让后面的回答失去上下文, 把这个后果指出来, 让用户决定. \n" diff --git a/src/sgr.h b/src/sgr.h index eb1cc72..890bb01 100644 --- a/src/sgr.h +++ b/src/sgr.h @@ -37,11 +37,3 @@ #define C_BRIGHT_MAGENTA "\033[95m" #define C_BRIGHT_CYAN "\033[96m" #define C_BRIGHT_WHITE "\033[97m" - -#define C_OFF "\033[0m" // 复原 -#define C_BOLD "\033[1m" // 粗体 -#define C_DIM "\033[2m" // 半亮度 -#define C_ITALIC "\033[3m" // 斜体 -#define C_CYAN "\033[36m" // -#define C_GREEN "\033[32m" -#define C_RED "\033[31m"