Skip to content

【No.8】Agentic 多协议一致性测试 - #338

Closed
Shuan-xx wants to merge 6 commits into
redai-studio:mainfrom
Shuan-xx:test/agentic-protocol-canonical-golden
Closed

Shuan-xx wants to merge 6 commits into
redai-studio:mainfrom
Shuan-xx:test/agentic-protocol-canonical-golden

Conversation

@Shuan-xx

Copy link
Copy Markdown

What

Adds a canonical-request golden test suite for the three Agentic ingress protocols — Chat Completions, Responses, Anthropic Messages — and tightens one validation gap in the shared canonicalizer.

  • tests/agentic/ (new): independent golden fixtures per protocol + three test modules (257 passed, 6 xfailed).
  • relax/agentic/session/state.py (+8): check_messages now validates the canonical image_url content block.

Two commits: fix(agentic): reject malformed image_url blocks in canonical message validation and test(agentic): cover cross-protocol canonical request goldens.

Relates to #<任务 No.8>

Why

messages / tools / chat_template_kwargs is the only input of _messages_tools_template_state_hash, i.e. the identity of every Session Forest node — prefix matching, partial-rollout resume and the training export all key off it. If two protocols map one semantic request to two different triples, the same trajectory reaches the Session under two different state hashes.

Nothing in the repo asserted that the three ingress paths converge, and nothing asserted that a malformed multimodal block is rejected before it enters the Forest.

Before this change, {"type": "image_url", "image_url": "https://…"} (object flattened to a string) passed validation and then raised TypeError: string indices must be integers from the export-side _multimodal_inputs_from_messages; url: 1 / url: "" were silently exported as {'images': [1]} / {'images': ['']}.

How

Convergence goldens — fixtures/{chat_completions,responses,anthropic_messages}.json each carry the same 17 case_ids (text, non-ASCII, system chunk merging, assistant tool call, parallel tool calls, tool result with empty output, reasoning-only, reasoning + text, HTTP-url image, base64 data-url image, image + multiple text blocks, tools with/without description, thinking on/off, full multiturn conversation). The three fixtures deliberately differ in surface form: scrambled function.arguments key order, arguments as a JSON string vs. a dict, instructions vs. system vs. system message, reasoning.effort vs. thinking.type, input_image vs. image(source) vs. image_url. helpers.py runs each payload through its normalizer and projects the result onto the canonical triple. Three assertion layers: cross-protocol agreement, agreement with the pinned fixtures/canonical.json, and identical state hash. REQUIRED_CASE_IDS fails the suite if a scenario required by the task brief is dropped.

Illegal input — 84 parametrized cases across the three protocols assert the exact message, param and status_code == 400, including nested field paths such as messages[2].tool_calls[0].id must be a non-empty string and messages[2].content[0].image_url.url must be a non-empty string. Covers unknown/illegal roles, missing or non-string tool-call ids, empty-string and empty-list content, malformed tool arguments (invalid JSON, JSON scalar, list, duplicate key, non-finite number), incomplete tool results, and 13 illegal image shapes (Chat 5 / Responses 2 / Anthropic 6). Two extra tests assert a rejected image block can never reach multimodal extraction, and a canonical one always survives it.

Stable serialization — key-order invariance of the state hash over every canonical field, digest shape, content sensitivity (trailing space, dropped tool, thinking toggled off), uniqueness across the 17 goldens, tool-argument collapse to a single JSON form (including the JS JSON.stringify integral-float rule and non-ASCII kept literal), recursive nested chat_template_kwargs sorting, and function-schema field projection.

Validation fix — check_messages rejects a canonical image_url block whose payload is not an object (TypeError) and whose url is not a non-empty string (ValueError), so all three protocols now refuse it through the shared收口 with a full field path. Unknown block types are intentionally left as-is.

Testing

$ pytest tests/agentic tests/test_agentic_rollout.py -q
294 passed, 6 xfailed in 14.09s

$ pytest tests/agentic --collect-only -q
263 tests collected

Non-vacuity was checked by mutating one protocol's fixture in a throwaway copy (Responses text_only → "What is 2+3?") and confirming all three layers catch exactly that case and nothing else:

equivalence caught : ['text_only']
golden caught      : ['responses/text_only']
state-hash caught  : ['text_only']

The fix was checked against HEAD by loading git show HEAD:relax/agentic/session/state.py as a separate module: all four malformed image shapes are ACCEPTED at HEAD, all four REJECTED after the change, and the flattened-string shape crashes the export helper at HEAD.

Equivalent-to-hook local checks (same versions as .pre-commit-config.yaml):

