feat(tokenless): extend AgentScope support - #2553
Conversation
kongche-jbw
left a comment
There was a problem hiding this comment.
Review baseline: aa8b023862bf...bfab7055570a
[P2] 同步新的 AgentScope 支持范围到规范用户指南
docs/user-guide/en/token-saving/tokenless/framework-integration.md:209
仍声明仅支持 >=2.0.5,并在 222-266 行让所有用户调用旧的
register_tools() 和单 middleware App 流程。这个 PR 将依赖下限降到 1.0.11,
而 2.0.0-2.0.2 的 Toolkit 没有 add_tool;按当前规范指南接入会在
TokenlessMiddleware.register_tools() 抛错,1.x 用户也找不到新增的
install() 流程。中文镜像在
docs/user-guide/zh/token-saving/tokenless/framework-integration.md:216
同样过期。建议同步两份指南中的版本矩阵和 1.x/2.x/App 示例,并扩展文档一致性检查,
避免支持范围和公开入口再次漂移。
bfab705 to
a7c579c
Compare
|
@kongche-jbw 该意见正确,已在
验证通过:六档真实依赖 smoke(1.0.11、1.0.21、2.0.0、2.0.1、2.0.3、2.0.6)及中英文文档站 production build。 |
kongche-jbw
left a comment
There was a problem hiding this comment.
Review baseline: c122a0a8f582...a7c579c6f673
[P1] 将 JSON 数值解析限制作为普通文本处理
src/tokenless/python/tokenless/python/anolisa_tokenless/tool_response.py:213
会先对每段工具文本调用 json.loads(),但这里只捕获 JSONDecodeError。
在 CPython 3.11+ 中,4,301 位纯数字工具文本会因整数转换上限抛出
ValueError;该异常会穿透 AgentScope 1.x postprocessor 和 2.x middleware,
让工具调用失败,与 Tokenless 失败时保留原响应的契约不符。
Possible direction: 将这类解析限制视为普通文本并继续 fail-open,补一条通过
compress_text() 处理 4,301 位数字且不抛异常的回归测试。
Forrest-ly
left a comment
There was a problem hiding this comment.
Code Review: APPROVE
Commit: a7c579c · 范围: +1668/−533,20 个文件,代码、测试、文档均已逐行审查。
总体评价
代码质量高。框架无关的压缩策略被提取到 anolisa_tokenless.tool_response(TokenlessConfig + ToolResponseCompressor),AgentScope 集成按大版本拆分为 _v1.py / _v2.py(导入时按已安装版本分发),旧 TokenlessMiddleware 通过 middleware.py shim 保留,2.x 既有调用方兼容。测试覆盖全面:新增 v1 单元测试与 v1 real-wheel smoke、6 环境真实 wheel 矩阵(1.0.11 / 最新 1.0.x / 2.0.0 / 2.0.1 / 2.0.3 / 最新 2.0.x)、文档一致性检查与 policy drift 检查。kongche-jbw 先前的 P2 建议(同步文档版本矩阵并扩展文档一致性检查)也已落实。
独立验证
我对照 AgentScope 上游源码(v1.0.11 / v2.0.0 / v2.0.1 / v2.0.3)逐一核实了代码中的框架 ABI 假设:
- 1.x
Toolkit.call_tool_function是返回异步生成器包装的 coroutine → smoke test 中await toolkit.call_tool_function(...)正确 - 1.x
RegisteredToolFunction.postprocess_func为可变 dataclass 字段,支持 sync/async,返回 None 时保留原响应 →_wrap_postprocessor三种情况处理均正确 register_tool_function(json_schema=...)的格式与_retrieve_schema()的{"type": "function", "function": {...}}一致- 1.x 工具抛异常时 toolkit 生成
Error: {e}前缀文本 →_looks_like_error启发式与之匹配(方向保守、fail-safe) - 2.0.0
ToolBase无call(仅__call__),2.0.3+ 以call为新 override point →hasattr(ToolBase, "call")能力选择正确 - 2.0.0
MiddlewareBase无list_tools,2.0.1+ 有 →app_options()对 2.0.0 的 RuntimeError gating 正确 - 2.0.1
create_app(extra_agent_middlewares=..., extra_agent_tools=...)的 factory 签名(user_id, agent_id, session_id) -> Awaitable[list[...]]与app_options()一致
安全姿态未退化:retrieval 仍限定 marker 可见 + 24 位 hex hash 校验;App 模式按 user/agent/session sha256 派生隔离 data_dir;压缩失败 fail-open。
小建议(P3,不阻塞合并)
_v1.py:compress_text(..., session_id=None)—— 1.x 的 stash/stats 失去 session 维度归属;如后续需要按 session 统计,可考虑允许通过 config 或install()传入 session ID。_v1.pyretrieve 的可见范围是整个 Agent memory,而 2.x 限定为模型可见的 context+summary —— 1.x 范围更宽但安全(memory 是 1.x 的唯一事实来源,文档已写明);建议在代码中加一句注释说明该差异的原因,便于后续维护。pyproject.toml.in的mcp>=1.13,<2pin 作用于所有 AgentScope 版本,但严格来说只有 2.0.0 需要(2.0.0 未 cap MCP);文件内注释已解释,未来移除 2.0.0 支持时可考虑放宽。
CI 提示
review 时该 commit 的 "Test tokenless" CI 仍在 pending(前一 commit 为 12/12 通过),合并前请确认 CI 通过。
Forrest-ly
left a comment
There was a problem hiding this comment.
补充 Review:REQUEST_CHANGES(撤回此前的 APPROVE)
前一条 APPROVE 提交后,复核了 kongche-jbw 在 10:17 提出的 P1,独立验证为真实缺陷,因此改判为 REQUEST_CHANGES。其余审查结论(架构、ABI 适配、测试覆盖、文档)维持正面评价,仅此一处阻塞。
P1 验证:json.loads 整数位数限制导致 fail-open 契约被破坏
本地复现(Python 3.8.17,该限制同样适用;包要求 requires-python = ">=3.11" 必然命中):
>>> json.loads("9" * 4301)
ValueError: Exceeds the limit (4300) for integer string conversion: value has 4301 digitsanolisa_tokenless/tool_response.py:213 _normalize_input 只捕获 json.JSONDecodeError,而 CPython 3.11+ 对超过 4300 位的整数字面量抛出的是裸 ValueError(JSONDecodeError 虽是 ValueError 子类,但此处抛出的不是它)。该异常会从 compress_text 穿透 AgentScope 1.x postprocessor(_postprocess_tool_response 无异常兜底)和 2.x on_acting,导致工具调用整体失败——当工具正常返回 4301+ 位纯数字文本(大整数计算、长数字序列等)时,Tokenless 反而把成功的工具调用打挂,违背"失败时保留原响应"的 fail-open 契约。
建议修复
_normalize_input:将except json.JSONDecodeError放宽为except ValueError(或同时捕获),把此类解析限制当作普通文本继续走 fail-open 路径。compress_text中压缩结果的json.loads(candidate_json)处(tool_response.py:186附近)存在同类写法,建议一并放宽为ValueError,做防御性处理。- 补回归测试:
compress_text()处理 4301 位纯数字文本时返回None(或正常压缩结果)且不抛异常,覆盖 1.x/2.x 两条路径。
修复后我这边可以直接通过。其余此前提出的 P3 建议(session_id 归属、memory 可见范围注释、mcp pin)不阻塞。
a7c579c to
79ceceb
Compare
|
@kongche-jbw 该 P1 判断正确,已在
验证通过:Runtime Wheel 14 项测试、AgentScope 1.x/2.x 单测,以及 1.0.11、1.0.21、2.0.0、2.0.1、2.0.3、2.0.6 六档真实依赖 smoke。 |
Extract framework-neutral response compression into the Python runtime and select an AgentScope lifecycle adapter from the installed major version. AgentScope 1.x uses Toolkit postprocessors while 2.x retains middleware and Tool integration. This avoids patch-specific public APIs and supports 1.0.11 through 1.0.x plus 2.0.x; AgentScope App remains available from 2.0.1. Assisted-by: Codex:0.147.0 Signed-off-by: 爱鲲 <jiawa.syx@alibaba-inc.com>
79ceceb to
1305d0a
Compare
|
@Forrest-ly 补充 P1 判断正确,已在
本地验证通过:Ruff、Black、Runtime Wheel 14 项测试、AgentScope 1.x/2.x 单测,以及 1.0.11、1.0.21、2.0.0、2.0.1、2.0.3、2.0.6 六档真实依赖 smoke。三条 P3 按 review 结论保持非阻塞,本次未扩大范围。 |
kongche-jbw
left a comment
There was a problem hiding this comment.
Review baseline: c122a0a8f582...1305d0afbe1f
[P1] 深层 JSON 仍会绕过 fail-open
src/tokenless/python/tokenless/python/anolisa_tokenless/tool_response.py:213
这里只捕获 ValueError。工具返回约 10,000 层嵌套的合法 JSON array 时,
CPython json.loads() 会抛出 RecursionError;该异常会从公开 compress_text()
穿透 AgentScope 1.x postprocessor 和 2.x middleware,使成功的工具调用失败。
这与共享压缩核心的 fail-open 契约不符。
Possible direction: 将解析深度异常也按普通文本处理,并补一个通过公开
compress_text() 的深层 JSON 回归测试,断言保留原响应且不抛异常。
Forrest-ly
left a comment
There was a problem hiding this comment.
总体评价
重写后的单 commit(1305d0a)已完整修复上一轮 kongche-jbw / Forrest-ly 提出的 P1 阻塞问题(json.loads 整数位数限制破坏 fail-open 契约),并在两处 json.loads 位点与回归测试上逐一落实;框架无关压缩核心的提取、AgentScope 1.x/2.x 双版本适配、测试矩阵与文档一致性均复查无误,CI(含六档真实 wheel 矩阵)全绿。
审查结论
approve
详细意见
🔴 必须修改(阻塞合并)
无。此前唯一阻塞项(P1)已修复并独立验证:
- 输入侧:
src/tokenless/python/tokenless/python/anolisa_tokenless/tool_response.py:214_normalize_input已放宽为except ValueError,超过 CPython 整数转换上限的数值文本按普通字符串继续 fail-open。 - 压缩结果侧:
tool_response.py:171对result.output的json.loads同样捕获ValueError,异常 Runtime 结果按无效压缩处理并保留原文。 - 回归测试:
python/tokenless/tests/test_runtime.py新增test_framework_core_treats_oversized_integer_as_text(4,301 位数字经公开compress_text()不抛异常);tests/test_agentscope_middleware.pyfail-open 参数表新增_CompressionResult("9" * 4_301, True)覆盖 2.x 输出侧;1.x/2.x 均委托该共享核心。 - 本地独立验证:Python 3.8.17 / 3.11.14 复现
json.loads("9" * 4301)抛裸ValueError;另以 stub runtime 对真实tool_response.py实测输入侧与输出侧均返回 None、无异常穿透。 - CI:头提交 1305d0a 8/8 checks 通过,含 “Test tokenless”(
test-python-runtime+ 六档真实依赖test-agentscope-integration),上一轮的 CI pending 提示已消除。
🟡 建议修改(不阻塞但推荐)
- [
src/tokenless/python/agentscope/src/tokenless_agentscope/_v1.py:48-57]install()的冲突检查只校验自身配置的retrieve_tool_name:若在同一 Agent 上再安装一个使用不同retrieve_tool_name的TokenlessAgentScope实例(默认名会被冲突检查拦下),可通过检查并对所有工具二次包裹 postprocessor(含第一个实例的 retrieve 工具)。后果是响应被串联压缩两次,且第一个实例 retrieve 工具的输出(不在第二个实例的排除集内)也会被再压缩,与 retrieve “取回被省略内容” 的目的相悖。该场景需刻意误配置,建议加一层防御即可:例如给包裹函数挂标记属性,install()发现已有工具的postprocess_func是 Tokenless 包裹时拒绝或跳过。(2.x 存在同构场景:同时传入两个 middleware 会在on_acting中双重压缩,同属误配置场景,可一并考虑。)
🟢 值得肯定
- P1 修复完整:两处
json.loads位点均按建议放宽为ValueError,输入侧与压缩结果侧各有回归测试,与修复建议逐条对应。 - 提取干净:与重构前
middleware.py逐行比对,2.xon_acting/_compress_response/ 阈值 / 排除 / retrieve 授权语义完全保留;middleware.pyshim 同时保留公开 API 与私有别名,policy drift 测试继续有效。 - 1.x 防御设计到位:install 幂等、retrieve 工具名冲突检查、既有 postprocessor 串接(sync/async/返回 None 三态)、partial/interrupted/
success=False/error-like 响应跳过均有单测覆盖;retrieve 限定 Agent memory 内 marker 可见。 - 测试矩阵充分:本地以 stub runtime 跑通 v1 单测(5 项)与 2.x 单测(20 项,Python 3.11.14);CI 以真实 wheel 覆盖 1.0.11 / 最新 1.0.x / 2.0.0 / 2.0.1 / 2.0.3 / 最新 2.0.x,且新增文档一致性 fragments 检查与打包元数据断言,防止支持范围再次漂移。
- 文档中英文同步齐全:用户指南版本矩阵、README 接入示例、设计文档边界描述一致,全仓 grep 确认无残留
>=2.0.5旧表述。 - 安全姿态未退化:retrieve 仍要求 24 位 hex hash + marker 可见;App 模式按 user/agent/session sha256 派生隔离 data_dir;各路径 fail-open 契约成立。
上一轮的三条 P3(1.x session_id 归属、retrieve 可见范围注释、mcp pin)维持 Forrest-ly review 结论:非阻塞,本次不重复列入意见。
Forrest-ly
left a comment
There was a problem hiding this comment.
结论:REQUEST_CHANGES
复核 head 1305d0a。kongche-jbw 于 10:46 提出的 [P1] 深层 JSON 仍会绕过 fail-open 在当前 head 上成立且未被任何 commit 处理,我独立复现并核对了完整代码路径。这是破坏 fail-open 契约的真实正确性缺陷,故本轮改判为 REQUEST_CHANGES。
独立验证(本地 Python 3.8.17 复现)
>>> import json
>>> json.loads("[" * 10000 + "]" * 10000)
RecursionError: maximum recursion depth exceeded while decoding a JSON array from a unicode string
>>> json.loads("[" * 1500 + "]" * 1500) # 超过默认递归上限 1000 即触发
RecursionErrorRecursionError 是 RuntimeError 的子类,不是 ValueError 的子类,现有 except ValueError 无法捕获。
代码路径(src/tokenless/python/tokenless/python/anolisa_tokenless/tool_response.py)
compress_text()在第 148 行调用self._normalize_input(text),该行位于第 150 行才开始的try/except TokenlessError块之外,因此_normalize_input抛出的任何异常都会直接穿透compress_text()。_normalize_input()第 213 行json.loads(text)仅except ValueError(214-215 行)。工具返回约 10,000 层(甚至约 1,500 层)嵌套的合法 JSON array 时抛出RecursionError并逃逸。- 该异常向上穿透 AgentScope 1.x postprocessor(
_v1.py:106)与 2.x middleware(_v2.py:236→_compress_text→ 260),两处调用点均无兜底 try/except(_v1.py唯一的 try/except 在 131-133 行,仅包住 retrieve,不包 compress_text)。结果是一次本来成功的工具调用被标记为失败——直接违背本 PR 反复强调的"异常时保留原文"fail-open 契约。
可达性:_normalize_input 仅在 len(text) >= min_chars(默认 200)时进入;约 1,500 层嵌套即约 3KB,轻松越过该阈值,真实工具(如递归结构、AST/DOM 序列化)完全可能命中。
修复建议(与 kongche-jbw 一致,按位点落实)
- 输入侧(必须,当前可利用):
_normalize_input()第 213-214 行,将except ValueError放宽为except (ValueError, RecursionError);命中时按不透明文本处理(走json.dumps(text)字符串压缩路径,保证可逆、可 retrieve)。 - 压缩结果侧(建议,纵深防御):
compress_text()第 170-171 行json.loads(candidate_json)同样补RecursionError。当前 native 运行时输出被max_depth(≤32)截断,实际不会触达递归上限,但补上后两处 json.loads 的 fail-open 契约保持一致,也防止将来放宽 max_depth 回归。 - 回归测试:仿照已有
test_framework_core_treats_oversized_integer_as_text,在test_runtime.py增加用例,断言对"[" * 10000 + "]" * 10000这类深层嵌套输入compress_text()不抛异常、走 fail-open(保留原文或压缩为可恢复字符串)。
其余结论
前几轮的正面结论维持不变:框架无关压缩核心的提取、AgentScope 1.x/2.x 双版本适配(含 Tool ABI 能力探测)、六档真实 wheel 测试矩阵、文档一致性均复查无误;上一轮 P1(整数位数限制 / ValueError)的修复也已确认落实。本轮唯一阻塞项即上述 RecursionError P1。
Why
Tokenless previously required AgentScope 2.0.5 or later and kept compression policy inside the
2.x middleware. Supporting AgentScope without maintaining patch-specific SDKs requires a stable
public entry point and a framework-neutral compression core.
What changed
TokenlessConfigandToolResponseCompressorAPIs toanolisa-tokenless.TokenlessMiddlewareAPI for AgentScope 2.x callers.Related issue
no-issue: framework compatibility expansion requested during SDK design work
User / Agent impact
AgentScope 1.x users can install Tokenless on an existing Agent, while AgentScope 2.x users use the
same configuration and entry point during Agent construction. Existing 2.x middleware callers
remain compatible.
Risk and compatibility
This adds public Python APIs and widens the AgentScope dependency range. Retrieval remains scoped
to markers visible in the current Agent memory or state, and the existing 2.x API is retained.
Validation
make test-python-runtime test-integration test-agentscope-integrationbash -n, andgit diff --checknpm run buildfromwebsite/Documentation and rollback
Updated the English and Chinese README, canonical framework-integration user guides, and
runtime-library design documents. Roll back by reverting this PR; existing AgentScope 2.x users
can continue using
TokenlessMiddleware.