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_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 正是每轮都会变的东西)。
- Layer 0 按「取回得回来吗」决定形态。超大工具结果以前一律落盘 + 在上下文里换成文件路径。可 CLI 之外没人能打开这个路径——一个只挂业务工具(无
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 的模型了,于是状态栏出现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)、
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 启动行同样全文