diff --git a/research-wiki/plans/2026-08-25-thinking-observability-plan.md b/research-wiki/plans/2026-08-25-thinking-observability-plan.md index 7fd1d7d..2e50eba 100644 --- a/research-wiki/plans/2026-08-25-thinking-observability-plan.md +++ b/research-wiki/plans/2026-08-25-thinking-observability-plan.md @@ -139,7 +139,7 @@ conda run -n PolyGateway lint-imports 更新 import:`client.py`(`from polygateway.providers import get_capability, get_provider, resolve_thinking` 拆成两行)、`transports/openai_compat.py`、`client.py` 的 `TYPE_CHECKING` 块里 `ThinkingCapability` 的来源。 -`__init__.py` 新增包根导出并加进 `__all__`(`__all__` 保持既有的字母序):`ThinkingCapability`、`ThinkingObservation`、`ThinkingUnsupportedError`、`get_capability`、`register_capability`、`resolve_thinking`。 +`__init__.py` 新增包根导出并加进 `__all__`(该列表**不是严格字母序**——`DEFAULT_PROFILES` 现在就排在 `AllSourcesExhausted` 前面;沿用文件既有排列,把新符号插到同类符号附近即可):`ThinkingCapability`、`ThinkingObservation`、`ThinkingUnsupportedError`、`get_capability`、`register_capability`、`resolve_thinking`。 `tests/unit/test_providers.py` 里针对被搬走符号的测试,整体移入 `tests/unit/test_thinking.py`。 @@ -177,9 +177,9 @@ conda run -n PolyGateway python -c "from polygateway import ThinkingObservation, `LLMResponse` 侧补 docstring:`UNKNOWN` = 本次无信号判不出,**不是**"没推理";非流式路径下部分模型推理已计费却不回传正文(M3 实测 completion 53 vs 关闭档 3),该档即为 `UNKNOWN`。 -`transports/openai_compat.py` 的两条组装路径(流式 `_complete_stream` 约 460-475 行一带、非流式 `_complete_once` 约 543-560 行一带)在构造 `TransportResult` 时调 `observe_thinking(thinking=thinking, reasoning_tokens=...)` 填入。两条路径都要填——**只填一条正是 L5 要抓的那类分叉**。 +`transports/openai_compat.py` 的两条组装路径(流式 `_complete_stream` 的 463-475 行、非流式 `_complete_once` 的 548-560 行)在构造 `TransportResult` 时调 `observe_thinking(thinking=thinking, reasoning_tokens=...)` 填入。两条路径都要填——**只填一条正是 L5 要抓的那类分叉**。 -`middleware/retry.py` 组装 `LLMResponse` 处(约 377-392 行一带)透传 `thinking_observation=result.thinking_observation`。 +`middleware/retry.py` 的 `_build_response`(372-393 行)透传 `thinking_observation=result.thinking_observation`。 ### 测试要求(先失败后通过) @@ -305,7 +305,7 @@ conda run -n PolyGateway pytest tests/unit/test_cache.py -v **recorder**:`sqlite.py` / `postgres.py` 的 `record_llm_call` 各加一参并接进取值元组,位置与 `COLUMNS` 严格同序。`sqlite.py:146` 的"24 字段冻结签名"改 25。 -**emitter**:`_AttemptUsage` 增 `thinking_observation: str = ThinkingObservation.UNKNOWN`,`of()` 从 response 取;三个 `emit_*` 各传一行(`emit_terminal_failure` 传 `ThinkingObservation.UNKNOWN`——无响应可言,默认值本身不撒谎);`_record` 签名增一参并下沉给 recorder。**所有新增字段只经 `_record` 这一个出口抵达 recorder,不新开调用点**(铁律:遥测调用点收敛为单一 helper,该出口已存在)。`middleware/telemetry.py:135` 的"组装 24 字段"改 25。 +**emitter**:`_AttemptUsage` 增 `thinking_observation: ThinkingObservation = ThinkingObservation.UNKNOWN`(**内部字段用枚举类型**,裸 `str` 归一化只发生在下沉 recorder 那一步),`of()` 从 response 取;三个 `emit_*` 各传一行(`emit_terminal_failure` 传 `ThinkingObservation.UNKNOWN`——无响应可言,默认值本身不撒谎);`_record` 签名增一参并下沉给 recorder。**所有新增字段只经 `_record` 这一个出口抵达 recorder,不新开调用点**(铁律:遥测调用点收敛为单一 helper,该出口已存在)。`middleware/telemetry.py:135` 的"组装 24 字段"改 25。 **recorder 收到的必须是裸 `str`,不是枚举实例**:`_AttemptUsage.thinking_observation` 内部用 `ThinkingObservation` 类型,但 `_record` 下沉给 recorder 时取 `.value`。`StrEnum` 虽是 `str` 子类,asyncpg 的参数编码对 `str` 子类不保证接受,而遥测写失败只会被降级成一条 warning——这类问题不会当场炸,只会让 Postgres 那一路悄悄少一列数据。归一化放在 emitter 侧,与 `tenant_id`/`meta`/`sampling` 由 emitter 定型后再交 recorder 是同一先例(`ports.py` docstring 明载该分工:recorder 只落库,不做语义判断)。 @@ -325,7 +325,7 @@ conda run -n PolyGateway pytest tests/unit/test_cache.py -v ### 测试要求(先失败后通过) -`tests/unit/test_ports.py`:现有 `TestTelemetryRecorderSignature` 的 parametrize 列表加入 `thinking_observation`,断言它存在、无默认值、是 KEYWORD_ONLY——改端口前必然红。 +`tests/unit/test_ports.py`:现有 `TestTelemetryRecorderSignature` **并不冻结完整参数列表**——它只 parametrize 了 `["tenant_id", "meta"]` 两项,断言其无默认值且为 KEYWORD_ONLY。把 `thinking_observation` 加进该 parametrize 列表,断言同样三条——改端口前必然红。 `tests/unit/test_telemetry.py`:列数与列序断言(上表);新增一条 round-trip——记录一条 `thinking_observation=OBSERVED` 的调用后从 SQLite 读回该列等于 `"observed"`。 @@ -415,7 +415,9 @@ conda run -n PolyGateway pytest tests/e2e/test_thinking_live.py -m slow -v ### 测试要求 -`tests/unit/test_thinking.py` 既有的能力表用例覆盖(`evidence` 非空、`can_disable` 取值),无新增行为。本任务是事实更新,测试证据由 Task 7 的 e2e 真跑承担。 +**先失败后通过不适用于本任务,理由须写进提交信息**:本任务只改 `evidence` 字符串与注释,`can_disable` 取值不变,**没有行为变更**,因而没有可先失败的行为断言(`test-driven-development` 的结果门约束的是行为变更)。声明依然成立这一事实,其证据是 2026-08-25 的复测与 Task 7 的 e2e 真跑,不是本任务能自造的单测。 + +`tests/unit/test_thinking.py` 既有的能力表用例(`evidence` 非空、`can_disable` 取值)须保持绿,作为回归证据。 ### 验证 @@ -441,6 +443,8 @@ conda run -n PolyGateway pytest tests/unit/test_thinking.py -q **`research-wiki/index.md`**:登记本 plan、design 与 finding。 +**先失败后通过不适用于本任务**:纯文档同步,无行为变更。其验收是下方 grep 的可见输出——数字与模块名对不上就是没改完。 + **`CHANGELOG.md`**:新增 1.3.1 条目。**断裂项置于条目最前**,沿用 1.3.0"请先读这一条"体例(设计 §13:版号既然不承担预警职责,预警由 CHANGELOG 独立扛)。三条必须显式列出——① `polygateway.providers` 的深路径 import 断裂(`ThinkingCapability` / `resolve_thinking` / `get_capability` / `register_capability` / `DEFAULT_CAPABILITIES` / `ThinkingUnsupportedError` 移入 `polygateway.thinking`,同时提升到包根,**推荐改用包根 import**);② `TelemetryRecorder.record_llm_call` 端口签名 24 参 → 25 参,自定义 recorder 实现须同步;③ M3 非流式开启推理时推理内容已计费却不回传,该档观测为 `UNKNOWN`,库现在会告警一次。 ### Wiki 注册 @@ -471,13 +475,15 @@ grep -rn '22 字段' research-wiki/schemas/llm-calls.md # 预期无输出 版本号两处改 `1.3.1`(`pyproject.toml` 与 `__init__.py.__version__` 必须一致);`CHANGELOG.md` 的"未发布"定版为 `## 1.3.1(2026-08-25)`。 -按 CLAUDE.md §4.4.1 发布清单**逐步执行,不得跳步**:README(Task 9 已完成)→ CHANGELOG 定版 → 版本号两处 → 合并 main(`--no-ff`)+ push → 打 tag 并 push → 构建 → 上传 registry → `pip download` 验证并解包确认新代码在内 → 建 Release + 挂仓库 + 核对包页面。 +本任务分两段,**中间是一道人类确认门**。 -合并前另需两道门:`verification-before-completion` 派全新上下文 verifier subagent 独立验证(跨 20+ 文件,属强制档);`requesting-code-review` 整分支审查。 +**第一段:分支内可自主完成的验证**——CHANGELOG 定版为 `## 1.3.1(2026-08-25)`;版本号两处改 `1.3.1`;`verification-before-completion` 派**全新上下文** verifier subagent 独立验证(跨 20+ 文件,属强制档);`requesting-code-review` 整分支审查;在分支上跑 `make ci` 与 `pytest -m slow`(约 20-40 分钟——四个 e2e 文件与 Redis 时间语义变体默认被 `-m 'not slow'` 排除,不显式跑等于没跑)。 -合并到 main 后在 main 上重跑 `make lint` 与全套件,**外加 `pytest -m slow`**(约 20-40 分钟,四个 e2e 文件与 Redis 时间语义变体默认被 `-m 'not slow'` 排除,不显式跑等于没跑)。 +**人类确认门**:以上全绿后停下,把验证结果交给人类,**取得明确同意后**才执行第二段。 -关闭 issue #16 与 #17,附修复说明与本次实测结论(诊断纠正 + 三层根因 + 落地形态)。 +**第二段:外发且难以撤销的动作,一律等确认**——合并 main(`--no-ff`)+ push → 打 tag 并 push → 构建 → 上传 registry → `pip download` 验证并解包确认新代码在内 → 建 Release + 挂仓库 + 核对包页面 → 关闭 issue #16 / #17 并附修复说明(诊断纠正 + 三层根因 + 落地形态)。顺序按 CLAUDE.md §4.4.1**不得跳步**:包上传与 tag 一旦推出去就收不回,registry 里的版本号也不能复用。 + +合并到 main 后须在 main 上**重跑** `make lint` 与全套件外加 `pytest -m slow`——分支上跑过不算,合并本身可能引入差异。 ### 验证