$ ruff check relax/agentic/session/state.py tests/agentic       # 0.15.9
All checks passed!
$ ruff format --check --line-length 119 ...
6 files already formatted
$ python .pre-commit-hooks/docformatter_compat.py --in-place --wrap-descriptions 79 ...
(no changes)

Committed blobs are LF (verified via git show, local core.autocrlf=true affects checkout only).

Not run locally, CI is authoritative:

  • pre-commit run --all-files — the hook repositories could not be cloned in this environment; the equivalent ruff / docformatter / whitespace / final-newline checks above were run individually instead.
  • tests/integration/ — requires multi-node GPU hardware; the dev machine is Windows without CUDA.
  • pytest tests/ in full additionally fails locally on torch._inductor.exc.InductorError: InvalidCxxCompiler: cl is not found (no MSVC toolchain) in tests/utils/replay and tests/utils/training, unrelated to this change.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

The remainder of the PR is test-only. No public API, argument parsing, or Controller/Service/Launcher logic is touched.

Known cross-protocol divergences (requesting a decision)

The suite found that the conversion layer converges correctly on all 17 equivalence cases; the remaining asymmetry is in validation strictness at each ingress, not in canonicalization. Fixing these changes runtime behaviour beyond this task's scope, so they are pinned with @pytest.mark.xfail(strict=True) — a fix surfaces as an unexpected pass rather than going unnoticed.

# Current behaviour Pinned expectation
D1 Chat accepts an assistant tool call with no id (it disappears from the canonical form); Responses reports input[1].call_id must be a non-empty string, Anthropic reports messages[1].content[0].id must be a non-empty string rejected by every protocol
D2 Chat accepts a tool result with no tool_call_id rejected by every protocol
D3 Chat accepts a function tool with no name, then drops it silently (tools == []) rejected, or kept consistently
D4 Responses drops a tool that omits type; Anthropic keeps and projects it identical projection
D5 Multiple text blocks: Chat keeps the list, Responses/Anthropic join into one string one canonical content
D6 Unknown content block type (document): Chat passes it through verbatim, others drop it identical handling

Related, for the "accurate field path" acceptance criterion: errors raised by a protocol's own validator use protocol-native paths (input[…], messages[…].content[…].source…), but errors raised after conversion, inside check_messages, report canonical-array paths. A Responses client that sends {"instructions": …, "input": [user, function_call]} gets param=input with messages[2].tool_calls[0]… — the field name changes and the index shifts with the presence of instructions. Chat is unaffected (canonical array == request array). Which path a client should see looks like a deliberate product choice, so it is reported rather than changed here.

Screenshots / Logs

n/a — pure CPU tests, no model service or network involved.

…validation

Chat Completions ingress accepted any `image_url` content block shape, so a
flattened or empty block reached the Session and later raised an opaque
`TypeError: string indices must be integers` from the multimodal extraction
helper, while `url: 1` / `url: ""` were exported as bogus image entries.

Validate the canonical block in `check_messages` so every protocol rejects it
with a full field path before it enters the Forest.
Chat Completions, Responses and Anthropic Messages must collapse onto one
`messages` / `tools` / `chat_template_kwargs` triple, because that triple is
the only input of the Session state hash used for prefix matching, resume and
training export.

Add independent golden fixtures per protocol over 17 semantically equivalent
requests (text, assistant tool calls, tool results, image inputs, tools,
thinking toggles and a full multiturn conversation), assert the three fields
and the resulting state hash agree, and assert illegal inputs are rejected
with exact field paths.

The 6 `xfail(strict=True)` cases pin the validation-strictness divergences that
still exist across the three ingress paths, so fixing them surfaces as an
unexpected pass instead of silently changing behaviour.
@rai-studio-bot

rai-studio-bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Nyanpasu 审查看板

审查状态: ✅ 已通过

审查版本: cedcde7

新头 cedcde7 仅为合并 main:PR 十个文件与已批准的 63f63ec 逐字节一致,合并未触及 PR 文件及其直接依赖。前轮遗留的 CI 待确认项已闭环——cedcde75 上 8 项检查(pre-commit、Python 3.10–3.12、GPU 单测与 GPU/NPU 集成)全部通过,其中 Python 3.10 全量套件含 tests/agentic 260 项结果。三项审查问题维持已解决,未发现新问题。PR 描述中的 D1–D6 协议验证差异及字段路径问题仍待维护者决策,属已报告事项而非本 PR 缺陷。

