Skip to content

v1.4.13

Choose a tag to compare

@shibing624 shibing624 released this 20 Aug 11:14
· 56 commits to main since this release

Agentica v1.4.13

Web 换成 Vite + React SPA(新增轨迹页、账号密码登录、界面可切中英文),并第一次提供 Desktop App 安装包

安装: pip install -U agentica==1.4.13(网页版:pip install -U "agentica[gateway]"

下载桌面版

系统 安装包
macOS 11+ Apple 芯片(arm64) · Intel(x64)
Windows 10+ x64 安装程序(NSIS)
Linux x64 AppImage · deb

桌面版是个壳,服务端仍是 Python gateway:装应用之前先 pip install -U "agentica[gateway]"。安装包未签名,首次启动被系统拦下时,按 README 里对应系统的一步操作解除。

完整对比: v1.4.12...v1.4.13


[1.4.13] - 2026-08-20

features

  • 新增 Desktop App 安装包.github/workflows/desktop.yml,推 v* tag 时构建并挂到该 tag 的 Release):macOS dmg(Apple 芯片 / Intel 各一个)、Windows x64 NSIS 安装程序、Linux x86_64 AppImage 与 amd64 deb,命名统一为 agentica-desktop-<platform>-<arch>.<ext>,README 的下载链接走 releases/latest/download/… 因此永远指向最新版。三个 OS 各跑一个 runner 不是为了并行快——NSIS 装包要 Windows、deb/AppImage 要 Linux,一台 macOS 开发机最多只能出那两个 dmg,所以这件事只能在 CI 做。构建未签名(Apple / EV 证书是账号问题,不是构建问题),因此 README 给了三条一次性解除拦截的步骤:macOS 的 xattr -rd com.apple.quarantine、Windows SmartScreen 的「仍要运行」、Linux AppImage 的 chmod +x安装包里没有 Python:桌面版是壳,服务端仍是 gateway,所以下载区把 pip install -U "agentica[gateway]" 写成前置要求而不是脚注。
  • 打包图标收敛成一个 1024² 母版desktop/build/icon.png,由 desktop/make_icon.pydocs/assets/logo.png 裁出那只挥手猫重建)。electron-builder 从一个正方形母版派生所有平台的图标且拒收小于 512 的,所以既有的 256² cat.png 和 48² favicon.ico 都不够用;顺带把运行期的两文件分支(Windows 用 ICO、其它用 PNG)也合成这一个——nativeImage 只在 Windows 上解 ICO,拿错平台得到的是空图,而空图和「没配图标」看起来完全一样。打包后的应用不再从磁盘读图标:安装包已经把它放到平台真正会看的位置(.app bundle / exe 资源 / .desktop),而从 app.asar 里读图不是 nativeImage 承诺的能力。端到端验证 tmp/desktop_package_smoke.py(跑真实构建产物:bundle 元数据、asar 载荷、图标、启动、收尸,17 项)。
  • Web / 桌面版界面改为默认英文,设置 › 常规 › 语言可切简体中文:此前界面文案是中文硬编码在 JSX 里的,一个开源项目的默认入口不该假定读者的语言,而想改也无处可改。新增 web/src/i18n.tsen 是唯一的字符串源,zhconst zh: Strings 声明成同一个类型,所以少一个键就是 tsc --noEmit 失败,而不是中文界面里冒出一个英文词(npm run build 已经先跑类型检查)。带数字或人名的条目一律写成函数(round: (n) => …),因为两种语言的语序不同——在调用点拼模板串是没法翻译的。选择存 localStorage.ag_lang(与主题同一套做法)并写到 <html lang>切换即时生效、不用刷新;语言选项各用自己的语言标注(English / 简体中文),落错语言的人才读得懂怎么切回去。故意不翻的是标识符:日志事件类型(tool_callrequest_begin)、配置字段名(base_urlmax_tokens)、审批模式与 profile 名——这些用户也要照着敲和 grep。桌面壳自己那几句(启动失败弹窗、View 菜单)也改成英文并且不跟随这个设置:它们要么是 Electron 按系统语言自己本地化的 role,要么是 gateway 没起来时弹的框——那时根本没有渲染进程可以读这个偏好。端到端验证在 tmp/web_i18n_smoke.py(真实窗口里点真的那颗设置按钮,23 项)。
  • 首次启动自动建 admin 账号并生成密码,网页登录不再需要 ?token=:URL 里带令牌那套对个人本机是最差的一种进门方式——令牌进程级、一重启就换,粘错一个字符只会得到 401,而桌面版根本不需要它(壳自己用本机令牌换会话),于是同一个产品的两个入口体验完全不同。现在首启动幂等地 seed 一个 adminAccountStore.seed_admin()),密码随机生成、以方框形式打进启动日志,同时写一份 0600$AGENTICA_HOME/gateway/initial-password 明文——重启后日志滚走了还得有地方能查。密码最短 6 位(原先 8 位;这是本机个人服务,不是公网多租户),GET /api/auth/status 增加 account_id / password_is_initial / min_password_length 三个字段,前端的下限跟着服务端走而不是自己写死一份。改密(终端或网页)会把 password_is_initial 落成 false 并删掉那份明文,之后启动日志只提示怎么登录、不再复述密码。相应地 --host 非 loopback 的守卫从「没设密码就拒绝」收紧成「密码还是生成的那个就拒绝」——一个印在日志里的密码暴露到网络上和没有密码没什么区别。
  • /traces/chat 合到同一个主页,「轨迹观测」改名「轨迹」:轨迹页原先是独占整屏、靠一个「返回」链接回聊天的独立页面,切过去等于离开产品。新抽出 web/src/components/AppShell.tsx(左侧导航 + 账号浮层 + 全局弹窗)由两个页面共用,会话选择器成为中间栏——和聊天的会话树同一个位置,所以「挑一个会话看它怎么跑的」不再需要先记住会话名。
  • 轨迹的耗时可视化从「按比例堆叠的阶段条」换成时序时间线:堆叠条只回答「思考占了几成」,回答不了「四个工具是并发还是排队」——而这正是看轨迹的主要目的。现在每轮画一条模型泳道加每个工具调用各一条泳道(按调用时刻排序、带序号),横轴是这一轮的真实时间,条的左边缘就是它开始的时刻,所以并发的批次一眼看得出是叠着的、串行的是错开的;轴上 5 个刻度标相对耗时,下方图例四色(思考 / 模型回复 / 工具参数生成 / 工具执行)。图例里不再重复各阶段的求和时长——它和横轴讲的是两件事,并排放只会让人把总和当成跨度读。此前唯一的进门方式是启动日志里那条 ?token=…,令牌是进程级的、一重启就变,也没法只吊销一个浏览器。现在 cookie 里装的是会话而不是令牌:会话存 $AGENTICA_HOME/gateway/auth.json0600,只存 sha256 摘要),7 天有效、最后一天内的请求自动续期,重启 gateway 不掉线。密码用 hashlib.scrypt 存(scrypt$n$r$p$salt$hash,参数随 hash 走,以后调高成本不作废旧密码),连续失败 5 次后按 1s/2s/4s… 上限 60s 退避并返回 429 + Retry-After。设置方式两条:终端 agentica-gateway --set-password,或网页 设置 › 常规 › 访问(改密码会让其他所有浏览器退出登录——会用到改密码的场景就是「cookie 可能被人拿到了」)。首次启动自动建 admin 账号并生成一个密码(下一条),所以启动日志不再打印 ?token=…;未登录访问 /chat / /traces 会 302 到 /login,API 返 401 并由前端跳转。机器令牌保留但改为只用来兑换会话(脚本仍可直接 Authorization: Bearer),令牌持有者改密码不需要旧密码——能读 0600 文件的人已经拥有这台机器。新增免凭据端点 GET /api/auth/status(门自己不能锁在门后),以及 /api/auth/{login,logout,password}
  • --host 不是 loopback 时,没设密码直接拒绝启动(退出码 2,提示怎么设)。原先 --host 0.0.0.0 只靠令牌门挡着,而令牌是打印在终端里、不重启改不了的凭据,不适合暴露到网络。GATEWAY_AUTH=false 不受这条约束:显式关门是明确意图,只给一条醒目告警——按项目既有的分工,显式参数可以报错,环境值只降级告警。
  • 带 cookie 的写请求要求 application/jsonX-Agentica-Client(CSRF 第二道防线,主防线仍是 SameSite=Lax):HTML 表单只能发三种 Content-Type、也伪造不出自定义头,所以这一条挡住的正是「你打开的某个网页拿你的 cookie 调本机 API」。/api/upload 必须是 multipart,因此前端给它带上了这个头。用 header 递交令牌的脚本不受影响(curl -d 默认就是表单类型,而表单发不出 Authorization)。
  • Gateway 加本机 token 鉴权,默认开启/api/*/ws 现在要带令牌。/api 能切 profile、读任意路径、跑 execute,此前只要连得上端口就全都开着。取令牌的方式是 Jupyter 那套:启动日志打印 http://127.0.0.1:8881/chat?token=…,第一次打开时服务端把 query 换成会话 cookie(agentica_session,HttpOnly + SameSite=Lax),之后直接开 /chat 书签即可,令牌也不再留在地址栏。非浏览器客户端用 Authorization: Bearer <token>X-Agentica-Token。令牌每次启动随机,AGENTICA_GATEWAY_TOKEN 可固定;GATEWAY_AUTH=false 整道门关掉。明确豁免(各有不得不豁免的理由):/webhook/*(飞书等第三方回调自带签名,无法携带我们的令牌,拦了等于让 IM 下线)、/health/api/health(就绪探针在知道令牌之前就要跑)、//assets/*(编译产物,无用户数据)以及 CORS 预检的 OPTIONS(按规范不带凭据)。/wsparams.auth.token 一直写在协议里但从未校验,现在生效,另外也认 handshake 的 cookie / ?token=CORS 同时收紧到 loopback:Starlette 对 allow_origins=["*"] + allow_credentials=True 的实现是把请求的 Origin 原样回显,令牌一进 cookie,任何你正好打开的网页就都能读这个 API 并调工具——这不是顺手加固,是这次改动必须一起做的部分。Vite 开发端口仍在放行的正则里。
  • Gateway --port 0 选空闲端口并把实际端口写出来,新增 --parent-pidagentica-gateway 从只读环境变量改为有真正的命令行参数(--host / --port / --parent-pid)。--port 0 由持有 socket 的一方解析——先 bind 再回读端口号交给 uvicorn,而不是「问系统要个空闲端口再把号码传下去」(那样中间的空窗期会被别的进程抢走)。运行进程把 pid / host / port / token / url 写到 $AGENTICA_CACHE_DIR/gateway/runtime.json0600,退出时删除,且只删自己那条),这样桌面壳或另一个终端才回答得出「这台机器上是否已经有 gateway、在哪个端口、令牌是什么」。--parent-pid <pid> 让 gateway 在指定进程消失后自杀(轮询,因为父进程被 SIGKILL 时它自己的清理代码根本不会运行),避免壳被强杀后留下一个占着端口和会话锁的孤儿。端口被占时报错会指出是哪个 pid 在服务哪个地址,而不是抛一段 traceback。
  • 新增 desktop/:Electron 薄壳(阶段二第一版,与 docs/learn_cc/web_v2.md 第 5 节一致)。壳只做机制:单实例锁、先 attach 再 spawn(同一 ~/.agentica 上已有 gateway 就连过去,不起第二个,也绝不在退出时杀掉不是自己起的那个)、拉起 agentica-gateway --port <上次那个> --parent-pid <自己>、等 runtime 记录与 /api/health 就绪、开窗口。窗口是普通浏览器:无 preload、无 IPC、nodeIntegration:false + sandbox:true,只有本实例自己的 origin 留在窗口里,其余(包括别的本机端口)交给系统浏览器。令牌先兑换成会话再通过 Electron 的 cookie API 注入,拼进 URL,所以渲染进程读不到它。两个 GUI 特有的坑一并处理:从 Dock 启动的应用不继承 shell PATH(conda/venv 里的 agentica-gateway 因此找不到),所以走登录 shell 解析,AGENTICA_GATEWAY_BIN 可直接指定;GUI 启动没有有意义的 cwd(从 Finder 启动是 /,agent 会把文件系统根目录当项目目录),所以子进程的 cwd 固定为用户 home。cd desktop && npm install && npm start;打包成安装包不在本次范围。
  • 桌面壳:端口粘滞、崩溃退避重启、优雅收尸、原生菜单。四条都是「壳是全产品里最难更新的一层」的直接推论——这里的 bug 要等下一个安装包,所以能想到的都在第一版做掉:端口粘滞,SPA 的会话树/当前会话/主题都在 localStorage,而它按 origin 隔离、origin 就是 127.0.0.1:<port>,纯 --port 0 会让每次启动的侧边栏都是空的(会话还在盘上,只是这个 origin 没见过);壳把真正绑定成功的端口记在自己的 userData 里,下次优先要它,被占了才退回 0,不引入任何固定端口(写死 8881 会撞用户自己起的 gateway,症状是白窗)。崩溃重启退避 1s/2s/4s,三次放弃并弹窗,连续健康一分钟重置额度。退出先请求 POST /api/desktop/shutdown(仅限令牌持有者)再 SIGTERM 再 SIGKILL,且 before-quit 会等到子进程真的没了才放行——Windows 上 kill() 是硬 TerminateProcess,IM 渠道不会断开、peers 记录不会清理。菜单给回 reload / force-reload / DevTools:没有菜单的窗口根本没法刷新,而做成页面能调的 bridge 就等于加了一个只有桌面版才有的能力。渲染进程崩溃会自动重载。新增 AGENTICA_DESKTOP_SMOKE=1:壳自己打印一行 DESKTOP-SMOKE-RESULT {json}(窗口 URL / 标题 / 是 spawn 还是 attach / 子进程 pid / 探到的 SPA 元素)然后走正常退出路径退出,所以自动化验的收尸就是用户实际经历的那条。
  • README 前置评测图,强调 CLI / Web / Desktop:主 README 开篇放 Polyglot 与 DABench 两张对照图;去掉 /goal 长任务介绍;自进化流程图改到 Skills 文档。DABench 对照图补上准确率,配色与 Polyglot 图一致。
  • 评测页换成 DABench 全量 257 题对照:公开表用 Agentica 20260820-153724(220/257,均 12.6s,499 次工具,输入 3.87M)对 Codex 20260820-134628(215/257,均 25.7s)。results/ 入库该跑次的 summary.json / predictions.jsonl。同一 Responses + deepseek-v4-flash-official + reasoning.effort=high
  • DABench 沿用 Polyglot 的评测 agent 面,并收紧 data-analysis prompt:核对过公开 DAEval validation(257 题):每题都有且只有一个 file_name(全是 csv),harness 把它拷进空 workdir,题面从不要求列目录或检索仓库。因此共用 build_coding_agent(无 todo,schema 去掉 ls / glob / grep)是按题目结构去的,不是照搬 Polyglot。prompt 点名 CSV、禁止多余清洗、算出 @tag 立刻停。Agentica / Codex 全量都走 Responses + 同一模型 + reasoning.effort=high(Codex 写进隔离 CODEX_HOMEwire_api / model_reasoning_effort;summary 两边都记下这两项,不再把 Codex 标成 wire_api: null)。
  • 评测增加 InfiAgent-DABench(data analysis)--bench dabench。公开 DAEval validation,agent 读 CSV 用 pandas/sklearn,@name[value] 封闭题判分(与官方 eval_closed_form.py 同口径)。--agent agentica|codex|claude 与 Polyglot 同一套 CLI 包装。--dry-run --bench dabench 不调 LLM、不下载。
  • 评测 Polyglot 默认 Responses,并收紧 coding agent 工具面--agent agentica 默认 --wire-api responses。评测 agent 关掉 todo,并从 schema 去掉 ls / glob / grep / undo_edit / apply_patch / request_path_access。题目已经点名要改的文件和测试文件;prompt 要求直接读、pytest 全绿立刻停,不再 list、不再复读、不加额外测试。
  • 评测页换成 Responses 34 题对照:公开表与柱状图用 Agentica 20260819-195326(34/34,均 43.7s,139 次工具,输入 1.24M)对 Codex 20260817-215956results/ 入库该跑次的 summary.json / predictions.jsonl
  • Gateway Web 换成 Vite + React SPA,并加上 Session Tracegateway/static 的 petite-vue 页面删除;源码在 web/,发版 npm run build 写入 agentica/gateway/ui/pip install agentica[gateway] 运行时不需要 Node。/chat 书签仍进产品。Runner 往同一份 Session JSONL 追加 type=event 生命周期(request_begin/end、thinking/text/tool_calltoken_usage),load() / resume 不把它们喂给模型。Gateway GET /api/sessions/{id}/trace/analysis 读时分析;SPA /traces 只画。旧 session 没有新事件时只给事件列表,不强行画时间轴。Desktop / Electron 不在本阶段。
  • Web SPA 补齐并超过旧 petite-vue 页面的功能面:旧页面的三块功能在第一版 SPA 里只剩壳子,等于换实现就把它们撤了,所以逐个补回并加长:Profiles 在设置弹窗里做全 CRUD(新建/编辑/切换/删除,provider·model·base_url·api_key·auxiliary model·tuning·ENV;api_key 回读时按 sk-d…xx1 掩码,留空即保持不改),Scheduled 全 CRUD(新建/编辑/删除/暂停/恢复/立即运行/运行历史),Plugins 分三页(Skills 全 CRUD 直接编辑 SKILL.md 正文、MCP 增删、Tools 检索,一个搜索框同时过滤三类)。共享层从组件里抽出来:web/src/data.ts(各面板刷新自己的列表)、web/src/sessions.ts(会话列表/归档/重放)、统一 askConfirm 确认弹窗(删除不再走浏览器原生 confirm)。npm run build 现在先 tsc --noEmit 再打包——类型错误以前只会在打出来的包里静默生效。
  • /traces 从「事件列表」重写成按会话分层的执行轨迹:左侧会话栏(同一工作目录下的 CLI 会话一并出现,Web 与 CLI 共用一份 session 日志),右侧全局统计(轮次、输入/输出 token 与缓存命中、成本、工具调用成功/失败、等待审批耗时、总耗时与模型耗时、输出 TPS),下面按(用户一次提问到终答)分卡片:分环节耗时条(思考 / 模型回复 / 工具参数生成 / 审批等待 / 工具执行 / 其它)、模型与每个工具各占一条的泳道图、以及本轮完整事件流。事件逐条可展开:system prompt 全文、thinking 推理正文、tool_call 的参数(格式化 JSON)、工具输出、token_usage 明细,另有「全部展开」和逐条复制。为此 Runner 的 type=event 补齐 session_meta(模型/provider/上下文窗口/工具数)、tool_list_readysystem_prompt(每会话去重一次),token_usageinput 改成与 cache_read 不重叠的口径,成本按模型 id 估算。轮次划分靠日志里的因果顺序,所以用户消息改为在 request_begin 之前落盘(原先在之后,一轮的事件会被算到下一个问题名下)。
  • 评测 --wire-api responses 与 Claude /v1/messages--agent agentica --wire-api responsesOpenAIResponses--agent claude--base-url 时写隔离 CLAUDE_CONFIG_DIR + ANTHROPIC_BASE_URL(去掉 OpenAI 兼容地址尾部的 /v1,Claude Code 再拼 /v1/messages),并设 ANTHROPIC_AUTH_TOKEN--bare 仍要 API key,Bearer 给走 Anthropic 协议的网关),不读 ~/.claude
  • --agent codex 认 Responses 关思考写法--extra-body '{"reasoning": {"effort": "none"}}' 写入隔离 CODEX_HOMEmodel_reasoning_effort。以前只读 reasoning_effort,缺这个键就默认 high,关思考传不进去。也认 thinking_enabled: false
  • README 增加 Polyglot 评测对照,对比表去掉 Gemini CLI:数字与 docs/guides/benchmark.md 同一跑次(Agentica / Codex 各 34/34)。原始 summary.json / predictions.jsonlevaluation/code_benchmark/results/
  • 系统 skill 只在 CLI/gateway 物化到 $AGENTICA_HOME/skills/.system/:包内 agentica/skills/bundled/agenticamulti-agent)仍是源;产品入口 load_system_skills() 按内容哈希同步到隐藏的 .system(升级覆盖)。SDK 的 load_skills() / Agent() / DeepAgent() / SkillTool(auto_load=True) 默认不扫 bundled、也不扫 .system,同机跑过 CLI 留下的文件不会漏进库调用。要覆盖内置:在 skills/<name>/ 写同名 user skill。
  • Layer 1 / Layer 2 压缩可关,默认仍开ToolConfig.enable_evict(淘汰旧工具结果)和 ToolConfig.enable_auto_compact(窗口满时自动摘要,含原生 compact 与 prompt_too_long 后的 reactive)。SDK 传 ToolConfig(...);CLI --no-evict / --no-auto-compact(也认 --evict / --auto-compact),以及 config.yamlsettings.enable_evict / settings.enable_auto_compact(gateway 同样读)。关掉自动摘要后 /compact 仍可用,跨 provider fallback 仍会压 portable transcript。两层都关则超窗时把 provider 错误原样抛出。
  • 无 Docker 的 coding-agent 评测入口 evaluation/code_benchmark/:本机跑 Aider Polyglot(Python 子集,agent 改文件 + pytest 判分)、LiveCodeBench(单轮生成基线)、BigCodeBench、EvalPlus(HumanEval+ 管道 smoke)。官方 Aider runner 绑 Docker,这里只用 polyglot-benchmark 题目和本机 pytest,分数可对表但不能直接贴官方榜。run.py --dry-run 不调 LLM,用 stub/canonical 自检判分;正式跑把 AGENTICA_HOME 指到输出目录,不写 ~/.agentica
  • coding-agent 评测补齐 TB2.1/Pro 风格指标summary.json 除 accuracy 外必报 wall-clock/task、tool calls 与 API calls(分列,且 tasks[] 按题罗列)、crash/timeout rate、completion honesty(声称 tests pass 但判分未过);另报 false-edit collateral(git diff 任务无关文件/行)、error recovery、human intervention、cache hit rate,以及 model / input·fresh·cached·output tokens / cost。Polyglot 在题目目录 git init 快照 stub,agent 跑完再 diff。--agent claude|codex 用同一套 pytest 包 Claude Code / Codex CLI 的 headless JSON,墙钟和对错在外面量,API/token 从它们自己的 JSON 解析。--extra-body 把 OpenAI 兼容网关的 thinking_enabled / reasoning_effort 原样传给模型。--agent codex--base-url 时写一份隔离的 CODEX_HOMEwire_api = "responses"),走网关的 Responses 接口,不碰 ~/.codex

changes

  • 站点图标换成透明底的 logo 猫,并压小体积docs/assets/favicon.ico 原先是整张白底 logo 塞进 ICO(约 226KB),现在是抠出的挥手猫、透明底、16/32/48 三档,约 7KB;同步恢复小体积 favicon.png 给 mkdocs。Web 侧栏、欢迎页和标签图标不再用蓝色圆形 SVG 猫头,改为同一只 logo 猫(web/src/assets/cat.png),去掉圆角裁切和白边。gateway 新增 GET /favicon.ico / /favicon.png 并列入免凭据路径——标签图标是从 origin 根请求的,/login 页在拿到会话之前也要能取到它。
  • 桌面壳用同一只猫做应用图标:窗口、任务栏和(开发运行时的)macOS dock 不再是 Electron 原子。两个文件不是冗余:Windows 要多尺寸 ICO,而 Electron 的 nativeImage 只在 Windows 上能解 ICO,在别处会解成一张空图——症状和「压根没配图标」一模一样,所以非 Windows 走 256² 透明 PNG。macOS 干脆忽略 BrowserWindow#icon(打包后图标来自 app bundle),因此那边只在未打包时显式 app.dock.setIcon。图标直接引用仓库里那两个文件,不在 desktop/ 下另存副本:多一份二进制就是多一只会漂移的猫。
  • Gateway 默认监听地址从 0.0.0.0 改为 127.0.0.1。这是行为变更:原先默认对整个局域网开放,且没有任何鉴权——同一个 WiFi 下的任何设备都能调 execute。要恢复局域网访问显式写 HOST=0.0.0.0--host 0.0.0.0,并且必须先设密码(见上)。
  • SDK 可用 delegate:凭据随 Model 对象走,不依赖 config.yamldelegate 的注册从 CLI 的 create_agent 移进 DeepAgentcli/runtime.py 只在无进程 registry / 超深时负责移除),SDK 用户传 background_process_registry= 即获得该工具。CLI 之外没有 config.yaml 可读,所以工具直接接收调用方的 Model 对象:provider_for_model() 按类 MRO 推导 provider(所有 agentica.DeepSeekChat/ZhipuAIChat/… 工厂都返回 OpenAIChat 实例,统一落 openai;Azure/Claude 各自命中),base_url 非机密走 --base_url flag,api_key 经子进程环境变量传递(OPENAI_API_KEY/ANTHROPIC_API_KEY/…,ps 看不到)——子进程以 --model_provider/--model_name/--base_url + env key 完整重建模型。无环境变量可用的 provider(如 Azure)直接拒绝并建议用进程内 task 工具;model 覆盖命中 config.yaml profile 时仍优先 --profile。顺手修了 test_partial_payload_carries_next_action_hint_and_run_id 的 flaky 断言(assert "2" in hinttimeout=0.05 场景恒假,改为解析建议超时数值并断言严格更大)。
  • delegate 的 model 覆盖映射到 config.yaml profile,未配置的跨 provider 覆盖直接拒绝:覆盖的模型名命中某个 profile 的主模型时,子进程改用 --profile <name> 启动,base_url/api_key/调优参数整套随行;本会话自己的模型同样先查 profile。原来只传裸 --model_provider anthropic --model_name claude-opus-5,子进程 resolve_model_configuse_profile=False,base_url 落到 provider 公共预设端点(如 api.anthropic.com)、key 落到多半不存在的 ANTHROPIC_API_KEY,存储的凭据从未离开本机就 401 invalid x-api-key。命不中任何 profile 且 provider 与本会话不同的覆盖,现在返回列出可选模型的拒绝信息,不再放出必然认证失败的子进程;同 provider 的裸模型名维持 flags 行为(子进程由 active profile 供凭据)。
  • 代码、注释、帮助文案和文档不再出现第三方代理名:CLI --cache_control_session_header / setup 向导举例改为 X-Session-Id,评测隔离 CODEX_HOME 的 provider 标识改为 gateway
  • Ctrl+O 展开 execute 时带上启动命令,且结果是完整的:前端只给命令/输出各留几行预览,展开页却经常只有输出(短命令根本不会单独入栈),用户对不上「哪条命令产出了这段」。现在同一次 execute 在 Ctrl+O 里是一块:$ 后面是完整启动命令, 后面是完整 stdout/stderr(含前端已经露出的那几行尾部),长命令先入栈、结果到达后原地升级,不会拆成两块。其它被折行的工具结果同样先写工具名和参数再写正文。
  • ask_user_question 不再用 auxiliary LLM 解析用户回答,原话直接交给模型_parse_reply / _resolve_parse_model 整块删除,结果 JSON 收敛为 prompt / response / options 三个字段(raw_input 一并删除——response 就是原话,第二份拷贝没有意义)。读这段回答的主模型正在这一轮的上下文里,它比一个只看到「问题 + 选项 + 一行回复」的便宜模型更有条件判断用户想说什么;多这一跳换来的是一次额外延迟、一个 30s 超时兜底、以及一次把「3 , 100题, workers=10 是ok的吧」改写成别的东西的机会。CLI 输入回调里「把 1 映射成该选项原文」那段(interactive/app.py)同时删掉——它是键盘和模型之间最后一层转换,而模型手里的信息更全:问题、编号选项、用户那一行都在同一个 tool result 里,3 / C / 「最后一个」/「便宜那个」它都是照着用户看的同一份列表读的。CLI 结果块随之少一行 (your input: …),答案按用户键入的原样回放(敲 1 现在 transcript 里就是 A: 1)。工具因此不再持有 per-agent 状态,set_parent_agent / clone()Agent._bind_tools_to_agent 里对应的分支一起删掉。
  • 推荐项是 prompt 约定,不是参数:把推荐的选项放 options 第一个、标签里自己写 (推荐) / (recommended) 即可(/cron 的确认框一直就是这么写的)。标记落在标签里无害——它只是给人看的文字;而「哪个是推荐」本来就是模型这一轮的判断,工具再记一份 recommended 字段既要校验(精确/忽略大小写匹配、不匹配报错)、又要维护带标记与不带标记两份列表,全是为了一句排序约定。
  • ask_user_question 结果块的 Q 带上候选项:翻回 transcript 时原先只看得到问题和最终答案,选项在提问组件消失后就没了,答案成了没上下文的标签。工具返回现在带上 options,结果块按 1. / 2. 列在 Q 下面(和提问组件同一套编号);旧 payload 没有 options 时回退读这次调用的 tool_args
  • Responses API 的 provider_data 只留 replay 真正读的部分:单条 assistant 少扛 29KB 请求回显_assistant_message 过去把 response.model_dump(exclude_none=True) 整包挂到 Message.provider_data 上并落盘,而这个 dump 绝大部分是请求回显:在一份 54MB 的存量 transcript 语料里,tools 一项占这些 blob 的 89.2%(395 条 assistant 条目共 11.7MB,全库只有 3 个 distinct schema 被逐条重复),其余是 temperature / tool_choice / truncation / store 这类请求参数。而它没有任何消费方:replay 侧 _assistant_items 只读 objectoutput(合计 7.9%),provider_data 是 local-only 字段、永不上 wire(Message.to_dict 白名单,由 tests/model/test_wire_payload_allowlist.py 钉住),Responses 的 stateful 链接走的是另一个字段 provider_checkpoint(单独落盘,previous_response_id 未被使用)。改成在生成处按白名单 {object, output} dump —— 不是把 tools 拉黑,这样将来新增的臃肿回显字段不会重新把日志撑大;内存里也不再扛这份拷贝。零信息损失:同文件既有的 replay 用例(reasoning → function_call → function_call_output 重建、encrypted_content 透传)原样通过。新增一条用例,构造带 8 个 500 字描述的工具 schema 的响应,断言 provider_data 键集恰为 {object, output} 且 replay 仍能重建;把白名单改回整包 dump 该用例立刻失败。
  • 两条 flaky 测试定位并修掉,全量套件从「偶发红」变成可连绿。都不是产品代码的问题,是断言写法对随机输入不成立:
    tests/cli/test_fork_command.py::test_an_unknown_fork_point_is_refused(实测 7/40 失败)。 /fork 的定位顺序是「先当索引、再当 uuid 前缀」,而 session fixture 用真实 uuid4()——只要某条目 uuid 恰好以 9 开头,本该越界被拒的 "9" 就会作为合法 uuid 前缀命中,fork 成功。改成在 fixture 里把 uuid4 patch 成确定的、字母开头的序列,数字前缀碰撞从此不可能。修后整文件 40/40 连绿。
    tests/gateway/test_gateway_channel_wechat.py::test_hex_encoded_aes_key_decrypts_voice_payload(实测 2/30 失败)。 用例要验证「hex 包装的 key 被当成 AES-256 会 unpad 失败」,但 pycryptodome 对同一件事有两种措辞——长度字节越界时是 Padding is incorrect.,填充字节不符时是 PKCS#7 padding is incorrect.,随机 key 下两者都可能出现,而 match="Padding is incorrect"大小写敏感正则,匹配不上后者的小写 padding。改成 match="(?i)padding is incorrect"。修后 60/60 连绿。
  • 缓存写入单独计量,两条测试 bug 修掉。三件小事,都是上一条 session-log 改动的收尾:
    trajectory_stats() 新增 cache_write_tokens 之前只统计两个「命中」计数(cached_tokens / cache_read_tokens),把写入缓存漏在外面——而 Anthropic 是按第三档单独计费的(写入 > 未命中输入 > 命中,Claude 那档是 $6.25 / $5 / $0.5 per 1M),漏掉它等于漏掉一整项成本。刻意不并进命中率:把写入算作命中,会把缓存支出报成缓存节省。取值口径与 split_prompt_usage / model/base.py 对齐——先读 prompt_tokens_details.cache_creation_tokens,回落到 cache_write_tokens,两个拼写是同一个量的别名所以取其一而非相加(provider 同时给出两者时相加会重复计费)。evaluation/run.py 同步追加 avg_cache_write_tokens。在 13 个真实 transcript 上验证过:抽 5 个逐字段与独立重算比对,cache_write / cache_read / cached / input 四项全部吻合(最大一个 426,433 写入 / 4,465,624 读取)。
    tests/model/test_cache_observability.py 删掉 TestStatusBarCacheSegment e9b1edb 已经故意把状态栏的 cache NN% 段连同 build_status_bar_fragments(cache_hit_ratio=) 参数一起移除(同时更新了 tests/cli/test_usage_display.py),但漏删了这个文件里的两条用例,于是它们一直以 TypeError: unexpected keyword argument 'cache_hit_ratio' 挂着。事件层的 cache_hit_ratio 仍在,同文件其余用例保留。
    tests/utils/test_langfuse_shutdown.py::test_peek_failure_degrades_to_none 的假通过修掉。 它用 _BoomRM 伪造「私有 API 漂移」,但 _instances 写成了普通 @property,而被测代码是在上取这个属性——类级取 property 只会拿到 property 对象本身(真值、不抛),于是根本没走到 except,而是继续调 get_client():隔离跑时它因未配置而抛异常,测试碰巧变绿。全量套件里前面的测试让 langfuse 可初始化后,get_client() 成功、真的 spawn 出一个常驻 langfuse-shutdown 线程,于是①本用例失败,②按字母序排在它后面的 test_returns_none_when_not_configured 也被这条残留线程带挂。改成把 _instances 放到 metaclass 上(类级访问真的抛),并补一条「降级为 None 时不得留下线程」的断言。全量套件从 3 failed 归零。
  • session log 从「回合末反推的镜像」升级为「可断言的轨迹」:日志一直是回合末遍历 run_response.messages 重建出来的,没有任何不变式保证「写进日志的 == 当时真发给模型的」——而这类 bug 已经发生过一次(runner/persist.py 的注释记着:上一版把所有 assistant 分组排在所有 tool 之前,resume 时 provider 400 messages with role 'tool' must be a response to a preceding message with 'tool_calls'),修好了却没留下断言,所以同类回归可以再次静默发生,且只在用户 resume 时才炸。四件事:
    ① 投影 + 等价断言。 新增 SessionLog.derive_messages()(薄封装 load()since_uuid= 切出本回合尾部;保存/恢复 _last_uuid,所以推导不扰动 append 链)与模块级 assert_trajectory_equivalent():只比结构——role 顺序、tool_calls id 的集合与顺序、每个 tool 结果必须应答紧邻它之前那个 assistant;不比 content(压缩、marker、synthesized 消息合法地改写 content)。两条归一化编码了「日志与内存之间合法的差异」:既无文本又无 tool_calls 的消息丢掉,连续的纯 user / 纯 assistant 折叠成一条(日志每回合只写一条 user 与一条终答)。runner/loop.py 在回合三段日志写完后挂上 _check_session_log_trajectory——DEBUG 才开、只 warning、整体 try/except:日志保真是可观测性问题,不该把用户正在进行的对话打挂;它真正的价值是让测试能抓住那类回归。负向测试手工构造「所有 assistant 在前、所有 tool 在后」的坏日志并断言能被检出(把校验函数改坏自检过:3 条负向用例确实会挂)。
    ② 回合内增量落盘。 每轮工具跑完就写(_flush_turn_tool_rounds),不再等回合末——进程内 cancel/异常本来就有兜底,但 SIGKILL / OOM killer / 断电会把跑了几分钟、几十次工具调用的整个 turn 全丢。回合末原有写入点变成补齐SessionLog.begin_turn() 记下本回合已落盘的 tool_call_id 与 assistant 轮次,_persist_assistant_tool_calls() 按 id 跳过,两条路径因此天然幂等(把去重关掉,端到端测试立刻看到同一个 tool 落盘三遍)。只写已应答的轮次(复用 _drop_unanswered_tool_calls):孤立的 assistant(tool_calls) 正是 provider 在 replay 时拒绝的形状。用户问题改由「本回合第一次落盘」写(而不是回合开始),所以顺序仍是 user → assistant(tool_calls) → tool → …,且被 input guardrail 挡下的输入不会留在日志里。fallback transaction 期间不增量写:那些结果最终要落成不可重放的 tool_audit,而这只在回合末才知道。
    ③ 被硬杀的 turn 在 resume 前封口。 SessionLog.seal_incomplete_turn():日志尾部若是「有问无答」(或停在 tool 结果上),先追加一条 assistant 标记再回放,否则剥离 tool 工件后 wire 上会出现两个连续 user turn,strict provider 拒收(同族注释见 runner/steer.py)。append-only、幂等,只在 resume 路径调用。
    ④ 轨迹指标从「写了没人读」变成指标。 SessionLog.trajectory_stats() 聚合日志里真实存在的字段:turn/step 数、tool 调用数、工具错误率(取 is_error——落盘用的是这个键名,_build_messages 读回来才叫 tool_call_error)、按 tool_name 分布、token(metrics.input_tokens/output_tokens/total_tokens)、缓存(metrics.prompt_tokens_details.cached_tokenscache_read_tokens,前者 OpenAI 兼容、后者 Anthropic 风格)、completion_tokens_details.reasoning_tokenscompact_boundary 计数。字段名先在真实 transcript 上核过才实现,没有数据的指标就是 0,不估算。evaluation/run.py 每个实例落一份 transcript 并追加 trajectory_statistics(含工具错误率与缓存命中),accuracy 与既有 statistics 块一字未动,老 summary 仍可比。
    测试:tests/memory/test_session_log.py(+25:derive 切片/边界、等价断言的 3 条负向用例、增量落盘幂等、硬杀后轨迹仍合法、封口幂等、指标聚合)、tests/runner/test_runner.py(+2 端到端:流式与非流式各驱动一个带工具调用的完整 turn,断言日志正好是 user/assistant/tool/assistant 一份不重)。
  • 派出去的活现在会自己回来:worker 完成回报的地址不再失效,header 也不再说「可以不回」。这三处凑在一起,效果是「手机派活 → 人必须自己去终端把结论抄回来」,也就是多会话协作里最费人的那一段:
    GatewayAgentPeers 的 peer 身份不再每次重建就换一个。 session_for 过去在缓存未命中时 new_peer_id(),而短名里嵌着 id 前两位(wecom-agentica-64),所以 LRU 赶出会话、delete_session、网关重启之后,同一个微信对话换了名字回来——CLI 半小时前被告知「做完回报给 wecom-agentica-64」,此时 send_message 直接是 no live session matches,报告发不出去,没人知道活干完了。现在 peer_id 由 session_id 派生(_stable_peer_id,sha1 前 8 位),名字成了这段对话的属性而不是缓存的属性。顺带修掉同一族的第二个漂移源:forget() 会把 note_route 记的回信路由一起丢掉,于是重建时渠道前缀退化成 web-wecom-agentica-64web-agentica-64——LRU 驱逐现在走 _unpublish()(只下线、保留路由),forget() 仍然整条清掉,留给真正的删除会话/停机。
    ② 注入 worker 上下文的消息头改口。 format_for_model 过去以 reply with send_message to X if needed(用户转发)/ only if it is waiting on an answer(另一个 agent)结尾——派活方从来不是「看得出在等」的样子,所以 worker 干完活谁也不通知。PEER_MESSAGING_POLICY 里那句「做完或卡住要回报派活方」是常驻指令,斗不过每条消息自带的这句。现在两个分支都点名回信地址并要求「done or you stop 时回报」,纯告知类消息仍然明说不需要回复。
    ③ 消息只带结论,证据发路径。 send_messagemessage 参数文档过去写「self-contained:接收方只看得到这段文字」,读起来就是「把 diff/日志粘进来」——而一条消息是直接注进对端上下文窗口的,粘 8k 字等于花掉对方用来干活的窗口。现在 docstring 与 policy 都写明:正文是指令要自洽(目标、边界、卡住怎么办),diff / 日志 / 长篇 review 落成文件、消息里给绝对路径(机器是共享的,对端要细节时自己读一次就行)。MAX_MESSAGE_CHARS = 40000 的硬顶不变,这条是让模型远在撞顶之前就走路径。
    测试:tests/gateway/test_gateway_agent_peers.pytests/peers/test_peers.py(新增用例在旧代码上确认失败)。端到端 smoke tmp/smoke_peer_reply_roundtrip.py 走完整链路(手机派活 → CLI 收到 header → 网关会话被驱逐 → 重建 → CLI 按原地址回报 → 结论推回微信),在改前的 agent_peers.py 上 4 条断言全挂,其中包括「回报根本发不出去」。
  • execute 收尸不再无界等待,界面上也不再出现 BaseSubprocessTransport.__del__ / Event loop is closed:起因是 node server.js & sleep 0.6; curl … 这类命令跑了几百秒不停止,Ctrl+C 之后一段 asyncio traceback 直接印在下一轮回答中间。三件事连成一串,逐个说:
    ① 卡住的原因是 stop() 看错了对象。 shell 把 node 丢到后台后自己退出,而 node 继承着我们的 stdout/stderr PIPE 写端——communicate() 等的是管道 EOF,不是子进程退出,所以永远等不到。120s 超时确实触发了,但 terminate_subprocess.stop() 第一行是 if process.returncode is not None: return:我们的直接子进程(shell)早已退出,于是 SIGTERM/SIGKILL 一个都没发出去,进程组照跑,接着 await process.communicate() 无界等待,整轮就停在那里。实测(tmp/probe_orphan_pipe_holder.py):这条路径要等后台进程自己寿终,脚本 45s 才返回;用户遇到的是几百秒。现在 killpg 打的是进程组,不再拿自己子进程的退出码当门禁;拆除阶段每次排空都有上限(DRAIN_TIMEOUT_SECONDS = 2),因为写端还可能被组外进程持有(setsid 出去的),EOF 只能尽力而为。
    ② 顺带不再留孤儿。 超时/取消的命令连同它在后台起的进程一起被杀 —— 之前 node server.js 会继续占着 8899 端口活下去。
    ③ traceback 是上面那次没排空干净的后果。 在收尸自己的排空里再按一次 Ctrl+C(CLI 就是提示"press Ctrl+C again"的),transport 会带着一个仍注册在 loop 上的读管道被丢下;下一轮 GC 时 __del__ 在已关闭的 loop 上 call_soon,CPython 把 Exception ignored in: BaseSubprocessTransport.__del__ / RuntimeError: Event loop is closed 写到 sys.stderr——而 prompt_toolkit.patch_stdoutstdout 和 stderr 都换成了 TUI proxy(上一条 changelog 说"只 patch stdout"是错的),所以它就印在回答中间。现在 transport 一律在活着的 loop 上 close()__del__ 变成 no-op。正常读完的命令不受影响(管道已在 EOF 时自己关闭,__del__ 本来就是 no-op —— 这也是为什么只有异常路径会漏)。
    收尸统一在 execute / grep / shell / verify_completion(test)finally,条件从 returncode is None 改成 not drained("命令没被正常读完"才是要收尸的条件,进程已退出而管道还开着正是要覆盖的那一种)。asyncio.shield 去掉了:实测单次取消并不会打断 finally 里的 await,而 shield 会在 loop 关闭时留下一个悬挂 task,换一种漏法。始终不装 sys.unraisablehook。测试 tests/utils/test_async_utils.py(7 例,其中 5 例在修复前的代码上确认失败:整套跑完从 144s 降到 2.7s,因为旧代码是靠干等把后台进程熬死的)。断言用的是 sys.unraisablehook 记录而不是 stderr 内容——pytest 的 unraisableexception 插件会先把它变成 warning,盯 stderr 的断言在坏代码上照样通过。
  • Gateway agent 的 peer 名从超长 gw-wechat-<openid>-xx 改成 CLI 同款短名:此前 session_for 把 IM 的 channel_id(微信 openid)塞进发布名,list_agents 里出现 gw-wechat-o9cq8035jyckmmlzta33-mkm-41,CLI 要 send_message 把完整结果推回手机时 target 没法敲。现在形状是 {渠道}-{cwd 末级目录}-{peer_id 前 2 位}(微信从 agentica 仓库起就是 wechat-agentica-41,网页是 web-proj-0f);openid 仍记在 note_route 里当回信地址,不进名字。渠道前缀继续把这类名字挡在 CLI 的 <folder>-<xx> 和 bridge 端点的 <channel>-<sender> 之外。测试 tests/gateway/test_gateway_agent_peers.py
  • Gateway 媒体路由改为「图片给底模,音视频给 Gemini」:删掉扫遍 config.yaml profile / 名字启发式(vl/4o/seed)/ modalities: 声明的三级探测——那套会让一张图静默落到随手加的某个 VL 上。现在两条规则:图片在底模 supports_images(或底模 id 是 Gemini)时挂 agent.run(images=),否则一次性让 settings.media_model 描述;语音/视频仅当底模是 Gemini 时直挂,否则同一 media_model 转写/描述。media_model 是 settings 里一个模型块(model_provider/base_url/api_keymodel_name 省略则 gemini-3.6-flash);没配就回复里说明怎么配,不再猜。测试 tests/gateway/test_media_understanding.py
  • Gateway 启动 banner 打印 Log File 路径,并写入与 CLI 同一套 pid 日志:SDK 默认不落盘(import agentica 不能在 ~/.agentica/logs/ 留文件),CLI 早就 opt-in 成 YYYYMMDD-<pid>.log,gateway 作为同样的长跑进程却只打 stdout——微信发图这类错误滚出终端就找不到。现在 lifespan 调同一入口 enable_process_file_logging(),banner 多一行 Log File (INFO): ~/.agentica/logs/20260814-65634.logAGENTICA_LOG_FILE="" 仍是显式关闭。测试 tests/utils/test_process_file_logging.py
  • 同文件多次写入不再把整份最终 diff 重复打三遍tool_display_meta(每次调用自己的 before/after)只活在 chunk.tool_call 上,run_response.tools 累积列表刻意剥掉以免 session log 存整文件。CLI 完成事件却去读 chunk.tools,meta 全部丢失,展示层只好拿「批次开始时的磁盘」对「现在的磁盘」——于是 apply_patch 之后两次 edit_file 同一文件,三条都是从文件头到改完的同一份 diff。现在完成事件改读 chunk.tool_call,每个调用只显示自己那一刀。
  • /status 显示当前 CLI log 路径:启动 banner 里的 Log File 滚上去就找不到。log 是这个进程的运行时事实(pid 日志),不是 /config 管的可改配置,所以只出现在 /status,不进 /config / /config path
  • CLI 的 execute 10 秒内不再显示耗时:完成行尾的时间戳把「多慢算慢」从统一 1s 改成按工具定(_MIN_ELAPSED_DISPLAY)——编译、跑测试合法地要花几秒,9.99s 还报 (9.99s) 纯噪音;execute 起报线 10s,其余工具保持 1s,subagent 的 verbose 完成行同规则。新增边界测试(0.5/1/9.99s 隐藏,10/65.4s 显示,grep 不受影响)。
  • apply_patch 预检失败不再贴文件头、也不再附带 read_file 教条:hunk 从文件开头搜不到时,以前 Actual from line 1 把文件头(encoding/@author)当成对照,模型会按这份无关文本重写 hunk;现在按期望上下文里独特的 def/class/长行定位真实区域,对不上就明说 None of the expected lines appear in the file.。末尾那句 Read or re-read each failed region… 已在工具 docstring 里,从错误正文里删掉。
  • apply_patch 预检失败的 Expected/Actual 预览不再把唯一的差异折掉:两个块各自硬切前 6 行,于是「前 6 行一致、第 7 行才不一样」的 hunk 打出两段逐字节相同的预览,真正对不上的那行藏在 ... (1 more context lines) 里——错误看起来自相矛盾(明明一样却说 context not found),模型只能瞎改重试。实测复现于 README 改动:上下文里那条很长的 v1.4.7 被写短了一截,报错却完全没显示它。匹配逻辑未动(_find_context 仍是整块精确比对 + 空白/引号 fuzz,这次失败本身是正确的),改的是 ContextFailure.render():先算出第一处不一致的下标,把 6 行窗口滑到能看见它(保留 2 行 lead-in,前面折掉的行注明 ... (N earlier lines match)),两个块用同一个窗口以便逐行对照,该行以 > 标出,末尾补一句 First difference at context line N (file line M);期望块比文件区域长(EOF 一带)时说明 the file region ends before the hunk does。短 hunk 的输出形状不变(不加窗口注记、不加 First difference 行)。
  • 路径不存在错误去掉 Next step 教条read_file/grep/glob/edit_file 找不到路径时仍报 Resolved pathNearest existing parent,不再附带 use ls/glob/grep… do not retry speculative absolute paths——怎么搜、别猜路径已经写在 tools.md 和工具 docstring 里,每条错误再复述一遍只占 CLI 和模型上下文。
  • Gateway 个人微信支持图片/语音/视频理解(多模态路由):此前微信收到的图片、语音、视频在 _process_channel_message 里被静默丢弃(只传文本)。现在两条规则、不扫 profile:图片在底模能看(supports_images,或底模是 Gemini)时挂 agent.run(images=) 让底模看像素;语音/视频仅当底模是 Gemini 时直挂(视频以 data:video/mp4;base64 内联块走 images=,Gemini OpenAI 兼容端点的惯例,SDK 并无模型消费原生 videos=)。其余一次性交给 settings.media_model(指向 Gemini,model_name 缺省 gemini-3.6-flash)描述/转写,注入用户文本并在回复加一行小注。未配置 media_model 则 warning + 回复说明怎么配。微信语音 silk 解码用 graiax-silkcoder(纯 Python,已加入 wechat extra)或 pilk,解不出时回复提示安装;>15MB 视频跳过(Gemini 内联上限 ~20MB)。新增 gateway/services/media_understanding.pyMediaUnderstandingService + 进程级单例,AgentService.chat(media=...) 透传)、channels/base.pyInboundMediaChannel.fetch_media()(默认空,WeChat 实现 CDN 下载+解密)、wechat 的 extract_media_typed()metadata["media"] 改存 {"kind", "media"} 类型化引用,视频缩略图不参与理解)。行为级测试 tests/gateway/test_media_understanding.py(底模直通、media_model 描述/转写、未配置、超大视频、silk 解码与缺库路径)+ wechat 渠道与 queue/agent_service 接线测试。
  • 多会话共用一个仓库:presence 带上 git 状态 + 一个任务一个 worktree(新增 agentica/git_state.pyagentica/worktrees.pyagentica/peer_conflicts.pyagentica/cli/worktree_binding.pyagentica/tools/worktree_tool.py):几个会话(终端 CLI、手机遥控的 CLI、网关 agent)改同一个仓库时,贵的不是合并冲突,而是互相覆盖互相打听。分两条治:
    ① 不用问就知道对方在干什么。 每个会话在既有心跳里发布 git 位置(分支 / head / 相对基准分支的 +ahead/-behind / 脏文件列表),list_agents/list-agents 直接显示 git: main @ 8ca321e · 3 dirty + dirty: a.py, b.py基准分支取本地 main(无则 master)而不是 upstream——另一个会话提交到本地 main 还没 push 时 origin/main 看不见它,而那正是你要撞上的人。采集有 10s 缓存、心跳仍只在变化或 30s 到点时落盘,不会变成每秒三次 git 调用。dirty_countOptional[int]None=没采集过、0=真干净,老记录只显示 git: <branch>不会冒充 "clean"("clean" 是别人据以决定能否 rebase 的信息)。
    ② 写文件时一次性提醒。 落盘那一刻若另一个 live 会话(同仓库,可以在别的 worktree)也把这个文件改脏了,写入结果追加一行点名是谁、在哪个分支哪个目录。只提醒不拦截(两个会话同改一个文件有时正是对的,拦住只会把 agent 逼死);只比同一个仓库(peer 发布 repo_root,否则满世界的 README.md 互相报警,而同仓库两个 worktree 仍会命中);同文件同 peer 只说一次(每次编辑都提醒等于训练模型忽略它)。
    ③ worktree 按任务而不是按会话。 agentica --worktree <任务> 启动即进入,或者让已经跑了几周的会话自己切:新增 worktree 工具(status / use / merge),所以「切到 gateway-peers 再改」这句话可以由 send_message 从别的会话或手机送达——这是长期不重启的会话唯一可行的路径。就地切换要把四件事一起动(进程 cwd、agent 执行环境=prompt+sandbox+每个工具各自捕获的 work_dir、live peer 记录、状态栏),只改 agent.work_dir 等于只改了 prompt 里那句话而工具还在往老 checkout 写;为此新增 Agent.rebind_work_dir() 与两个 builtin 工具的 set_work_dir()都不注册为 tool function——只有 rebind_work_dir 能移动会话,且一次全移)。peer 记录换目录但不换名字PeerSession.rebind):别的会话和手机 pin 的就是那个名字。transcript 不动,继续写在原处(session_base_dir 本来就是干这个的),一段对话不因为工作目录搬家被切成两个文件。
    ④ 合并回去但不删 worktree。 顺序是刻意的:先在 worktree 里把基准分支合进来(冲突就留在写这段代码的会话手上、在它自己的目录里,而不是把半合并的 index 扔在所有会话共用的主 checkout),再在主 checkout 里 fast-forward。两个会话同时 merge 时 git 自己的 index.lock 就是互斥锁,只等它(重试 5 次),不另造锁。副产品正是长期可用的关键:合完后 worktree 与基准分支齐平,下次接着用不背旧历史。从不删除、没有自动清理——它值钱的就是暖好的 IDE 索引、装好的 venv 和一屏 shell 历史。
    默认位置与配置:主 checkout 的兄弟目录 ../<repo>-<任务> + 分支 wt/<任务>(人可读、可 cd、和手工 git worktree add ../xxx 同一个直觉)。两种情况可改 settings.worktree.root:父目录塞了二十个仓库、或共享挂载父目录不可写(不可写时报错直接点名这个设置);写相对路径会落在仓库内(Claude Code 的 .claude/worktrees/ 形态)——支持但不默认,实测主 checkout 里一条 git clean -xdff 会把这棵被 gitignore 的树连同别的会话未提交的工作一起删(--dry-runWould remove .agentica/),而这些 worktree 是要长期活着的。.env 等 gitignored 文件按 settings.worktree.link symlink 而非拷贝(轮换密钥一次生效、机器上只存一份;缺 .env 的症状是"会话起来了但连不上模型",和原因八竿子打不着)。
    行为级测试 90 例(tests/peers/test_git_state.py / test_worktrees.py / test_worktree_binding.py / test_worktree_tool.py / test_peer_conflicts.py,全部用真 git 真目录):ahead/behind 取本地 main、老记录不冒充 clean、脏文件截断与计数、复用不重建 / 前台目录被手删后分支还能重新 checkout / 别人的同名目录只报错不清理、--git-common-dir 保证在 worktree 里再建 worktree 也落在主 checkout 旁、symlink 的四种边界、切换后工具与 sandbox 真的跟着走而 transcript 不动、peer 换目录不换名、merge 的 6 种拒绝与冲突留在 worktree、冲突提醒的跨仓库精确性与去重、工具 dispatch 的各种拼法。文档见新增的 docs/multi-agent/worktrees.md(含 4 个实测坑,例如 agentica 命令永远跑主目录代码、而 python -m pytest 在 worktree 里跑的是 worktree 代码)。
  • 仓库内 worktree 布局(worktree.root: .agentica/worktrees)升级为一等支持:Claude Code 的 .claude/worktrees/ 形态,此前只是"相对路径也能用",现在选它不需要任何准备。三处修好:① 路径不再插冗余的仓库名(仓库已由位置隐含,<repo>/.agentica/worktrees/<任务>);② 自我忽略——首次创建时在 .agentica/worktrees/.gitignore 写一个 *git status 干净且绝不改仓库里那个被跟踪的 .gitignore``**(共享文件,工具不该替人改;同 pip 缓存的做法);**③ 搜索工具跳过 .agentica**——实测仓库内布局会让 glob("/*.py") 把每个文件返回 N+1 份(噪音是小事,**改到副本那一份**才是大事),grep 走 ripgrep 本来就被 .gitignore 挡住,glob与 grep 的纯 Python 回退各自走自己的遍历、之前没挡。修的时候测试又逮到一个更严重的:排除规则原先匹配**整条绝对路径**的 parts,于是一个 work_dir 本身就在.agentica/worktrees/里的会话glob 自己的文件会**返回空**——改成只匹配搜索根**以下**的相对路径(_in_noise_dir)。 默认仍是 sibling(../-<任务>),理由是错误代价不对称,且两条都实测过:git clean -xdf(单 -f,最常打的那条)对嵌套 checkout 是 Skipping repository → **安全**;git clean -xdff(双 -f)是 Removing .agentica/→ 树连**别的会话未提交的改动**一起没,注册项变prunable。sibling 布局下这条命令完全无害。所以:sibling 选错 = 报错 + 加一行配置(可恢复、可见),仓库内选错 = 某次双 -f清理顺手删掉三个会话的活(不可恢复)。仓库内布局的优点(父目录不被污染、只要仓库可写就能用、删仓库时 worktree 跟着走、与.cursor//.claude/ 同一心智)全部保留,一行配置即可切换。 搜索排除随后改成**问 git 要**(git worktree list,按仓库 10s 缓存)而不是按名字匹配:嵌套 worktree 不一定是 agentica 建的——本仓库当时就有另一个会话手建的临时 .worktrees/wechat-media,实测主 checkout 里 glob("**/peers.py") 真的返回了它那一份副本。排除永不包含搜索根自身,所以绑定在该 worktree 里工作的会话照样看得见自己的文件;grep侧同时给 ripgrep 加--glob !<相对路径>/`(嵌套 worktree 不一定被 gitignore 挡住)。
  • 微信轮询循环把网络异常从 error 降为 warningrun_loop 的兜底错误日志对 requests.exceptions.RequestException(连接重置/超时等预期内网络抖动,退避后自愈)改走 logger.warning,其余异常仍为 logger.error;退避序列(2s/2s/30s)不变。行为级测试断言 ConnectionError 只产生一条 warning、无 error。
  • Gateway 自己的 agent 现在也能 list_agents / send_message 本机 CLI 会话(新增 gateway/services/agent_peers.py):此前 PeerMessagingTool 只在 cli/runtime.py:968 挂载,AgentService._build_agent 只加 cron + self_manage 工具——于是在微信里说「让本机所有 CLI 会话都把改动提交了」,接住这句话的 gateway agent 一个会话都看不见(实测调用 list_agents 返回 function not found)。能用的只有用户自己打 @<会话名> 逐个寻址的 bridge 路径,README 里「多个会话组成一支队伍 + 人可以离开现场」这条最大卖点,在离开机器时其实只剩半条。现在每个 gateway 会话(网页的、每个 IM 会话的)也是 peers 通道上的一个 peer,发布成短名(wechat-agentica-41:渠道 + cwd 末级目录 + 两位 id),拿到的就是 CLI 本来就有的那两个工具——没有新协议、没有为网关新造工具;反向也通了:CLI 会话可以主动 send_messagewechat-agentica-41 把结论推回手机。
    卡住这个设计的几条:渠道前缀不是装饰——match_peers() 把名字前缀当地址,第三种名字形态({channel}-{folder}-{xx})必须既不遮蔽 CLI 的 <folder>-<xx> 也不遮蔽 bridge 端点的 <channel>-<sender>邮箱谁来收取决于有没有在跑:agent 正在跑那一轮时由 Runner 收进上下文(agent.peer_session,与 CLI 同一边界——tool 批次之间,绝不打断正在执行的工具),空闲时由 1s 轮询推到对应 IM 会话(前缀 <发信会话名> ›);网页会话没有 IM 回信路径,邮件留在邮箱等下一轮,而不是被消费掉没处放。回信路由是记下来的、不是解析出来的main.py::_handle_channel_message 同时知道 session_id 和会话,事后再把 agent:{agent_id}:{channel}:{channel_id} 拆回去是猜。live 记录不能比会话活得久:agent 是 LRU 缓存、淘汰时不通知任何人,所以轮询按 has_cached_session 反查并 unpublish(该查询走新增的 LRUAgentCache.contains,不能碰 LRU 顺序——否则轮询自己会把陈旧会话续命、把用户正在说话的那个挤掉),delete_session 则立即 unpublish。cron 不参与:它每次跑都是用完即弃的 agent,发布邮箱没人读。只有一个开关PEER_BRIDGE=false 同时关掉两条路径,因为是同一个信任边界(网关能否往你的终端里敲字)。网关 agent 自己被排除在 @list@ 寻址之外(不打 @ 就是在跟它说话,转发给它只会原样回声)。
    行为级测试 tests/gateway/test_gateway_agent_peers.py(21 例):短名(渠道+目录+两位 id,不含 openid)与网页 web-<folder>-<xx>note_turn 发布 task/busy、能给 CLI 发信且 from_kind="agent"、回信推到 IM 会话、忙时/无路由时不动邮箱、缓存淘汰后不再可寻址而在跑的会话不被误清、stop() 全部下线、bridge 的两处排除、_build_agent 挂上工具并设置 agent.peer_session(cron 会话不挂)、has_cached_session 不扰动 LRU,以及跑真实 lifespan 断言 deps.agent_peers 与 bridge 的 gateway_peer_ids 确实接上了。
    顺带修掉一个只有真跑一遍才会暴露的坑:agent/permissions.pyREAD_ONLY_TOOLS 是一份按名字硬编码的白名单,"ask" 模式下不在名单里的工具连 schema 都不发——而 ChatRequest.approval_mode 默认就是 "ask",于是网页/HTTP 侧问「有哪些会话在跑」,模型回的是「我没有 list_agents 工具」(实测复现,就是该模式 instruction 里警告的那种「function not found」形状)。list_agents 只读一个目录、什么都不改,加入白名单;send_message 仍然不在——它往别人邮箱里写、代表别人行事。端到端实测两条 surface(真实 lifespan + 真实模型):网页侧与 IM 侧都能 list_agents 到本机 3 个 CLI 会话,网关自己的 peer 按会话发布成 web-agentica-0f / wecom-agentica-f7 并在 shutdown 时全部下线。
  • 修复微信 session 过期后 getUpdates 死循环刷屏(err -14 session timeout,每 ~150ms 一条):bot token 过期后旧代码只清游标、不清 token、不退避,于是拿死 token 以服务器应答速度空转。按 iLink 协议规范(openclaw-weixin protocol-spec §4.4)重写错误路径:get_updates 对任何非零 errcode/ret 直接抛异常交给轮询循环退避(前 2 次 2s、第 3 次起 30s),不再 warn-and-return 制造热循环;-14 判定为 session 过期,同时清掉持久化的 token 与游标(此后即使只重启 gateway 也会直接走扫码登录,不必手动 rm token 文件),并抛 SessionExpiredErrorrun_loop 自动发起与首次 connect 相同的 QR 重登。重登限 3 次、每次间隔 60s——无人值守时不会无限弹二维码;3 次都没扫则停止轮询,错误日志写明人工修复办法:rm <token 文件>(按实际路径输出,默认 ~/.agentica/cache/wxbot_token.json)后重启 gateway。行为级测试覆盖:-14/ret=-14 清凭证并抛 SessionExpiredError、其它 errcode 抛 RuntimeError、退避序列 2s/2s/30s、过期自动重登、重登 3 次未果后停轮。
  • Peer bridge 默认开启(PEER_BRIDGE 默认 true),并删除 bridge 自建的「空 allowlist 拒绝转发」防护:个人助手定位下该防护属过度设计——channel 层的 allowed_users(若配置)本就会在消息到达 bridge 前完成过滤(如 wechat.py _on_native_message),bridge 不再叠加第二道门;需要限制谁能和 bot 说话时仍用 <CHANNEL>_ALLOWED_USERS,对 gateway agent 与 bridge 同一生效。默认开启的影响面:没打 @ 的消息照旧走 gateway 自己的 agent,不拿走任何东西;没装/没开 CLI 时 bridge 空转(无端点时 1s 轮询零开销),@list 回复「本机没有 live 会话」并给出所搜目录;显式 PEER_BRIDGE=false/0/no/off 关闭。可见范围表述从「this OS user」改为「this machine」:真正的约束是 gateway 与 CLI 共享同一个 AGENTICA_HOME(peers 树是其下的 per-install 状态),启动日志与空列表回复均按此口径命名所搜目录。docs/advanced/gateway.md 的 PEER_BRIDGE 节同步改写(默认开启、安全前提改为 channel 白名单单一门、去掉「同系统用户」表述)。
  • docs:gateway 补 PEER_BRIDGE 主打功能,个人微信渠道前移docs/advanced/gateway.md 新增「手机遥控本机 CLI 会话(PEER_BRIDGE)」整节(@list / @<name> <text> / 裸文字续发 / @off 用法表、转发行按 from_kind="user" 投递的权限含义、必须与 CLI 同一 AGENTICA_HOME 的前提、"无新协议,bridge 只是 peers 通道上又一个 peer" 原理;默认开启与防护口径随上方行为变更条目同步改写);个人微信(扫码即用、最核心的接入)从文档末尾前移到各渠道小节之首(飞书之前),渠道一览表、安装 extras、适用场景列表、整体架构图的平台顺序同步把 WeChat 排第一。
  • docs:gateway「整体架构」图换为纯文本图:mkdocs 用的 readthedocs 主题不含 mermaid 渲染(无 mermaid.js、superfences 也未配 custom fence),站点上整段 flowchart 源码直接外露。docs/advanced/gateway.md 的 mermaid 块替换为等宽的纯文本架构图(```text),GitHub / 站点 / 编辑器预览三处均可正常显示,信息与连线关系不变。
  • CLI 退出提速(修 Langfuse atexit 阻塞):交互模式打印 Goodbye 后进程还要再等 ~2s+ 才真退(实测 1 turn 后 linger 2.87s)——python 解释器销毁时跑 langfuse 的 atexit shutdown:tracer_provider.force_flush()(网络上报缓冲的 OTEL spans)+ join 两个各 ~1s 轮询周期的 score/media 消费线程,空队列也照等;host 不可达时会被网络超时顶到更久。修复为退出路径先在 daemon 线程提前启动 shutdown(与 cron 停止、summary 打印重叠),最后按 0.8s 预算 join:健康网络上 force_flush ~0.1-0.3s 即可完成,被截断的只是 langfuse 的空转轮询 join;最多丢一个 flush 间隔内的 telemetry,shell 提示符立刻可用。agentica "query" 非交互路径同样接入有界关闭(utils.langfuse_integration.start_shutdown_thread / shutdown_langfuse_bounded),行为级测试见 tests/utils/test_langfuse_shutdown.py(不测时间,只测线程语义与 join 预算契约);SDK 进程同样受 langfuse atexit 影响——OpenAIChat.get_client() 无条件包装 LangfuseAsyncOpenAI(与 enable_tracing 无关),SDK 集成方应在自身 shutdown hook 调用 shutdown_langfuse_bounded()(模块 docstring 已补用法)。
  • CLAUDE.md 大幅瘦身(1167 → 786 行),derivable 架构内容拆到 docs/:按「CLAUDE.md 不是中央仓库」原则,删除模型 ls + read_file 即可推出的内容——Project Overview、Common Commands、Architecture 模块地图/key files/代码示例、Unified Configuration System、RAG & Knowledge System、Database/File/Media、Safety Mechanisms、Swarm、MCP、Tool System Expansion、Key Patterns。其中 Unified Config 和 Storage(DB/File/Media)docs 此前缺失,新建 docs/guides/config.mddocs/concepts/storage.md 收容(mkdocs nav 已加),其余 docs 已有对应文件(concepts/agent.mdconcepts/rag.mdconcepts/tools.mdadvanced/run-config.mdadvanced/mcp.mdmulti-agent/swarm.md)。保留的是不可推导的 gotchas 与不变量:Context Compression 两层 + 形态差异、Compaction invariants(三 store 表 + 6 规则)、CLI/SDK assumptions table、Prefix-Cache 优化(marker/breakpoints/routing/cache cliff)、Recording-a-rule-is-not-a-tool、Forking/Delegate/Peer Messaging/Shell escape/Peer bridge、Concurrency is opt-in、Learnings/Code Style/Refactoring patterns。顶部加一句指针指向 docs/。
  • CLI 接入 SDK 已有的 cross-provider fallback 重试机制,config.yaml 适配 fallback_models:近期 LLM API 偶发不稳定,主模型重试(max_api_retry,CLI 默认 2 次)后仍失败则自动降级到 fallback_models 链重试,尽量保证 agent harness 稳定持续运行。profile 新增 fallback_models(模型块列表,字段与 auxiliary_model 同构:model_provider/model_name/base_url/api_key + 可选 tuning)与可选 max_api_retry;同 provider 的 fallback 继承主模型 endpoint/key,跨 provider 用自身 preset / 匹配 profile 的 key,绝不复用主模型 key。/model <profile>/resumeagentica setup 重跑都会携带这两个字段不丢失;环境上下文新增 Fallback models: 一行便于观测。属临时加固行为,不新增 /model 管理 UI。
  • CLI 工具报错样式去黄色,与正常输出同色edit_file/write_file/apply_patch- error 后缀([yellow])及各工具错误正文的 dim yellow 统一改为 dim,⎿ ⚠ 前缀保留——工具报错探针(path not found、grep 无匹配)是 agent 正常试探流程的高频事件,不应以告警色打扰;execute 的 lint 诊断(is_diagnostics,非错误)保留 dim yellow
  • README 三语重构:安装后新增「配置 / Configuration / 設定」章节(env / ~/.agentica/.env / agentica setup 三选一,指向安装文档);快速开始改为 CLI 优先(agentica + 截图),SDK 示例与 DeepAgent 紧随其后,删掉与「Agent 用例」重复的代码块;功能特性按核心引擎 / 长任务与协作 / 记忆与进化 / 集成四组重排;对比表从 LangChain/AutoGen/CrewAI/Pydantic AI 换成直接竞品 Claude Code / Codex CLI / Gemini CLI(模型选择、跨会话协作、/goal、Gateway、自进化 Skill、Python SDK、开源七个维度);News 移除未发版条目,只保留已发布版本;图片统一为 raw URL 并补 alt,LICENSE/CONTRIBUTING 等相对链接改绝对 GitHub URL(修复 PyPI 渲染时的 404 与裂图),badge 8 → 5,「引用」节补 BibTeX 与 CITATION.cff 指引;README_EN.md / README_JP.md 与中文版结构完全对齐,JP 版补上此前缺失的 task/delegate/peer 协作小节。
  • CLI 工具报错全文显示(execute 除外)edit_file/write_file 的错误原本裁成 80 字符尾部、apply_patch 裁成 8 行 tail + 120 字符行宽、其余工具走 4 行 tail 窗口——但这些内置工具的错误是单条诊断消息,病因("found 2 occurrences"、preflight 的失败文件/hunk、unknown config key)全在开头和中段,tail 窗口恰好只留下模板尾巴(... read_file, copy the exact current text into old_string, then retry the edit.),用户完全不知道为何失败。统一改为 _display_full_result_lines 全行全文输出(错误样式、⎿ ⚠ 前缀不变),并加 markup=False/highlight=False 防错误文本里的 [brackets] 被 Rich 当样式吞掉;task 的 subagent 错误同样去掉 120 字符截断。execute 保留 tail 窗口豁免——命令输出长度无界,诊断本就在尾部。
  • CLI diff 展示改为软换行,超长单行不再被屏幕右缘裁掉edit_file / write_file / apply_patch 的 diff 用默认 word_wrap=FalseSyntax 渲染——纵向确实是「FULL」从不折叠(注释也这么写),横向却把超宽行无声裁掉:CHANGELOG 一个条目就是 1.5KB 单行,-/+ 的差异点常年躺在第 300 列以外,用户看到的是两条一模一样的截断前缀,完全不知道改了什么。四处 diff 渲染点(stream.py 的 edit/write/patch 三处 + messages.display_diff)全部加 word_wrap=True,行为级测试用真实 60 列 Console 断言长行尾部 marker 留在输出里。
  • 修复 OpenAI 兼容代理偶发 data: data: {...} 双重包装导致的流式中途崩溃:代理把已是 SSE data 的内容又套了一层 data: ,OpenAI SDK 只剥一个前缀,剩下的 data: {...} 被直接喂给 JSON parser,抛 json.JSONDecodeError: Expecting value: line 1 column 1 (char 0)——此时往往已输出过 chunk,stream_with_retry 按设计不能重试(重试会重复输出/重复工具调用),整轮任务挂在半路。合法 chat.completion.chunk payload 只会是 JSON 对象或 [DONE],绝不会以 data: 开头,所以这个 framing 错误可以无歧义地修复:新增 agentica/model/openai/sse_sanitize.pyTolerantSSETransportOpenAIChat.get_client() 的默认 http client 改走该 transport,对 text/event-stream 响应按行重组字节流并折叠重复的 data: 前缀(跨 chunk 断行、CRLF、多重包装、data: data: [DONE] 均覆盖),非 SSE 响应(文件下载等二进制)字节级原样透传。SDK 的 create(stream=True) 调用路径不变——langfuse tracing、stream_with_retry 语义、CLI 的 raw error 展示全部保持原样;用户自己传的 http_client 不动。继承 OpenAIChat.get_client() 的子类(含 OpenAIResponses)自动获得修复。
  • CLI /usage 与每轮 footer 的 token/cost 口径对齐 + 修精度 bug:footer 领先字段从「本轮总 token(input+output)」改为「净新增 token(fresh input + cache write + output)」——旧值九成是缓存重读,+ 前缀却暗示"往上下文加了 84.3K",与真实上下文增长(仅 20.8K)矛盾,且 +84.3K 与紧邻的 in 83.9K · out 423 冗余。净新增只排除重读缓存:cache write 是首次发送且按溢价计费的内容,若一并排除,会话首轮/prefix break 后的重建轮(全场最贵)反而显示成最小的数。全缓存轮(净新增为 0)省略领先字段,与成本为 0 时的省略规则对齐。成本格式化统一为 format_cost_usd()(<1 分钱显示 4 位小数,否则 2 位;小到 4 位都留不住的非零成本显示 <$0.0001 而非继续"看着免费",该下限不带 + 号),修掉 footer +$0.00/usage ~$0.0040 打架的精度 bug(stream.py 原本固定 :.2f,非零成本被渲染成"免费");footer 侧补了亚分成本的集成断言,避免只锁住格式化函数、接线仍可静默回退。删除死参数 cache_counts_inside_input 与按 model.__class__.__module__ 做模块名嗅探的 cache_counts_inside_input_for_model(判别逻辑早已在 split_prompt_usage 内靠 key 名完成,该参数从未被引用)。/usage 取本轮 usage entries 改用与 footer 相同的 turn-start baseline(_turn_usage_entry_baseline,交互主路径;无 tui_state 的非交互回退仍按 turns 切片,但夹到 0 以免 turns 计及 aux 模型调用时负索引混入上一轮)。/usage 面板重构:session 级指标(API calls / active time / cost)独立成 Session 分组,不再混在 Latest Turn 下;新增 Net new tokens (fresh + cache write + output) 行与 footer 领先字段对账、Total tokens 改标 Total tokens (billed) 以区分两种口径;Input tokens 行标注 (avg X/call)API calls this turn 注明 (tool rounds + final answer) 以解释与 footer「N tools」差 1 的关系;Messages 移入 Context Window 分组。
  • 压缩投影补概念层:boundary 记 lineage + resume 校验回退 canonicalappend_compact_boundary() 增加 model / lineage_key / covered_prefix_hash(hash 由压缩管理器对"被摘要替换的消息段"按 role+content 计算,观测用);SessionLog.lineage_key() = session|cwd|git_branch|model(刻意排除时间戳/条数/hash,Reasonix promptCacheKey 移植)。load(model=...) 时若最后 boundary 的 lineage 与当前会话不符(切模型/换分支),跳过陈旧 summary、回放完整 canonical transcript(summary 之前的内容物理上一直在盘上,新增 load_pre_boundary() 读取入口);无 lineage 的历史 boundary 与未传 model 的调用方保持原行为。runner 与 /resume 两条 resume 路径都传入当前 model。
  • 辅助任务独立 session(Reasonix planner/executor 分离模式的 judge 版):新增 agentica/aux_session.pyAuxSession——按用途持有界且与主对话完全隔离的小 history(system 首位恒定 + 每轮 append 一组问答 → 前缀自动缓存友好;超限按"最老一组"裁剪;reset() 随任务身份切换)。judge_goal 支持传入 session,解析成功的问答才入 history(解析失败/调用失败不污染);GoalManager 为 per-turn judge 持有 _judge_sessionset() 新目标时重置防止跨目标锚定。不传 session 保持原无状态行为。
  • 缓存命中观测三件套(对齐 Reasonix CacheState):(1) Usage/RequestUsage 新增 cache_hit_ratio()split_prompt_usage 归一化 OpenAI inclusive / Anthropic exclusive 两种口径,无缓存数据返回 None 而非误导性 0%);(2) 每请求的 context.usage 事件扩展携带上一条请求的命中率与本轮 prefix_break_index(对每条请求消息算 role+content+tool_calls 摘要、与上次请求 diff 出首个变化下标——append-only 为 None),TUI 状态栏新增 cache NN% 段、break 事件进 debug 日志;(3) SessionLog.cache_warmth_hint() 给出 resume 前 warm/cold/unknown 估计(无 boundary → unknown;lineage 不符或超 TTL → cold),/resume 时写入日志。
  • 开放唯一压缩策略旋钮 AGENTICA_EVICT_THRESHOLD_RATIO:Layer 1 evict 触发比例(默认 0.8)可通过环境变量做项目级 override(放 .env 即可);非法值回退默认并告警,越界值钳到 (0, 0.95) 内——必须严格低于 Layer 2 的 0.95,防止重开分层倒置。Layer 2 触发比例与 EVICT_TARGET_RATIO 刻意不暴露(前者一动就回到倒置陷阱,后者是独立的"压多狠"问题)。under_pressure() 每轮按生效值判定。
  • 缓存字节稳定性变成有测试守护的契约:新增 tests/agent/test_prompt_byte_stability.py(system prompt 两次构建逐字节相等 + volatile marker 前稳定区相等 + 跨 PYTHONHASHSEED 子进程相等——同进程 hash seed 固定会假绿,set 遍历/异步注册顺序漂移只有跨进程才现形)、tests/agent/test_tools_schema_stability.py(tools JSON 两次构建/跨 cwd/跨进程逐字节相等,cwd 泄漏即前缀漂移)、tests/model/test_wire_payload_allowlist.py(载满哨兵字段的 Message 经 OpenAI 与 Anthropic 两条路径的 wire payload 都不含 metrics/references/provider_data/thinking 等本地字段;Anthropic block 透传面显式钉住)。claude.py:format_messages 补注释钉清该边界。审计结论:to_model_dict() allowlist + claude 的属性直读均无可修泄漏,故只加守护不加新出口。
  • 修复 Anthropic 长请求下 auto-compact 摘要必然失败_summarise_conversation 走非流式 invoke(),而 Anthropic SDK 对预计超过 10 分钟的请求直接报 Streaming is required for operations that may take longer than 10 minutes 并拒绝——需要压缩的会话恰恰是最大、最慢的那一类,等于摘要在最需要它的时候永远拿不到,只能落到 logger.warning + return None,再撞熔断器(连续 3 次失败后跳过压缩)。现在只对这一条错误消息做 fallback,改用 response_stream() 收集分片拼回摘要(_summarise_conversation_stream),其余异常行为不变;流式路径同样经过 redact_sensitive_text 并更新 _conversation_previous_summary,迭代摘要链不断。
  • 压缩触发双阈值改为纯窗口比例,修掉小窗模型每轮触发 auto-compact 的 bugEVICT_THRESHOLD_RATIO 0.7 → 0.8(evict 起压点),Layer 2 should_auto_compact 从固定的 window - 13_000 改为 int(window * 0.95),随字段 _auto_compact_buffer_tokens 一并删除。原 13_000 是照抄 Claude Code 的绝对值(设计点是固定 200K 窗口,约 6.5%),套到本库 8192–1M 的窗口分布两头都错:小于 13K 的窗口(gpt-4=8192)threshold 变负导致 should_auto_compact 恒真、每轮烧一次 LLM summary——本次修复的核心 bug;1M 窗口则只留 1.3% 余量。行为变化:小窗模型不再每轮触发;200K 触发点 93.5% → 95%(略后)、1M 98.7% → 95%(略前)。两个比例取 0.8/0.95 而非 0.9/0.95 的理由:给 evict 压到 0.5 target 留 15% 缓冲(0.9 起跳要一轮腾出 40% 窗口的 tool result,文本为主的会话做不到,Layer 2 会在 5% 间隙内接力触发,等于同轮两次前缀破坏);且避开与 IRREDUCIBLE_PROMPT_RATIO = 0.9 的字面量撞车。两层同为纯比例后 layer1 < layer2 对任意窗口恒成立,此前"过原点比例 vs 带截距直线"的交点倒置隐患同步消失。
  • native compaction 限额同步去掉绝对 buffer,堵掉同族"小窗恒触发"地雷Model.native_compaction_token_limit() 的默认实现 max(1, window - 13_000) 删除——声明 supports_native_compaction = True 的子类必须自己覆写,否则直接 NotImplementedError(旧默认在 window < 13K 时塌成 1,任何新 provider 置 flag 而不覆写就会每轮触发 native compaction,此前只是被"base flag 默认 False"掩盖);OpenAIResponses.native_compaction_token_limit()max(1, window - output_limit - 8192) 同样会在小窗塌到 1,改为 max(int(window * 0.8), window - output_limit - 8192)——保留"为 compaction 响应预留输出空间"的语义,但地板对齐下游 should_native_compact min-cap 的 80%,塌缩点不再改变行为。新增 TestNativeCompactionLimits 覆盖:base 默认必须 raise;responses 限额在 8192–1M 全窗口 ≥80% 且 < window 并随窗口单调增长。
  • CLI 工具输出展示精简(对齐 codex 风格):工具报错不再用红色(改 dim yellow + ),且一律「省略前头、保留后面」——单行错误消息保留末尾 77 字符(异常类型在结尾),apply_patch 错误体与通用错误结果保留末尾若干行;execute 输出从 head 10 + tail 10(最多 20 行)改为 tail-only 窗口(≤10 行全显,超长只留尾部 6 行、错误 12 行、diagnostics 8 行),折叠部分以一行 … +N lines (Ctrl+O to expand) 开头提示;工具耗时下限提到 1 秒——(47ms) 这类快调用不再显示,只有耗时较久的调用(主要是前台/后台长任务 execute)才带 (N.NNs)
  • 本地 SDK/CLI 启动不再被 provider SDK 与远程 model catalog 拖慢import agentica 不再 eager import openai/anthropic,CLI provider registry、Anthropic setup helper、图片 OCR fallback 都改为按需加载;agentica --version 这类非交互命令也不再提前加载 interactive UI。模型构造时查询 context window / vision 支持不再同步访问 https://models.dev/api.json:catalog lookup 只读新缓存、旧缓存和内置 fallback,CLI 在进入 agent 路径后用带 10 秒硬超时的 daemon 后台刷新,SDK 也公开 refresh_model_catalog() / refresh_model_catalog_in_background(),离线或 TLS 握手慢时不会把首屏卡 10 秒以上。console script 直接指向 agentica.cli.main:main,包根 main 在任意导入顺序下保持 callable;公开 MODEL_REGISTRY 继续返回 provider class/function,不把 lazy import tuple 泄漏给调用方。本机验证:真实交互首 prompt(--no-workspace)从约 12.9s 降到约 1.9s,create_agent() 从约 10.6s 降到约 1.15s,安装包 agentica --version 热启动约 0.87s。
  • CLI profile 改为 session 级持久化,/resume 不再被同目录其它会话的 project active profile 带偏。每个 session 的 <id>.meta.json 现在记录 profile_name/profile_sourceagentica resume <id> 与交互式 /resume <id> 会优先恢复该 session 的 profile,再按 config.yaml 当前 profile 定义解析 provider/model/key/tuning,因此同一 work_dir 下用不同 profile 开发的 session 可以各自恢复原 profile。project.json.active_profile 继续保留为该 work_dir 新开 CLI 的最近 active profile,显式 /model <profile> 与 resume 成功恢复 session profile 后会刷新它;--profile/--model_name/... 启动时的显式模型参数仍优先,不会被 session sidecar 覆盖。顺手修正 /model --clear:清掉 project override 后应用 global/default profile,但不再立刻把同一个 fallback 写回 project override。
  • 反转执行中输入的默认语义:普通输入默认 steer 当前任务,/queue 才创建下一轮任务。此前执行中敲的字一律排队等当前 run 结束——纠偏("不是这个文件""报错其实是 503")赶到时 agent 已按旧条件改完,下一轮只能返工。现在普通文本经 Agent.steer() 在下一个 tool 批次边界注入当前 run(不中断正在执行的工具),接受时显示 ↪ Guidance added · /queue to always run next,若 run 先于消费结束则显示 ↪ Current task finished before using the guidance · queued next 并降级入队;/queue <prompt> 显式排队,/btw 并行问无关问题,行为不变。边界:带图片附件的输入与 /requesting-code-review ... 这类 skill 调用仍按新任务排队(steer 通道只承载文本)。可靠性契约"永不丢消息"随之补齐:steer() 已接受、但因到达于 run 的最后一次推理期间而未消费的文本,此前被 _end_steer_window() 静默清空,现在由 agent 暂存(pop_undelivered_steer()),CLI 在 run 结束后自动转成下一轮输入并插在 goal 续跑 prompt 之前;来源属性随 steer 缓冲全程携带(steer(..., relayed=True)),被 park 的 peer/后台消息降级时仍以 __RELAYED__ 标签入队,不会重新获得 slash 命令派发权。连续多条 steer 保持输入顺序、合并为一条在下个推理边界注入(原有行为)。/steer 命令保留为显式形式。
  • /stop 现在必须显式给目标,且只管后台任务/stop <id|pid|#n> 停一个、/stop all 停全部,不带参数只打印用法并列出可选目标,什么都不停(此前空参数 = 停掉所有后台 agent 任务和后台终端命令——它和 /stop <id> 只差一个 token,又常常在别的任务正跑时被敲下,把"全停"当成缺参数的默认值代价太大)。当前这一轮的中止权收归 Ctrl+C 独有,/stop 刻意不做这件事:Ctrl+C 做的事比 Agent.cancel() 多(唤醒卡在 ask_user_question 上的线程、把常驻 goal 置为 paused 以免轮次钩子立刻续跑、连按第二次升级为强制退出),而且 agent 正等你回答问题时输入框里的任何一行都会被当成答案提交(tui.pyinput_request 分支),/stop 那时根本到不了命令处理器——最需要停的时候它恰好不可用,再加一条更弱的取消路径只会在关键场景上和 Ctrl+C 分叉。同步改掉所有指路文案:/ps 页脚、状态栏后台提示(/stop <id> to close,窄栏变体不再单列 /stop)、/help 里的 /stop <id|all>Ctrl+C: Interrupt the current task、命令注册表描述。
  • Ctrl+C 中断提示不再宣传 Ctrl+\ 这个当场按了没反应的键:prompt_toolkit 的 raw_mode 会清掉 ISIGinput/vt100.py:262),所以按 Ctrl+\ 在健康的事件循环下只是个被吞掉的 0x1c 字节(仓内没有任何 c-\ keybinding),压根不产生 SIGQUIT——而那条提示恰好只在「循环健康、你刚按了 Ctrl+C」时打印。实测(真 pty:raw 下子进程读到 b'\x1c',cooked 下收到 SIGQUIT)确认 app.py 装的 SIGQUIT 硬逃生口只在 run_in_terminalcooked_mode 窗口内生效,也就是循环被后台写入饿死、正等用户回答那个状态。因此提示挪到 ask_user_question 提问组件下方(Enter to answer · Ctrl+C to cancel · Ctrl+\ if frozen),那里才是它成立的地方。顺带把提问组件的行文本抽成 _ask_prompt_lines,渲染与预留高度同源,不会再各算一套。
  • 修复同批次多次编辑同一文件时 CLI 重复展示最终 diffedit_filewrite_fileapply_patch 现在把工具执行时掌握的真实 before/after 快照作为 display-only 元数据随完成事件透传,CLI 不再根据批次开始/结束时的磁盘状态猜测,也不在展示层重放 edit_file 替换语义。每个调用只显示自己实际执行的变更;多文件 patch 同样使用原子预检得到的各文件快照。元数据不进入模型 API 内容。
  • 交互式 CLI 的 Ctrl+C 中断摘要精简:执行中按 Ctrl+C(含 AgentCancelledError 路径)现在只打 ⚡ Agent cancelled + Worked for … 分隔线,不再附带 Token usage 与 agentica resume <id> 提示——会话还在继续,这两样是噪音。完整摘要只在真正离开会话时打:退出(Ctrl+D//exit)、/new 切换、一次性模式(-p)中断退出。format_session_summary 新增 brief 参数。
  • 删除审查确认的死代码与重复 API 面(均先经全库调用点核实;均不在 docs/API.md 稳定 API 清单内,但属可外部 import 的面,下游若有使用需迁移):compression/tool_pairs.py 整删——sanitize_tool_pairs 唯一生产调用方曾是 context_overflow_threshold 的 FIFO 按位置丢弃,那条路径删掉后只剩测试在引用;Agent.from_parts 及四个 grouped config(AgentDefinition / AgentExecutionConfig / AgentMemoryConfig / AgentSafetyConfig)——同一套 flat 参数的三层包装,仓内唯一调用方是自己的测试,且已与持续扩张的 Agent(...) 参数面漂移;SubagentRegistry 的 listener 机制(on_complete 全库零调用方,且在 "running" 等非完成状态也会触发,契约本身不准确);Agent.clone()已声明字段getattr(self, "_tool_runtime_configs", {}) / _skill_runtime_configs 防御改为直接访问。连带删掉 CLAUDE.md 整个 Temporal 段落(agentica/temporal/ 已不存在)与 docs/advanced/compression.mdsanitize_tool_pairs 的条目。
  • PEER_MESSAGING_POLICY 大幅精简agentica/tools/peer_tool.py,70 行 → 28 行),并补上 worker 完成回报的正向指令。旧 policy 里"只在 sender 等答案时才回""仅告知的消息无需回复""别发 bare acknowledgement"三条叠加,把 planner-worker 里 worker 做完该发的完成回报也压成了"无需回复的告知"——这正是最近改完 prompt 后 worker 不再主动回报的根因。新 policy 明写:work 做完或卡住时,把结果发回派活方(sender 看不到你的终端,"done" 是你发出去的、不是它能观察到的),同时保留 ask_user_question 相关的限制(peer 派的活,问题用 send_message 回派活方而非 ask_user_question,因为那个框只在你自己终端、没人看)。其余过度限制(label source、log_file/session_log 指引、never ask what permissions refused、when no session affected don't send 等)一律砍掉——LLM 自己能判断。multi-agent skill 的 "Talking to it" 同步精简并补上同一句完成回报。
  • 删除 workspace 根目录的 AGENTS.md:不再创建、不再注入 system prompt,Workspace.exists() 改看 users/ 目录、list_files() 改报这个 user 自己那份。常驻规则只认 users/{user_id}/AGENTS.md + 项目链(repo 根往上)——根目录那份随包发的模板本身就是 You are a helpful AI assistant / 默认 lint 配方这种零信号 boilerplate,而它和用户级那份注入成同一个 prompt 里相邻的两块,读的人无从区分谁说了算。预算顺序同时反过来:现在 users/…/AGENTS.md 排在项目链前面,挤爆时留下的是常驻规则(项目链就在仓库里,一个 read_file 的距离),此前反而是它先被丢。连带删掉 DEFAULT_GLOBAL_FILESinitialize(force=...)——force 唯一的作用就是重写那个模板,文件没了它就只是个不做事的参数。为兼容主流 coding agent,默认 CLI workspace 会把 ~/.agentica/AGENTS.md 维护成指向 ~/.agentica/workspace/users/default/AGENTS.md 的 symlink;非 default user 和自定义 workspace 不使用这个全局入口。
  • tool_error / success_pattern 卡片不再进 system promptCompiledExperienceStore.get_relevant 过滤,success_pattern 原本已过滤)。实测一个真实会话的 ## Learned Experiences 五条里五条都是 read_file_file_not_foundexecute_command_timed_outtmux no server running 这类我们自己工具的失败遥测——它们既不告诉模型用户要什么,还把真正会改变行为的用户纠正挤出了 top-k。捕获照旧开着capture_tool_errors=True 不变):skill upgrade 正是拿 tool_error / tool_recovery 事件和卡片给生成的 SKILL.md 里的 gotchas 找依据,关掉捕获等于把那条流水线的证据源抽走,而用户抱怨的只是 prompt。判据写进 ExperienceConfig 的 docstring:capture 决定落盘,injection 决定进不进 prompt,两件事

features

  • /rewind 端到端接线:一键回退到任意历史 turn(code + conversation),取代 /checkpoint(Reasonix Esc-Esc rewind 的命令形态,5.3 收尾):turn 循环在 _process_stream_response 里自动 checkpoint——每轮开头 begin_turn(记 msg_index = 当前会话长度)、TOOL_STARTED 事件在文件工具真正写入前(Model._run_function_calls_impl Phase 1 先 yield started、Phase 2 才执行)对 write_file/edit_file/apply_patch 的目标路径 first-touch snapshotfinallyfinalize_turn(成功/取消/报错都落点)。/rewind list 列出历史 turn(号 + 时间 + prompt 预览 + 改动文件数),/rewind <n> 预览、/rewind <n> --yes 执行 RewindScope.BOTH——复用 CheckpointManager.restore 回滚文件(含删除本 turn 新建文件)并按 msg_index 截断会话重建 runs。这就是「agent 跑偏了,不知道它改了哪 20 个文件也能一键回退」的闭环。删除 /checkpoint(手动 list/create/diff/restore)及其测试;新增 agentica/cli/rewind.pyextract_rewrite_paths / truncate_conversation / get_turn_checkpointer)与 tests/cli/test_rewind_cli.pyEsc-Esc 键位:双 Esc 触发 /rewind list(不做交互式选择面板,CLI 不做重 TUI),/undo 弃用并重定向到 /rewind list;修复 /rewind <n> 数字语法解析与 usage 文案里 [list]/[--yes] 被 Rich markup 吞掉的问题。

  • per-turn 自动 checkpoint + rewind(Reasonix internal/checkpoint 移植,5.3,复用 CheckpointManager:新增 agentica/checkpoint.py::TurnCheckpointer——把 Reasonix 消解「per-edit 快照爆炸」的三个设计移植过来:per-turn 聚合(每个 user turn 一个 checkpoint,而非每个 edit 一个)、路径去重(同 turn 内 first-touch 才捕获,后续触碰忽略)、first-touch 捕获(在第一次触碰时读原文,rewind 恢复 turn 起点而非 mid-turn 状态)。配 RewindScopecode/conversation/both)与 RewindResultcode 复用 CheckpointManager.restore 回滚文件(含删除本 turn 新建的文件);conversation 返回 msg_index 会话截断边界(不写文件);both 两者。Checkpoint/manifest 向后兼容新增 turn/msg_index/prompt 字段。这是长时自治运行「可读、可撤销」产品主线的第一步——turn 边界 API 供 CLI prompt loop / Runner / 工具包装器调用,刻意不接进 BuiltinFileTool 编辑路径(同 base 原语的理由:自动捕获属于 turn 循环,不属于 per-edit 工具)。测试 tests/agent/test_turn_checkpoint.py(13 例:dedup、first-touch、多 turn 独立、三 scope、跨进程、无快照 turn 也记录边界)。

  • use_capability 稳定代理 + McpTool.defer_schema(Reasonix use_capability 移植,P0):新增 agentica/tools/use_capability_tool.pyUseCapabilityTool,一个 schema 恒定为 action(list|inspect|call|decline) + name + arguments 三字段的代理工具,模型经它发现/检查/调用/拒绝 deferred 工具;McpTool 新增 defer_schema 参数,置 True 时 MCP 函数以 deferred=True 注册——仍在 host registry 可执行,但不再展开进 provider 可见的 top-level 工具 schema。装上/刷新一个 MCP server 不再让 tools 数组漂移、不再从 tools 起点冷启动 prompt cache(v1 G2 里「MCP 漂移」那条 P0-2 刻意不 cover 的场景,由结构解决而非测试豁免)。默认 defer_schema=False 保持原行为,配 UseCapabilityTool 一起用才生效。测试 tests/tools/test_use_capability.py(proxy schema 固定三字段 + list/inspect/call/decline 派发 + deferred 不进 top-level)、tests/tools/test_mcp_defer_schema.py(defer_schema 标记 deferred / 默认展开)。

  • Cache-impact PR 门禁 + cache-guard(Reasonix 工程纪律移植,P0):新增 scripts/check-cache-impact.sh(改到 cache 敏感目录——prompt/tool 序列化、provider wire/cache-control、compression、session_log、cost_tracker——的 PR 必须在 body 里填 Cache-impact + Cache-guard,touch system prompt 面还要 System-prompt-review,empty/todo/tbd/n/a 拒绝)、scripts/cache-guard.sh(跑字节稳定 effect test:system prompt / tools schema / wire allowlist / use_capability)、scripts/check-cache-impact.test.sh(门禁自测)。.github/workflows/ubuntu.yml 新增独立 cache-guard job(仅 pull_request 触发),把「缓存稳定是一等契约」从「有测试」升级为「有流程门禁」。

  • agent 能自己维护常驻规则了,但没有为此加任何工具:memory 的 system prompt(以及 self_manage 的说明)现在写明 AGENTS.md 就是「每个后续会话都会全量注入 system prompt」的长期记忆,用户级在 <workspace>/users/{user_id}/AGENTS.md(CLI 就是 default user)、项目级在 repo 根,用 edit_file / write_file 追加一行即可,并明说链每会话冻结一次、所以下个会话才进 prompt(不说的话模型看本轮行为没变会以为写失败又写一遍)。此前用户说「记住:以后都要 X」时,agent 手上只有 save_memory——那存的是事实、按 query 相关性召回、可能永远不被召回;要真正常驻只能 write_file 一个它得自己猜的路径。注入的每个文件都带 <!-- 路径 --> 头,所以「在哪」本来就在上下文里,缺的只是「这文件是干什么的」。
    同一轮里先写过一个 remember(text, scope) 工具又整个删掉(含 Workspace.remember_rule<!-- agentica:rules:start --> 托管区块、去重、CLI 完整展示、subagent 黑名单):它解决的问题是模型不知道路径,而这一句 prompt 就够;工具那套路径解析、格式约束和幂等比较全是为此付的利息,而托管区块还等于要求人和 agent 在一个「谁都不必约定格式」的文件里约定格式。留下的判据:一个工具要能做到 read_file/edit_file 做不到的事才配存在——有 schema 要校验的文件(config.yaml 的 round-trip,self_manage)、压根没有文件工具的 surface、或者背后没有文件的操作(pip 升级);「模型不知道路径」不在其列。用户级路径在运行时解析Workspace.user_agent_md_path(),原 _get_global_agent_md_path 转为公开——prompt 文本要点名这个文件,它就成了契约的一部分),不在 prompt 里写死 ~/.agentica/AGENTS.md:default CLI 会把它作为 symlink 兼容入口,但多用户 workspace 每个 user 仍有各自一份 canonical 文件,不能把 tenant 的规则写进 default user。
    端到端实测(tmp/agents_md_rule_smoke.py,隔离 AGENTICA_HOME 起两次真实 CLI):第一次说「记住:改完代码要更新 CHANGELOG.md」→ 模型自己用文件工具建出并写进 workspace/users/default/AGENTS.md;随后清空同目录下其它文件(否则「记住」也会被 experience 捕获钩子记下,第 2 步会自己通过)→ 第二次全新会话被问起时直接复述出来。

  • 企微 / 微信 / 飞书 / Telegram 直连本机 CLI 会话PEER_BRIDGE=true,新增 gateway/services/peer_bridge.py):手机上 @list 看本机有哪些 CLI 会话、@nlp-f1 <话> 说给它、之后裸文字继续发给同一个会话、@off 收手。没有新协议——bridge 就是已有 peers 通道上的又一个 peer(每个 IM 用户一个 PeerSession),所以 CLI 在 list_agents 里直接看到手机、用它本来就有的 send_message 回话,mailbox 顺序、背压、重复/限频刹车、「在 tool 批次边界投递」全部继承而非重写。没打 @ 的行照旧走网关自己的 agent,所以开了这个功能不拿走任何东西。
    几条卡死的前提:转发的话按 from_kind="user" 发出,接收端当成在那个终端里敲的字——这正是目的(确实是用户在敲),也是为什么它默认关空 allowlist 直接拒绝转发Channel.check_allowlist 把「没配 allowed_users」当成所有人,用来和网关 agent 聊天没问题,用来往你的终端里敲字就是把机器交给任何找到这个 bot 的人(拒绝语里直接写该设哪个环境变量)。它必须和 CLI 同一个 AGENTICA_HOME,否则永远看到空的 live/,而症状和「没开会话」一模一样,所以启动日志和空列表回复都会报出搜索的目录。bridge 消息不进按会话的入站队列:它不跑 agent,而一条 @session 停 要是排在网关 agent 当前那一轮后面就失去了唯一的意义。自己的端点从所有列表与寻址里排除(每个 IM 用户都是已发布的 peer,否则会互相遮蔽真实会话的名字);未知或歧义的名字回的是实时会话列表——手机上你接着需要的是重试用的名字。

  • CLI 里 peer 消息的展示分清了谁在说话:用户从另一个终端(或手机)转发的指令走和你自己打字一样的 面板——那个面板是 CLI 唯一的「人说的话」信号,而转发的指令带的正是这份权限;另一个会话的 agent 走不带面板的 Agent <name> › 行,两者再也不会混。此前两类消息都被塞进 里,还连带把给模型看的 [Message from another agent session '…' — reply with …] 授权头印给了用户;现在那个头只留在 format_for_model(模型上下文)里,不进 transcript。

  • ask_user_question / confirm 的问答完整留痕:调用行不再重复一份被裁剪的问题(问题和选项在 TUI 的提问组件里已经完整渲染过,重复一遍等于同一个问题印两次、两次都不全),改由结果块完整回放 Q/A 两侧且不折行截断;工具返回里的 prompt 也不再截到 200 字符。提问组件活在 prompt_toolkit 的布局里、用户一答就消失,结果块是这次交互唯一的持久记录,几周后翻回去要能看到当时问了什么、用户怎么定的。

  • wait(timeout=...) 去掉 300 秒硬上限_MAX_WAIT_SECONDS_DEFAULT_WAIT_SECONDS,300 现在只是默认值)。此前传 400 会被静默夹到 300,返回一句「还在跑」——调用方并不知道自己的意图被改过。注:这使 1.4.12 里「单次上限 300 秒」的说法失效。

  • wait / delegate / 后台 execute 的命令与输出不再省略_FULL_RESULT_TOOLS 增加 waitdelegate;后台 execute 的启动行与完成回执单独走完整显示)。这些结果的正文就是「哪条命令、日志在哪、退出码多少」——被折成一行 之后,用户既没法接着排查也没法把路径复制出来。

  • OpenAI 侧补上 prompt_cache_key(默认开启,enable_prompt_cache_key,取 session_id,没有会话时退到该端点的粘性路由 id。此前只有 cache_control_session_header 一条路径,那是部分代理的专有 header;跑原生 OpenAI / DeepSeek / Qwen 时走的是隐式缓存,一个路由提示都没传。而隐式缓存命中要同时满足两件事:前缀一致、且请求落到存着这份前缀的那台机器上——路由只哈希 prompt 前约 256 个 token,共享前缀的请求本来就会被摊到多台机器做负载均衡,缓存不会跟着走。prompt_cache_key 会并进这个哈希,官方案例里某编码客户接入后命中率从 60% 升到 87%,gpt-5.6 起更是用上更可靠匹配的前提。作用域按会话:官方建议单个 key 不超过约 15 请求/分钟,一个会话不可能超。
    enable_cache_control 正交——后者是 Anthropic 的断点协议,这条只是路由亲和性,不认这个字段的端点忽略即可(已在真实兼容端点上验证不会 400),所以默认开启;遇到严格校验未知字段的端点可关。

  • list_agents / /list-agents 多报会话的配置与负载:profile、model(provider/name)、idle/busy、context 已用/窗口。这些字段从 peer 心跳每秒 tick 里带出,读的是状态栏同一份 tui_state——/model/model --clear/config set 都能改模型,靠各处 push 迟早漏一条;心跳本身只在值真变了才写盘,不会把 30s 的 presence 写成每秒一次。字段描述的是配置与代价,不是容量:忙着的会话仍会在 tool 间隙收信,将近满的窗口也只是「再塞东西会触发对方压缩」,不是拒收墙。

  • /debug [on|off] 改成会话内开关 verbose 日志(对标启动时的 --debug):无参翻转,on/off 显式设置;写回 agent_config 以便 /model/resume 重建 agent 后仍在。原先打印的会话事实(model、history 条数等)本来就在 /status

changes

  • README 重写,并删除 README_JP.md(日文版不再维护,语言切换只剩中 / 英)。开头改成居中的 logo + 一句话 + 下载入口,然后是「为什么选 Agentica」四条(评测、多会话成队、自进化、人可以离开现场),评测只放一张图加一句结论——逐项指标和复现命令本来就在评测页,README 里再抄一遍就是两份会各自过期的数字。「架构」整节移出 README(连同架构图与 agent loop 图)到 架构文档:读 README 的人在决定要不要装,五层抽象不是那个决定的输入。
  • 评测图换成一张纯英文、两个题库维度对齐的对照图,并入库生成脚本 evaluation/code_benchmark/plot_pk.py。原先是两张分别画的中文图(coding 4 个面板、data analysis 5 个面板),刻度和口径都不一样,没法一眼比;现在 2 行(Coding / Data analysis)× 3 列(Accuracy ↑ / Wall-clock ↓ / Input tokens ↓),两边都是实测数字。成本没有单独一列——两边跑的是同一个模型,成本差就是 token 差,而 Codex 侧 summary.json 里的 sum_cost_usd0.0(没实测),单独立一列只能靠换算填。脚本从 results/<run-id>/summary.json 现读现画,不手抄:图和它旁边的表此前就漂过一次(重跑更新了表,图还在讲上周的结果)。字体钉死 DejaVu Sans(matplotlib 自带),所以本机和 CI 出的是同一张图。
  • 文档里的图片链接统一为 raw.githubusercontent.com/.../main/... 绝对路径docs/guides/benchmark.md 用的是 github.com/.../blob/...png——那在 GitHub 上会重定向,但 mkdocs 站点里渲染成的是一个 HTML 页面而不是图,也就是文档站上那两张图一直是坏的;docs/index.md 用的 /raw/ 形式虽然能出图,但目录一挪就断。
  • .gitignore 里 Python 打包用的 build/dist/ 收紧为 /build//dist/,并补上 desktop/dist/。未锚定的 build/desktop/build/icon.png(打包图标母版)也一起吞了——和上一轮 lib/ 吞掉 web/src/lib/ 是同一个错:为仓库根写的打包规则,匹配了任意深度的同名目录。
  • DeepAgent.__init__ 不再收 **kwargsAgent 的 41 个参数全部显式声明(同时删掉 kwargs.setdefault("enable_experience_capture", True) 那处 hack,改成签名里的默认值)。此前多出来的参数被收进 **kwargs 转发给 Agent.__init__——那边是纯 keyword-only 且没有 **kwargs,所以一个拼错或被上游改名的参数死在 Agent 里,报的那句 TypeError 从不提 DeepAgent;对按名字拼 kwargs 的服务来说这就是构造期崩、每个请求 500。现在原生 TypeError 直接点名 DeepAgent.__init__() 和那个参数,且发生在默认模型构造之前(不然会被「没配 key」盖掉)。参数齐备性由测试守住(test_deep_agent_declares_every_agent_parameter):给 Agent 加了参数却忘了加这里,等于让它在预设里静默不可达。
    连带的判据:include_* 不是可以按名字拼 kwargs 的稳定契约。同一轮里提过 builtin_tools=False 这个「一键关掉全部 builtin」的总闸,没有采纳——它只管那 8 个 include_*DeepAgent 仍然 auto_load_mcp=Truemcp_config.json 里有什么就加载什么)并注入 BuiltinMemoryTool,于是这个名字承诺了一个它兑现不了的保证,对白名单业务面比逐个列 include_* 更危险(tmp/deep_agent_no_kwargs_smoke.py 第 4 步就是把这个跑出来看)。真要白名单:用裸 Agent + 只声明需要的预设,再对最终工具表做一条构造期断言——那是唯一能拦住「上游新增一个默认开启的 builtin」的检查。
  • 用户级 AGENTS.md 改为按 user 存放:canonical 文件是 <workspace>/users/{user_id}/AGENTS.mdWorkspace.user_agent_md_path();默认 CLI workspace 额外维护 ~/.agentica/AGENTS.md~/.agentica/workspace/users/default/AGENTS.md 的 symlink)。此前 default user 特例落在 AGENTICA_HOME、其他 user 落在 users/{id}/,一个概念两个位置:CLI 与 SDK 行为分叉,任何调用方想回答「这个 user 的偏好存在哪」都得先判断自己处于哪种模式。现在只有一套真实布局,CLI 不是例外,它就是 default user;home 入口只是兼容别的 coding agent 的文件系统别名。顺带删掉只在旧 home 模式下生效的 AGENT.md(单数)fallback。
    手写过 ~/.agentica/AGENTS.md,第一次运行会先保留内容:若 canonical 文件还不存在就移动过去;若已存在且内容不同,就合并进 canonical 文件,然后把 home 路径替换为 symlink。把内容编译进那个文件的外部 workflow(如 learn-from-experience)可以继续写 ~/.agentica/AGENTS.md,最终会落到 users/default/AGENTS.md
  • 删除 memory/experience → AGENTS.md 的编译路径Workspace.sync_memories_to_global_agent_mdCompiledExperienceStore.sync_to_global_agent_mdWorkspaceMemoryConfig.sync_memories_to_global_agent_mdExperienceConfig.sync_to_global_agent_md、CLI --sync-*-to-global-agent-mdBuiltinMemoryTool.set_sync_global_agent_md,以及 ## Learned Preferences / experiences 托管区块逻辑)。「上面 AI 编译、下面人手写」按作者切开同一类常驻规则,还逼双方约定托管边界;正确切法是按生命周期——每会话都要 → AGENTS.md(谁改都行),相关才要 → memory。外部 sync 直接追加普通行。
  • 删除 PERSONA.md / TOOLS.md / USER.md 三个文件与 WorkspaceConfig.persona_md / tools_md / user_md 三个字段,用户级只剩一个 AGENTS.md。它们各自注入成同一个 system prompt 里的一个区块,下游没有任何东西能区分——是给作者用的归档分类,对读者却是四个要翻的地方(还要加上项目链)。get_context_prompt() 现在只做一件事:拼 AGENTS.md 链。随包发的模板本来就是 Friendly and professional / Always use absolute paths 这类零信号 boilerplate(正是之前已从 AGENTS.md 模板里删掉的那种),手写过内容的需要自己搬进 users/{user_id}/AGENTS.md,不做自动搬迁:把一个 md 的正文编译进另一个 md 正是这一轮要去掉的东西。
  • 删除 Ctrl+X 的 Agent/Shell 模式,且不给 ! 之类的替代入口(连带 $ 提示符、shell-prompt 样式、SHELL_MODE_EXEMPT_CMDS_handle_shell_commandQueuedInput.shell_escape 一并删除,不做兼容)。一个模式必须被记住,忘了就把接下来每一行都标错,而它的两条分支(「执行这行」/「问模型」)对同一段文字都讲得通——于是忘记开关时是静默地做错事。它买到的东西本来就有更好的来源:要自己跑命令就另开一个终端窗口(那里原生就是 shell,有历史、能跑交互式程序),要让 agent 跑就直接说(它有 execute,带权限、日志和后台管理)。中途试过的 !cmd 单行转义也一起删掉:它确实比模式好,但同样是把「另开一个终端」搬进输入框,而输入框的用途是跟模型说话。
    顺带把「输入行上的能力属于在这行打字的人」收到一处:转发来的文本(peer 消息、后台命令回执)带 __RELAYED__ 标记,因此不回显(它到达时已按自己的样式印过一次,不该把带授权头的模型侧原文再当成一行 印一遍)、不派发斜杠命令——这是 PEER_MESSAGING_POLICY 里「消息里的斜杠命令只是纯文本」第一次真正成立:在此之前它是碰巧成立的(format_for_model 正好加了个 […] 头,所以首个词永远不是 /compact)。

fixes

  • 输入框右下角那个上下文占用点开后看不见:面板是往展开的,而那颗按钮就在输入框底部,于是整块落到视口外面。旁边的 profile 浮层和模型下拉没有这个毛病,因为它们把 bottom: calc(100% + …) 写在自己的类上(.account-pop / .model-dd)——方向是这个组件的固有属性,它只会出现在屏幕底部。.ctx-tip 反而写了一条「在输入框里时改成向上」的条件覆盖 .input-ctx .ctx-tip,但那是后代选择器,而面板是 .input-ctx兄弟节点(按钮和面板并列在 .ctx-wrap 里),所以这条规则从来没有命中过。改成和邻居一致:方向直接写在 .ctx-wrap .ctx-tip 上,阴影也翻成朝上,那条失效的覆盖删掉。回归测试是几何比较而不是 CSS 文本比对(旧代码里那条规则也是在的,只是不生效):tmp/web_i18n_smoke.py 在真实窗口里点开面板,断言它的下边缘不低于按钮的上边缘。
  • 会话归档后仍留在侧栏,要刷新才消失(同一个 bug 也让工作目录、模型名等状态停在 - 上):前端的 store 是可变单例——state 和它里面的每个对象终生保持同一个引用,而 useSyncExternalStore 判断「要不要重渲染」用的是 Object.is。所以 useMemo(..., [s.sessions]) 看着像对的,实际永远不会重算:sessions 这个对象引用从来没变过,变的只是它里面的 archived 标志。改成每次 setState / bump() 递增一个 state.rev,订阅和 useMemo 都锚在它上面。
  • 设置里「默认工作目录」旁边的「浏览」点了没反应:那颗按钮在设置弹窗里没有接目录浏览的实现,只有新建会话的目录弹窗里有一份。抽成共享组件 web/src/components/DirPicker.tsx(路径输入 + 浏览开合 + 历史 + 列表,单击进目录、「选此目录」确认、「上一级」回退),两处共用。顺带修掉抽出后暴露的样式问题:.dm-row 的 flex 布局原先只写在 .dir-modal 选择器下,搬到设置里就退化成行内流,路径输入框被挤到一小段、「应用」掉到第二行。没有改用系统原生文件夹选择器:浏览器里根本没有这个能力(webkitdirectory 要求用户上传目录内容,拿不到路径),而 gateway 可能跑在另一台机器上——那时该选的是服务端的目录,弹本机选择器只会选出一个对面不存在的路径。.gitignore 里那条 Python 打包用的 lib/(本意是仓库根的 build/lib)把 web/src/lib/ 一起吞了,npm run build 报 5 个 TS2307: Cannot find module '../lib/format'。开发分支上的构建之所以通过,是因为文件就在工作区磁盘上、只是从未入库。规则改为只锚定仓库根的 /lib/,文件补回;顺带把 web/node_modules/ 改成通用的 node_modules/(现在有 web/desktop/ 两个 Node 项目)。
  • Skills 的改、删返回 200 但列表里什么都没变GET/PUT/DELETE /api/skills):路由用 SkillLoader().load_all() 读回列表,而它填的是进程级全局 registry,register() 又是同名保留第一个——本进程加载过一次之后它就是空操作。于是编辑保存后列表还是旧描述、删掉的技能还在列表里,反复点也没用(写入其实已经落盘了,读的是缓存)。改成 reload()(先清空再读)。同时三个写入路由都补 AgentService._invalidate_cache():本会话已经建好的 agent 冻结着旧的 skills 目录(按会话冻结是 prompt cache 的要求),不失效就要等下次重建才认新技能。测试 tests/gateway/test_plugins_web.py(3 条断言在 load_all() 版本上确认失败)。
  • 定时任务的编辑表单会把日程改成别的时间GET /api/scheduler/jobs 只给 schedule 这一个字段,值是 schedule_to_human() 的展示文本(Daily at 7:30Every 2 hours),而 PUT 收的是 parse_schedule() 能认的表达式——用展示文本回填输入框再保存,要么 400,要么被解析成另一个日程。新增 agentica.cron.jobs.schedule_to_expr()(cron 原式 / every Ns / ISO 时刻,与 parse_schedule 严格来回),响应里多一个 schedule_expr,前端表单读它;schedule 保留给展示。测试 tests/cron/test_cron.py::test_schedule_to_expr_parses_back
  • Gateway Web SPA 拉到 /api/status 之后界面仍显示 Dir: - / 模型 -:store 对 state 原地 Object.assignuseAppState 却把同一个对象交给 useSyncExternalStore 当 snapshot。React 用 Object.is 判断,引用没变就不重渲染,所以 boot fetch 成功也画不出来。现在 snapshot 改成递增的 rev,订阅方每次 bump() 都会重绘。
  • 微信发图走到「请看这条图片。」后立刻 'dict' object has no attribute 'detail':解密已经成功,gateway 把 {"url": "data:image/jpeg;base64,..."} 挂到 agent.run(images=...)OpenAIChat.process_image 认这个形状),但 count_image_tokens 假定每项都是带 .detailagentica.media.Image——Layer 1 一算 token 就炸,整轮 AgentService.chat 失败。现在 dict 先收成 Image(url=..., detail=...) 再计数;空/无 url 的 dict 走 85 token 的低细节默认值。测试 tests/model/test_tokens.pytest_media_understanding.py
  • 个人微信图片/语音下载报 Padding is incorrect,失败后空消息再把会话打崩:iLink 入站 CDNMedia.aes_key 有三种编码(base64(16 字节)base64(32 位 hex)image_item.aeskey 裸 hex)。旧代码一律 b64decode 后丢给 AES——hex 形态解出 32 字节,按 AES-256 去解 AES-128 密文,pycryptodome 就报 Padding is incorrect.。语音/文件走的正是 hex 包装,所以两种媒体一起挂。修在 WxBotClient._parse_aes_key(与 openclaw-weixin parseAesKey 对齐),extract_media_typedimage_item.aeskey 拷进 media,下载缺 full_url 时用官方 CDN https://novac2c.cdn.weixin.qq.com/c2c。下载仍失败时不再 agent.chat("")——空 user 消息会被 Claude/兼容代理拒成 messages.N: user messages must have non-empty content 并写进历史;改为回复「没能下载这条图片/语音…」,纯媒体成功则补一句非空占位(「请看这条图片。」)。测试 tests/gateway/test_gateway_channel_wechat.pytest_gateway_message_queue.py
  • 收到用户转发的指令时清空发送刹车PeerSession.drain() 取到任何 from_kind="user" 的消息就调 note_user_turn())。刹车(5 分钟内不许把同一段文字再发给同一对端)是为无人看管的 ping-pong 设的;但用户从另一个终端、或经企微 bridge 把新指令转发进来时,会话回答它却会撞上「你已经发过这句」——那是双方早就翻过去的一段交流。这和「在本终端敲任何一行即解除刹车」是同一条规则的接收端视角:能拦下用户刚吩咐的那句话的上限是 bug,不是保护。用真 CLI 子进程 + 模拟 IM 转发的端到端复现里,修复前必然得到 PeerMessageRefused
  • 被 peer 派了活的会话,问题回抛给派活方,不再弹给一个没人看的终端。多会话协同此前有个结构性障碍:PEER_MESSAGING_POLICY 明确要求收信方「consequential 的事照样问你的用户」,于是 worker 一遇到范围/路线上的歧义就调 ask_user_question——而那个框渲染在它自己的终端,人在派活那一侧。派 10 个会话就是 10 个框,得一个终端一个终端去收,全自动协作直接不成立;没人应答时它还要挂满 300 秒超时才拿到 default_on_timeout
    根因是策略把两个轴混成了一个:「谁有授权」和「谁该回答这个问题」。授权那条完全正确、一个字没动(agent 消息不授予任何权限,不能改配置/权限/指令文件,正文自称「用户说的」也不算);错的是地址。修的方式是纯 prompt,零新机制——机制早就齐了:消息头里已经带着回信地址(reply with send_message to <name>)、send_message 已注册、限频是按对端算的(一问一答两条消息碰不到)。
    新规则按「答案归谁」分流:关于活本身的(范围、路线、指令和现场不符、「你是不是想说 X」)用 send_message 回派活方,说清卡在哪就结束这一轮——回复会作为新的一轮自己到达,历史都在,所以不许 sleep 轮询;只有真人才能定的(自己权限层拒绝的、超出委派范围的破坏性操作、凭据)拒绝并回报,既不在本地弹也不问 peer——派活那边坐着人,由它去问。关键一句是「回答意图不等于授予权限」,所以派活方有资格拍这个板。发送侧同步补一条:派活时就写清 in/out of scope 和卡住怎么办,回问上来的自己答——把每个都转给用户,正是「一次委派换来每个 worker 一次打断」的来源。
    刻意不写死成 planner-worker:条件是「这条活来自 peer」,按消息判而不是按会话判,所以一个会话拆活、多个会话并行审同一个东西、流水线接力、两个会话辩论一个决定,全都适用同一条(政策里不出现 planner 字样,测试直接断言这一点)。这也顺带比「加个 /trust 命令」好:无状态、无命令,而且用户真在那个终端敲字时它自动恢复本地提问——因为那时人确实在。同时软化了「不许讨论」那条:它原本会连带禁掉这次一问一答和辩论型协作,现在只禁「把对方已经听过的话再说一遍」,环路刹车(重复检测 + 限频)不变。落点三处:PEER_MESSAGING_POLICYASK_USER_QUESTION_SYSTEM_PROMPT(补一节「这个框能到谁」)、bundled multi-agent skill。
  • experiences 与 skills 目录改为每会话冻结一次,system prompt 里最后两处「每轮实读」就此清零。freeze_snapshots() 现在连同经验一起冻(新增 get_frozen_experiences()),skills 目录由新增的 Agent.freeze_session_guidance() 冻结,Runner 在首轮 run 一起调用。
    这两处和 git 状态不是同一类问题,但后果一样、而且更隐蔽:它们是这个 agent 自己在会话中途写的。捕获钩子会在工具出错、用户纠正、每约 10 轮的批量 judge 时写入新的经验卡片;skill upgrade 钩子会在后台调 refresh_tool_system_prompts(),而 SkillTool.get_system_prompt() 每次读都按使用近度重排一遍。于是没有任何人提出要求,一次后台写入就改掉 system prompt 的字节——经验那块还在 VOLATILE_SYSTEM_MARKER 之后(尾巴仍在所有 message 断点的前缀里,历史照样重写),skills 那块直接在 marker 之前,连 system head 那个断点都保不住。
    代价用一行指针补回来:经验块写明是会话开始时选中的,并给出 EXPERIENCE.md 的索引路径(新增 Workspace.experience_index_path),要最新的让 agent 自己 read_file 一次。skills 不需要指针——/skills install 走的是重建 agent,新 agent 会重新冻结,所以新装的技能照样立刻可见;被挡住的只有无人看管的后台升级。clone() 会重跑 _init_runtime,subagent 因此从未冻结状态开始,不会继承父 agent 的快照。/context 的 Skills 行改为统计实际注入的块,否则冻结后它报的是一份没人用的列表。
  • system prompt 不再注入 git 状态Workspace.get_git_context() / _git() / _is_git_repo 整块删除。prompt cache 是按字节精确的前缀匹配,而 system message 位于后续每一个缓存断点的前缀里——所以 git status --short 每轮变一行(写代码的会话里等于每一轮),失效的不只是 tools+system 那个断点,而是连同整段对话历史一起按 1.25x 重写。它是 marker 之前变得最勤的东西:工作区上下文和记忆早就由 freeze_snapshots() 冻结、日期只到天(skills 目录同在 marker 前、经验在 marker 后,两者也会中途变,见上一条),所以这一条注入实际上单独承担了 Claude 上偏低的复用率和过半的 cache write 开销。
    分支 / 未提交变更 / 最近 commit 改由 agent 需要时自己跑一次 git 获取——这本来就是一次工具调用的事,而且模型每一次编辑都已经在自己的工具结果里看见了。顺带每轮省掉 4 次 git 子进程(本仓库约 38ms)。
    两条走不通的替代方案:① 挪到 VOLATILE_SYSTEM_MARKER 之后——只保住 system head 那一个断点,volatile 尾巴仍在所有 message 断点的前缀里,历史照样每轮重写;marker 成立的前提是它后面的内容跨轮稳定(冻结的记忆、天级日期),不是每轮都变的东西换个位置继续变。② 像工作区上下文那样每会话冻结一次——省下了钱,换来的是一份越来越旧的文件列表,模型把 20 轮前的状态当成现状,比没有更糟。
  • /help 里带方括号的命令名(如 /model [p/m]/debug [on|off])不再被 rich 当成 markup 静默吃掉;先 pad 再 escape,对齐也不歪。
  • ask_user_question 重构为「问一句、答一句」,用户回答交给 LLM 解析:删掉 mode 参数(confirm / text / select 三种模式)和 confirm() 方法,签名收敛为 ask_user_question(prompt, options=None)——问一个 plain 问题,可选给一组 options,用户用自己的话回答,auxiliary LLM 按语义把回答解析成纯文本(选中项的原文 / yes/no / 简洁复述),结果只含 promptresponseraw_input 三个字段。用户输入永远不可枚举——"3 , 100题, qwen,bge服务支持100并发,workers=10 ok" 这种带理由的自由文本,旧代码先要求整段是纯数字(isdigit 失败)、再做前缀模糊匹配(方向反着)、最后静默回退到 options[0],把用户选 3 曲解成选 1;中间一版改用正则抠 {"option_index": N} 同样是在猜。现在没有 mode、没有正则、没有 isdigit——text in / text out,LLM 读上下文给答案。LLM 不可用(SDK / cron 没绑 agent)、调用失败或返回空时返回用户原话,绝不伪造一个用户没选的选项。
    ask_user_question 改为 async:阻塞的 input 回调走 run_in_executor 不卡事件循环,LLM 解析直接 await model.invoke,30s 硬超时兜住慢/坏的 auxiliary model。工具新增 set_parent_agent / clone(),在 Agent._bind_tools_to_agent 里绑定,复用 resolve_auxiliary_model("ask_user_question") 拿便宜模型。破坏性变更mode 参数与 confirm() 方法已删除,旧调用需改为 ask_user_question(prompt, options=...)
  • SSE 解析失败时,报错终于说得出端点发了什么Malformed stream from the model endpoint 的 raw 一直是 str(JSONDecodeError),也就是 Extra data: line 1 column 309 那一句——屏幕上已经完整印过,所以 Ctrl+O 展开出来的是同一句话,纯属白按。坏掉的那段字节只在 JSONDecodeError.doc 里,别处没有第二份拷贝,于是这个错从来没有可诊断的信息。现在 .doc 整段进 view["raw"](Ctrl+O 和日志共用同一份,不会只补一半),屏幕上另给一行断点前后 60 字符的窗口——两个 SSE 事件粘在一行、HTML 错误页、半截 JSON,一眼就能认出来,不必开 pager。
  • peer agent 消息的展示和工具调用风格统一:来自另一个会话 agent 的消息原来显示成 Agent dep-11 › <消息> 的 grid 行,现在改成 ↳ 🖥️ dep-11 头 + 缩进消息体——和 ↳ 🔧 tool 的工具调用同一套视觉语言,读起来像「一个进来的事件」而不是一行带 的标签。仍然不带 面板,agent 流量不会被误当成用户自己说的话。多行消息每行单独缩进,不再挤在 grid 的一个折叠单元里。同时删掉了消息下方的投递时机说明行(starting a turn / will reach the agent between tool calls)——光看这行不够直白,且消息何时进上下文由 Runner 的注入边界决定,用户不需要每条都被告知。
  • 运行报错的原文写进文件日志。此前它只存在 Ctrl+O 那个进程内缓冲里,而新一轮用户消息开头就 clear_truncated_blocks()(为了让 Ctrl+O 展开当前轮)——于是「看到报错、下一轮已经发出去、再按 Ctrl+O 什么都没有」,日志里也查不到,原文彻底消失。现在 display_agent_execution_error 同时 logger.error(折成一行,便于 grep),~/.agentica/logs/ 里随时可查;CLI 默认已 suppress console logging,所以不会在屏幕上打印两遍。