Changelog 存档¶
当前在开发的变更写在仓库根目录 CHANGELOG.md([Unreleased] + 最新发布版)。这里只放已经过期的发布说明,不要往这里追加新条目。
[1.4.15] - 2026-09-01¶
breaking¶
- CLI
/export默认导出 session JSONL,不再是对话瘦 JSON:以前/export//save把working_memory.messages存成一份没有 event、没有工具正文的 JSON。现在默认拷贝磁盘上那份<session_id>.jsonl(与 Web 轨迹同一文件)。旧行为改为/export messages [path];/export analysis [path]写出与GET /api/sessions/{id}/trace/analysis相同的 JSON。 apply_patch只精确匹配上下文:不再对空白 / 引号做 fuzz。对不上就是Hunk N: context not found。死的 SDK 类PatchTool(双格式 unified/V4A)删除;补丁走BuiltinFileTool.apply_patch,模块只留apply_diff/parse_patch_envelope。- 删除
write_html和ShellTool:长报告用write_file写 HTML;shell 用execute/BuiltinExecuteTool。get_builtin_tools/DeepAgent去掉include_html_report。 grep只留pattern/path/limit:去掉include、output_mode、case_insensitive、fixed_strings、context_lines/before_context/after_context。按文件名过滤走glob或execute的rg -g。DeepAgent/get_builtin_tools去掉peer_conflict_checker:编辑成功后不再附「别的会话也改了这个文件」提醒。
features¶
- npm 包
@agentica-ai/sdk(sdk-ts/):给跑着的agentica-gateway用的 TypeScript HTTP 客户端(session / chat SSE / 审批),不是 PythonAgent的移植。Web 启动路径不变,仍是agentica-gateway;PyPI wheel 继续打进编译好的 UI。发布走 GitHub Actions:tagv*/sdk-v*(或手动 Run workflow)npm publish到https://registry.npmjs.org/@agentica-ai/sdk(orgagentica-ai,secretNPM_TOKEN,npmjs 账户shibing624-xm)。已经发布过的 version 会跳过,避免 Python 发版 tag 把同一版再推一次。 - Gateway Docker 镜像:
Dockerfile用 Node stage 编 Web UI,再pip install ".[gateway]"。仓库根docker-compose.yml一键起自托管服务;本机pip/ Desktop 不受影响。 - explore / code 子代理会用只读
execute管道:execute本来就在allowed_tools里(execute_policy: read_only),但 explore 的 prompt 只提glob/grep/read_file,包装后的说明也只写 git/测试,模型就不会cd到工作区外的树去rg。现在 prompt 和只读execute说明都写上rg/find/head管道。 execute输出里出现的路径也算 grounded:rg等扫到的精确路径字符串可以直接给read_file/apply_patch,不必再绕一次glob。read_file仍只 grounded 它打开的那一个文件。web_search新增serply引擎(SearchSerplyTool):Serply 的 Google 搜索 API,SERPLY_API_KEY,无额外依赖(pip install agentica[serply])。同一个 key 还覆盖 Google News / Google Scholar:SDK 传SearchSerplyTool(search_type="news"|"scholar"),web_search分发器路径用AGENTICA_SERPLY_SEARCH_TYPE切换,模型看到的工具名不变。CLI--tools search_serply。API 文档见 serply.io/docs。apply_patchdocstring 示例以+#插入注释开头:@@后第一行就是新增,避免模型先整文件空格拷贝造成空操作。空操作不再报Malformed patch。首行已是*** Update/Add/Delete File:时自动补Begin/End Patch(正文语法不变);markdown 围栏和其它缺信封仍拒绝。- CLI 并行工具改成 Kimi 式整块 flush:未完成的调用停在输入框上方的 live 窗口,按开始顺序等前缀都结束后才把「调用行 + 结果」一起打进 scrollback。以前
execute一开始就打印调用行,并行的grep/write_file结果会插在调用和⎿之间,看起来像挂错工具;现在不再需要↳锚点。--print不变。 - CLI / SDK 与 Web 共用同一套 session 轨迹出口:一份 JSONL +
SessionLog.analyze()。SDK:agent.session_log(公开句柄)、.format_trace()、.export()。CLI:/trace打 rounds/tokens/工具(/trace <n>展开一轮),/status增加 Session log 路径(原Log file改名为 Debug log,避免和 jsonl 混淆)。Gateway/trace/analysis改为调用log.analyze(),不再手拼一份。公开 API 见docs/API.md。 /goal默认 token 预算不限:CLI/goal xxx与 SDKrun_goal()不传token_budget时不再回落 500_000(DEFAULT_TOKEN_BUDGET改为None)。要限额度显式传--tokens N/token_budget=N;-1仍是不限。Web 目标芯片默认显示「预算不限」,点一下才打开 Token 预算输入(空=不限,支持500k/2m),Escape 关掉输入框而不退出目标模式。
fixes¶
- CLI 渲染工具输出 / 纯文本回答时不再把
[/xxx]当成 Rich 闭合标记:execute结果里的[/usr/bin/cmake]、grep 路径、agent 纯文本里的[/red]以前走console.print(..., markup=True),Rich 抛MarkupError,整轮报Agent execution failed。现在不受信任的正文markup=False,拼进标记串的字段先escape()。 - Web 打开 CLI 会话(以及刷新后的网页对话)不再丢掉 tool call / 结果:
hydrateSession以前只把 session JSONL 里的user/assistant正文拼成气泡,assistant.tool_calls和type: "tool"行直接丢掉,所以侧栏里点开 CLI 会话只剩问答、没有 WorkGroup。现在按 harness 同一条规则重建:一轮用户提问折成一条 assistant(思考 → 工具卡 → 终答),参数和结果走与实时 SSE 相同的parts。打开会话时以服务端日志为准覆盖本地缓存(正在流式的会话不覆盖)。 search_memory搜对话归档不再NameError:按---切 block 用了re.split,模块顶上没import re,一点到 conversation 源就炸。apply_patchdocstring 补回「改之前先read_file」:Update/Delete hunk 必须对着当前文件原文,不能凭记忆拼上下文。- CLI 回答
ask_user_question时能看到完整选项:提问组件和 live 窗口抢同一块底部高度,live 的LIVE_MAX_ROWS=12把选项挤出屏幕。有未决提问/审批时收起 live;选项折行按显示宽度(get_cwidth)预留行数,中文不再按len少算导致后几项被裁。 apply_patch去掉误导性 Expected/Actual 预检预览:上下文对不上只报Hunk N: context not found;缺*** Begin Patch、hunk 行没以空格/-/+开头,报Malformed patch,不再包装成「preflight + Actual from line N」。原子写入(失败不改任何文件)仍在。整函数重写用write_file。- 工具结果不再塞启发式旁白:
execute非零退出只报 exit code(仍按命令决定要不要 raise),不再附 Note;write_todos不再 verification nudge;空文件返回File is empty: …而不是<system-reminder>;fetch_url去掉四条 IMPORTANT;background=True成功结果只留 id / pid / log。路径 grounded 政策只写在tools.md。编辑后的 LSP/Pyright 诊断仍附在write_file/apply_patch结果上(--enable-diagnostics),缩进对不上时能直接看到。 - CLI live 窗口跨线程读写加锁,provider 断流也会 flush 在飞工具块:spinner 每 120ms
compose_live与 turn 线程同时改LiveToolStore的 OrderedDict,迭代中增删会让 spinner 线程静默死掉、状态栏冻结。store 加锁、读侧快照,spinner 循环包异常;泛化except Exception与取消路径一样调abandon_live()。 - CLI live 窗口跟进:并行
task按 description 绑 subagent;结果 id 对不上时先打完整 call+result;剥 Rich 标签不再吃正文[...];删掉已无生产引用的_ToolResultSequencer;行数上限LIVE_MAX_ROWS=12只定义一处。 - Layer 2 空摘要不再当成压缩成功:
auto_compact/_summarise_conversation对摘要strip(),空白或抽不出正文(含把空respstr()成对象 repr 的路径)整段放弃,不替换messages、不写compact_boundary。WorkingMemory 里的空白 session summary 不再走 SM-compact,回落到真正的摘要 LLM。 - CLI 状态栏未配置的思考强度显示
default而不是off:config.yaml 没写reasoning_effort/reasoning时请求里根本不带这个字段,API 用它自己的内置强度;以前describe_thinking_mode()把「没写」当成关,状态栏就打出opus-5-openoneapi openai/claude-opus-5 off。现在未覆盖是default,只有显式off/none/disabled才显示off。
[1.4.14] - 2026-08-25¶
breaking¶
agentica.subagent/agentica.subagent_loader挪到agentica.subagents:内置定义从agentica/agents/*.md改到agentica/subagents/bundled/*.md(和skills/bundled同结构)。运行时from agentica.subagents import SubagentRegistry;加载器是agentica.subagents.loader。不留旧模块。用户/项目目录.agentica/agents/、$AGENTICA_HOME/agents/和/agents斜杠命令不变。SkillRegistry.get_skill_instruction()/generate_skills_prompt()删除:生产 catalog 一直是SkillTool.get_system_prompt()(自进化 spawn 也是写盘后再走这条)。两条是死 API,catalog 预算改挂在活路径上,不再维护第二套按注册序截断的 XML 列表。GET /api/memory不再返回用户级 AGENTS.md:payload 改为{auto_extract, index_path, entries}(MEMORY.md 索引)。常驻规则改走GET/PUT /api/user_agents_md。PUT /api/memory删除(405),避免旧前端把 AGENTS.md 正文写进记忆结构。- 权限档
ask改为 Codex「Ask for approval」:以前ask会从 schema 里藏掉写工具(write_file/execute/apply_patch等,模型看到 Function not found);现在三个档工具都在,差别只在真正execute之前要不要停车等人。ask:读(含工作区外;文件工具 + 只读 shell,含cd && git diff | head包装)自动放行;写(write_file/apply_patch,含工作区内)、会改状态的execute、web_search/fetch_url、硬不安全路径/命令停车。auto:再放行工作区内写、普通execute和网络。allow-all不询问、不拒绝(含硬不安全;项目「拒绝类似」只约束 ask/auto)。删掉模型可见的request_path_access,改由公开grant_path_access写入沙箱白名单。默认不变:CLI / SDK 仍是allow-all,Web 仍是auto。无人值守(无 LiveTurn:IM / cron / 非流式POST /chat/--print)立即把工具结果写成Tool call denied by user.,不 500、不 sibling-abort。 - 审批增加
deny路由和「拒绝类似」:classify返回allow | ask | deny。三档权限嵌套:auto⊃ask,allow-all是 root。ask下web_search/fetch_url和工作区内写文件弹卡;auto放行网络和工作区内写,硬不安全仍弹卡。新增deny_prefix(CLIx/4,Web 拒绝按钮下拉「拒绝类似」),写入project.jsonapprovals.deny_command_prefixes等,下次同类在 ask/auto 下不弹卡直接拒。allow-all忽略项目拒绝,只 warning + 记approval_decision(allow_all_ignore_deny/allow_all_hard_unsafe),命令照跑;进程用当前 OS 用户的权限,能 sudo 就能 sudo。用户在 ask/auto 点允许之后,执行时的check_command_safetyblock / sandboxblocked_commands不再跟卡打架。 - 系统 skill 不再拷贝到
$AGENTICA_HOME/skills/.system/,改为包内原位加载:CLI / gateway 的load_system_skills()把搜索路径末尾直接指向包内agentica/skills/bundled/,ensure_system_skills()、skills/.system、内容哈希指纹同步整套删除(同仓库subagents/bundled一直是这个模型)。拷贝本来就不是正确性前提(复制失败时回退的就是包路径),却让每次产品启动都扫树做 sha256、只读 home / 容器 / CI 走 OSError 降级、.system与bundled/双源难查、模型看到的Path:在 home 里像用户资产其实升级即被覆盖;SDK 侧「不捡到 CLI 残留」的隔离测试面也随之消失(.system点开头,目录发现本来就不扫)。覆盖方式不变:~/.agentica/skills/<name>/同名 user skill 优先级更高;Web 插件 API 的editable = (location == "user")不受影响,bundled 依旧不可编辑。
fixes¶
- 权限档文案与
classify()对齐:ask的读(含工作区外)与auto相同,只拦写、会改状态的 shell 和联网。CLI--permissions//permissions、Web Permission 提示不再写成「只有工作区内读才自动」或「ask 只有只读工具」。 execute超大输出改为落盘预览,不再静默丢掉中间段:原先max_output_length(默认 20k)先做 40/60 头尾截断,中间直接丢;max_result_size_chars=50k的 Layer 0 落盘永远触发不了。现在超过 20k 写入会话tool-results/,上下文只留路径 + 行数 + 头尾预览,后续用read_file/grep查。成功与非零退出(失败命令的巨型 traceback)都走这条路——Layer 0 钩子挪到结果内容组装之后(错误文本先脱敏再落盘,spill 文件不碰密钥),原先挂在「仅成功」分支上,报错路径仍然丢中间。- SDK
print_response_stream不再把工具事件漏进回答:show_tool_calls=False时原先不跳过ToolCallStarted/ToolCallCompleted,Running tool: read_file会被当成正文打印。现在走和 CLI / Web 同一套classify_run_response,工具事件无论显不显示都不会进答案。 - CLI 审批卡裁掉「拒绝」、两项时跳号:
rm /path这类不能「允许类似」的命令本来就只有允许/拒绝,但 TUI 把整段卡当成一行再多塞一个换行,高度少算一行,拒绝被裁掉,屏幕上只剩1. Yes, proceed (y)。同时拒绝还写死成3.,2仍绑「允许类似」。现在按可见选项连续编号(两项就是 1 允许 / 2 拒绝;四项仍是允许、允许类似、拒绝、拒绝类似),数字键跟着走;长命令按终端宽度预留折行高度,不再把选项挤出屏幕。SDK / Web 同一套options,没有类似可批时本来就不渲染那两个按钮。 - CLI 拒绝文案不再催用户写原因,审批后命令留在 transcript:拒绝从
No, and tell the agent what to do differently (esc)改成No, deny (esc/n)(esc/n都是直接拒,不追问理由)。审批卡画在输入区 layout 里,决定后整段卸掉,$ rm …会跟着消失。现在卸卡之后把命令和✓ allowed/✗ denied打进 scrollback。 - Web 审批卡按 Enter 会整轮「已中止」:卡上「允许一次」标了 ⏎,但焦点在输入框时 Enter 被当成「空输入为停止」。有未决审批时输入框禁用(附件 / slash / 粘贴也停),只留停止;Enter 允许一次、Esc 拒绝。残留草稿不再插入当前轮。
- Web 侧栏会话状态与对话页被误删:
deny_prefix那次提交冲掉了sessions.ts的lastTs/unread/syncSessionStatus,以及ChatPage的左侧 query 导航(ChatNav)、settleWork、已读markSessionRead、以及running清位。已从上一版补回,审批卡仍带deny_prefix。
features¶
- 工具说明去掉二进制黑名单,改为正面能力描述:
executedocstring、tools.md、ShellTool不再写「not cat / not find / not sed -i / MUST avoid find」式禁令——实测这类禁令把强模型推向更差的绕行路径,且多处文案互相矛盾。管线正例只留程序输出整形(pytest | rg '^FAILED' | sort、rg … | head、git diff | tail),不点名、不示范find/cat/ls/awk/sed -i。grepcontent 模式的limit是全局条数上限:stdout 读满limit行即杀掉 rg(不再用每文件--max-count,也不再communicate()整棵树再切片)。read_file支持tail和负offset(offset=-50即最后 50 行,配limit取窗口内最旧的若干行);tail/负offset不再被 256KB 守卫拦住(守卫只拦从头分页;报错只建议tail,正offset/limit过不了守卫)。从尾扫描超过 20 秒超时。 - SDK
print_response/print_response_stream默认展示工具,调用行跟 CLI 同一套文案:默认show_tool_calls=True(仍可关掉)。工具行用format_tool_display(read_file foo.py (L1-80),不再打原始 dict);read_file/glob/grep成功时只留调用行,和 Web 一致。思考在工具之后再出现会再打一次💭 THINKING,时间线是思考 → 工具 → 思考 → 回答。 - Skills catalog 按模型上下文窗口的 2% 做 token 预算(无窗口时退回 8000 字符)。单条 description 硬帽 1024 字符(从前截)。超预算先从列表尾部丢掉 description(保留名字),仍超才 omit 并注明条数。卫生规则写进 prompt:匹配才用、当轮读完
get_skill_info、不跨轮携带、不深追 references。freeze_session_guidance()仍然冻住整块,不每轮重排。 - 粘贴/输入里的非法 UTF-8 不再打崩 CLI、Web、SDK:孤立 surrogate(例如
U+DCE5)以前在.encode("utf-8")上炸掉 Enter 写历史、agent 落盘和 Langfuse OTLP。CLI 入口、Agent.run/steer、WebChatRequest/SteerRequest/GoalRequest都换成 U+FFFD,相邻汉字不动。 - Web 侧栏会话时间改为「多久没发起 query」:原先
agoStr用的是first_timestamp(会话第一次请求),所以一直聊的对话也会显示7h。现在用last_timestamp,有本地 transcript 时再取最后一条非 steer 用户消息;文案按界面语言粗粒度显示(刚刚 / 5 分钟一档 / x 小时前 / x 天前,英文 just now / 5 min ago / x hours ago / x days ago)。选中行的时间不再叠一层background(原先--bg2盖在--accent-soft上会变成一块深色芯片)。组内会话按最近一次 query 排序;项目组顺序仍按该目录第一次对话。 - Web 侧栏会话状态:回复中转圈 / 未读绿点 / 空闲时间:发完 query 切到别的会话时,原会话 title 右侧不再显示「刚刚」,AI 思考、tool call、压缩期间是转圈;回复结束后如果没打开过就是绿点,点进去看过才回到时间标签。
last_read_at写在 session sidecar,GET /api/sessions带unread/running,打开会话会POST /api/sessions/{id}/read。旧会话没有last_read_at不当未读。 - Web 对话左侧 query 导航条对齐 Codex minimap 设计:左侧留白区域默认只展示极窄短横刻度,鼠标悬浮短横时在右侧弹出不透明浮窗(圆角、边框),展示截断 query(前 50 字、最多两行);点击平滑直达对应问答轮次并带微光闪烁反馈;当浏览器宽度收窄(≤ 1024px)时自动隐藏,不侵占主对话区域。
- 自动抽取的记忆带上会话证据,
search_memory回读 provenance:write_memory_entry(evidence_refs=)原先只在手动写入时用得上,抽取结果没有来源。现在MemoryExtractHooks把抽取窗口归档后写入evidence_refs;search_memory每条结果带title/memory_type/memory_source/evidence_refs。评测脚本在evaluation/reconcile_mem/。 auto只拦工作区外的文件写:读(含工作区外的 skill/SKILL.md)、工作区内写、全部execute、网络、以及self_manage/cronjob/ skill 等内置工具(不看action)一律过。ask的读与auto对齐(含工作区外);拦写文件(含工作区内)、会改状态的 shell、以及web_search/fetch_url。记忆、task/delegate、skill 和其它内置工具直接过。self_manage show不再把compact_token_limit当成 secret;api_key只藏中间(含fallback_models/auxiliary_model里的 key)。- 轨迹
approval_decision带上审批卡和用户决定:停车才写。事件里有工具名、入参、question/preview、可选「允许类似」标签、档位、等待秒数(wait_s,两位小数)、以及用户点的allow/allow_prefix/deny。选了允许类似时把写入project.json的 grant(命令类 / 路径前缀)记在同一条上,不再只剩decision+tool_call_id。 - 工具审批停车(CLI / Gateway / Web 同一套
ApproveFn):Runner 在fc.execute()之前、wait_for120s 超时之外调用Agent.approve。Gateway 的ApprovalRegistry只住在LiveTurn上(不是 Agent 缓存);SSEtool_call带tool_call_id,新事件approval_request(刷新按 seq 回放并重推未决项,不看当时有没有 SSE 订阅者);POST /api/sessions/{session_id}/approvals/{tool_call_id}提交{decision: allow|allow_prefix|deny|deny_prefix}(按账号鉴权,未知 id 404)。取消或结束deny_all;有未决审批的 Agent 不被 LRU 清掉。CLI 对齐 Codex 选项(y/p/esc/x),并行 tool_call 串行弹卡,Ctrl+C 拒绝全部。Web 底部固定一张卡(拒绝 Esc + 下拉「拒绝类似」/ 允许一次 Enter / 下拉「允许类似rm -f命令」),刷新按tool_call_id挂回工具行。allow_prefix/deny_prefix批的是命令类(rm -f,不含文件名),写入该项目project.json的approvals(与work_dir/active_profile同文件),重启 CLI / gateway 仍生效;「允许/拒绝一次」不落盘。复合命令不能「允许/拒绝类似」。/permissions改为三档说明。 - Web 对话 run 与 SSE 连接拆开:刷新 / 断网只断开订阅,后端继续跑。
POST /api/chat/runs立刻返回run_id;GET /api/chat/runs/{id}/events?after=按序号重连去重;GET /api/chat/runs/active找回当前 session 的 run;POST /api/chat/runs/{id}/cancel显式取消并等到 session lock 释放(已结束则幂等返回终态)。Stop 在 lock 释放前不允许发下一轮(新输入排队)。发送后立刻出现 WorkGroup「正在思考」转圈(有附件时是「正在准备附件」),不再空白等到首字。live_turn / Agent 缓存 / session lock / work_dir / 审批档都按(账号, session_id)分区,session_id是客户端可控字段,不能让 B 账号的 409 或删除碰到 A 的 run。list_sessions会并上该账号尚未落盘的 live run,刷新不会让刚发出的新对话从侧栏消失。 - Web 工具调用入参跟 CLI 同一套展示:对话里不再
json.dumps原始参数。read_file/glob/grep一行路径;write_file/apply_patch文件名或文件数(结果仍是 diff);execute命令;write_todos清单;web_search/fetch_url查询或 URL;task/delegate/send_message全文;list_agents只显示名字;其余key=value摘要。轨迹页仍是原始 JSON。 - Worktree 默认短命、落在仓库内:默认路径改为
<repo>/.agentica/worktrees/<任务>(不再是兄弟目录../<repo>-<任务>;settings.worktree.root: sibling可回到旧布局)。worktree(action="merge")合入本地 base 后删除 checkout 和wt/<任务>分支,会话回到主目录;进行中的同名use仍复用。新增remove。会话绑定期间git worktree lock(agentica pid=<pid>),活着的别人的锁和用户手加的锁都不抢;pid 已死才偷锁。删除前的安全标准是「agentica 的wt/分支 + 没有独有工作」(干净且不 ahead 本地 main),不是「是否已 push」;detached / 手建 / Claude Code 的树一律不删。退出 CLI 时,没有独有工作的wt/worktree 自动清掉,有独有工作的保持 lock。.env仍是 symlink。use发现别人持锁时会点名 pid。工具remove先校验再搬家。 web_search默认引擎从baidu换成exa:走 Exa 公开 MCP(https://mcp.exa.ai/mcp),不设EXA_API_KEY用共享免费池(有限流),设了走自己的额度。和 OpenCode 同一条免费接入;AGENTICA_WEB_SEARCH=baidu或provider="baidu"仍可选。环境变量配了需要 key 却没给 key 时,降级目标也跟着改成 Exa。- Web 对话回答列对齐 980px、正文 14px:原先
.msg-stack再套一层min(760px, 88%),宽窗口两侧留白,开着工作区面板时表格还被压到约 600px。现在 assistant 列跟.msgs同宽,用户气泡仍是聊天宽度;输入框max-width:980px与对话列对齐。段距 / 列表 / 表格 / 标题一起从 compact 终端密度调到网页可读。 glob/grep硬超时从 3s 调到 20s(_GLOB_TIMEOUT/_GREP_TIMEOUT):3s 会误杀本地大仓、冷盘和没有rg时的 Python fallback;仍不暴露给模型。对齐 kimi-cli / Claude Code 的 ripgrep 20s 上限。glob 继续留超时:当前实现是pathlib.glob整树走完才返回,结果上限挡不住 NFS hang。- 设置 › 模型配置顶部只留
config.yaml路径:去掉当前 profile / 模型 / effort / context / compact 那一行摘要,列表里本身就能看到谁在用。 compact_token_limit:压缩工作阈值,不改模型context_window。1M 窗口可以配300000,Layer 2 在min(该值, 窗口×0.95)触发摘要;Layer 1 的 0.8/0.5 相对min(该值, 窗口)。不配则和现在一样(约 95% 窗口才摘要)。写在 config.yaml 的 profile 上,也可写settings.compact_token_limit做全局默认。CLI:/config set compact_token_limit 300000、--compact-token-limit。Web 模型配置有填才显示。SDK:ToolConfig(compact_token_limit=300000)。- 设置 › 记忆拆成两层:上半编辑用户级
AGENTS.md(常驻规则,进 system prompt);下半「生成对话记忆」开关仍接auto_extract_memory,列出的是MEMORY.md索引条目,不再把 AGENTS.md 预览假装成抽取结果。CLI / SDK / Gateway 同一套注入:会话开头把 MEMORY.md 索引(标题+hook+相对路径,最多 60 行 / 4KB)冻进 prompt 并点名绝对路径,memory/*.md正文按需search_memory/read_file。抽取仍大约每 10 轮。已知限制:write_user_agents_md会清掉共享 Workspace 上的_context_snapshot,Gateway 下一轮所有会话会重建 AGENTS.md 前缀(快照应按会话持有,v1 不改)。 - 「Profile」中文改叫「模型配置」。
- 个人助理的 Web 地址可点:和
config.yaml一样走POST /api/open,用系统默认浏览器打开http://127.0.0.1:8881/chat。 - 个人微信默认就能扫码:
WECHAT_TOKEN_FILE默认~/.agentica/cache/wxbot_token.json,WECHAT_ALLOWED_USERS默认空。gateway 启动不再弹码;网页点「配置」才生成二维码(过期约 120 秒)。没扫或过期显示「失败」;离开再进个人助理页,码是空的,要再点配置。 - 微信 / 网页 / 桌面是同一个人:点「配置」扫码时登录的账号就是机主。IM 不再用微信 openid 另开
users/<openid>/分区;会话、记忆、AGENTS.md 都进这个账号。已连上的再点一次「配置」也会绑到当前登录账号。 - 设置 › 个人助理:设置弹窗第二个 tab(常规旁边),展示本机 Web 地址和微信 / 企微 / QQ / 飞书等 IM 的接入状态与连接方法。账号菜单同一项打开这个 tab。
/assistant仍是同一套内容的独立页。GET /api/channels增加catalog/web_url/listen。 - 桌面版第一次打开会自己装 Python runtime:安装包仍然不含解释器(壳最难更新)。找不到
agentica-gateway时用 uv 在 Application Support 装 CPython 3.12 +agentica[gateway],不进~/.agentica;已经 pip 过的继续用原来的。AGENTICA_GATEWAY_BIN/AGENTICA_DESKTOP_NO_BOOTSTRAP可跳过。 - CLI 运行中输入提示改直白:Enter 插话显示
Steered the current query. Tip: Tab or /queue queues a request.;Tab 排队显示Queued the next request. Enter steers the current query.。原先Guidance added to the current task没说清 steer 的是当前这一问,也没点出/queue。 - CLI 回答按 Markdown 块增量落屏:原先
stream_response把整段缓冲到finalize才一次性print(Markdown),长回答只能盯着answering…。现在围栏感知的空行一旦闭合一块(标题/列表/代码/表格)就立刻渲进 scrollback,未完成的尾部仍等结束;不用 Rich Live,避免和 prompt_toolkit 抢光标。cli_markdown=off仍整段纯文本。 - 设置 › Profile 顶部是本地
config.yaml:原先「当前配置」和预览弹窗在常规页。现在这块在 Profile 最上面,路径是可点链接,走已有的POST /api/open用系统默认编辑器打开(本机 gateway / 桌面版);打不开就 toast。预览弹窗去掉。GET /api/config/file仍给别的调用方打码读文件。 - Web 思考与工具按真实调用顺序穿插:思考是组里一行(11px、和工具同行高),正在流式的那段默认展开、正文随页面贴底,不再用
max-height把最新思考裁在卡片里。工具同样一行一卡。正文仍会打断分组,所以时间线是思考 → 工具 → 回答 → 思考 → 工具 → 回答。流式光标只留在最下面那一个气泡。 - Web
/goal过程流式:原先POST /api/goal只推status条,正文等整轮done才一次性出现。现在每轮走run_stream,和普通对话同一套thinking/tool_call/tool_result/contentSSE,工具组和吐字穿插出现;顶栏 tokens 进度仍用status。停止按钮、插话 chip 也挂到这一轮 live 消息上。 - Web
/goal只限 tokens,默认不限:原先网关硬编码 turns 15 / tokens 80,000 / wall 300s,状态条三项一起显示。现在对齐 CLI 主闸:只传token_budget(GoalRequest.token_budget,-1= 不限,网页默认)。输入/goal进入目标模式:先填 token 预算(空=不限,支持 k/m),再写目标。运行中状态条只显示tokens used或tokens used/budget。CLI--turns/--wall不动。 - Web 流式吐字贴底跟滚:回答过程中每条 token 不再立刻重跑 Markdown / KaTeX / 高亮(那会把主线程卡住,看起来像输出冻住);按动画帧合并绘制,流式时用轻量渲染。用户没上翻时始终跟到底;程序化滚动不再被误判成「用户离开底部」。
- Web 插话(steer):输入栏只有一个按钮。空内容是停止;有内容则发送(纯文本走 CLI 同一条
agent.steer())。要排到下一轮用/queue <prompt>(/q同义),和 CLI 一样;附件和其它斜杠命令仍排队。插话气泡带图标、虚线框。来不及送达的插话在本轮结束后自动改排队。API:POST /api/chat/steer、POST /api/chat/steer/take。 glob/grep不再把timeout暴露给模型(grep同时去掉multiline):内部硬超时,不给模型覆盖。此前无上限的timeout让模型随手填 60 就能拆掉 fail-fast;跨行正则 fallback 又不支持,有rg和没rg会搜出两套结果。超时错误改为让模型收窄path/include,不要原样重试或改走execute。context_lines > 0时忽略before_context/after_context(与 rg-C一致),写进 docstring。execute/wait的timeout不动。- 文件工具去掉
ls/edit_file/undo_edit:编辑只留apply_patch+write_file(read_file/glob/grep仍在),方法本身也删了,schema 里不再出现。列目录改用glob(pattern="*")——它本来就返回子目录,工具 docstring 和tools.md里点明了这一点,免得模型退回execute("ls")。Web 默认审批档是auto(ChatRequest.approval_mode),不是ask。task、web_search、memory、list_agents/send_message仍常驻。CLI / Gateway 不再保留对ls/edit_file/undo_edit/multi_edit_file的历史日志渲染分支。越权路径不再走模型可见的request_path_access(已删除),改为审批卡停车后grant_path_access。 - 权限档以 Agent 为准,工具自己那份只在脱离 Agent 单用时生效:
Agent在接线工具时(_wire_tools_to_self)把tool_config.permission_mode灌给它持有的每个BuiltinFileTool。get_builtin_tools()因此不再有permission_mode参数——同一个决定不留两个入口。工作区外写入走审批卡 +grant_path_access,不再挂模型可见的request_path_access。 - Web / 桌面版变成真正的多账号产品,seed 的账号从
admin改名default:账号名同时是数据分区名(它命名users/<id>/,也就是会话与记忆的落盘位置),叫admin就意味着首启动会凭空造出一个users/admin/分区,而这台机器上已有的对话全在users/default/下——同一个人换个入口进来看到的是空列表。现在默认账号名直接取settings.default_user_id(即default),并对已装过上一版的机器做一次性迁移:auth.json里那条admin记录连密码带会话一起改名成default,所以旧密码继续能登、开着的浏览器不掉线。左下角的账号入口显示用户名 + 角色(原先显示的是当前 profile 名——profile 是模型配置,和「我是谁」无关)。账号名会先规范化再当目录名(清理后 2–32 位、字母开头、仅字母数字下划线)。 - 每个账号有自己独立的会话、归档与定时任务:
AgentService的每条入口(chat/chat_stream/run_goal、会话列删改归档、trace 读取)多一个owner参数,由服务端从会话 cookie 解析,agent 实例的缓存键也带上它。相应地ChatRequest/GoalRequest/MemoryRequest/CronJobCreateRequest删掉了user_id字段——请求体里的身份是客户端说了算的,等于任何登录用户都能把别人的user_id填进来。网页侧边栏只展示服务端返回的会话(不再把同一个浏览器里上一个账号的localStorage合并进来,那会让kk登录后看见default的对话)。定时任务按账号切开:/api/scheduler/jobs只列出当前账号的任务,创建时把 cookie 里的账号名写进CronJob.user_id,别人的任务 id 一律 404;调度器仍会执行所有到期任务,跑的时候用任务上的user_id作为数据分区。归档是会话上的标记,跟着会话走。「用户管理」是唯一的管理员专属功能(/api/auth/users*非管理员一律 403):模型 profile、工作目录、技能、MCP 仍是这台机器的配置,任何账号都能改。删账号只吊销登录,users/<id>/的数据留在盘上。端到端验证tmp/multi_account_smoke.py(真 gateway、真流式对话,两个账号互不可见、非管理员 403、管理员改密、删号后数据仍在)。 - 用户管理从设置弹窗里拆成右侧独立页
/users,新增 / 改密改成完整表单:原先塞在设置 tab 里的一行输入框既看不清也做不全。现在主侧栏不变、右侧是账号表;新增用户是二级窗口:用户名会先规范化再当目录名(小写、空格/连字符变下划线、其它非法字符丢掉、数字开头加u_、超长截断;清理后须 2–32 位且字母开头),初始密码(管理员自己填,至少 6 位)。规范化后与已有账号撞名则拒绝。用户名合法时提示将创建默认 Project<id>(不再带-default_project后缀,目录就是账号名)。只能新增用户角色——第二名管理员会把「谁能改谁的密码」搅成一团,seed 的那一个就是唯一管理员。去掉重置密码:改自己(内置管理员)要填当前管理员密码(提示写明初始密码打印在首次启动的终端里,形如abcd-1234),改普通用户只填新密码 + 确认。改密只在用户管理页(以及设置 › 访问);左下角账号菜单改为 退出登录,方便切账号。 - 网页去掉「让模型输出推理过程」开关,thinking 默认一直开:这不是给用户选的产品能力——支持 thinking 的模型本来就该把推理过程流出来,关掉只是让界面少一块;不支持的模型会忽略。删除
/api/config/thinking与设置里的勾选,Gateway 构建主模型时固定传thinking="enabled"。 - 轨迹是会话的附属,不再是独立 tab:主侧栏去掉「轨迹」按钮,中间那列会话列表也去掉。唯一入口是对话标题右边的「查看轨迹」;轨迹页左上角「返回对话」回到该会话。直接打开
/traces(不带sessionId)会回到聊天。 - 对话页右侧「打开工作区」:顶栏去掉「目录: …」那一行,改成打开当前会话工作目录的文件面板(Web / Desktop 同一套 SPA)。面板标题是「文件」,工具条是「根目录」加详情 / 刷新 / 上传;点进文件可预览(文本 / Markdown 渲染与源码 / 图片 / PDF / HTML 沙箱),超过 256 KiB 截断并提示下载(源码高亮到 64 KiB 为止,更大的 Markdown 默认开源码视图以免卡主线程),预览页可返回、可下载。对话里提到的路径(行内代码、
read_file/write_file/apply_patch参数、上传附件)会聚成文件卡片,点「预览」打开同一块面板。路径必须落在工作目录内(..与 symlink 逃逸拒绝)。API:GET /api/workspace/files、/content、POST /stat、/upload。 - 对话输入框右下角的上下文占用改成 CLI
/usage同款会话级拆分:鼠标悬停看到 Context Window(已用 / 窗口)、消息数、API 调用、费用、本会话输入/缓存命中/输出,以及 system prompt / 规则与工作区 / 技能 / 工具说明 / 工具定义 / 对话各自占用的 token。各行是并列拆分(加起来等于下一轮请求),不是父子:System prompt是整段 system message 扣掉已归因块后的剩余,工具定义是 API schema、不在那段字符串里。分段只报 token 数,不再配百分比、进度条和缩进;/usage同样去掉% full, estimated和 ▓ 条。数字来自同一条measure_context路径。API:GET /api/sessions/{id}/usage;流式done事件也带上这份快照,圆环百分比跟占用走。 - 侧栏每个 Project 行右侧「+」在该项目下新建对话;不再显示 project 路径(工作区已经有路径)。工作区 Details 点外面或 Esc 关掉。Markdown 预览支持 HTML 标签(如
<p align="center">)和 LaTeX;源码 / 纯文本预览右上角可复制可见文本。每轮结束后,鼠标悬停答案显示日期、↑输入 / ↓输出 tokens、input cache tokens 与 cache hit(与 CLI footer 同一口径)、tok/s、费用、耗时和复制;footer 每一项都有 tooltip(费用是「成本(USD)」)。用户消息悬停只显示简写日期时间和复制。新建对话可在输入栏选择/更换 work dir(浏览上级目录、改用临时工作区)。输入/可触发/goal、/compact和已安装 skill。可粘贴/预览图片:底模能看图则直接附上,否则由settings.media_model配置的模型描述后转交(未配置时提示如何在 config.yaml 里写media_model)。流式输出贴底滚动,上滑后页面中间出现下拉箭头可跳回最新。输入框随内容长高。 - Web 读图提示不再写死 Gemini:底模不能看图时,tip 显示
settings.media_model里实际配置的model_name;未配置则说明在 config.yaml 里怎么写该块(需要model_name,以及model_provider或base_url)。GET /api/status增加media_model。IM 渠道未配置时的回复同样不再写「指向 Gemini」。 - Web 输入栏、footer、/goal 与新建会话目录:去掉输入框上方的
Enter to send · …tip(换行/粘贴写进 placeholder)。每轮 footer 的 cache 改成数据库图标,夹在输入 tokens 和输出 tokens 中间。/goal运行时输入框上方显示 CLI 同款状态(Goal [active]: …+ tokens/turns),期间再发的消息进入队列而不是 409「already has an active run」。新建会话选目录时可直接输入绝对路径;路径不存在或无法访问时提示「目录不存在或无法访问,已回退」,仍停留在旧目录上可继续改。 - 设置 › 默认工作目录:历史条目可单条删除;已不存在的路径(包括 pytest 留下的临时目录)打开设置时自动丢掉。输入绝对路径点应用 / 回车,非法路径同样提示「目录不存在或无法访问,已回退」。
DELETE /api/config/dir_history?path=。 - Web 偏好落盘到 gateway,localStorage 只做首屏缓存:主题、语言、审批档、上次打开的会话写入
$AGENTICA_HOME/gateway/prefs/<账号>.json(GET/PUT /api/prefs)。清浏览器缓存或换浏览器后仍能从本机读回;第一帧仍读 localStorage,登录后再对账。会话正文仍是 SessionLog,没有另起一份 SQLite。 - 输入框 Skills / Permission 对齐可发现性:Skills 按钮(打开的书 + 下箭头)弹出带搜索的列表,展示技能名和描述,悬停 tip 是完整 desc。Permission 三档跟
agentica.agent.permissions一致,不是 Codex 截图里的「每次询问」:ask是征求批准(读含工作区外、只读 shell 自动;写文件 / 改状态的 shell / 联网停车)、auto是代为批准、allow-all是完全访问。图标按 Codex 习惯:眼睛 / 闪电 / 三角感叹号;按钮带下箭头,括号里跟英文 id。/斜杠菜单过长时列表可滚、键盘选中项滚进视口,技能行也用打开的书。 - 源码预览按语言语法高亮:工作区打开
.md/.py/.yaml/.toml/.java等文本文件(Markdown、HTML 切到「源码」也一样),以及对话里的围栏代码块,都画成带语言标签和复制按钮的代码卡片,用 highlight.js 着色(标题蓝、引用绿、跟常见 GitHub 主题同一套 token)。Markdown 渲染视图不变。源码卡片自己管横竖滚动(不再把纵向甩给外层、横向甩给<pre>):两条都是 8px 细条、始终可见、可拖滑块,点轨道跳到对应位置。 -
工作区预览上限从 12000 字符改成 256 KiB:原先一个普通源码文件就会被截成「请下载」。
GET /api/workspace/content?preview=1现在只读文件头TEXT_PREVIEW_BYTES(256 KiB),不再整文件进内存再切;超过 64 KiB 关掉语法高亮,更大的 Markdown 默认开源码视图,避免卡主线程。 -
查看轨迹后侧栏丢掉刚建的对话:进
/traces会拆掉整棵AppShell再GET /api/sessions覆盖列表;而这个接口只列默认工作目录下的 session,刚在别的 project 里问完的对话(轨迹能打开,因为它按 session id 找)就不在列表里,覆盖时连 localStorage 一起冲掉。现在 chat/traces/users 共用一个 layout,切轨迹不再重拉列表;list_sessions列出该账号下每一个 project,并带上work_dir。 - gateway 重启后「查看轨迹」提示盘上没有 jsonl:
_session_work_dirs是进程内存,重启后空了就去settings.base_dir找;对话其实写在另一个 project 下。session_log_for/ 删改归档现在用SessionLog.find_sessions在该账号所有 project 里按 session id 定位。轨迹页 cache 改成与对话 footer 同一套数据库图标 + tip。 - 轨迹时间线此前是一根实心条,现在是真的分段:
thinking/text/tool_call三种事件是在一次请求结束后才一并落盘的,时间戳几乎相同,于是分析出来的模型泳道要么只有一段、要么后面几段宽度为零——时间线看着像没坏,但它讲的不是这一轮实际发生的事。现在流式循环在推理段和正文段真正结束的那一刻记下time.time(),SessionLog.append_event()接受显式timestamp,落盘前按发生顺序排序,所以「思考 2.3s、回复 36ms」这种分布才画得出来。 -
工具泳道按工具名合并,图例五项:原先每个工具调用各占一条泳道,一轮里调 20 次
read_file就是 20 条几乎一样的线;现在同名工具收进一条泳道(并发调用才在道内错开分层,道尾标调用次数),并且图例五个阶段常驻(思考 / 模型回复 / 工具参数生成 / 审批等待 / 工具执行)——只画本轮出现过的颜色,会让两轮之间的同一个颜色对应不同阶段。不再画「其它」:那是墙钟减去各段之后的残差,时间线上本来就没有对应的条。 -
Web 工作组在回答结束后不再显示「运行中」:思考切片的
ms没冻住时,组头和「正在思考」按墙钟一直加,隔天变成 45h。现在所有未收口的思考都冻住(中间段用下一段工具/t0,不要用现在的墙钟);回合结束走settleWork;已经不在流式的卡片即使旧数据缺ms也不再转圈。 - 沙箱
blocked_commands不再把rm -rf /当成所有绝对路径删除的前缀:以前只卡左侧边界,rm -rf /Users/.../__pycache__和rm -rf /tmp/x都被当成删根目录拦掉(测试还把这个漏洞写成预期)。现在按操作数匹配:/是完整路径,后面不能再接Users;rm -rf /、rm -rf /*、echo x; rm -rf /仍拦。对齐 Codex / OpenCode / Penguin:工作区内删除走审批或 OS 围栏,不用字符串前缀当禁令。 - 工具审批分类器与「允许类似」:
is_destructive第三方工具(如cronjob)在 ask 下停车,不再误落到兜底allow;auto下放行(只拦工作区外文件写)。命令「允许类似」按命令类(rm -f,不含路径/文件名),rm -f a批过之后rm -f b不再问、git add不会放行git push;类短于 2 个 token 时不提供「允许类似」(bash deploy.sh不能永久放行任意bash -c);echo hi && rm这类复合命令不能用首段越权,只提供允许一次。已写入project.json的单 token 前缀(["bash"])读取时忽略。ask下工作区write_file/apply_patch也会弹卡。todo / memory / skills /ask_user_question不弹卡。Web 输入框 Enter 不再误批。tool_resultSSE 带tool_call_id。真正停过车才写approval_decision。 - 输入栏弹出层点外面或 Esc 就关掉:模型 profile、权限档、Skills 打开后只能再点一次按钮才关。现在点对话区、输入框或其它按钮会立刻收起,Esc 也先关菜单(有菜单开着时不拿去停止生成)。
- Web 生成中空 Enter 会停止,Stop 按钮一直在:placeholder 写着「空输入为停止」,但空 Enter 直接 return;输入框有草稿时按钮还会变成发送。busy 期间按钮恒为 Stop,空 Enter / Esc 都停止。
- 创建 run 途中点 Stop 不再留下孤儿任务:
POST /api/chat/runs返回后若用户已停止,立刻cancelRunApi,避免 UI 已 aborted、后端仍占 session lock。 - SSE 空闲 15s 发 keepalive;干净 EOF 且未见
done/error/aborted则按断线重连:前置代理闲置掐流不会再被当成回合完成。 - 已结束的 live_turn 10 分钟后从注册表丢掉:完成/取消后仍可重连回放,超时释放事件缓冲,删会话立刻清掉。
- live_turn 事件缓冲和订阅队列有上限:运行中不再按 token 数无限 append;连续
content/thinkingdelta 在缓冲里合并;慢订阅者队列满则断开,让客户端按after=seq重连,避免长/goal+ 多标签页把网关打到 OOM。 - Steer 打断思考时上一张思考卡片停止计时:插话把思考切到下一张卡,但上一张
ms没冻住,墙钟还在涨。现在插入 steer 或开新思考切片都会finishThink。 - Web 侧栏按会话创建时间排序:jsonl 首行
first_timestamp是第一次请求。GET /api/sessions和侧栏都按它倒序(新在上),发消息不再把老会话顶上去;「3d」小字也是创建时间,不是最后活动。Project 分组按该目录第一次出现会话的时间排,给旧项目加 New Chat 不会把它顶到最上面。CLI/resume仍按 mtime 倒序。 - Web 对话不再狂打
/api/workspace/stat:消息底部的文件卡片原先跟着流式正文每一帧重抽路径,数组换了引用就 POST 一次,长回答会打满日志。现在流式期间不查;路径集合没变也不查;同一帧里多条消息合并成一次请求。 - Web 工具调用一开始就上屏:思考是流式的,但
execute这类要跑一会儿的工具要等完成才出现——flushStream()以前只在有挂起的 rAF 时才bump(),思考停了之后那条tool_callSSE 改了数据却不重绘。现在结构事件立刻绘制。 - Web 回答过程闪屏:每 token 整段 Markdown 重挂载(
components={{}}每次新对象)、.bub pre{transition:all .2s}让代码块从 0 高度动画出来。改成 120ms 节流、冻结的ChatMarkdown(流式跳过高亮/KaTeX,结束后高亮一次)、光标改成独立 caret,去掉 pre 的全属性 transition。 - Web 工作组耗时按思考 + 工具切片求和:组头原先用「第一刀到现在」的墙钟,思考 step 又不冻
ms,最后一轮在回答已经结束后仍因isLast每秒刷新,总时长跟着墙上的钟涨。现在每段思考结束就记下切片,组头 = 各思考耗时 + 各工具耗时;思考行也显示这段时长。回答生成时间不再算进运行过程。 - Web tool result 不再 500 字截断:网关原先一律截成 500 字,前端再
slice(0, 8000)且完成后默认折叠,所以execute的 stdout 看起来像没了。只有read_file/glob/grep成功时不展开结果(本地读文件,调用行够了);write_file/apply_patch/write_todos/web_search/fetch_url/save_memory/search_memory/execute/task等展开时同时展示完整入参和结果(write_file/apply_patch不再只送行数摘要),默认展开,只有超过 10 万字才截。task不再收成只有tool_count的_task_metaJSON,展开子 agent 的逐步调用和完整回答。出错的结果一律展示。 - 没有
rg时grep的 context 不再静默失效:纯 Python fallback 原先丢掉context_lines/before_context/after_context,模型传了-C 3却只拿到匹配行。现在按 rg 同样的优先级切片(context_lines > 0忽略 before/after,重叠窗口合并,组间--)。 - Web
/compact发出后立刻显示用户那一行,压缩完成前不能再发:以前要等压缩 API 回来才把/compact写进对话,期间发送按钮也不算 busy,于是用户以为没发出去再敲一次,第二条撞上 session 锁变成「already has an active run」。现在用户气泡马上出现;压缩期间 Enter / 发送键挡住(不是发出去再 409)。普通对话和/skill本来发出去就上屏,这条没改。 - 对话 footer 和轨迹同一轮的 tok/s、耗时对不上:footer 用整段
chat_stream墙钟(含建 agent)当分母,10 token / 3.6s 显示成2.8 tok/s、耗时再四舍五入成4s;轨迹用 jsonl 里 request 的 LLM 时间,同一轮是5.3 tok/s/1.90s。现在done带上last_completed_round的duration_ms/llm_ms/tps(跟 View trace 同一套分析),footer 用 LLM 时间算 tok/s、用 round 墙钟显示耗时。两边共用fmtDurationMs(不到 10s 两位小数)和fmtTps(一位小数)。 - 轨迹页顶栏的 git 分支和工作目录是会话自己的,不是 gateway 进程的:jsonl 原先每条都盖
os.getcwd()和进程里的git branch,网页会话于是一律显示git:main和…/Codes/agentica。现在按会话work_dir打戳、在那个目录探分支;没有 git 就不显示git:。已写出的旧记录打开轨迹时也用会话目录覆盖。
[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¶
- Gateway Web 构建产物不再进 git:
agentica/gateway/ui/是 Vite 每次npm run build都会换哈希的 dist(旧 hashed 文件还会在 git 里堆积),源码只留web/。发版 / CI 在python -m build之前跑cd web && npm ci && npm run build,MANIFEST.in把该目录打进 sdist/wheel,所以pip install agentica[gateway]仍然不需要 Node。顺带修了package-data只收 js/html/css、KaTeX 字体和 png/ico 进不了 wheel 的问题。本地开发继续npm run dev;没 build 时:8881/chat返回 503 并提示怎么 build。 - 站点图标换成透明底的 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。 顺序是刻意的:先在 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 的四种边界、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(../-<任务> */peers.py")),理由是错误代价不对称,且两条都实测过: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("真的返回了它那一份副本。排除永不包含搜索根自身,所以绑定在该 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¶
- 工作区外会话重启后 title 变成 Chat、轨迹空白:对话其实写在该会话
work_dir对应的 project 目录,但发消息前_touch_session_log在settings.base_dir下先touch了一个 0 字节 jsonl。重启后_locate_session把这个空文件当成命中,不再搜其它 project,侧栏name退化成Chat,「查看轨迹」读到空列表;浏览器 localStorage 里的对话还在所以看起来「记录还在」。现在空 stub 不算 transcript:新会话的 jsonl 建在会话自己的 work_dir 下;列表和定位都优先有内容的那份,并删掉盖住它的 0 字节影子文件。 - 输入框右下角那个上下文占用点开后看不见:面板是往下展开的,而那颗按钮就在输入框底部,于是整块落到视口外面。旁边的 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,所以不会在屏幕上打印两遍。
[1.4.12] - 2026-08-10¶
features¶
- 为 CLI 做的功能不再默认落到 SDK 用户头上。CLI 只是产品之一,但它和后端服务共用同一套 builtin 与同一个 Runner,于是「CLI 一定有的能力」被当成了前提而不是需要检查的条件。这一批统一改成先问再做:
- Layer 0 按「取回得回来吗」决定形态。超大工具结果以前一律落盘 + 在上下文里换成文件路径。可 CLI 之外没人能打开这个路径——一个只挂业务工具(无
read_file、无execute)的服务型 agent 拿到的是一句它读不了的路径,等于数据直接丢了,还会诱导模型去调一个它没有的工具。现在can_recover_spill(model.functions)检查会话里有没有read_file/execute:有就照旧落盘给路径(<persisted-output>),没有就不写盘、如实截断成<truncated-output>并说明原因和「请缩小查询范围」。 - 批次预算从固定字符数改成窗口比例,并且只管本轮新结果。旧的 200K 字符预算在 512K token 的窗口上误伤、在 8K token 的窗口上完全不触发;新的是
0.25 × model.context_window(TOOL_BATCH_BUDGET_RATIO)。作用点也收回到结果产生的地方(Model.run_function_calls),runner 里那次扫全量历史的重复兜底整段删除——那是第二套治理同一件事的阈值,而历史该由 Layer 1 按真实窗口压力管。留在Model这一层还顺带解决了 provider 形态问题:这里看到的是打包成 Anthropic content block 之前的普通结果消息,两边都生效。函数改名enforce_tool_result_budget→enforce_tool_batch_budget。 - 落盘目录 key 从
run_id改成session_id(缺省"default",仍按workspace.user_id隔离租户)。run_id 每轮一个新 uuid,按它分目录等于把一次会话打散成几十个目录,既找不回也清不掉。 - 压缩次数写进
RunResponse.context_compactions。Layer 2 不可逆、要花一次 LLM 调用,而它此前唯一的证人是 CLI 的事件回调——SDK 调用方只看到某一轮突然变慢、变贵、早期对话不见了,却没有任何东西可归因。native / 本地摘要 /prompt_too_long之后的 reactive 三条路径都计数;Layer 1 淘汰免费且可重跑恢复,不计数。 - 无 registry 时
execute不再提供background。以前它会自己 new 一个BackgroundProcessRegistry,那个 registry 没有任何界面能看到——background=True起的进程活得比 agent 还久,没人能列出它、也没人能停它。现在没有共享 registry 就:schema 里没有background这个参数、wait根本不注册、prompt 里那段说明也不发(省 940 字符),模型硬传background=True也只会拿到明确报错而不是一个孤儿进程。 web_search缺 key 分两种情况:provider="serper"是代码里写死的意图,缺 key 仍然直接抛(否则你以为在用 Serper,实际在用百度);而AGENTICA_WEB_SEARCH来自部署环境,可能是同一台机器上另一个进程留下的,缺 key / 名字拼错时降级到默认引擎并 warn——不能让一个可选工具的一个可选 key 把整个服务的启动搞挂。- 新增
Agent(enable_session_log=False)。session_id以前强制在进程 home 下写一份 JSONL,那份文件只有/resume、/fork、/export会读;自己已经有会话存储的服务拿到的是逐轮增长、永远没人读的第二副本。session_id本身不受影响。 Workspace记住「这里不是 git 仓库」。~/.agentica/workspace不会中途变成一个仓库,但每轮 system prompt 都要 spawn 一次git rev-parse去重新确认。否定答案缓存,肯定答案不缓存(branch / status / commits 正是每轮都会变的东西)。execute支持同轮真并行:execute(command=..., parallel_safe=True)的多个调用会进asyncio.gather同时跑。以前 docstring 写着「多条独立命令就并行发多个 execute 调用」,但调度器只 gatherconcurrency_safe的函数,而execute是False——这个承诺从来没兑现过(和当初task的同一个 bug)。模型于是拿background=True当并行入口用,导致「到处 background」:那是生命周期开关(命令能否活过这一轮、超时/取消后日志还在不在),不是调度开关。两件事现在分开了,background的 docstring 明确说要速度请用parallel_safe,wait仍然只服务background。 并行安全性做成逐次调用声明而不是工具级常量:execute既跑pytest tests/a也跑git commit,注册时给不出正确答案——填False白白串行化互相独立的活,填True则让git add/git commit这种批次直接竞争。机制是新增的Function.parallel_arg(execute设为"parallel_safe"),调度器改问FunctionCall.is_concurrency_safe();不带这个参数时行为完全不变:整批串行、保持模型发出的顺序、其中一条报错取消后面的。默认值是安全的那一侧,所以标错只会发生在模型显式声明「这批互不相干」的时候。 顺带补上 sibling-abort 的一个洞:shell 调用报错取消批次剩余部分,现在不区分它跑在哪个阶段。否则把一条 execute 放进并行阶段,就能让后面一条有依赖的命令在失败留下的状态上继续跑。parallel_safe只影响调度,不参与权限判断(execute仍是is_destructive=True)。- 删除全库自造的
when_to_use/when-to-use:Skill frontmatter、Agent/AgentDefinition、as_tool()回退链一律不再有这个字段。发现/委派时机写进description(或as_tool(tool_description=...))——以前它进了对象却常常进不了模型上下文,等于假配置。bundled skill 与 VaG seed 已并进 description;残留 frontmatter 键会被 Skill 解析静默忽略 - 新增
agentica --profile <name>:本次会话改用某个已保存的 profile,什么都不写(config.yaml 的active_profile和项目级覆盖都不动,用户自己那个会话不受影响)。这是命令行上换 provider 的唯一方式——--model_name只能在当前 endpoint 内换模型,base_url 和 key 仍然来自当前 profile,所以「给 worker 配一个别家的便宜模型」以前在命令行上根本做不到,只能让用户先切 active profile。名字不存在直接退出并列出可用的,不会悄悄退回默认;显式指定了 profile 时 onboarding 也不再回头改写这个选择。想永久切换仍然用会话里的/model <profile>(写项目级覆盖) - 框架开始自带 skill:
agentica/skills/bundled/下的SKILL.md随包发布,SkillLoader把它作为最后一条搜索路径、bundled在LOCATION_PRIORITY里排最低——同名的用户/项目 skill 一定赢,我们发的只是默认值,不是从用户手里拿走的决定。首批两个,都用「薄指针」写法:只写变化慢的概念和决策规则,flag、命令、配置项一律不抄进正文,改成告诉模型去agentica --help、~/.agentica/config.yaml现查——手抄一份手册进包里,两周后就开始骗模型。agentica(CLI 斜杠由name自动生成/agentica,不写非标准trigger字段)讲怎么查关于 agentica 自己的任何事,以及一条模型经常搞错的边界:斜杠命令是用户在输入框里敲的,模型输出里的/status只是文本,答案在斜杠命令后面时应该告诉用户去敲哪一个。multi-agent(/multi-agent)讲三种多 agent 机制怎么选(task只读、便宜、可并行;delegate要的是一个答案;tmux 里另起一个 CLI 要的是一条人能看见、能 attach、比你活得久的工作线),以及一套实测出来的注意事项:会话名是从 cwd 目录名派生的(没有--name,所以目录名就是名字)、寻址用名字不要用 session id(启动瞬间它还是空的)、--model_name只能在当前 endpoint 内换模型(base_url/key 仍来自 active profile,跨 provider 换不了)、子会话默认放开工具权限且无人值守所以要给它独立目录(worktree)、发完消息不要sleep轮询等回复(回复会作为新一轮自己到达)、干完要tmux kill-session收摊。端到端实测:在 tmux 里起一个真 CLI,像人一样把请求敲进去,它自己起了第二个 CLI、list_agents找到对方、send_message派活、拿到结果,57 秒 - 新增
delegate工具:一个交互会话可以把整块工作丢给另一个 agentica 进程去做(自己的上下文窗口、模型、工作目录、session log),做完把结论交回来——「主 CLI 分派、子 CLI 并行、结果汇总」这个场景以前只能靠人手开终端。它不是新机制:子进程就是agentica --query <task> --print --permissions <父会话当前模式>,通过已有的BackgroundProcessRegistry起,所以/ps、/stop、wait工具、完成回执全部直接可用。BackgroundProcess.kind(command/delegate)决定这些界面渲染成「一个任务」还是「一条 shell 命令」,也是并发计数的依据。task(进程内 subagent)仍是便宜的那个选项,delegate的 docstring 明确把小活推回task。约束:同时最多 3 个(MAX_CONCURRENT_DELEGATES,对齐SubagentRegistry.MAX_CONCURRENT),第 4 个被拒并告诉它去wait哪一个;只有一层(AGENTICA_DELEGATE_DEPTH传给子进程,create_agent超过MAX_DEPTH直接不建这个工具——agent 生 agent 的树没人看得住账单);权限用可调用对象实时读agent.tool_config.permission_mode,因为/permissions是原地改模式不重建 agent(ask模式下这个工具根本不在READ_ONLY_TOOLS里,自然不出现);模型默认继承父会话,可用model="provider/name"或裸模型名覆盖,API key 绝不上命令行(子进程自己读config.yaml);工具只在有 registry 的地方存在(交互式 CLI),一次性--query和 cron 起的 agent 没有 registry,它们派出去的活根本无法被 wait 或回收。子进程不出现在list_agents里:只有run_interactive才发布 PeerSession——委托是有返回值的父子关系,peer 消息是平级会话说话,两者刻意不混 - 新增
agentica --query "..." --print:只把最终回答写到 stdout,没有 banner、没有日志(suppress_console_logging),且走sys.stdout.write而非console.print——rich 会把回答里的[warn]当 markup 吃掉。这是delegate读子进程结论的方式,也可以直接用在脚本/管道里;一次性运行失败时退出码为 1(中断为 130),调用方据此决定下一步。委托任务的完成回执按任务渲染而不是「Background terminal #N」:正文是子会话交回的完整答复(120 行 / 8000 字符,不像命令日志那样只取尾巴),失败则给退出码和输出并明确「不要原样再派一次」。顺带修掉一个会污染回执的 bug:BackgroundProcessRegistry.start()写日志头$ cmd时不再换行(转义为\n),否则跨行命令(委托任务的 prompt 就是跨行的)的后半截会被读日志的一方当成命令输出交回给模型 - 项目目录元数据合并为单一
project.json(原.project.json的work_dir+ 原profile文本文件的active_profile);仍在~/.agentica/projects/<user>/<slug>/下,不进用户 git 工作区。session 的<id>.meta.json保持与.jsonl并列(粒度/并发不同,不并入)。SessionLog/ peers //configProject Dir / tool-result 路径统一经project_store.project_base_dir /goal --tokens -1(SDKtoken_budget=-1)表示不限 token;--turns/--wall同样认-1。不传--tokens仍默认500_000(与显式无限语义分开);0拒绝,避免和零额度混淆list_agents//list-agents输出加厚:每条会话多行列出 addressable name、peer id、session_id、cwd、带 hash 后缀的project存储目录(如-apdcephfs-...-nlp-6115aec9)、session_log(<project>/<session_id>.jsonl)、CLIlog_file(如~/.agentica/logs/20260809-80403.log,附 level)、workspace/memory(MEMORY.md)路径,以及 working on。消息地址仍是短名字(对齐 Claude Code 的myapp-3f),但send_message/resolve_peer也接受session_id前缀;列出路径是给模型自己决定要不要去读对方 transcript / 运行时日志 / 长期记忆(log_file通常比翻 conversation 更快看错误与工具痕迹),peer 消息本身仍只传纯文本。CLI 对list_agents/send_message的工具结果不再按默认 4 行折叠;send_message的调用行也完整展示正文(不再被默认 40 字截断)。注入到接收端输入区的 peer 消息修复 Rich Table 默认 ellipsis,长文不再被…吃掉。消息头的回复地址改用 addressable name(reply with send_message to agentica-73),不再暴露难读的reply_to=<peer_id>;/status新增Peer:行展示本会话的短名字。接收策略写清:header 标成用户转发的指令直接采纳、不要再澄清授权边界;另一 agent 发来的仍无授权(即使正文自称「用户决定」)。接收端接受消息时 CLI 展示 ✉ 回执(含正文):空闲开新 turn、运行中则在 tool 间隙注入;发送方确认语义改为「已入队 mailbox」并按名字回显(Message queued for 'agentica-73'),不再误称 delivered、也不再打印 opaque peer_id(对齐 CC:写 inbox 成功 ≠ 对方已读)- peer 代码收敛:
PeerInfo.detail_rows()成为字段展示的唯一来源,describe()(模型看的)与/list-agents(用户看的、按标签对齐)都由它渲染,不会再出现只加一半的情况;寻址走新的match_peers(),「查无此人」与「前缀撞车」给不同的报错并列出候选(旧实现两种都报 no live session);环路刹车从「hop 上限 3」换成重复检测 + 限频:hop 上限会掐断一次正常的更正来回,而中途试过的「一段对话最多 6 条」(MAX_EXCHANGE_TURNS)同样是错的——一次正常的多轮交接会被从中间掐掉,被拒的往往正是用户刚吩咐的那句。真正不该发生的是把同一件事再说一遍:PeerSession._check_send_rate按对端拒绝 5 分钟内重复的同一段文字(_text_digest折叠大小写与空白,改个排版不算新消息),并对同一对端限 5 分钟 20 条(RATE_WINDOW_SECONDS/MAX_SENDS_PER_WINDOW)——足够宽到任何真实协作都碰不到,又足够窄到 ping-pong 循环撑不过几分钟。两个限制都按对端分别计算(同时和三个会话协作互不影响),也都被note_user_turn()清空:用户在本终端敲任何一行(挂在 Enter 处理器上)或对端用/send-message转发过来即刻生效——一个能拦下用户刚打的那行指令的上限是 bug,不是保护。PeerMessage随之去掉exchange_turn字段与last_of_exchange(两端不再需要就一个计数达成一致);PeerMessage去掉从未被读的path字段、新增to_name(发送方按名字确认,mailbox 文件也能直接看出收件人);PeerSession.publish()遇到不存在的字段名直接报错而不是静默丢弃;/send-message改用split(maxsplit=1),目标名后多打的空格不再被当成空消息 /resume <id>和agentica resume <id>不再要求先cd回原目录。session 仍按项目(work_dir)分区存放,但查找变成「当前项目优先,未命中再搜该用户的全部项目」,所以No session matching '7e17bc1f-...'这类报错消失了,agentica resume也开始接受 id 前缀而不只是完整 uuid。分区目录名是sanitize_path单向哈希出来的、无法反推,因此每个项目目录写一个project.json记下它代表哪个 work_dir(一项目一文件,不是一 session 一份;同文件还可存项目级active_profile);早于这个改动的目录回退到读最新 transcript 首条的cwd——实测本机 140 个历史项目目录全部能正确反查,查找耗时 9ms。命中的 session 属于别的目录时,仿 Codex 给四选一:用 session 目录 / 用当前目录 / 总是用 session 目录 / 总是用当前目录,后两个写进~/.agentica/config.yaml的settings.resume_cwd(session/current),之后不再问;改回ask或删掉该行即可恢复询问。选了 session 目录会同时os.chdir并改 agent 的 work_dir——工具认 work_dir,但 git 状态、@file补全和 shell-out 认进程 cwd,两者必须一起动,否则会分裂成半个目录的状态。关键约束:无论选哪边,transcript 都继续追加到它原本所在的项目目录(Agent新增session_base_dir参数把存储位置与工作目录解耦),否则同一个 session 会裂成两个文件、从哪边都 resume 不全。session 目录已被删除时不再询问,直接留在当前目录并说明原因;prompt 处 Ctrl+C 取消则整个 resume 中止,不会退而求其次地建出一个无关 agent。另增/resume all列出所有项目的会话(附各自目录),其序号会被记住,随后的/resume <n>指向刚才看到的那一份而不是当前项目重新编号的列表- 内置
web_search的搜索引擎可替换:BuiltinWebSearchTool变成薄分发器,模型看到的工具名、参数、docstring 恒定不变,只换背后的引擎,所以 prompt、RunConfig(enabled_tools=["web_search"])、权限规则都不受影响。内置baidu(默认,行为不变)/duckduckgo/exa/bocha/serper/zhipu,选择优先级provider=参数 >AGENTICA_WEB_SEARCH环境变量 > 默认。引擎不按 API key 自动推断——为别处设的 key 不该悄悄改变 agent 的搜索行为;指定了需要 key 的引擎却没给 key 会直接报错,而不是静默退回百度(否则你以为在用 Bocha,实际在用百度)。CLI 无需新增 flag:把AGENTICA_WEB_SEARCH和 key 写进~/.agentica/config.yaml的env:块即可,apply_global_config()已经会投射进os.environ - 自定义搜索引擎三条接入路径,按「要不要写代码」「CLI 能不能用」区分:
AGENTICA_WEB_SEARCH=mcp+AGENTICA_WEB_SEARCH_MCP_URL/_TOOL零代码接任意 MCP 搜索服务(CLI 可用);register_web_search_backend(name, factory, method, key_env=...)注册命名引擎(注册后 env/CLI 也能选);BuiltinWebSearchTool(search_fn=...)直接传 async 函数(最灵活,适合包装已有 MCP client)。契约只有一条:async(queries, max_results) -> str - 新增
McpSearchTool:用 httpx(核心依赖)直接讲 MCP Streamable-HTTP,不经过需要可选mcp包的agentica.mcp,因此能当默认web_search引擎。Exa 是随附预设——其公开端点可匿名调用(共享免费池、有限流),设EXA_API_KEY则走自己的额度;把url/tool_name指向别的服务就是自定义引擎 - 六个搜索后端签名统一为 async
(queries: str | list[str], max_results: int) -> str且全部支持多 query,分发层因此不需要每后端的参数适配分支:SearchBochaTool的count、SearchExaTool的num_results改名max_results,ZhipuWebSearchTool补max_results,DuckDuckGoTool.duckduckgo_search补多 query web_search_pro_tool.py/WebSearchProTool更名zhipu_web_search_tool.py/ZhipuWebSearchTool(CLI--tools web_search_pro→zhipu_web_search),并从早已过时的/paas/v4/tools通用工具端点换到专用的/paas/v4/web_search。原来是把搜索伪装成一次 chat 调用(messages=[{"role":"user"...}]),结果得靠data['choices'][0]['message']['tool_calls'][1]['search_result']这种按下标摸出来——响应结构一变就崩,而且整个 API 只能传 query,条数只能拿回来再客户端切。新端点直接收count(服务端就按条数返回,不再传完再丢)、search_domain_filter、search_recency_filter、content_size,并暴露智谱的 4 档引擎:search_std(0.01 元/次)/search_pro(0.03,默认)/search_pro_sogou(0.05)/search_pro_quark(0.05),web_search分发器路径可用AGENTICA_ZHIPU_SEARCH_ENGINE切换。实测两点值得知道:一是count只是建议值(search_pro_sogou向上取整到 10/20/30/40/50,其他档位在部分查询上也超发),所以max_results由客户端截断兜底——单条正文约 1000 字,要 3 条收到 10 条会白烧几千 token;二是智谱自研的search_std/search_pro约 40% 的查询整批返回空link(同一查询要么全有要么全无),需要每条可溯源时用 sogou/quark 档(实测这两档 100% 带 link),文档已写明。顺带修了鉴权:原先 header 直接发裸 api_key,新端点要求Bearer。引擎/时间范围/摘要长度的非法取值在构造时就报错并列出可选值,而不是等 API 返回一句看不懂的错误;结果里剔掉icon(favicon URL)和refer(角标序号)——对模型是纯 context 浪费。默认 timeout 从 300 秒收到 60 秒(一次搜索挂 5 分钟不是超时,是卡死)DuckDuckGoTool/SearchExaTool改为直接用 httpx 讲 HTTP,删掉duckduckgo-search和exa_py两个可选依赖(pyproject的ddg/exaextra 清空为占位):两者都只是 HTTP 封装,而作为可换的web_search引擎,「装了才能用」意味着切过去才发现要装包。DDG 走公开 HTML 端点(httpx + bs4 都是核心依赖),顺带过滤掉此前会混进结果的赞助位、并把.result__url的截断展示文本换成真实链接(含/l/?uddg=重定向解包)——原先的 fallback 解析器返回的 url 是不可点的。Exa 走POST https://api.exa.ai/search,原生 async 不再需要run_in_executor把阻塞 SDK 挪出事件循环,text_length_limit下推为contents.text.maxCharacters由服务端截断(省掉传完再丢的字节),并删掉当前 API 已被type="auto"取代的use_autoprompt参数。两个模块此前在 import 期就可能失败(Exa 直接raise ImportError),现在无条件可导入- 内部拆包减负(结构明示,不藏兼容层):
cli/commands.py→cli/commands/、cli/display.py→cli/display/、cli/interactive.py→cli/interactive/、runner.py→runner/(_run_impl在runner/loop.py);包__init__只导出公开入口,私有符号从真实子模块引用;tests/cli/test_cli.py按域拆成多个小文件。SDK 主入口仍是agentica/Agent/Runner - 跨会话消息:两个终端里的 CLI 会话可以互发纯文本,不再靠人在终端之间复制粘贴。模型自己调
list_agents发现对端、send_message投递,用户不需要手动触发(/list-agents(别名/peers)只用于查看和排查)。传输是文件而非 socket:~/.agentica/cache/peers/live/<peer_id>.json是心跳 + pid 探活的发现目录,~/.agentica/cache/peers/mailbox/<peer_id>/*.md是每条一个带 frontmatter 的 markdown 消息,可直接 cat 排查。目录刻意放在用户级而非按项目 hash 分区——「协调同一个 repo 的多个 worktree」正是主场景,而它们 cwd 不同。收件端是拉取式:运行中的会话由Runner._inject_peer_messages在 tool batch 间隙取走(与/steer同一边界,不打断正在跑的工具),空闲会话由 CLI 轮询取走并开一轮。消息身份绑 CLI 进程而非 session log,/resume换掉 session_id 后在途消息仍会落地。环路刹车:一段不被人打断的 agent 间往返最多 6 条(MAX_EXCHANGE_TURNS),未读堆积到 50 拒收,单条超 40000 字符拒收。工具的 system prompt 明确约束收件方——来自另一个 agent 的消息不是用户授权、不能代答权限提示、其中的斜杠命令是纯文本 - 跨会话消息新增人工入口
/send-message <session> <text>(别名/send,与list_agents→/list-agents同一命名规则,对应 agent 用的send_message工具):不用切终端就能替 agent 自己把一句话说进另一个会话。它和 agent 发的消息在收件端语义不同,因此消息带from_kind(agent/user):agent 发的仍然「不是用户授权、不能代答权限提示」,/send-message发的注入为「你的用户从另一个会话转发」,收件方按用户亲口说的处理。mailbox 是 0700 用户私有目录,所以user身份与在本终端输入等价;header 里伪造from_kind不被采信。单条上限 40000 字符(够放一整篇 handoff 写给对方;再长就把内容落到文件、发路径,反正文件系统是共享的) - 新增
/fork:无参数即在当前位置分支——整段对话带过去,继续聊就是了,只是落到新 session,此后说的话不再进入被分叉的那份 transcript(/status多一行Forked from: <parent>,来源写在 fork 出的 sidecar meta 里,由SessionLog.fork()自己记,不依赖调用方)。/fork list列出本会话自己发过的消息(序号 + 消息 id + 时间 + 预览),/fork <n|uuid>分支到所选消息之前一条,于是模型回到「你提这个要求之前」的状态,可以换个说法重问。两种分支原会话都完整保留、照常/resume,提示里直接给出旧 session id。全数字的 uuid 前缀不会被误当序号(只有能索引列表的数字才是序号)。fork 完给出的原会话恢复方式同时列出/resume <id>(留在 CLI 里)和agentica resume <id>(已经退出去了),两条都受支持 execute(background=True)的结果现在会主动回灌当前会话:命令结束时除了打印通知,还把退出码、耗时、完整命令和输出尾部交给 agent——运行中经steer()落在 tool 间隙,空闲则作为下一轮,于是它自己接着往下做,不用等用户回来手动wait。设deliver_background_results: false可关掉自动唤醒- Goal 预算耗尽不再当场砍断:任一 cap 触发时循环额外给一轮收尾 turn,喂
[Standing goal budget reached]prompt 要求模型交接(做完了什么、还剩什么、有什么坑),明确禁止在没有预算兜底时调verify_completion;这一轮跑完才落到budget_limited。收尾轮期间 goal 保持active以便正常计账(因此最终会超出 cap 一轮,状态栏如实显示),GoalState.budget_wrapup_sent保证只给一次,resume()重置。CLI/goal与 SDKrun_goal()共用同一条路径 - Goal 主成本闸改为默认开启的
token_budget=500_000(CLI/goal与 SDKrun_goal一致);turn_budget默认None(仅--turns/ 显式参数时生效)。/goal status与状态栏在执行中显示tokens used/budget(如goal 12.3K/500K) execute(background=True):长命令可立即返回,由共享BackgroundProcessRegistry托管进程组;stdout/stderr 写入~/.agentica/projects/<user>/.../background/日志。CLI 新增/ps列出后台 terminal 与 background agent,/stop <id|pid|#n>可按目标停止(空参停全部);状态栏显示正在运行的 background terminal 数量。registry 经create_agent/ session rebuild 路径注入,与/backgroundagent 任务共用同一套/ps//stop入口wait(id=...):等待execute(background=True)启动的后台命令,命令一退出立即返回并给出退出码、耗时和日志尾部;未结束则在超时后返回当前进度且不停止命令,单次上限 300 秒,让调用回到模型循环以便用户打断。后台命令的退出状态只报给用户,wait是它回到对话里的唯一途径。它补的是「命令可能活得比一次工具调用久」这个断层:前台命令被 timeout 或被取消的一轮杀掉会丢掉全部输出,而这类任务此前只能退回sleep N && tail log的盲等。一次调用跑得完、当下就要结果的命令仍应留在前台并调高timeout;真正跑几小时以上的任务则不该wait,等一两次仍未结束就结束轮次,由用户收到的完成通知驱动后续- CLI 渲染
apply_patch的真实多文件 unified diff:在原子写入前解析 patch envelope 并捕获每个目标文件的原始内容,完成后展示一份合并的 old→new diff,替代 executor 的文本摘要 - CLI execute 工具调用行支持宽度感知预览:普通长命令和 heredoc 统一最多展示 3 行正文,Ctrl+O 可分别展开完整 command 和折叠 output
changes¶
- 上下文压缩从五个 stage 塌成两层,Stage 4(
CompressionManager.compress规则压缩)整个删除。让一个超窗口的请求装下只有两种操作:免费地扔掉单个条目(可通过重跑工具找回)和花一次 LLM 把历史换成摘要(不可逆)。原来的 stage 3 和 stage 4 在做同一件事的两个版本——都是「截断最旧的工具结果」,只是各有一套阈值和保护参数,于是 bug 可以出在两个地方而不是一个。现在 Layer 1 =evict_context()(淘汰 + 收缩 tool_call 参数),Layer 2 =auto_compact()(原生 compact 是它的 provider 变体,reactive 是它加force=True);此外还有一个不算压缩的 Layer 0——工具结果预算,那是「别让超大输出进上下文」的输出策略,每条结果只在产生时跑一次。随之删除:should_compress/_truncate_oldest_tool_results/_drop_old_messages/_archive_dropped_messages/_llm_compress_old_tool_results/_still_over_limit/get_compression_ratio,配置字段compress_tool_results(ToolConfig与CompressionManager两处)/truncate_head_chars/keep_recent_rounds/use_llm_compression/compress_tool_call_instructions/workspace,以及随之失去调用方的agentica/prompts/compression/。「丢弃最旧的消息轮次」不再存在:它会静默吞掉用户自己提过的问题,那正是摘要该做的事,且做得更好。_shrink_assistant_tool_call_arguments不跟着删而是并入 Layer 1——Layer 1 只碰role="tool",够不到一次write_file塞进 assistant 消息的整段 payload,这是真实缺口。_sanitize_tool_pairs提为模块级agentica/compression/tool_pairs.py::sanitize_tool_pairs,并接到唯一真正需要它的地方:context_overflow_threshold的 FIFO 丢弃是按位置删消息的,会留下没有结果的 tool_call - 压缩的入口收敛到 runner 一处(
evict_context→auto_compact,外加prompt_too_long之后的 reactive),另外两条自己动手缩上下文的路径整个删除。留着它们的代价不是多几行代码,而是同一个决定有第二套阈值,两套必然漂移,且副本总是更差的那个实现。删掉的第一条是ToolConfig.context_overflow_threshold(连同Agent._build_pre_tool_hook、_overflow_warning_emitted、runner 里两处调用、DeepAgent的0.8默认值、examples/agent_patterns/11_model_hooks.py):它是个 pre-tool hook,用自己的chars/4估算判断超过阈值就 FIFO 丢弃最旧的非 system 消息——丢消息会静默吃掉用户自己提过的问题,而摘要覆盖同一段历史还能把问题留下,所以它没有存在的理由;上一轮我只把它的第一阶段换成了 Layer 1 淘汰,那只是让 hook 和 runner 做重复的事。删掉的第二条见下条/compact /compact只走真实压缩:native checkpoint →auto_compact(force=True)→ 失败就打印失败并原样返回,不动messages/runs/ session log。删除的_rule_based_compact兜底是「成功」得最响的那个失败——它messages.clear()之后不放回 system prompt(违反 CLAUDE.md 里 compaction 不变量第 4 条),把每条消息截成 300 字符拼成摘要,还往 session log 写一个空的compact_boundary,于是/resume也回不去。用户在网络不稳的时候敲一次/compact,丢掉的是 system prompt、全部历史原文和恢复的可能,屏幕上显示的是绿色的「Context compacted」。auto_compact返回 False 时本来就没改过消息列表(摘要拿到手之后才重写),所以「失败即不变」不需要额外的回滚。随之删掉已无发射方的compact.rule_basedCLI 事件分支- Layer 2 现在默认可用。
ToolConfig.compress_tool_results默认False,意味着compression_manager为None,于是默认配置下原生 compact、auto-compact 和prompt_too_long之后的 reactive 补救全都不跑——长会话唯一的结局是被 provider 拒绝。该 flag 已删除,Agent.__init__在compression_manager为 None 时总是建一个
fixes¶
- 超长单条 query / aux 记忆提取 /
/compact→/fork三条边界:① 尾部 user 轮已独自占满窗口时,reactive compact 救不了(Layer 2 会原样保留该轮),改为直接抛出 provider 的context_length_exceeded,CLI 用「Input exceeds model context window」展示原文,不再先白跑一次摘要再包成 fallback 总错误;②MemoryExtractHooks按 aux 的context_window截断 transcript(保留尾部),避免主模 1M、aux 128k 时静默 400;③/compact与 runner Layer 2 先打on_pre_compact,auto_compact写compact_boundary后立刻把preserved_tailappend 进 session log,使立刻/fork或/resume仍能看到与内存一致的上下文(只写尾巴:摘要那一轮由load()从 boundary 自己合成,一起写会让摘要在 resume 后出现两遍)。on_pre_compact会经 aux 模型 flush 记忆/经验缓冲,所以阈值判断从auto_compact内部上提到 runner(CompressionManager.should_auto_compact转为公开、可传入已算好的 token 数)——此前它在每一轮都先打 hook 再让auto_compact返回 False,把「几十轮一次」的边界变成了每轮一次的付费旁路调用;reactive compact 之前也只打 pre 不打 post,现在三条压缩路径的 hook 成对触发 - 压缩两层都改为以「一条工具结果」而不是「一条消息」为单位,Anthropic 路径上压缩此前从来没生效过:只有 OpenAI 系是「一条结果一条
role="tool"消息」,Anthropic 把一整轮打包进单条role="user"消息的 content 列表({"type": "tool_result", "tool_use_id": ...}block,见AnthropicClaude.format_function_call_results)。Layer 1 只扫role == "tool",于是在 Claude 上一条都没淘汰过——不报错、不告警,静默失效,整个提供商的上下文只能靠 Layer 2 兜。tool_resultblock 不带工具名(只有tool_use_id),占位符改为回查发起调用的那条 assistant 消息的tool_calls拿到名字和参数,所以两边的占位符信息量一样。同一个形态差异连带修掉两处:sanitize_tool_pairs只认role="tool"形态,Anthropic transcript 在它眼里像是每个调用都没有回复,重建会给每个调用插一条占位role="tool"消息,把本来没坏的 transcript 弄坏(现在这类 transcript 原样返回);auto_compact保留「最后一条 user 消息之后的整段尾巴」,而 Anthropic 的工具轮本身就是 user 消息,从那里切会留下一批tool_result、它们对应的tool_useblock 却在刚被摘要替换掉的 assistant 消息里——这种孤儿 block 会被 API 直接拒绝,现在判断尾巴时跳过承载工具结果的 user 消息。Message._evicted语义随之收紧为「这条消息里的结果全都淘汰完了」(一轮多条结果时不能提前关门),单条结果是否已淘汰改看占位符前缀。Layer 0 的批量预算曾经也只看role="tool"、在 Anthropic 上不生效,后来随enforce_tool_batch_budget一起搬回结果产生的地方解决了:那里看到的还是打包成 block 之前的普通结果消息 - 修掉「读了又读」死循环,并把 micro-compact 整个换成
agentica/compression/evict.py:模型在一轮里并行read_file六个片段,下一轮这些结果已被清成占位符,于是它把同样六个读原样再发一遍,无限重复。旧实现两个缺陷叠加:没有压力闸门(每轮无条件清,200k 窗口只用了 6k 也照清,省下的上下文没人要,代价是模型重跑工具),按条数保护且当轮结果已在计数里(keep_recent=5遇上 6 个并行调用,最旧那条在模型第一次看到它之前就被清了)。新实现只有两个量,没有「保留最近 N 条」这类参数——任何固定条数都必然输给 count+1 大小的批次,调大 N 只是把复现门槛抬高:占用低于context_window的 70%(EVICT_THRESHOLD_RATIO)一条都不动,超过则按最旧优先淘汰,直到降回 50%(EVICT_TARGET_RATIO)就停——最近的结果自然幸存,因为根本轮不到它们;目标取得比阈值低是为了迟滞,否则清一条刚跌破阈值下一轮又超,变成每轮清一条的抖动。消息尾部那一段连续role="tool"(模型还没看过的当前批次)整体排除在可淘汰集合之外,压力再大也不动它,那种情况该走摘要而不是丢掉本轮自己的证据。占位符写明是哪个调用(read_file(file_path=..., offset=...))让模型能原样重发;不再先落盘:取回动作两边都是一次工具调用,成本一样,而对read_file落盘副本严格更差——原路径上的内容更新鲜,快照只是过期拷贝加白占磁盘(persist_full_result()随之删除)。已被 tool-result budget 落过盘的结果(含<persisted-output>)跳过,它携带的路径是那份过大输出唯一的抓手。没有采用「豁免 read_file」:read_file恰恰是体积最大的消费者,永久豁免等于让上下文被文件正文填满直到触发破坏性大得多的整轮丢弃,而且它只挡读批次——6 个并行grep会以完全相同的方式空转。Message._micro_compacted更名_evicted,CLI 事件compact.micro更名compact.evict(仍然静默) glob/grep默认超时从 10s 收到 3s:NFS 大树上慢搜尽快失败好让模型收窄 path;调用仍可传更大的timeout/list-agents(及list_agents工具)对本会话与其他 live session 用同一套字段:project/session_log/log_file/workspace/memory/mailbox都会列出。对端若是旧版未发布这些路径,本机按 cwd/pid/peer_id 补全能确定的项,不再只剩 session_id+cwd+working on- LSP 编辑诊断不再把
agentica启动打崩,也不拖慢启动:CLI 默认仍开--enable-diagnostics,但 language server 懒启动(第一次改文件才initialize,create_agent不阻塞)。半残 pyright(只pip install pyright没[nodejs])或 NFS 超时只会在首次编辑时 warning 降级并杀掉进程;initialize的约 5s 是 deadline 不是 sleep(正常几百毫秒就返回)。安装提示改为pip install 'pyright[nodejs]' - 状态栏和
/status不再报一个和正在跑的模型对不上的 profile 名:两处都读resolve_active_profile_name(),那回答的是「config.yaml 指向哪个 profile」,而agentica --model_name X之后跑的已经不是那个 profile 的模型了,于是状态栏出现opus-4.8 openai/deepseek-v4-flash这种自相矛盾的一行。改为由真正做决定的resolve_model_config把结果记在agent_config上(profile_name/profile_source),新的setup.session_profile()是所有展示面的唯一读取口:--profile指定的显示为那个名字并标flag,模型被 flag 覆盖时显示「无 profile」——此时确实没有哪个 profile 能描述这个会话,报一个名字只会让人以为自己在那个 profile 上。空字符串是「明确没有」,键缺失才回退到 config 级答案,所以手搓agent_config的调用方(测试、其他入口)行为不变。/model <profile>切换时同步这两个字段,/config在会话 profile 与 config.yaml 不一致时多打一行说明 - 一条消息里发出的多个
task现在真的并行:执行器把一批 tool call 按concurrency_safe分成两组,True 的走asyncio.gather,其余在 for 循环里一个接一个跑,而task注册时漏了这个标记(register(self.task)默认 False),于是三个 subagent 排队执行,第一个跑完才起第二个——它自己的 system prompt 里那句「Launch independent tasks in one message to run them in parallel」一直是空头支票,只读的read_file/glob/grep/web_search/search_memory/list_agents全都标了、唯独它没有。subagent 本就只读(写操作和状态变更命令会被拒),且每个都跑在自己克隆的 model、HTTP client 和 Agent 上(_clone_parent_model的注释早就写明「parent 的 client 属于 parent 的事件循环,会和并发 subagent 抢」),符合concurrency_safe的语义。回归测试直接断言调度结果而不是这个标记:三个 task 必须同时在飞(peak == 3)。同时删掉task上的interrupt_behavior="block"——这个字段只在串行分支被读取,改并行后对它永远不生效,留着就是假配置 task的并发有了上限:spawn_batch一直按SubagentRegistry.MAX_CONCURRENT(3)限流,但模型是一条消息里发 N 个task、走的是spawn(),之前没有任何闸门——标上concurrency_safe之后,8 个 task 就是 8 个 subagent 同时烧钱。BuiltinTaskTool自己持一个懒初始化的asyncio.Semaphore(SubagentRegistry.MAX_CONCURRENT)(按事件循环重建,跨run_sync的临时 loop 不会串),多出来的 task 排队而不是被拒;system prompt 里也写明「最多 3 个同时跑」,免得模型以为一次发 10 个能一起完成- 系统提示里的 git 状态不再卡住整个事件循环:
get_git_context()用四次同步subprocess.run(每次 5s 超时)拿rev-parse/ branch / status / log,而它挂在每一轮的 system prompt 构建路径上——仓库大或磁盘慢的时候,这几百毫秒到几秒里所有并发工作(并行工具、后台进程回执、peer 消息投递)全部停摆。改为asyncio.create_subprocess_exec,且先rev-parse确认是仓库,再把 branch / status / log 三个读用asyncio.gather一起发出去(它们互不依赖)——实测本仓库 38ms,事件循环最大停顿 9ms - 只读外部工具补上并行标记:搜索类(serper / exa / bocha / 百度 / DuckDuckGo / 智谱 / jina)、
wikipedia、arxiv、dblp、hackernews、weather、newspaper、yfinance全部只读 HTTP,sql的list_tables/describe_table是只读 schema 查询(run_sql_query仍串行,它可能是 DML/DDL),code的 AST 分析与 lint、lsp的goto_definition/find_references/hover_info也是只读——之前它们全部落在串行分支,一次「查三个来源」等于三次网络往返相加。同时修掉 LSP 并行后才会暴露的问题:_send_message写 server stdin 没有加锁,两个查询同时写会把 Content-Length 帧交错成乱码 - guardrails 的
run_in_parallel从死字段变成真并行:InputGuardrail.run_in_parallel默认 True、也在文档里写着,但执行引擎run_guardrails_seq只有串行一条路,这个字段从来没人读——三个各 0.3s 的审核就是 0.9s 全加在用户面前。新的run_guardrails把连续的可并行 guardrail 归成一批asyncio.gather,run_in_parallel=False的自己一批:声明顺序仍然决定短路语义(串行的那个拦下来,排在它后面的根本不会启动),报告出去的也仍是声明在最前面的那个而不是最先跑完的那个。输出 guardrail 保持串行——答案已经生成,第一个拦下就结束,后面的都是白花的钱 - RAG 检索不再阻塞 async 路径:
knowledge.search从查询 embedding(HTTP)到向量库再到 reranker(HTTP)全程同步,而get_relevant_docs_from_knowledge是直接调的——开了add_references的会话,每一轮都要在事件循环里干等这一整趟往返。改为run_in_executor,get_user_message/search_knowledge_base随之变成 async - 并行分支补上取消检查:串行分支每次执行前都看
agent._cancelled并尊重interrupt_behavior,并行分支完全没有这一段——Ctrl+C 之后,那一批里的每个读、以及(task并行化之后)每个 subagent 照样全部启动。两个分支现在共用同一个判断:interrupt_behavior="cancel"的直接跳过并回「Tool cancelled by user」,"block"的(起来了就没法干净拆掉)仍然放行 - 工具参数不再被悄悄改写:
get_function_call此前在json.loads之前对整段 JSON 原文做"True"→"true"/"False"→"false"/"None"→"null"替换,本意是容忍模型吐出 Python 字面量,实际把字符串内容一起改了——send_message里一段swapped = True的代码到了对端就成了swapped = true,接收方照着报 NameError,两个 agent 于是围着一个根本不存在的 bug 争论。改为先正常json.loads,只有解析失败才用ast.literal_eval兜底(只认字面量、不执行代码,且不碰字符串内部)。同时删掉紧随其后的一遍「清洗」:它对每个字符串参数strip()并把"none"/"true"一类的值按字面转成None/True,与声明的类型无关——于是消息正文和文件内容的首尾空白被吃掉、一条内容恰好是None的消息变成空值。类型修正现在只由 schema 感知的coerce_tool_args负责("3"→3、"true"→True仍然照做,但只在参数确实声明为该类型时)。sanitize_arguments开关随之删除(Function字段、@tool参数、Tool.register()参数):它已无任何作用,留着就是假配置 - 换 provider 后 resume/fork 不再 400(
cache_control cannot be set for empty text blocks):session log 无论哪家 provider 写的,tool 轮次一律按 OpenAI 线格式落盘(assistant 带tool_calls、结果是role="tool",且那条 assistant 的正文是空串)。Anthropic 的/v1/messages收不了这种形状——空正文被包成{"type":"text","text":""},滚动 prompt cache 又恰好把断点打在它上面,于是整轮请求被拒。两层修:Model新增supports_replayed_tool_history(Claude 为 False),resume/fork 时对这类 provider 把历史降级成纯 user/assistant 文本(复用/model切换早就在走的strip_all_tool_artifacts),问答记忆保留、tool 轮次不跟过去;Claude.format_messages同时不再产出空 text block,整条内容为空的消息直接跳过(只带图片的 user 消息照常保留)。同 provider 的 resume 不受影响,tool 历史照旧完整回放 - 命令处理器发起的用户提问不再被看门狗秒取消:
ask_user_question_callback原先在轮询到空队列时以not agent_running判定「这一轮已经结束、没人会来回答了」,但斜杠命令恰恰跑在两轮之间(agent_running为 False),于是/cron的确认框和新的/resume目录选择在第一次轮询就自己取消掉。改为记录提问是否在 run 中发起,只有那种才受 run 结束影响 /newchat不再继承agentica resume <id>留在agent_config里的 session:此前新会话会沿用被恢复的 session_id,继续往它本该离开的那份 transcript 里追加/resume <id> at <uuid>现在真的 fork:此前它复用原 session_id,分支的新对话被追加进它所分叉的那份 JSONL,两条线混在一个文件里、各自都无法独立 resume。现在在create_agent这个唯一入口调已有的SessionLog.fork()生成新 session(--resume-at-uuid启动参数走同一条路径),原分支保持不变;fork 点读取即消费,后续/model重建 agent 不会从同一点反复分叉- 修复
/steer在 goal 循环中丢词:agent 不在 run 中时(一轮刚结束、goal 正在判定的那几秒,以及 UI 检查到steer()之间的 TOCTOU 窗口)原先只打印一句"用 /queue"就把用户输入丢掉。现在统一降级为排队执行,并插在待跑的 continuation prompt 之前,纠偏不会被一整轮无关工作挡住;被插队的 continuation 也不会被重复排入 execute(background=True)完成后,CLI 现在会主动异步显示成功或失败、退出码、尾部输出和完整日志路径;通知由 registry 的完成事件驱动,不会唤醒 LLM。/stop和 CLI 退出触发的终止不会再重复显示为任务失败,等待ask_user_question输入期间的完成事件会保留到安全时机再展示- 修复
execute(background=True)遇到以&结尾的命令时误报完成:shell 会 fork 掉真正的工作并立刻退出,registry 追踪到的是那个空壳,于是任务还在跑就宣布结束。该组合现在直接拒绝;前台的nohup ... &仍照常执行,只在结果末尾注明它未被追踪(/ps、/stop和完成通知都看不见它,且取消或超时的一轮会连同进程组把它杀掉),并建议改用background=True execute拒绝 120 秒以上的前台起始sleep:观察到的轮询写法sleep 330 && tail log会把刚被后台化释放的这一轮重新堵死。阈值对齐前台默认 timeout,短重试sleep 2 && curl ...不受影响。拒绝信息同时给出两条正路:等后台命令用wait(id=...),等没有完成事件的外部条件用until curl -sf ...; do sleep 5; done这类成功即返回的重试循环execute(background=True)的返回值和 docstring 改为直接声明契约:退出状态报给用户而非模型,后续步骤需要它的结果就调wait(id=...),不要自行用 sleep/轮询/阻塞 tail 模拟等待。旧的「读日志查看进度」措辞正是诱导模型接前台轮询的来源- 修复后台 terminal registry 接线导致 CLI 启动失败:
SessionState现在先于首次create_agent()初始化,再将同一份BackgroundProcessRegistry注入 execute 工具、/ps、/stop和状态栏 - 修复工具取消时子进程清理不彻底:新增
terminate_subprocess(),在活跃 event loop 上终止并完全回收 asyncio 子进程(communicate()排空管道,支持进程组 SIGTERM→SIGKILL 宽限升级),应用于 execute/grep、shell 及 goal verify 等工具,消除取消后管道传输回调泄漏到已关闭 event loop 的问题 - CLI 顶层 agent 执行错误改为结构化展示:429/限流等 provider 异常显示红色摘要、可操作
/retry提示和 code/spanId 诊断字段,完整原始异常保留到 Ctrl+O 展开 - 文件工具缺失路径错误只暴露真实路径状态:
read_file/edit_file/glob/grep缺失路径时返回 resolved path 和 nearest existing parent,并提示从ls/glob/grep重新定位;不再猜测候选路径或在read_file内容尾部追加 metadata - 文件编辑默认收敛到
apply_patch:multi_edit_file不再注册为内置工具,复杂/多 hunk 编辑走上下文 patch;edit_file保留为单个短且唯一的 literal 替换工具。edit_file的String not found保持无状态重读指引;apply_patch的 context mismatch 同时展示 expected context 和 actual 当前行,便于用真实当前内容重建 patch - 修复 CLI 输出 OSC 8 超链接泄漏:Rich 为 Markdown 链接生成 OSC 8 终端超链接,prompt_toolkit 的 ANSI 解析器不识别 OSC 序列会把 payload 渲染成可见文本,渲染前剥离不支持的 OSC 8 包装(保留链接样式文本)
- 修复 Ctrl+O 分页器在输出含控制字符时停在 less 的 binary-file 确认提示:less 调用统一加
-f强制打开 /compact原生压缩失败的回退提示改为logger.warning,不再向终端打印打断对话流- 拆包后清理机械复制遗留的死 import:
runner/(compress/core/loop/persist/retry_fallback/steer/stream)与cli/commands/(context/cron_cmd/goal/helpers/model_config/runtime/session/tools_skills)各文件只保留本地真实引用,删掉约 680 行从原单文件带过来的未用 import;tests/cli/test_cli_configuration.py的 4 个 patch(reset_skill_registry/load_skills/get_skill_registry/create_agent)从cli_tools_skills改到cli_helpers——拆包后实际调用点在helpers._refresh_skills_session,原 patch 打在错模块是无副作用的 no-op,autoflake 删掉未用 import 后才暴露 tools/buildin_tools.py(2154 行)拆成tools/builtin/包:file_tool.py(BuiltinFileTool+path guards+_GLOB/_GREP_TIMEOUT)、execute_tool.py(BuiltinExecuteTool+exit-code helpers+_MAX_WAIT_SECONDS)、__init__.py(get_builtin_tools+re-export 7 个工具类);原task_state_tools.py/web_tools.py已在包内。agentica/__init__.py与tools/__init__.py改从agentica.tools.builtin取,所有直接引用者(tests/examples/docs/agent/acp/gateway/evaluation)改到新路径,测试 patch 字符串(_GREP_TIMEOUT/shutil.which/asyncio.create_subprocess_exec/terminate_subprocess→file_tool;_MAX_WAIT_SECONDS→execute_tool;_detect_python_error_hint/_interpret_exit_code/_is_blocked_device/_check_sensitive_write_path→对应子模块)同步更新agent/base.py(2091 行)抽出 goal 闭环到agent/goal_mixin.py(GoalMixin:get_goal_manager/enable_goal_tool/run_goal/run_goal_step),Agent继承链加GoalMixin;base.py降到 1794 行,只留公开 run API 面与薄委托,不再内嵌 goal 闭环。MRO 透明,所有agent.run_goal()调用无需改动
docs¶
apply_patchdocstring 明确要求 Update/Delete 操作前必须先read_file,禁止凭记忆构造上下文- README News 区重构:旧版本条目折叠进
<details>区块 - 终端文档更新
/new命令别名说明及退出 CLI 后按 session ID 恢复的用法 - 文档与 README 补充 CLI
task/delegate/ peer 选型:docs/getting-started/terminal.md、docs/multi-agent/choosing.md、docs/concepts/tools.md、docs/multi-agent/subagent.md;中/英/日 README News + 协作对照表 - CLI 对
task/delegate(及结果锚点)完整展示任务正文,不再 40/80 字截断;子 agent 启动行同样全文
[1.4.11] - 2026-08-04¶
Added¶
- OpenAI Responses API support. The new top-level
OpenAIResponsesmodel supports sync and streaming text, reasoning summaries and encrypted reasoning-state replay, image input, function tools, parallel tool calls, and structured output. CLI and Gateway profiles select it with OpenAI-onlywire_api: responses; profilemax_tokensmaps tomax_output_tokens, and Responses reasoning usesreasoninginstead ofreasoning_effort. - Provider-native Responses compaction.
OpenAIResponsesnow calls/responses/compactbefore destructive local compression, replays the returned canonical window unchanged, persists opaque checkpoints across session resume, and retains a portable transcript for cross-provider fallback. Endpoint failures andprompt_too_longrecovery use the existing local summary/rule-based pipeline; manual/compact [instructions]follows the same priority. - Markdown-configured subagents and CLI management. Packaged
explore/research/codedefinitions now live inagentica/agents/*.md. Projects can add or override definitions in.agentica/agents/, users can share definitions through~/.agentica/agents/, and/agents list|create|reload|removemanages the effective configuration. The defaultreviewsubagent was removed so code review stays with the main agent and its full context; partial runs retain bounded tool inputs for resume. - Built-in
apply_patchcan update multiple files in one tool call. One strict patch envelope may add, update, or delete several text files. Agentica validates every path and hunk before writing, reuses the existing sandbox and sensitive-path guards, and keepsmulti_edit_fileunchanged for established single-file batch edits. CLI and Gateway summaries report the aggregated file and line counts.
Changed¶
- Shell tools preserve the model's exact command string.
executeandShellToolno longer normalize arguments, rewrite Python literals, or convertpython -ccommands into heredocs. Safety policies may block a command and secret redaction may sanitize returned output, but the command sent to the shell is otherwise unchanged. - CLI now warns after successful main-agent context compaction. Automatic compaction recommends
/newwhen long-session accuracy may degrade, repeated successful auto-compactions escalate the warning with a session-local count, and reactive recovery explains that compaction happened before retrying. Spinner lifecycle events, failed attempts, manual/compact, and subagent compactions do not affect the count. - Removed the
read_filefreshness/staleness machinery entirely (codex-style simplification):FileReadState,_file_read_state,_record_file_read,mark_read_context_stale,mark_all_read_context_stale,_edit_freshness_tip,Agent.mark_evicted_file_reads,Agent.append_evicted_file_read_notice, and the[Context maintenance]eviction notices are gone. Edits return the absolute path + diagnostics only; a failededit_file("String not found") remains the natural signal to re-read. - Default model
context_windowraised from 128k to 200k acrossModelbase, OpenAIChat, LiteLLM, Ollama, and Claude, reducing premature tool-result compaction that caused repeatedread_filecalls. - Prompt and tool-schema token cost reduced. File-tool guidance is gated on registered file tools (no phantom tool names); dead
# Available Toolstable generation is removed; parallel/batch call guidance is restored;grep/globdocstrings are slimmed;taskpolicy lives only in the tool system prompt;write_todosclarifies steps vs tool calls and returns a short status ack instead of echoing the full list. write_todoskeeps per-step progress updates. Completions are still not batched (one sync per finished step) so the CLI progress bar stays live; only the tool-result payload shrank.
Fixed¶
- CLI resume restores both model context and the visible transcript. Resumed JSONL history is hydrated into canonical run-response messages used by later prompts, while user, assistant, tool-call, and tool-result entries are replayed in the terminal. Session summaries now print the executable
agentica resume <id>command, and patch failures retain actionable multi-line details instead of collapsing the error to one short line. - CLI context usage now reflects the current session prompt instead of a token watermark. The status bar is updated from each main-agent request's actual messages and tool schemas, re-measured after each completed turn or
/compact, and ignores subagent/auxiliary LLM calls. Per-turn footer tokens remain cumulative API consumption across retries and tool loops, so they no longer share a misleading data path with context occupancy. - Quote-tolerant file edits no longer rewrite unrelated punctuation.
edit_fileandmulti_edit_filenow use normalized quote text only to locate a match, then apply replacements against the original content so curly quotes elsewhere in the file remain unchanged. - Stale
model_pricing_cache.jsonis no longer discarded. Catalog loading previously treated TTL expiry as "no cache" and, when the network refresh failed, silently fell back to the hardcoded pricing table — so new models (e.g.claude-opus-5, 1M context) never resolved. Refresh failures now fall back to the stale-but-valid cache file. - CLI resize no longer leaves repeated
Enter to sendghost lines. The_resize_collapsedflag (meant to shrink the bottom frame to a single row during a terminal resize) was set but never read by the layout, so the full multi-row frame redrew on everySIGWINCHand multiplied ghost copies in scrollback. The collapse is now actually wired across the input prompt, queue bar, status bar, and input height, and the post-resize restore does a clean erase + absolute-cursor redraw instead of a diffinvalidate(). - File tools now expose clearer recovery signals.
edit_file/multi_edit_fileappend one stateless "read or re-read the relevant region" action afterString not found, without tracking session read state or blocking edits.grepnow documents thatpathaccepts either a file or directory, supports file paths in its Python fallback, and reports missing inputs asPath not found. - Learned Experiences no longer accumulate frontmatter debris.
strip_frontmatterparses line-based---delimiters (values containing---no longer truncate the body); bumping a card refreshes its body from the new content; injected card bodies are capped; captured tool errors keep head+tail within a fixed budget instead of parking whole tracebacks in the system prompt.
[1.4.10] - 2026-07-24¶
Added¶
- Native image capability routing. Model image support is now resolved from
models.devcatalog metadata, with explicitsupports_imagesoverrides for private or aliased endpoints. Vision-capable base models receive original images directly; text-only models use the external OCR fallback. - Simplified session naming. CLI
/rename <name>replaces/session rename, and/resumeaccepts a session number, name, or ID prefix.
Fixed¶
- Pillow is declared as a core dependency. The model and default provider import paths use Pillow for PIL image inputs and image format detection, so the
1.4.10wheel now installsPillow>=10.0automatically. - Wheel installation is validated without checkout shadowing. CI imports both the latest PyPI release and the newly built wheel outside the repository root, and verifies that the current wheel installs and imports Pillow.
[1.4.9] - 2026-07-21¶
Added¶
- Unified 3-tier tool permission model (
ask/auto/allow-all) across SDK, CLI, and Web/Gateway, centralized inagentica/agent/permissions.py.askexposes only read-only tools;autoallows reads everywhere + writes restricted towork_dir(sandbox-enforced);allow-allis unrestricted.ToolConfig.permission_modecarries the mode;Agent.set_permission_mode()flips it at runtime without rebuilding the agent. CLI--permissionsand/permissionscommand, Gateway per-session approval mode, and the SDK constructor all share one source of truth. Removed legacyyolo/full/strictnaming. - Claude-over-OpenAI-compatible
<invoke>tool-call compatibility inOpenAIChat: when a Claude model is reached through an OpenAI-compatible proxy that leaksantml:invokeblocks into text content (instead of structuredtool_calls), the model layer now buffers the turn, parses the XML, and rewrites it into standard OpenAI function calls — so the tool actually executes and neither the leaked XML nor stray preamble (e.g.course) enters assistant history or the CLI. Native Anthropic (model_provider="anthropic") and standard OpenAI/DeepSeek/Qwen paths are unchanged.
Changed¶
- Subagents are read-only by design. All built-in subagent types (
explore/research/code) now denywrite_file/edit_file/multi_edit_file/execute; thetasktool's defaultsubagent_typechanged fromcodetoexplore, and its system prompt states subagents are read-only and the main agent does all edits. This fixes the root cause of "the LLM delegated my query to atasksubagent and the cheap auxiliary model wrote garbage code" — subagents run on the auxiliary model and can no longer edit. User-registered custom subagents are untouched. edit_file/multi_edit_filefreshness checks are advisory tips, not hard blocks. Stale/unread/externally-modified files now produce afreshness_tipappended to the result (or included in aString not founderror) instead of rejecting the edit. Sensitive-path writes still hard-fail. File version tracking is lazy (content hash only when mtime+size match) and_record_file_readhashes in-memory content to avoid re-reading just-written files.- Evicted
read_fileresults are surfaced to the LLM. When compression / context-overflow evicts aread_filetool result, a[Context maintenance]user message is appended naming the affected paths and advising a re-read before editing — so the agent no longer silently edits from stale memory.
Fixed¶
- CLI
ask_user_questionfreeze. A callback-lessAskUserQuestionToolcould fall back to bareinput()and deadlock against prompt_toolkit's stdin ownership. Added a process-wide default-callback registry (set_default_ask_user_question_callback) the TUI registers at startup, a watchdog that aborts/re-arms the prompt if the agent's turn ends or the request is overwritten, a_cprintfreeze while an ask prompt is active, and aSIGQUIT(Ctrl+\) hard-escape that callsos._exit(1).
[1.4.8] - 2026-07-07¶
Fixed¶
- TaskAnchor no longer leaks
agent.run(message)'s first message into the system prompt every turn.TaskAnchorgains asource: Literal["message", "goal"] = "message"field that gatesto_prompt_block(). Only explicit goal entry points —Agent.run_goal(), CLI/goal, and an active session-log goal — producesource="goal"anchors that render as## Original Task. Ordinaryagent.run(message)producessource="message"anchors that are still used as the retrieval query but stay out of the system prompt. This restores pre-1.4.0 prompt behavior for plainagent.run()callers (e.g. private chat seed, workflow handoff, session resume) where the "first message" is a transcript / replay / dump and pinning it system-wide was a bug. Callers that need long-task drift defense should useAgent.run_goal()or setagent.task_anchor = TaskAnchor(..., source="goal")explicitly.
Changed¶
- Claude
max_tokensresolution (ported from hermes-agent'santhropic_adapter.py): - Default changed from
max_tokens: int = 8192tomax_tokens: Optional[int] = None. WhenNone, a per-model output ceiling is looked up from_ANTHROPIC_OUTPUT_LIMITS(Opus 4.6/4.7 → 128K, Sonnet 4.5/4.6 → 64K, 3.5 Sonnet → 8192, etc.). Previously every model was capped at 8K which starved thinking-enabled models (thinking tokens count toward the limit). - Resolved cap is clamped to
max(context_window - 1, 1)for small custom endpoints whose context window is smaller than the model's native output ceiling. No-op for full-size native models. - Positive-finite guard rejects locally:
max_tokens=0 / -1 / 0.5 / NaN / Trueno longer leak to the API and 400 — they fall back to the model ceiling. - Claude auto-recovery from "max_tokens too large given prompt":
Claude.invoke()andinvoke_stream()now parse the API error message foravailable_tokens: Nand retry once withmax_tokens = N - 64(safety margin). Prompt-too-long errors are NOT touched — that path still flows through_learn_context_limit_from_error. New module:agentica/model/anthropic/_max_tokens.pywithresolve_anthropic_messages_max_tokens+parse_available_output_tokens_from_error(28 unit tests).
Removed (Breaking)¶
agentica.model.providersmodule deleted (ProviderConfig,create_provider,list_providers,register_provider,PROVIDER_REGISTRY). The registry indirection had a single concrete output (OpenAILike(**config)) so every OpenAI-compatible factory now directly constructsOpenAIChatwith hardcodedbase_url/api_key_env/default_model/context_window.agentica.OpenAILikedeleted. Was a 22-line subclass ofOpenAIChatwhose only behavior was a placeholder-api_keywarning. UseOpenAIChat(id=..., api_key=..., base_url=...)for custom OpenAI-compatible endpoints.agentica.model.openai.likedeleted.AzureOpenAIChatnow subclassesOpenAIChatdirectly.
Changed (Breaking)¶
- Each
XxxChatis now a thin top-level factory inagentica/__init__.py(e.g.DeepSeekChat,ZhipuAIChat,QwenChat,ArkChat, …). Added 5 previously-only-by-slug factories:NvidiaChat,SambanovaChat,OpenRouterChat,FireworksChat,InternLMChat. - New
agentica.PROVIDER_FACTORIES: dict[str, Callable]exposes slug → factory dispatch for gateway / multi-tenant code (replacesPROVIDER_REGISTRYlookups). agentica.model.defaults.create_default_model()now uses an inline env-var table +PROVIDER_FACTORIES.agentica.gateway.services.model_factory.create_model()dispatches viaPROVIDER_FACTORIESinstead ofcreate_provider.
Migration¶
# Before
from agentica.model.providers import create_provider
model = create_provider("deepseek", id="deepseek-v4-pro", api_key="sk-...")
# After
from agentica import DeepSeekChat
model = DeepSeekChat(id="deepseek-v4-pro", api_key="sk-...")
# Custom OpenAI-compatible endpoint
# Before:
from agentica import OpenAILike
model = OpenAILike(id="my-model", api_key="sk-...", base_url="https://...")
# After:
from agentica import OpenAIChat
model = OpenAIChat(id="my-model", api_key="sk-...", base_url="https://...")
Added¶
- Standing-goal loop judge hardening (hermes-validated + beyond):
- Tool-call summary fed to judge:
Agent.run_goal()extracts(tool_name, is_error)pairs from each turn'sRunResponse.tool_callsand passes them tojudge_goal. Judge prompt now includes aTools used this turn: edit_file, run_pytest(error), lsline so it can distinguish "answered with no tools" from "actually did work". Zero extra LLM calls — names + flags only. New optionaltool_callsparam onGoalManager.evaluate_after_turn()andjudge_goal(). - Tool-stuck auto-pause:
GoalState.consecutive_tool_failurescounts consecutive turns where every tool call errored. AfterMAX_CONSECUTIVE_TOOL_FAILURES = 3the loop auto-pauses withpaused_reason="tool-stuck". Any successful tool call resets; turns with no tool calls do NOT reset (a "just thinking while stuck" turn shouldn't get a free pass). - Subgoal "find evidence" rule: when subgoals are present, judge prompt now demands concrete evidence for each criterion (file excerpt / command output / result value) and explicitly rejects vague summaries like "all requirements met". Borrowed from hermes-agent's hard-won production prompt.
- JSON parsing accepts weak-model output:
_parse_judge_responsenow coerces"yes","true","1","done","y"strings and numeric1todone=true(small chat models and some reasoning models don't always emit JSON booleans). - Static prompts lifted to
agentica/prompts/base/md/:goal_judge.md(judge system prompt) andgoal_continuation.md(continuation template) now live alongsidesoul.md/heartbeat.mdfor consistency. New moduleagentica/prompts/base/goal.pyexposesGOAL_JUDGE_SYSTEM_PROMPT,GOAL_CONTINUATION_PROMPT_TEMPLATE, andrender_goal_continuation_prompt(). The dynamic per-turn user prompt stays ingoals.py(it's conditional logic, not a static template). -
Reasoning-judge guidance documented, no magic in code: judge models that need a large output budget (DeepSeek-Reasoner, o-series, qwq) must be constructed with
max_completion_tokensset explicitly by the caller. The prior in-place mutation helper_ensure_judge_output_budgetwas removed — it was opaque, surprising, and mutated user-owned state. Seedocs/advanced/goals.md"Reasoning judge 的特别注意" for the recipe. -
Standing-goal loop P0 + P1 (S + A tiers):
- Ergonomic SDK surface on
Agent:Agent.run_goal(objective, *, turn_budget=..., token_budget=..., wall_clock_budget_sec=..., attach_goal_tool=True, event_callback=...) -> GoalRunResult— one-liner that drives the whole loop. Replaces the previous low-levelGoalManager(agent._session_log, judge_model=...)+ hand-written driver loop.Agent.get_goal_manager(...)for power users who want to drive turns by hand without touchingSessionLog.Agent.enable_goal_tool()attachesGoalTool.update_goalso the model can self-markcomplete/paused.Agent._session_logandAgent.goal_managerare now formally declared dataclass fields (nogetattrspeculation).- New
agentica.goals.GoalRunResult(status, reason, run_response, goal, turns_used)withresponse_contentconvenience property.
Runner._run_implearly-loads any persisted activeGoalStatefromSessionLogand bindsTaskAnchorto the goal objective — SDK paths now get goal-aware retrieval automatically, not just the CLI.GoalStategainstoken_budget/tokens_used/wall_clock_budget_sec/wall_clock_used_secand a newbudget_limitedstatus (semantically distinct frompaused). Hard budget caps take precedence over tool short-circuit and judge.agentica.tools.goal_tool.GoalTool.update_goal(status, reason): receive-only model tool letting the agent self-markcompleteorpaused(cannot rewrite the objective). CLI auto-attaches on/goalset and detaches on goal termination.RunEventType.goal_set / goal_continuing / goal_completed / goal_pausedevents emitted through an optionalGoalManager.event_callback.- New example
examples/cli/03_goal_loop_demo.py: 4-scenario SDK tutorial (run_goal()one-liner / budgets / event_callback / manual loop) against a real LLM.
Changed¶
GoalManager.evaluate_after_turnnow charges turn counters (turns_used,tokens_used,wall_clock_used_sec) BEFORE any short-circuit branch so per-turn cost is always tracked, even when a tool ends the loop. Decision priority is now: budget cap > tool signal > judge.GoalRunResultfield renamedfinal_response→run_response(typedOptional[RunResponse], was untypedAny) and the convenience propertyfinal_text→response_content, to align with Agentica's existingAgent.run_response/RunResponse.contentterminology.final_*was an LLM-style modifier that didn't add information.agentica.goals.DEFAULT_TURN_BUDGETbumped 20 → 100. Rationale: withtoken_budgetandwall_clock_budget_secnow acting as the real hard caps,turn_budgetis the safety-net against runaway loops; aggressive values (20–50) tripped accidentally on real coding workflows. Token / wall-clock budgets still bound actual cost, so a loose default is safe.
Changed¶
- Top-level lazy imports (e.g.
from agentica import Knowledge,Claude,SqliteDb,Swarm, ...) no longer emitDeprecationWarning. They are now treated as stable v1.x public API alongside the sub-module paths. TheDEPRECATED_TOP_LEVELregistry has been removed; the planned v2.0 forced migration is dropped. SearchSerperTool: fix misuse oflogger.warning(..., DeprecationWarning)for theserper_api_keyalias (the extra arg was silently ignored).
[1.4.5] - 2026-05-13¶
Fixed¶
search_memorynow falls back to recent long-term memories when keyword search has no high-confidence matches.- Langfuse tracing now preserves Agent
user_idandsession_idon both root traces and OpenAI wrapper metadata.
[1.4.2] - 2026-05-10¶
Fixed¶
- Default model resolution now preserves OpenAI priority when
OPENAI_API_KEYis configured, falls back to Anthropic whenANTHROPIC_API_KEYis configured, and then checks OpenAI-compatible provider keys. - Agent-owned LLM tools now reuse the parent agent model before resolving a fallback provider, avoiding accidental OpenAI usage when another main provider is configured.
- Experience capture and skill upgrade LLM calls continue to follow the agent auxiliary model or main model instead of creating a separate provider.
[1.4.0] - 2026-04-23¶
Added — Gateway IM Channels¶
agentica.gateway.channels.QQChannel: 接入 QQ 开放平台机器人(qq-botpyWebSocket,C2C 私聊 + 群 @ 消息),自动缓存最新msg_id用于回包;新增 extrasagentica[qq]agentica.gateway.channels.WeComChannel: 接入企业微信智能机器人(wecom_aibot_sdkWSClient),按chat_id缓存入站frame用于reply_stream;新增 extrasagentica[wecom]agentica.gateway.channels.DingTalkChannel: 接入钉钉机器人(dingtalk-streamStream 长连接 + HTTP 回包),自动管理accessToken缓存与续期;区分 1-to-1(channel_id=staffId)与群(channel_id="group:<openConversationId>");新增 extrasagentica[dingtalk]agentica.gateway.channels.WeChatChannel: 接入个人微信(内联WxBotClient走 ilinkai 私有 HTTP 长轮询,QR 扫码登录 + token 持久化),后台线程跑阻塞 loop,跨线程call_soon_threadsafe派发到主事件循环;新增 extrasagentica[wechat]ChannelType: 扩展QQ与WECOM两个枚举值Settings: 新增qq_*/wecom_*/dingtalk_*/wechat_*字段及对应环境变量加载(QQ_APP_ID/WECOM_BOT_ID/DINGTALK_CLIENT_ID/WECHAT_TOKEN_FILE…)docs/advanced/gateway.md: 新增 Gateway 完整文档,覆盖架构图、所有 IM 渠道的环境变量配置、HTTP API、自定义渠道、故障排查- 34 个新单测:
tests/test_gateway_channel_{qq,wecom,dingtalk,wechat}.py,全部 mock 各家 SDK,无外部依赖
Changed¶
agentica/gateway/main.py::_setup_channels():按需注册 4 个新渠道,凡是缺关键凭据自动跳过并打日志agentica/gateway/channels/__init__.py:re-export 新增的 4 个 Channel 类- 版本号:
1.3.6rc1→1.4.0(按 SemVer:新增公共 Channel 类 → minor bump)
Notes¶
- 所有新渠道都遵循"懒加载 SDK + 缺失依赖时抛清晰
ImportError"的现有模式 - WeChat 渠道走的是非公开私有协议(ilinkai),仅推荐个人 / 内部场景使用
Added (Stage 2 + Stage 3)¶
_DEPRECATED_TOP_LEVELmapping inagentica/__init__.py: 35+ symbols flagged for v2.0 migration- DeprecationWarning emitted when accessing top-level deprecated paths like
from agentica import Knowledge/Claude/VectorDb/SqliteDb/Swarmetc., guiding users to explicit sub-module imports agentica.workspacepackage: Split monolithicworkspace.py(1402 lines) into a package structure for incremental modularization
Changed (Stage 2 + Stage 3)¶
agentica/__init__.pydocstring: rewritten with v1.3.6+ recommended import style guide + backward-compat noteagentica/workspace.py→agentica/workspace/base.py(file move, zero business code change)agentica/workspace/__init__.pyre-exportsWorkspace,WorkspaceConfig, plus module-level constants for test mockingtests/test_workspace.py: updated 3 patch paths fromagentica.workspace.AGENTICA_HOME→agentica.workspace.base.AGENTICA_HOME(reflects new package structure)tests/test_skill_lazy_loading.py: updatedimportlib.reloadtarget fromagentica.workspace→agentica.workspace.base
Compatibility¶
- 100% backward compatible: all top-level imports still work; only emit DeprecationWarning
from agentica.workspace import Workspacepath is unchanged for all 11 internal usages and external users
[1.3.6] - 2026-04-18 (sdk-dev branch)¶
Added¶
pyproject.toml: 新打包配置,对标 agno 细粒度 extras 风格 + 超级组合 extrasdocs/API.md: Public API Tier 1/2/3 稳定度合约- 20+ 细粒度 extras:
agentica[rag]/[qdrant]/[chroma]/[gateway]/[mcp]/[acp]/[arxiv]/[yfinance]/[browser]/[ddg]/[exa]等 - 8 个超级组合 extras:
[tools-search]/[tools-research]/[tools-finance]/[tools-media]/[tools-browser]/[vectordbs]/[storage]/[models]/[tracing]/[full] agentica.model.anthropic.Claude: Anthropic 直接默认装(核心 provider)- 友好
ImportError提示:未安装对应 extras 时,agentica.gateway/agentica.mcp/agentica.acp/agentica.db.SqliteDb等会抛出带pip install agentica[xxx]命令提示的清晰错误
Changed¶
- 依赖瘦身:默认
install_requires从 23 个 → 19 个(M1-核心 A+ 方案;瘦身 17%) - 默认产品化能力保留:Workspace / CLI / DeepAgent 内置工具(web_search, fetch_url, file, shell, todo, task)全部默认可用
- 核心新增 6 个:
beautifulsoup4/lxml/markdownify/requests/puremagic/tqdm,确保agenticaCLI 和 DeepAgent 默认工作 setup.py→pyproject.toml(PEP 621 标准)requirements.txt:更新为核心 19 个依赖的参考清单,实际以pyproject.toml为准agentica/__init__.pylazy loading:增加_LAZY_ATTR_OVERRIDES修复LiteLLM/DeepSeek/Moonshot等 alias 的延迟加载(pre-existing bug)
Fixed¶
test_lazy_loading.py::test_all_public_names_accessible:修正对缺失 extras 时的友好 ImportError 处理,不再误报- CLI 默认可用性:之前一度把
bs4移到[crawl]extras 导致agentica --querycrash;本版通过把 6 个工具依赖纳入核心保证 CLI / DeepAgent 默认开箱即用
Removed¶
- 无(1.3.6 是内部收敛 + 打包优化,不删除 Public API)
Migration Notes¶
- 向后兼容 100%:装
pip install agentica即可获得 v1.3.5 的"开箱即用 DeepAgent + CLI"完整体验 pip install agentica[full]等价于 v1.3.5 完整能力(含 RAG / Gateway / MCP / 40+ 第三方工具)- 仍使用
setup.py等旧安装方式的场景需迁移到pyproject.toml(PEP 621 自 Python 3.10 标准)
[1.3.5]¶
Added¶
MemoryTypeenum — four-type memory classification (user,feedback,project,reference) for workspace memory entriesMemoryEntryPydantic model — typed memory entry withname,description,memory_type,file_path,contentfieldsWorkspace.write_memory_entry()— write a typed memory as an individual.mdfile with YAML frontmatter, auto-updatesMEMORY.mdindexWorkspace.get_relevant_memories()— relevance-based recall: parsesMEMORY.mdindex, scores entries by keyword overlap against current query, loads only top-k content files; supportsalready_surfacedset for session-level dedupWorkspace._update_memory_index()— enforces MEMORY.md hard limits (200 lines / 25KB); FIFO eviction of oldest entriesWorkspace._score_memory_entries()— hybrid keyword scoring (word-level + char 2-gram) supporting both English and CJK queriesWorkspace._strip_frontmatter()— strips YAML frontmatter before injecting memory content into system prompt- Memory drift-defense note — appended to all injected memory to guard against stale file/function references
WorkspaceMemoryConfig.max_memory_entries— max memory entries to inject per run (default: 5); replaces removedmemory_daysAgent._surfaced_memories— session-level set tracking surfaced memory filenames, prevents cross-turn re-injection of same entriesAgent.get_workspace_memory_prompt(query)— now acceptsqueryparameter, passes it toget_relevant_memories()for query-aware recallCompressionManager.auto_compact(working_memory=...)— reusesWorkingMemory.summarydirectly when available, skipping LLM summarization call; faster and cheaper with no information lossSandboxConfig.allowed_commands— optional command whitelist forexecutetool (prefix-matched on first token)Agent._runningflag — concurrent reuse of the same Agent instance now logs a warningWorkingMemory.max_messages— soft FIFO eviction limit (default: 200) to prevent unbounded memory growthMessage.rolefield validator — rejects invalid roles at construction time (system,user,assistant,toolonly)
Changed¶
Workspace.get_memory_prompt(days=N)removed — replaced byget_relevant_memories(query, limit, already_surfaced); full-dump memory injection is no longer the default behaviorWorkspaceMemoryConfig.memory_daysremoved — no longer needed; relevance-based recall replaces time-window-based loading- System prompt memory zone: both
_build_default_system_messageand_build_enhanced_system_messagenow extractself.run_inputas query and pass it toget_workspace_memory_prompt(query=...)
Fixed¶
update_model()now clearsmodel.functionsandmodel.toolsbefore each run, preventing tool accumulation on reused Agent instancesOpenAIChat.response()raisesValueErrorinstead ofIndexErrorwhenchoicesis emptyAnthropicChat.response()raisesValueErrorinstead of crashing whencontentis emptyFunctionCall.execute()generator result concatenation now usesstr(item)to preventTypeErroron non-string generatorsOpenAILikewarns at construction time whenapi_keyis still the placeholder"not-provided"_load_mcp_toolsremoved redundantif/elsebranch (both branches were identical)task()recursion depth capped at 5 levels via_task_depthcontext propagation
Added (Tests)¶
tests/test_workspace.py::test_get_memory_promptupdated to coverwrite_memory_entry()+get_relevant_memories()with and without querytests/test_hooks.py— AgentHooks, RunHooks,_CompositeRunHooks, ConversationArchiveHookstests/test_runner.py— empty message guard, concurrent warning, run_timeout, structured output fallbacktests/test_swarm.py— parallel mode, partial failure, duplicate name detectiontests/test_model_validation.py— empty choices, usage=None, Message role validator, structured output fallback
[1.3.2] — 2026-03-17¶
Added¶
Swarm— multi-agent parallel autonomous collaboration (agentica/swarm.py)ConversationArchiveHooks— auto-archives conversations to workspace after each run_CompositeRunHooks— internal wrapper for composing multipleRunHooksinstancesRunConfig.enabled_tools/enabled_skills— per-run tool/skill whitelistingAgent.disable_tool()/enable_tool()/disable_skill()/enable_skill()— agent-level runtime controlAgent._load_runtime_config()— loads tool/skill enable/disable from.agentica/runtime_config.yamlSandboxConfig.blocked_commands— command-level blacklist forexecutetoolexamples/agent_patterns/08_swarm.py— Swarm usage exampleexamples/agent_patterns/09_runtime_config.py— Runtime config exampleexamples/agent_patterns/10_subagent_demo.py— SubAgent example
Changed¶
deep_agent.pyrenamed totools/buildin_tools.py;DeepAgentnow usesBuiltinFileTool,BuiltinExecuteTool,BuiltinWebSearchTooletc.Runner._run_impl— removed duplicate auto-archive logic; archive is now handled exclusively byConversationArchiveHooks
[1.3.1] — 2026-03 (v3 post-merge cleanup)¶
Added¶
WebSearchAgentwith search enhancement modules (search/orchestrator.py,query_decomposer.py,evidence_store.py,answer_verifier.py)- Extended thinking support for Claude and KimiChat models
- Kimi provider integration (
model/kimi/)
Fixed¶
- Preserve tool call messages in multi-turn conversation history
- Deduplicate Model layer, unify
RunConfigsignatures
[1.3.0] — 2026-03 (v3 architecture refactor)¶
Changed (Breaking — internal architecture, public API preserved)¶
- Phase 1: Removed 19 thin provider directories; unified via
model/providers.pyregistry factory - Phase 2: Converted
Modelhierarchy from PydanticBaseModelto@dataclass - Phase 3: Async interface consistency + structured output for all providers
- Phase 4: Added
@tooldecorator and global tool registry (tools/registry.py) - Phase 5: Extracted
RunnerfromRunnerMixin;Agentnow delegates execution viaself._runner - Phase 6: Unified guardrails with
core.pyabstraction layer - Phase 7: Simplified
__init__.pylazy loading - Phase 8: 35 new v3 tests
Added¶
AgentHooks,RunHookslifecycle hooks systemRunConfigper-run configuration overridesSubAgentfor isolated ephemeral task delegation- Skill system (
skills/) — Markdown+YAML frontmatter skill injection - ACP server for IDE integration (Zed, JetBrains)
[1.2.x] and earlier¶
See git log for historical changes prior to the v3 refactor.