v1.4.13
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):macOSdmg(Apple 芯片 / Intel 各一个)、Windowsx64NSIS 安装程序、Linuxx86_64AppImage 与amd64deb,命名统一为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.py从docs/assets/logo.png裁出那只挥手猫重建)。electron-builder 从一个正方形母版派生所有平台的图标且拒收小于 512 的,所以既有的 256²cat.png和 48²favicon.ico都不够用;顺带把运行期的两文件分支(Windows 用 ICO、其它用 PNG)也合成这一个——nativeImage只在 Windows 上解 ICO,拿错平台得到的是空图,而空图和「没配图标」看起来完全一样。打包后的应用不再从磁盘读图标:安装包已经把它放到平台真正会看的位置(.appbundle / exe 资源 /.desktop),而从app.asar里读图不是nativeImage承诺的能力。端到端验证tmp/desktop_package_smoke.py(跑真实构建产物:bundle 元数据、asar 载荷、图标、启动、收尸,17 项)。 - Web / 桌面版界面改为默认英文,设置 › 常规 › 语言可切简体中文:此前界面文案是中文硬编码在 JSX 里的,一个开源项目的默认入口不该假定读者的语言,而想改也无处可改。新增
web/src/i18n.ts:en是唯一的字符串源,zh用const zh: Strings声明成同一个类型,所以少一个键就是tsc --noEmit失败,而不是中文界面里冒出一个英文词(npm run build已经先跑类型检查)。带数字或人名的条目一律写成函数(round: (n) => …),因为两种语言的语序不同——在调用点拼模板串是没法翻译的。选择存localStorage.ag_lang(与主题同一套做法)并写到<html lang>,切换即时生效、不用刷新;语言选项各用自己的语言标注(English/简体中文),落错语言的人才读得懂怎么切回去。故意不翻的是标识符:日志事件类型(tool_call、request_begin)、配置字段名(base_url、max_tokens)、审批模式与 profile 名——这些用户也要照着敲和 grep。桌面壳自己那几句(启动失败弹窗、View菜单)也改成英文并且不跟随这个设置:它们要么是 Electron 按系统语言自己本地化的role,要么是 gateway 没起来时弹的框——那时根本没有渲染进程可以读这个偏好。端到端验证在tmp/web_i18n_smoke.py(真实窗口里点真的那颗设置按钮,23 项)。 - 首次启动自动建
admin账号并生成密码,网页登录不再需要?token=:URL 里带令牌那套对个人本机是最差的一种进门方式——令牌进程级、一重启就换,粘错一个字符只会得到 401,而桌面版根本不需要它(壳自己用本机令牌换会话),于是同一个产品的两个入口体验完全不同。现在首启动幂等地 seed 一个admin(AccountStore.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.json(0600,只存 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/json或X-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(按规范不带凭据)。/ws的params.auth.token一直写在协议里但从未校验,现在生效,另外也认 handshake 的 cookie /?token=。CORS 同时收紧到 loopback:Starlette 对allow_origins=["*"] + allow_credentials=True的实现是把请求的 Origin 原样回显,令牌一进 cookie,任何你正好打开的网页就都能读这个 API 并调工具——这不是顺手加固,是这次改动必须一起做的部分。Vite 开发端口仍在放行的正则里。 - Gateway
--port 0选空闲端口并把实际端口写出来,新增--parent-pid:agentica-gateway从只读环境变量改为有真正的命令行参数(--host/--port/--parent-pid)。--port 0由持有 socket 的一方解析——先 bind 再回读端口号交给 uvicorn,而不是「问系统要个空闲端口再把号码传下去」(那样中间的空窗期会被别的进程抢走)。运行进程把 pid / host / port / token / url 写到$AGENTICA_CACHE_DIR/gateway/runtime.json(0600,退出时删除,且只删自己那条),这样桌面壳或另一个终端才回答得出「这台机器上是否已经有 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)对 Codex20260820-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_HOME的wire_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)对 Codex20260817-215956。results/入库该跑次的summary.json/predictions.jsonl。 - Gateway Web 换成 Vite + React SPA,并加上 Session Trace:
gateway/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_call、token_usage),load()/ resume 不把它们喂给模型。GatewayGET /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_ready、system_prompt(每会话去重一次),token_usage的input改成与cache_read不重叠的口径,成本按模型 id 估算。轮次划分靠日志里的因果顺序,所以用户消息改为在request_begin之前落盘(原先在之后,一轮的事件会被算到下一个问题名下)。- 评测
--wire-api responses与 Claude/v1/messages:--agent agentica --wire-api responses走OpenAIResponses。--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_HOME的model_reasoning_effort。以前只读reasoning_effort,缺这个键就默认high,关思考传不进去。也认thinking_enabled: false。- README 增加 Polyglot 评测对照,对比表去掉 Gemini CLI:数字与
docs/guides/benchmark.md同一跑次(Agentica / Codex 各 34/34)。原始summary.json/predictions.jsonl在evaluation/code_benchmark/results/。 - 系统 skill 只在 CLI/gateway 物化到
$AGENTICA_HOME/skills/.system/:包内agentica/skills/bundled/(agentica、multi-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.yaml的settings.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_HOME(wire_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.yaml。delegate的注册从 CLI 的create_agent移进DeepAgent(cli/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_urlflag,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 hint对timeout=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_config里use_profile=False,base_url 落到 provider 公共预设端点(如api.anthropic.com)、key 落到多半不存在的ANTHROPIC_API_KEY,存储的凭据从未离开本机就 401invalid 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只读object与output(合计 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 前缀」,而sessionfixture 用真实uuid4()——只要某条目 uuid 恰好以9开头,本该越界被拒的"9"就会作为合法 uuid 前缀命中,fork 成功。改成在 fixture 里把uuid4patch 成确定的、字母开头的序列,数字前缀碰撞从此不可能。修后整文件 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 400messages 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_tokens与cache_read_tokens,前者 OpenAI 兼容、后者 Anthropic 风格)、completion_tokens_details.reasoning_tokens、compact_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-64变web-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_message的message参数文档过去写「self-contained:接收方只看得到这段文字」,读起来就是「把 diff/日志粘进来」——而一条消息是直接注进对端上下文窗口的,粘 8k 字等于花掉对方用来干活的窗口。现在 docstring 与 policy 都写明:正文是指令要自洽(目标、边界、卡住怎么办),diff / 日志 / 长篇 review 落成文件、消息里给绝对路径(机器是共享的,对端要细节时自己读一次就行)。MAX_MESSAGE_CHARS = 40000的硬顶不变,这条是让模型远在撞顶之前就走路径。
测试:tests/gateway/test_gateway_agent_peers.py、tests/peers/test_peers.py(新增用例在旧代码上确认失败)。端到端 smoketmp/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_stdout把 stdout 和 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_key,model_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.log;AGENTICA_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 的
execute10 秒内不再显示耗时:完成行尾的时间戳把「多慢算慢」从统一 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 path和Nearest 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,已加入wechatextra)或pilk,解不出时回复提示安装;>15MB 视频跳过(Gemini 内联上限 ~20MB)。新增gateway/services/media_understanding.py(MediaUnderstandingService+ 进程级单例,AgentService.chat(media=...)透传)、channels/base.py的InboundMedia与Channel.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.py、agentica/worktrees.py、agentica/peer_conflicts.py、agentica/cli/worktree_binding.py、agentica/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_count用Optional[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-run报Would remove .agentica/),而这些 worktree 是要长期活着的。.env等 gitignored 文件按settings.worktree.linksymlink 而非拷贝(轮换密钥一次生效、机器上只存一份;缺.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 降为 warning:
run_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_message给wechat-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.py的READ_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 也会直接走扫码登录,不必手动rmtoken 文件),并抛SessionExpiredError由run_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.md与docs/concepts/storage.md收容(mkdocs nav 已加),其余 docs 已有对应文件(concepts/agent.md、concepts/rag.md、concepts/tools.md、advanced/run-config.md、advanced/mcp.md、multi-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>、/resume、agentica 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=False的Syntax渲染——纵向确实是「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.chunkpayload 只会是 JSON 对象或[DONE],绝不会以data:开头,所以这个 framing 错误可以无歧义地修复:新增agentica/model/openai/sse_sanitize.py的TolerantSSETransport,OpenAIChat.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 校验回退 canonical:
append_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.py的AuxSession——按用途持有界且与主对话完全隔离的小 history(system 首位恒定 + 每轮 append 一组问答 → 前缀自动缓存友好;超限按"最老一组"裁剪;reset()随任务身份切换)。judge_goal支持传入 session,解析成功的问答才入 history(解析失败/调用失败不污染);GoalManager为 per-turn judge 持有_judge_session,set()新目标时重置防止跨目标锚定。不传 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 的 bug:
EVICT_THRESHOLD_RATIO0.7 → 0.8(evict 起压点),Layer 2should_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_compactmin-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 importopenai/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_source;agentica 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.py的input_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会清掉ISIG(input/vt100.py:262),所以按Ctrl+\在健康的事件循环下只是个被吞掉的0x1c字节(仓内没有任何c-\keybinding),压根不产生 SIGQUIT——而那条提示恰好只在「循环健康、你刚按了 Ctrl+C」时打印。实测(真 pty:raw 下子进程读到b'\x1c',cooked 下收到 SIGQUIT)确认app.py装的 SIGQUIT 硬逃生口只在run_in_terminal的cooked_mode窗口内生效,也就是循环被后台写入饿死、正等用户回答那个状态。因此提示挪到ask_user_question提问组件下方(Enter to answer · Ctrl+C to cancel · Ctrl+\ if frozen),那里才是它成立的地方。顺带把提问组件的行文本抽成_ask_prompt_lines,渲染与预留高度同源,不会再各算一套。 - 修复同批次多次编辑同一文件时 CLI 重复展示最终 diff:
edit_file、write_file、apply_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.md里sanitize_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-agentskill 的 "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_FILES与initialize(force=...)——force唯一的作用就是重写那个模板,文件没了它就只是个不做事的参数。为兼容主流 coding agent,默认 CLI workspace 会把~/.agentica/AGENTS.md维护成指向~/.agentica/workspace/users/default/AGENTS.md的 symlink;非 default user 和自定义 workspace 不使用这个全局入口。 tool_error/success_pattern卡片不再进 system prompt(CompiledExperienceStore.get_relevant过滤,success_pattern原本已过滤)。实测一个真实会话的## Learned Experiences五条里五条都是read_file_file_not_found、execute_command_timed_out、tmux 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_implPhase 1 先 yield started、Phase 2 才执行)对write_file/edit_file/apply_patch的目标路径 first-touchsnapshot、finally里finalize_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.py(extract_rewrite_paths/truncate_conversation/get_turn_checkpointer)与tests/cli/test_rewind_cli.py。Esc-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 状态)。配RewindScope(code/conversation/both)与RewindResult:code复用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.py的UseCapabilityTool,一个 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-guardjob(仅 pull_request 触发),把「缓存稳定是一等契约」从「有测试」升级为「有流程门禁」。 -
agent 能自己维护常驻规则了,但没有为此加任何工具:memory 的 system prompt(以及
self_manage的说明)现在写明AGENTS.md就是「每个后续会话都会全量注入 system prompt」的长期记忆,用户级在<workspace>/users/{user_id}/AGENTS.md(CLI 就是defaultuser)、项目级在 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增加wait、delegate;后台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_usd是0.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__不再收**kwargs,Agent的 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=True(mcp_config.json里有什么就加载什么)并注入BuiltinMemoryTool,于是这个名字承诺了一个它兑现不了的保证,对白名单业务面比逐个列include_*更危险(tmp/deep_agent_no_kwargs_smoke.py第 4 步就是把这个跑出来看)。真要白名单:用裸Agent+ 只声明需要的预设,再对最终工具表做一条构造期断言——那是唯一能拦住「上游新增一个默认开启的 builtin」的检查。- 用户级 AGENTS.md 改为按 user 存放:canonical 文件是
<workspace>/users/{user_id}/AGENTS.md(Workspace.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 不是例外,它就是defaultuser;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_md、CompiledExperienceStore.sync_to_global_agent_md、WorkspaceMemoryConfig.sync_memories_to_global_agent_md、ExperienceConfig.sync_to_global_agent_md、CLI--sync-*-to-global-agent-md、BuiltinMemoryTool.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_command、QueuedInput.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:30、Every 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.assign,useAppState却把同一个对象交给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假定每项都是带.detail的agentica.media.Image——Layer 1 一算 token 就炸,整轮AgentService.chat失败。现在 dict 先收成Image(url=..., detail=...)再计数;空/无 url 的 dict 走 85 token 的低细节默认值。测试tests/model/test_tokens.py、test_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-weixinparseAesKey对齐),extract_media_typed把image_item.aeskey拷进 media,下载缺full_url时用官方 CDNhttps://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.py、test_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_POLICY、ASK_USER_QUESTION_SYSTEM_PROMPT(补一节「这个框能到谁」)、bundledmulti-agentskill。 - 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 / 简洁复述),结果只含prompt、response、raw_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,所以不会在屏幕上打印两遍。