games-development-ai/cheap-worker/planning_team_tools_patch.py
lili c372837762
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(deps): 升级 AgentScope 2.0.2→2.0.3(代码+文档,创始人 2026-07-06 拍板)
不为某个新特性,而是没道理把底座停在带并行 tool-result 缺陷的补丁版;2.0.3
是同线相邻补丁版,升级面窄、收益确定。承 tech-decisions §11-13「护城河进洋葱、
框架件谨慎审计」线。

代码:两 worker requirements 钉 2.0.3+溯源注;4 个 cheap-worker patch 的版本
guard 更 2.0.3——主控逐一源码核实 2.0.3 锚点仍成立:compress_context@_agent.py:259
未漂移 / _parse_stream_response@_openai_chat/_model.py:297·index 分桶:417 未漂移 /
list_tools@_local_workspace.py:660 / get_toolkit 装配点迁 _chat.py:354(patch 替
绑定名不依赖行号);patch 头 pin 声明同步 2.0.3+复核标记,根因 2.0.2 期实证保留。

文档:current-truth 面扫除(agentscope-2.0-facts 改掉一处「2.0.3 是笔误」错陈述+
行号漂移开源码更新 594→622/136→162/33→42/857→884/875→902、tech-decisions §15
升级决策散文、运行时 SoT 当前选型句、活代码注释、tier2 README 死链修);留痕层
(plans/HANDOFF/patch 根因分析/ADR 历史核验句)按两层纪律不动。

兼容三证:源码逐处 diff(RedisMessageBus/OpenAI formatter 零变化、Anthropic
formatter 仅并行 tool-result bug 修、ModelCallEndEvent 无变化、MiddlewareBase
加性 get_middleware_key)/ cheap 393+tier2 140+patch 20 测试全绿 / dev 两 venv
升级后 3 服务健康。收敛真跑:cheap 便宜档 ReAct 全链路 356 trace 零升级异常+
真出游戏过九门 9/9;tier2 Anthropic 工具循环跑满 40 轮写 15 文件 gen.log/trace
零升级异常(decision=fix 属 tier2 生成质量、非升级回归)。

2.0.3 新增原生 ReplyBudgetControlMiddleware(历史反复核验 2.0.2 不存在的类)/
tool 级洋葱/RAG/mem0,列而不迁(自建 ¥ 两段式 fail-closed 硬闸是护城河,原生
token 软控替不了),follow-up 见 §15。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 02:06:14 -07:00

141 lines
9.1 KiB
Python

