docs(design): 给 0008 与 0009 补上外部先例,两个决定都是印证不是改动

0008:主流框架没有一家把截断量做成工具返回值上的字段(Anthropic 的 tool_result 只有三个
字段,OpenAI/LlamaIndex/Pydantic AI 的结果类型都没有),唯一结构化记下来的 LangChain 走的
是通用 artifact 通道而不是专设字段。另补「将来要加时怎么加」的正反两例:Pydantic AI 在允许
返回的类型集合里加一个新类型、旧路径不动,零兼容问题;OpenAI Agents SDK 改成运行期形状
嗅探,一个返回 {"msg": ...} 的普通工具被当成图片输出转掉、线上 400,三天后回落。

0009:Pydantic AI 2026-07 加过一条几乎同形的 meta-test(扫描包、新增公共数据类超过一个位置
参数就失败),PR 正文两句正好对上本文两条论证——「一条老在评审里被提却没有机器执行的意见」,
以及「不给已有的数据类补,那会打断按位置构造的调用方;白名单只减不增,清空它得发新 major」。
它已经付了本文想避开的那笔账。另记一条连带影响:kw_only 字段不进 __match_args__。
This commit is contained in:
2026-08-10 01:21:12 -04:00
parent 6fafd95d6c
commit 7da5e07726
2 changed files with 39 additions and 0 deletions
@@ -194,8 +194,21 @@ outputBytes / maxLines / maxBytes` 这一组**绝对量**加两个布尔,不
**停机不是截断**——本库把它落成了停止判定 B 档的 `context_overflow`。也就是说本库这条主路上
根本不该发生截断,`observation_truncated_chars` 恒为 0 不是缺口,是这条路径的真实情况。
**主流框架没有一家把截断量做成工具返回值上的字段。** Anthropic 的 `tool_result` 内容块只有
`tool_use_id` / `content` / `is_error` 三个字段;OpenAI Agents SDK 的三个输出类型、LlamaIndex
`ToolOutput`、Pydantic AI 的 `ToolReturn` 都没有截断字段。唯一把「截了多少」结构化记下来的
是 LangChain 的 shell 中间件,而它走的是通用的 `artifact` 通道(一个随便装什么的字典),不是
为截断专设的字段。截断本身也都发生在工具实现或宿主那一层,不在循环框架里。
## 留给后续的
**真出现一个要报截断量的消费者时,加的应该是一个新的返回类型,不是往 `str` 上打补丁。** 那时
`ToolHandler` 的返回改成联合类型(`str | ToolOutput`)对已有实现是兼容变更,而字段形状按
那个消费者真实要的来定,不按今天猜的来定。
这条路有现成的正反两例。**Pydantic AI 就是这么做的**:它在允许返回的类型集合里加了一个
`ToolReturn`,旧的「返回任意对象」那条路一点没动,所以没有兼容问题、也不需要迁移说明
PR #2060,随 v0.3.5 发布)。**反面是 OpenAI Agents SDK**:它在 v0.4.0 里改成对返回值做运行期
形状嗅探,一个本意只是返回 `{"msg": "foobar"}` 的工具被当成图片输出转换掉,线上直接 400,
三天后不得不收紧规则、让不合形状的字典回落成 `str()`PR #1898 → issue #1930 → PR #1965)。
区别在于加一个显式类型还是去猜一个已有类型的意图——**将来加这个能力时走前一条**。
@@ -37,6 +37,32 @@ PolyGateway 是这个实验室里唯一另一个「库」形态的项目,它
PolyLoop 还没有任何下游装上,这个约束现在可以不长出来。
## 库外面也有人踩到同一处,做法几乎一样
Pydantic AI 在 2026 年 7 月加了一条 meta-test,扫描整个包、对**新增的**公共数据类强制只收
关键字参数,超过一个位置参数就失败(PR #6458)。那份 PR 正文里有两句话正好对上本文的两条
论证:
> "Pretty much all plain dataclasses need `_: KW_ONLY`!" — the most-repeated unenforced review nit.
(一条老在评审里被提、却没有机器执行的意见——所以他们把它写成了检查。)
> It deliberately does **not** add `_: KW_ONLY` to any existing dataclass — that would break
> positional callers. The allowlist only ever shrinks; its drain path is a major version.
(不给已有的数据类补,因为那会打断按位置构造的调用方;白名单只减不增,清空它得发一个新
major。)
它已经付了本文想避开的那笔账:存量类型只能进白名单、只能等下一个 major 才清得掉。本库现在
一个存量调用方都没有,所以不需要白名单,也不需要弃用期。
## 一个已知的连带影响
`kw_only` 的字段不进 `__match_args__`Python 官方文档 `dataclasses` 一节写明),也就是说
结构化模式匹配里那种按位置解构的写法(`case ToolSpec(name, description)`)不成立,要写成按
关键字的(`case ToolSpec(name=name)`)。本库现在没有一处按位置解构公共类型,接受这个影响;
它和位置构造是同一件事的两面,留着其中一面就等于留着字段顺序这份承诺。
## 决策:`src/polyloop/` 下每一个数据类都加 `kw_only=True`
包括三个内部模块里的。一条规则不留判断余地,也才写得成机器检查。