审查阶段进度范围与结果
常规审查 ✅ 已完成 核对 cedcde7:relax/agentic 与 tests/agentic 相对已批准的 63f63ec 无任何差异;合并引入的 46 个文件均在 PR 范围之外,测试直接依赖(session/service.py、utils/types.py、utils/logging_utils.py)未变;生产改动重读确认为已批准的 +8 行 image_url 校验。
深度审查 ✅ 已完成 必要性审查按当前头核验后沿用:审计对象文件在本头逐字节未变,前轮按维护者要求完成的测试质量深审结论(F1–F3)继续成立,生产与测试精简决策见下方审计记录。

审查发现

待处理
编号 严重性 问题状态规则来源
暂无待处理的记录。
已解决或已取代
编号 严重性 问题状态规则来源
F1 Medium severity 尾随空格测试同时替换消息,无法捕获空白裁剪回归 ✅ 已解决 —
F2 Medium severity D4 不应要求缺少 type 的 Responses 工具与合法请求收敛 ✅ 已解决 —
F3 Low severity 删减 JSON 入口不可达的 Python 对象测试 ✅ 已解决 —
提交范围 · 接收 10 · 建议移出 0 · 待确认 0

接收 10 个文件 · 建议移出 0 个文件 · 待确认 0 个文件。移出与待确认部分暂停深审,不代表审查通过。

文件结论仓库维护必要性依据替代去向或方案
relax/agentic/session/state.py
接收 共享收口 check_messages 新增 canonical image_url 块结构校验(载荷须为对象、url 须为非空字符串),修复畸形块进入 Session 后在导出侧 _multimodal_inputs_from_messages 崩溃或静默导出 {'images': [1]} 的真实缺口;三个协议入口与显式导出路径都经过该函数,必须在仓库中维护。 PR 描述 What/Validation fix 部分;调用方为 relax/agentic/session/state.py 的 check_messages 及导出侧 _multimodal_inputs_from_messages。 在三个协议入口分别校验会重复同一逻辑;仅在导出侧兜底无法阻止畸形请求进入 Session Forest 影响状态哈希身份。
tests/agentic/__init__.py
tests/agentic/helpers.py
tests/agentic/test_canonical_serialization_stability.py
tests/agentic/test_protocol_canonical_golden.py
tests/agentic/test_protocol_canonical_invalid.py
接收 任务 No.8 明确要求三个 Agentic 入口协议的 canonical 请求 golden 一致性测试;测试驱动真实入口(relax/agentic/session/service.py 的三个 normalizer 与状态哈希)并断言独立期望值,是持久回归覆盖而非一次性验证脚本。 任务简报 redai-studio/community contributor-program/2026-cohort-2 official-task.md#task-8;PR 描述 How/Testing 部分。 仅将验证结果作为 PR 附件留档会使仓库失去对跨协议收敛性与状态哈希稳定性的回归防护,无法满足任务验收标准。
tests/agentic/fixtures/anthropic_messages.json
tests/agentic/fixtures/canonical.json
tests/agentic/fixtures/chat_completions.json
tests/agentic/fixtures/responses.json
接收 golden 断言需要固定的期望数据;四个 fixtures 按同一组 case_id 组织三协议表面形态与 canonical 期望,是测试套件的必要组成部分,与测试模块共同维护。 任务简报同上;tests/agentic/helpers.py 读取 fixtures 参与跨协议一致、golden 对照与状态哈希三层断言。 在测试代码中内联期望值会降低可读性,且无法让三协议 fixtures 按相同 case_id 逐例对照。
精简审查与验证依据
审查范围进度结论
生产代码 ✅ 已完成 生产必要性结论沿用前轮深审并按当前头核验:image_url 校验为必要的外部输入边界,保留;无进一步精简空间。
测试 ✅ 已完成 测试必要性结论沿用前轮深审并按当前头核验:不可达输入用例与错误的 D4 收敛 xfail 已在前轮删除,保留用例均覆盖真实可达输入,无进一步删减建议。

生产代码的必要性与替代方案

范围必须保留的契约更简单的方案结论依据与限制
relax/agentic/session/state.py check_messages 新增的 image_url 块校验(+8 行) 三个协议入口(relax/agentic/session/service.py 的 normalizer)与显式导出均经 check_messages 收口;畸形 image_url 块此前会在导出侧 _multimodal_inputs_from_messages 崩溃或静默导出非法值。 维持合并前行为(不加校验)会留下导出侧崩溃路径;改为各协议入口分别校验会在共享收口之外重复逻辑。 保留 前轮深审确认该边界必要(Chat 客户端可真实传入错误结构,下游直接读取 image_url.url);本轮核验文件与已批准版本逐字节一致,CI 全量套件(含 tests/agentic 260 项结果)在 cedcde7 通过。