"""planning_team_tools_patch.py — 便宜档 Service 收窄 agent 工具面:关 Planning / Team / Schedule 三组框架工具。
【坐实根因(2026-07-04 工单 h;接 ws_builtin_tools_patch 的收窄线)】agentscope 2.0.2 Service 路的工具面装配
在 app/_service/_toolkit.py:get_toolkit 里,除了 workspace 内建(已由 ws_builtin_tools_patch 关)之外,还**无条件**
向 agent 工具面并入三组与便宜档无关的框架工具:
① Planning —— TaskCreate / TaskList / TaskGet / TaskUpdate(:109-110 明文 "Planning tools — always on");
② Team —— agent_record.source=="team" 时挂 TeamSay,否则挂 TeamCreate / AgentCreate / TeamSay / TeamDelete
全套(:144-168),便宜档是单机单游戏、无团队/无子 agent 编排;
③ Schedule —— session 配了 chat_model_config 时,挂一个名为 "schedule_tools" 的 ToolGroup(cron 定时新起 chat,
:117-142),便宜档不做定时任务。
后果:便宜档单机单游戏场景用不到这三组,纯属**工具面噪声**——80011 生产实录里 12+ 工具语义重叠正是 M3 flail
(反复空转/选错工具)的根因之一;工具面越窄,混乱税越低(护城河 I1「六工具白名单」的邻域治理)。CLI 路
(cheap_run)从来只有六工具;Service 归并(配置控制面阶段一②)经框架 get_toolkit 把这三组带了进来。
【修法】cheap Service 进程内 monkeypatch,**包裹 get_toolkit 的唯一运行时调用者**——
app/_service/_chat.py:37 以 `from ._toolkit import get_toolkit` 把 get_toolkit 绑成 _chat 模块的本地名,:354 是
全仓唯一装配调用点(2.0.3 复核;旧 2.0.2 为 :33 / :240,patch 替绑定名、不依赖行号);故替换 `_chat.get_toolkit`
(替源模块 _toolkit 的属性对已绑定的 _chat 无效)。包装体调原
函数拿到装配好的 Toolkit 后,**重建一个干净 Toolkit**:从 basic 组按 .name 剔除 Planning+Team 工具、丢掉整个
schedule_tools 组;workspace skills / MCP / 其余工具(便宜档六工具、后台控制 ToolStop)一律原样保留。重建而非
原地改,是为避开半构造对象的陈旧缓存(Toolkit.__init__ 会据 tool_groups 建 meta 工具 ResetTools)。作用域
正确性:本进程(cheap_service_app)只跑便宜档 agent,进程内全局补丁 = 恰好全体便宜档 agent;tier2 服务是独立
进程,不受影响。
【钉 agentscope==2.0.3;2026-07-06 自 2.0.2 升级复核】运行时 monkeypatch,绝不改 venv/site-packages 文件本体。
依赖三个契约,2.0.3 已逐一复核仍成立:① get_toolkit 由 _chat 模块 `from ._toolkit import get_toolkit` 本地绑定
(_chat.py:37)、且 _chat.py:354 是唯一装配调用者;② Planning/Team/Schedule 工具的 .name 为固定类名(见下方三张
名单),且 Schedule 四工具仍装在名为 "schedule_tools" 的 ToolGroup 里;③ Toolkit.tool_groups[0] 为 "basic" 组、
其 .tools/.skills_or_loaders/.mcps 可用于重建。test_planning_team_tools_patch 全绿。上方根因是 2.0.2 期工单 h
的诊断实证,原样保留。此后再升级仍按此复核这三点并重跑该单测(版本漂移时 apply 响亮 warning、但仍应用——
不应用等于把这三组噪声放回便宜档工具面)。
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: # pragma: no cover —— 仅类型提示,运行时不 import(agentscope.tool 顶层牵重依赖)
from agentscope.tool import Toolkit
# ── 便宜档要关掉的框架工具面(按 .name 精确匹配;钉 2.0.3 固定类名契约)──
# Planning:框架 TODO 计划板(get_toolkit "always on"),便宜档不需要 agent 自管任务板。
_PLANNING_TOOL_NAMES = frozenset({"TaskCreate", "TaskList", "TaskGet", "TaskUpdate"})
# Team:多 agent 团队 / 子 agent 编排,便宜档单机单游戏无团队。
_TEAM_TOOL_NAMES = frozenset({"TeamCreate", "AgentCreate", "TeamSay", "TeamDelete"})
# Schedule:cron 定时新起 chat 的四工具,正常装在名为 schedule_tools 的独立 ToolGroup(见 _SCHEDULE_GROUP_NAME)。
_SCHEDULE_TOOL_NAMES = frozenset({"ScheduleCreate", "ScheduleView", "ScheduleDelete", "ScheduleList"})
# Schedule 组名:整组移除即可;下方仍把 schedule 工具名并进黑名单,兜「未来某路径把它落进别的组」的漏。
_SCHEDULE_GROUP_NAME = "schedule_tools"
# 统一黑名单(按名从任意组剔除):Planning + Team + Schedule 三组工具名。
_BLOCKED_TOOL_NAMES = _PLANNING_TOOL_NAMES | _TEAM_TOOL_NAMES | _SCHEDULE_TOOL_NAMES
def _filter_toolkit(toolkit: "Toolkit") -> "Toolkit":
"""把框架 get_toolkit 产出的 Toolkit 重建成便宜档干净版:去 Planning/Team 工具 + 去 schedule_tools 组。
只读 Toolkit 的公开结构(tool_groups / group.name / group.tools / group.skills_or_loaders / group.mcps),
据此重建一个新 Toolkit(basic 组按名剔黑名单、其余组丢 schedule_tools、组内再兜底剔黑名单名),
skills / MCP 原样透传。任何结构异常(无 tool_groups 等)大声告警并**原样返回**——宁可多留几件工具,
也不让 Service 装配崩(收窄是降噪、不是关键路径)。
"""
from agentscope.tool import Toolkit # noqa: PLC0415 —— 惰性 import(顶层不牵重依赖,保 6c6g/无 [service] 环境可 import 本模块)
groups = list(getattr(toolkit, "tool_groups", None) or [])
if not groups:
print("[planning-team-patch] WARNING: Toolkit 无 tool_groups,2.0.3 结构可能已变——原样返回、不收窄。", flush=True)
return toolkit
def _keep(tool) -> bool: # noqa: ANN001 —— 只看 .name,替身/真工具通用
return getattr(tool, "name", None) not in _BLOCKED_TOOL_NAMES
# basic 组恒在 index 0(Toolkit.__init__ 保证);稳妥按名找,找不到再退 index 0。
basic = next((g for g in groups if getattr(g, "name", None) == "basic"), groups[0])
kept_basic_tools = [t for t in getattr(basic, "tools", None) or [] if _keep(t)]
# 其余组:丢掉整个 schedule_tools 组;别的组(若有)保留,组内再兜底剔黑名单名(正常不会命中)。
extra_groups = []
for g in groups:
if g is basic or getattr(g, "name", None) == _SCHEDULE_GROUP_NAME:
continue
g.tools = [t for t in getattr(g, "tools", None) or [] if _keep(t)]
extra_groups.append(g)
# 重建:basic 组的 skills/MCP 原样带回(工单不动 skills/MCP 装载);其余组带回。
return Toolkit(
tools=kept_basic_tools,
skills_or_loaders=list(getattr(basic, "skills_or_loaders", None) or []),
mcps=list(getattr(basic, "mcps", None) or []),
tool_groups=extra_groups,
)
def _install_get_toolkit_wrapper(chat_mod) -> None: # noqa: ANN001 —— 传 _chat 模块(或测试用假模块)
"""把 chat 模块命名空间里的 get_toolkit 换成「调原函数 → _filter_toolkit」的包装(幂等)。
_chat.py:33 `from ._toolkit import get_toolkit` 使 get_toolkit 是 _chat 模块的**本地名**,:240 按该名调用;
故必须替换 chat_mod.get_toolkit(替源模块 _toolkit 的属性对已绑定的 _chat 无效)。包装函数带
`_cheap_ptl_patched` 标记位:重复 install 短路、不再包一层(与 ws_builtin_tools_patch 同款幂等纪律)。
"""
orig = chat_mod.get_toolkit
if getattr(orig, "_cheap_ptl_patched", False):
return # 幂等:已包装,直接返回
async def _wrapped(*args, **kwargs):
# 与 2.0.3 get_toolkit 同签名(async;_chat.py:354 全 kwargs 调用,这里 *args/**kwargs 透传兜位)。
toolkit = await orig(*args, **kwargs)
return _filter_toolkit(toolkit)
_wrapped._cheap_ptl_patched = True # type: ignore[attr-defined] —— 幂等标记
_wrapped.__name__ = "get_toolkit__cheap_ptl" # 便于日志/调试辨识
chat_mod.get_toolkit = _wrapped
def apply_planning_team_tools_patch() -> None:
"""便宜档 Service 进程内关 Planning/Team/Schedule 三组框架工具(幂等,钉 2.0.3)。
惰性 import _chat:它经 agentscope.app 牵 apscheduler 等 [service] extra 重依赖,顶层 import 会污染无该 extra
的环境(如本机 6c6g / 单测机);故只在 apply(运行时、Service 进程内)时 import。
"""
import agentscope # noqa: PLC0415
from agentscope.app._service import _chat as _chat_mod # noqa: PLC0415 —— 惰性:牵 apscheduler,只在运行时 import
ver = getattr(agentscope, "__version__", "?")
if ver != "2.0.3":
# 版本漂移:响亮告警但仍应用(不应用 = 三组噪声放回便宜档工具面,与收窄意图相反)。
print(
f"[planning-team-patch] WARNING: agentscope 版本 {ver} != 钉住的 2.0.3,"
"get_toolkit 装配点 / 工具类名契约可能已变,升级复核未做——补丁仍应用,请尽快复核。",
flush=True,
)
already = getattr(_chat_mod.get_toolkit, "_cheap_ptl_patched", False)
_install_get_toolkit_wrapper(_chat_mod)
if not already:
print(
"[planning-team-patch] 已关 Planning(Task*)/Team(Team*·AgentCreate)/Schedule(schedule_tools 组)"
"——便宜档单机单游戏工具面收窄,只留六工具 + 后台控制 + skills/MCP(钉 2.0.3,升级必须复核)。",
flush=True,
)