Skip to content

v1.4.12

Latest

Choose a tag to compare

@shibing624 shibing624 released this 10 Aug 07:44
· 14 commits to main since this release

Agentica v1.4.12

两层上下文压缩、CLI 多会话协作(peer / delegate / task),以及「CLI 一定有的能力」不再默认落到 SDK。

安装: pip install -U agentica==1.4.12

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


[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_windowTOOL_BATCH_BUDGET_RATIO)。作用点也收回到结果产生的地方(Model.run_function_calls),runner 里那次扫全量历史的重复兜底整段删除——那是第二套治理同一件事的阈值,而历史该由 Layer 1 按真实窗口压力管。留在 Model 这一层还顺带解决了 provider 形态问题:这里看到的是打包成 Anthropic content block 之前的普通结果消息,两边都生效。函数改名 enforce_tool_result_budgetenforce_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 调用」,但调度器只 gather concurrency_safe 的函数,而 executeFalse——这个承诺从来没兑现过(和当初 task 的同一个 bug)。模型于是拿 background=True 当并行入口用,导致「到处 background」:那是生命周期开关(命令能否活过这一轮、超时/取消后日志还在不在),不是调度开关。两件事现在分开了,background 的 docstring 明确说要速度请用 parallel_safewait 仍然只服务 background
    并行安全性做成逐次调用声明而不是工具级常量:execute 既跑 pytest tests/a 也跑 git commit,注册时给不出正确答案——填 False 白白串行化互相独立的活,填 True 则让 git add / git commit 这种批次直接竞争。机制是新增的 Function.parallel_argexecute 设为 "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 / AgentDefinitionas_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 把它作为最后一条搜索路径、bundledLOCATION_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/stopwait 工具、完成回执全部直接可用。BackgroundProcess.kindcommand / 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.jsonwork_dir + 原 profile 文本文件的 active_profile);仍在 ~/.agentica/projects/<user>/<slug>/ 下,不进用户 git 工作区。session 的 <id>.meta.json 保持与 .jsonl 并列(粒度/并发不同,不并入)。SessionLog / peers / /config Project Dir / tool-result 路径统一经 project_store.project_base_dir
  • /goal --tokens -1(SDK token_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)、CLI log_file(如 ~/.agentica/logs/20260809-80403.log,附 level)、workspace / memoryMEMORY.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.yamlsettings.resume_cwdsession / 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.yamlenv: 块即可,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,分发层因此不需要每后端的参数适配分支:SearchBochaToolcountSearchExaToolnum_results 改名 max_resultsZhipuWebSearchToolmax_resultsDuckDuckGoTool.duckduckgo_search 补多 query
  • web_search_pro_tool.py / WebSearchProTool 更名 zhipu_web_search_tool.py / ZhipuWebSearchTool(CLI --tools web_search_prozhipu_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_filtersearch_recency_filtercontent_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-searchexa_py 两个可选依赖(pyprojectddg / exa extra 清空为占位):两者都只是 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.pycli/commands/cli/display.pycli/display/cli/interactive.pycli/interactive/runner.pyrunner/_run_implrunner/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_kindagent / 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 与 SDK run_goal() 共用同一条路径
  • Goal 主成本闸改为默认开启的 token_budget=500_000(CLI /goal 与 SDK run_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 路径注入,与 /background agent 任务共用同一套 /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_resultsToolConfigCompressionManager 两处)/ 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_contextauto_compact,外加 prompt_too_long 之后的 reactive),另外两条自己动手缩上下文的路径整个删除。留着它们的代价不是多几行代码,而是同一个决定有第二套阈值,两套必然漂移,且副本总是更差的那个实现。删掉的第一条是 ToolConfig.context_overflow_threshold(连同 Agent._build_pre_tool_hook_overflow_warning_emitted、runner 里两处调用、DeepAgent0.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_based CLI 事件分支
  • Layer 2 现在默认可用。 ToolConfig.compress_tool_results 默认 False,意味着 compression_managerNone,于是默认配置下原生 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_compactauto_compactcompact_boundary 后立刻把 preserved_tail append 进 session log,使立刻 /fork/resume 仍能看到与内存一致的上下文(只写尾巴:摘要那一轮由 load() 从 boundary 自己合成,一起写会让摘要在 resume 后出现两遍)。on_pre_compact 会经 aux 模型 flush 记忆/经验缓冲,所以阈值判断从 auto_compact 内部上提到 runnerCompressionManager.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_result block 不带工具名(只有 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_use block 却在刚被摘要替换掉的 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 懒启动(第一次改文件才 initializecreate_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 的模型了,于是状态栏出现 venus-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)、wikipediaarxivdblphackernewsweathernewspaperyfinance 全部只读 HTTP,sqllist_tables / describe_table 是只读 schema 查询(run_sql_query 仍串行,它可能是 DML/DDL),code 的 AST 分析与 lint、lspgoto_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.gatherrun_in_parallel=False 的自己一批:声明顺序仍然决定短路语义(串行的那个拦下来,排在它后面的根本不会启动),报告出去的也仍是声明在最前面的那个而不是最先跑完的那个。输出 guardrail 保持串行——答案已经生成,第一个拦下就结束,后面的都是白花的钱
  • RAG 检索不再阻塞 async 路径:knowledge.search 从查询 embedding(HTTP)到向量库再到 reranker(HTTP)全程同步,而 get_relevant_docs_from_knowledge 是直接调的——开了 add_references 的会话,每一轮都要在事件循环里干等这一整趟往返。改为 run_in_executorget_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_patchmulti_edit_file 不再注册为内置工具,复杂/多 hunk 编辑走上下文 patch;edit_file 保留为单个短且唯一的 literal 替换工具。edit_fileString 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.pyBuiltinFileTool+path guards+_GLOB/_GREP_TIMEOUT)、execute_tool.pyBuiltinExecuteTool+exit-code helpers+_MAX_WAIT_SECONDS)、__init__.pyget_builtin_tools+re-export 7 个工具类);原 task_state_tools.py/web_tools.py 已在包内。agentica/__init__.pytools/__init__.py 改从 agentica.tools.builtin 取,所有直接引用者(tests/examples/docs/agent/acp/gateway/evaluation)改到新路径,测试 patch 字符串(_GREP_TIMEOUT/shutil.which/asyncio.create_subprocess_exec/terminate_subprocessfile_tool_MAX_WAIT_SECONDSexecute_tool_detect_python_error_hint/_interpret_exit_code/_is_blocked_device/_check_sensitive_write_path→对应子模块)同步更新
  • agent/base.py(2091 行)抽出 goal 闭环到 agent/goal_mixin.pyGoalMixinget_goal_manager/enable_goal_tool/run_goal/run_goal_step),Agent 继承链加 GoalMixinbase.py 降到 1794 行,只留公开 run API 面与薄委托,不再内嵌 goal 闭环。MRO 透明,所有 agent.run_goal() 调用无需改动

docs

  • apply_patch docstring 明确要求 Update/Delete 操作前必须先 read_file,禁止凭记忆构造上下文
  • README News 区重构:旧版本条目折叠进 <details> 区块
  • 终端文档更新 /new 命令别名说明及退出 CLI 后按 session ID 恢复的用法
  • 文档与 README 补充 CLI task / delegate / peer 选型:docs/getting-started/terminal.mddocs/multi-agent/choosing.mddocs/concepts/tools.mddocs/multi-agent/subagent.md;中/英/日 README News + 协作对照表
  • CLI 对 task / delegate(及结果锚点)完整展示任务正文,不再 40/80 字截断;子 agent 启动行同样全文