测试的必要性与替代方案

范围必须保留的契约更简单的方案结论依据与限制
tests/agentic 测试套件(3 个测试模块 + helpers + 4 个 fixtures) 任务 No.8 要求三协议 canonical 请求 golden 一致性;套件驱动真实 normalizer 入口,断言跨协议一致、golden 对照与状态哈希三层独立期望。 前轮已删去 JSON 传输边界不可达的 Python 对象用例(非字符串键、set)及要求缺 type 的 Responses 工具收敛的 D4 xfail;进一步删减将失去对真实可达非法输入(非法 JSON 字符串、重复键、非有限数字、畸形图片结构)的拒绝覆盖。 保留 F2/F3 修复于 63f63ec 验证(隔离套件 255 passed、5 xfailed);本轮文件未变,CI Python 3.10/3.11/3.12 全量套件在 cedcde7 通过。
Powered by Nyanpasu with glm-5.3[1m] xhigh, please check the suggestions carefully.

@rai-studio-bot rai-studio-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已完成对 fb36c51c 的审查,未发现阻塞问题;一项测试盲点及修正建议见行内评论。图片结构校验与现有 canonical/导出合同一致。

函数级依赖隔离验证为 257 passed、6 xfailed。完整 pytest 因本地缺少 PyTorch、Ray、NumPy 而未能收集;当前无 CI 检查记录,未运行需要多节点 GPU 的集成测试。PR 列出的既有协议差异仍需维护者决定统一策略。

Powered by Nyanpasu with gpt-6-astra medium, please check the suggestions carefully.

("mutate", "expected_changed_field"),
[
pytest.param(
lambda m, t, k: ([{"role": "user", "content": "hi "}] + m[1:], t, k), "messages", id="trailing-space"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 优先级:P2

请让这个用例只增加尾随空格。_MESSAGES[0] 是 system: "S",当前写法把它整体替换成 user: "hi ";即使哈希错误地忽略尾随空格,role 和正文的变化仍会让断言通过。函数级隔离验证中,在哈希入口对字符串 content 执行 rstrip() 后,新增套件仍为 257 passed、6 xfailed。保留原消息,只追加一个空格,才能捕获该回归。

Suggested change
lambda m, t, k: ([{"role": "user", "content": "hi "}] + m[1:], t, k), "messages", id="trailing-space"
lambda m, t, k: ([{**m[0], "content": m[0]["content"] + " "}, *m[1:]], t, k),
"messages",
id="trailing-space",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 0f0180a1 验证修复:用例现在保留原消息,只追加尾随空格。函数级隔离套件为 257 passed、6 xfailed;在哈希入口注入 rstrip() 后,恰好 trailing-space 用例失败,其余为 256 passed、6 xfailed。该问题已解决。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已确认并修复。原写法把 _MESSAGES[0] 整体替换,role 与正文一并变化,尾随空格并非唯一变量;现改为在原消息上追加一个空格({**m[0], "content": m[0]["content"] + " "}),使空白字符成为唯一差异。

按建议的函数级隔离方式复核:在 _messages_tools_template_state_hash 入口对字符串 content 注入 rstrip() 后,修改前的用例哈希仍然变化(断言通过,盲区复现),修改后的用例哈希不再变化(断言失败,回归被捕获);去掉变异体后两者均正常,套件 294 passed / 6 xfailed,tests/agentic 263 collected。

@SigureMo SigureMo self-assigned this Sep 18, 2026
The old mutation swapped the system message for a user one, so role and text changed together and the case stayed green even when the hash ignored trailing whitespace. Verified with an rstrip() regression injected at the hash entry point: undetected before, failing now.

@rai-studio-bot rai-studio-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已完成 0f0180a1 复查,上轮测试问题已修复,未发现新增问题,同意本次变更。修复验证见原讨论。

函数级隔离验证为 257 passed、6 xfailed,尾随空白裁剪变异已被测试捕获。完整测试仍受本地 PyTorch/Ray/NumPy 依赖缺失限制,当前无 CI 检查记录;需要多节点 GPU 的集成测试未运行。

Powered by Nyanpasu with gpt-6-astra medium, please check the suggestions carefully.

@SigureMo

Copy link
Copy Markdown
Member

@rai-studio-bot 请再次深度 review,专注于:

  • 是否有不合适的测试 case,不要测试不会发生的场景
  • 是否有过度防御逻辑,防御要适当,非实际可能发生的场景,不要单独防御
  • 代码是否足够精简,在能够完成任务的前提下,在不丧失可读性的前提下,可以考虑精简掉无用逻辑

可以进一步参考 https://github.com/ShigureLab/nyanpasu/blob/main/AGENTS.md 中的原则进行 review

@rai-studio-bot rai-studio-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

针对本轮三项重点重新审查 0f0180a1,发现两项非阻塞的测试设计/精简问题,详见行内评论。原先的通过结论补充为本轮意见;未发现需要阻塞的生产逻辑缺陷。

新增 image_url 校验是必要的外部输入边界校验:Chat 可传入错误结构,显式导出也复用它,下游会直接读取 image_url.url。建议保留。工具 arguments 中的非法 JSON 字符串、重复键和非有限数字同样可真实到达,不宜一并删除。

本地函数级隔离验证仍为 257 passed、6 xfailed。CI 的 Python 3.10–3.12 与 GPU 单测已通过;pre-commit 因 clang-format 修改三个协议 JSON fixtures 而失败,请应用对应格式化结果。多模态集成检查仍在运行。

Powered by Nyanpasu with gpt-6-astra medium, please check the suggestions carefully.

CHAT, _chat([_user("hi")], tools=[{"type": "function", "function": {"name": "f", "parameters": {}}}])
)
)
responses = canonical_of(normalize(RESPONSES, _responses_input("hi", tools=[{"name": "f", "parameters": {}}])))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 优先级:P2

D4 把协议格式差异当成了应修复的 canonical 差异。仓库 docs/en/guide/agentic-rollout.md 已区分:Responses function tool 使用 type/name/parameters,Anthropic 允许省略 type;_FUNCTION_TOOL_FIELDS 也明确实现了这一区别。这里却删除 Responses 的 discriminator,再要求它与合法 Chat/Anthropic 请求得到相同结果。这个 strict xfail 因而把“为缺少 type 的 Responses tool 猜测默认类型”变成了期望行为,而非验证合法等价输入。建议移除这条收敛 xfail;如需记录缺字段行为,将其作为 Responses 自身的异常输入测试。合法工具的跨协议一致性已有 golden 覆盖。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 63f63ec9 确认修复:删除了要求缺少 type 的 Responses 工具与合法请求收敛的 xfail,合法工具 golden 保持不变。保留的当前行为断言不再要求改变协议语义,此问题已解决。

Comment on lines +180 to +181
pytest.param({1: "x"}, "JSON object keys must be strings", id="non-string-key"),
pytest.param({"a": {1, 2}}, "must contain JSON-compatible values, got <class 'set'>", id="python-set"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 优先级:P3

按本轮“只测试真实可达输入”的要求,建议删去 non-string-key、python-set 两例,以及下方第 195 行的整数键转换断言。三种 HTTP 入口先经过 request.json(),agent 显式导出也先经 json.loads()(runner/ipc.py:281–283):整数对象键到达这里时已经是字符串,Python set 则无法通过 JSON 序列化。这几条直接调用内部 helper 的测试固化了传输边界之后不可达的 Python 状态。保留非法 JSON 字符串、重复键、溢出数字和转义 surrogate 等用例,它们确实可以出现在工具 arguments 字符串中;不需要为此次测试清理扩大生产代码改动。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 63f63ec9 确认删除整数键、Python set 两例及 template kwargs 整数键转换断言;真实可达的非法 arguments 字符串测试仍保留。函数级隔离套件为 255 passed、5 xfailed,此问题已解决。

- Remove the D4 convergence xfail: a Responses tool without its required
  type discriminator is malformed input, not a legitimate cross-protocol
  equivalent; the drop behavior stays pinned in the divergence inventory.
- Remove non-string-key / python-set argument cases and the integer-key
  template-kwarg assertion; all transport entries JSON-decode first, so
  those Python states cannot reach the helpers.
- Apply clang-format to the three protocol JSON fixtures (pre-commit).

@rai-studio-bot rai-studio-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已完成 63f63ec9 复查,上轮两项测试意见均已修复,未发现新增问题,同意本次变更。验证细节已回复原讨论。

函数级隔离验证为 255 passed、5 xfailed;三个协议 JSON fixtures 仅调整格式,解析后的内容与上版完全一致。当前提交暂无 CI 检查记录,格式检查是否通过仍待新一轮 CI 确认;本地完整测试及多节点 GPU 集成测试未运行。

Powered by Nyanpasu with gpt-6-astra medium, please check the suggestions carefully.

@Shuan-xx

Copy link
Copy Markdown
Author

@SigureMo 任务已完成,请求 review

@SigureMo

SigureMo commented Oct 8, 2026

Copy link
Copy Markdown
Member

本赛题已经由 #335 合入并锁定,感谢参与~

@SigureMo SigureMo closed this Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants