feat(harness): add canonical tool invocation boundary

This commit is contained in:
zhuyongxin
2026-07-21 19:36:25 +08:00
parent 6b74990f86
commit 0dbdd7d8d3
31 changed files with 1543 additions and 1 deletions
@@ -0,0 +1,43 @@
# Acceptance: single-react-tool-invocation-store
## 实现结果
- 新增 canonical record/limits/exceptions/store interface/Redis JSON adapter。
- 新增 ToolBoundary envelope/result、executor/projector interfaces 和稳定错误码。
- 实现 preflight、Run/Tool budget、Run capacity、PROJECTING、READY/ERROR、evidence semantics、TTL 和 UTF-8 limits。
- 旧 ToolInvocationRecorder、JPA、Chat/AIOps、Controller、Repository 和公开协议未修改。
## 静态验证
- `openspec validate single-react-tool-invocation-store --strict`:通过。
- 新 Harness 包 Redis 引用仅为 `RedisCanonicalInvocationStore`。
- legacy recorder/JPA/Chat/AIOps/Controller/repository/resources diff:为空。
- staged diff check 与 Secret scan:提交前执行并通过。
## 脚本验证
- `mvn -q -DskipTests compile`:通过。
- `mvn -q '-Dtest=CanonicalInvocationStoreTest,ToolBoundaryTest' test`:通过。
- 综合阶段 0/1/2/3A suite(含 Harness/ACI/Core/retry/key/config/ChatController):通过。
## 浏览器/人工验证
- 不适用。本阶段没有 UI、Controller、SSE 或公开协议变化。
## 未验证
- 未连接真实 Redis,未做 ACL/network/TTL live 验证;最终 E2E 阶段执行。
- 未接入 Alibaba ToolInterceptor,真实框架 ID 传播留给后续 Agent/application stage。
- 未实现 RAG/log/MySQL projector,留给 3B/3C。
- canonical update 的 read-TTL-write 并发窗口已记录为风险,尚未 Lua/CAS 化。
## 剩余风险与后续门禁
- 新旧 JPA audit 与 canonical store 短期并存,后续 projector 必须只以新 boundary 的 READY record 作为引用来源。
- 下一阶段 3B/3C 必须复用本 ToolBoundary,不复制 Redis 状态机。
## 状态
- Stage acceptance: accepted
- OpenSpec archive: archived at `openspec/changes/archive/2026-07-21-single-react-tool-invocation-store`
- Main spec sync: `openspec/specs/canonical-tool-invocation-store/spec.md`(7 added requirements)
@@ -0,0 +1,29 @@
# Brief: single-react-tool-invocation-store
## 背景
旧 `ToolInvocationRecorder` 依赖 ThreadLocal 和 JPA preview,不能证明完整 Tool 结果、生命周期和当前 Run 所有权。阶段 2 已提供 RunContext/Key/Capacity,需要统一 canonical ToolBoundary 和 Redis store 供后续 projector 复用。
## 目标
- 统一 Pre-Tool 门禁、PROJECTING/READY/ERROR 状态和 evidence semantics。
- 在同一 Redis record 保存 request/raw_response/agent_result、框架 ID、Run、时间和错误。
- 固定 TTL 不续期、容量/结果大小 fail-closed、raw 不静默截断。
- 用 Fake Tool/Projector/Store 覆盖 duplicate、cross-run、no-evidence、error、TTL 和 oversize。
## 范围
- Canonical invocation model/store、Redis JSON adapter、ToolBoundary 和 focused tests。
- 复用阶段 2 Core、budget、capacity、ToolCallKeyFactory。
## 非目标
- 不实现 RAG/log/MySQL projector。
- 不修改旧 recorder/JPA、Chat/AIOps、Controller/SSE 或公开协议。
## 元数据
- 分档:complex
- 接口影响:L2 内部 Harness boundary/store
- 关联 Issue:ISS-014 阶段 3A
- 关联 OpenSpec:`openspec/changes/single-react-tool-invocation-store`
@@ -0,0 +1,106 @@
# Decisions: single-react-tool-invocation-store
## 规模与入口
- 分档:complex。
- 入口:ISS-014 阶段 3A;阶段 0/1/2 已 Archive 并由 `58c3910`、`4274f33`、`6b74990` 提交。
- 目标:统一 ToolBoundary 和 Redis canonical invocation store,不实现 Tool-specific projection。
## Context
- 阶段 1 主规格已冻结 RAG/log/MySQL Agent-facing Request/Result 和 `EvidenceStatus`。
- 阶段 2 主规格已冻结 `RunContext`、预算、取消、Key Factory 和 strict retry。
- 旧 `ToolInvocationRecorder` 依赖 `SessionContextHolder`、JPA `ToolInvocation` 和 500 字符 preview,属于 durable audit 兼容路径,不是 canonical store。
- 既有 `SessionConfiguration` 提供 `RedisTemplate<String,Object>` + JSON serializer;新 store 复用 bean,不新增连接配置。
## Question Pool
| 维度 | 问题 | 模式 | 证据与结论 | 状态 |
|---|---|---|---|---|
| 术语 | canonical invocation 与旧 JPA ToolInvocation 是否同一记录? | evidence-driven | ISS-014 数据分层明确 Redis canonical 保存完整 request/raw/agent,JPA 只做 durable audit;两者分离。 | 已解决并汇报 |
| 术语 | `PROJECTING/READY/ERROR` 与 evidence status 如何组合? | evidence-driven | 阶段 0/1 规格:只有 READY 可为 FOUND/NO_EVIDENCE,ERROR 不可引用;PROJECTING 是内部暂态。 | 已解决并汇报 |
| 边界 | 阶段 3A 是否实现 RAG/log projector? | evidence-driven | ISS-014 3A 明确不实现 Tool-specific projection,3B/3C 单独接入。 | 已解决并汇报 |
| 边界 | Redis 读取是否刷新 TTL? | evidence-driven | ISS-014 固定“创建设置、读取/更新不续期”;更新采用当前剩余 TTL,不恢复初始 TTL。 | 已解决并汇报 |
| 验收 | raw 超限是否静默截断? | evidence-driven | ISS-014 明确 `RESULT_TOO_LARGE` ERROR,raw 不静默截断;agent projection 才可按 projector 预算截断并标记。 | 已解决并汇报 |
| 验收 | 缺失/重复/cross-run ID 如何处理? | evidence-driven | 阶段 3A 任务明确覆盖;Key Factory 保留框架 ID,ToolBoundary 在 store 创建前校验 Run/ID 和 duplicate。 | 已解决并汇报 |
| 技术 | Redis 如何避免 update 重置 TTL? | evidence-driven | 既有 RedisTemplate;begin 使用 setIfAbsent + TTL,update 先读取剩余 TTL 再写回相同/更短 TTL,get 不调用 expire。 | 已解决并汇报 |
| 技术 | 是否修改旧 recorder 以复用新 store? | evidence-driven | 旧链路大量测试依赖 JPA preview/evidence_refs;本阶段零消费者,保持旧 recorder 不变,避免行为回归。 | 已解决并汇报 |
## Grill 结论
- 所有术语、边界、验收和技术问题均由 ISS、阶段规格、旧代码和 Redis 配置事实证明。
- 没有新增产品偏好或兼容性取舍需要 user-interview;并发更新窗口作为已接受风险记录。
- `grill-with-docs` 的代码可证结论已回写 proposal;没有未确认问题。
## 能力与工具限制
- Discover 能力来源:`sm-flow` + `grill-with-docs`。
- 当前无 `codebase-retrieval`/LSP;使用 `rg`、源码、既有测试、本地 Redis 配置和 focused fake tests 进行等价核对。
## Cross-artifact 对齐
| 链路 | 状态 | 结论 |
|---|---|---|
| brief 目标/范围/非目标 -> proposal | 已对齐 | Boundary、canonical record、TTL/size、Fake tests 和 legacy isolation 全部覆盖。 |
| proposal 范围/约束/承诺 -> design | 已对齐 | Redis value、状态机、preflight 顺序、raw/projection 和失败处理均已设计。 |
| design 决策/接口影响/风险 -> specs/tasks | 已对齐 | L2 boundary、TTL 更新窗口、oversize/error、ID/Run 所有权有对应要求和任务。 |
| specs 可观察行为 -> tasks | 已对齐 | 7 条 requirements 分解为 store、boundary、limits/failure 和隔离验证纵向切片。 |
## Architecture Audit
- 能力来源:`zoom-out`,按 RunContext、Invocation Status、Evidence Status、canonical invocation 和 durable audit 术语审计。
- ToolBoundary 只编排一次调用;CanonicalInvocationStore 独占 Redis 状态转换;DiagnosisHarnessCore 独占 Run budget/cancellation;旧 recorder 只写 JPA audit。
- request/raw/agent result 归同一 canonical record,Agent 只获得 ToolBoundaryResult,不存在 raw 旁路。
- Redis adapter 是唯一 Redis 访问点,接口/Fake 不依赖 Redis;后续 3B/3C 可直接复用而不复制状态机。
- 风险集中在 read-TTL-write 并发窗口和 canonical raw 敏感性,已进入 design/spec/limits,无架构或 ADR 冲突。
## Commit Gate Preflight
- proposal、design、specs、tasks 完整,OpenSpec status complete,strict validation 通过。
- question pool 无未汇报 evidence-driven 或未确认 user-interview 项。
- L2 内部接口影响已记录;旧 JPA/Chat/Controller/协议不改。
- cross-artifact 无 gap,所有错误、TTL、size、ID/Run 所有权和隔离要求可由 Fake tests 验证。
- Apply 已获持续授权,范围只包括新 store/boundary 与 focused tests。
## Pre-apply Research
### 参考实现与复用
- 复用 `DiagnosisHarnessCore` 的 active/deadline/Tool budget/Run bytes 门禁。
- 复用 `ToolCallKeyFactory` 精确保留框架 Tool Call ID 并隔离 Run key。
- 复用阶段 0 `InvocationStatus` / `EvidenceStatus`,不创建字符串状态副本。
- 复用 `SessionConfiguration` 提供的 `RedisTemplate<String,Object>` 和 Spring Boot ObjectMapper。
- 旧 `ToolInvocationRecorder`/JPA preview 仅作为 durable audit 反例,本阶段不修改或调用。
### 技术栈清单
- canonical record:Java 17 record + Jackson JSON String,所有状态组合在 record transition 方法中校验。
- Redis create:`ValueOperations.setIfAbsent` + TTL;update:读取当前 remaining TTL 后写回;get 不调用 expire。
- 大小:UTF-8 bytes;record/agent result/store limits 与 RunContext 累计 capacity 双重门禁。
- 测试:Mockito RedisTemplate/ValueOperations 验证 TTL API;In-memory fake store + Fake Tool/Projector 验证 boundary,不连接外部 Redis。
### 新建类型
- canonical model/limits/store/exceptions/Redis adapter。
- ToolCallRequestEnvelope、ToolBoundaryResult、ProjectedToolResult、ToolExecutor、ToolResultProjector、ToolBoundary 和稳定 error codes。
- Redis adapter tests 与 boundary fake tests。
### 影响半径
- 新 package 在阶段 3A 保持零现有消费者。
- Redis 访问只允许出现在 `RedisCanonicalInvocationStore`;旧 Chat/AIOps/Controller/JPA/Recorder 不修改。
## Apply 结果
- 冲突分类:一次 focused test 断言错误(duplicate 场景应调用 `setIfAbsent` 两次)已修正并复跑通过;无规格偏离。
- 新增 canonical record/limits/store exceptions、Redis JSON adapter、ToolBoundary envelope/result/interfaces 和 stable error codes。
- ToolBoundary 已实现 preflight、Core budget/capacity、PROJECTING、raw/projection size、READY/ERROR 和安全返回边界;未接具体 projector。
- 首模块对齐:store/boundary/limits/failure 与 design/tasks 全部完成;RAG/log/MySQL adapters 仍留给后续阶段。
## Apply 验证
- 编译:`mvn -q -DskipTests compile`:通过。
- Store/Boundary focused:`mvn -q '-Dtest=CanonicalInvocationStoreTest,ToolBoundaryTest' test`:通过。
- 综合回归:`mvn -q '-Dtest=CanonicalInvocationStoreTest,ToolBoundaryTest,HarnessContractTest,RagToolContractTest,QueryLogsToolContractTest,MysqlToolContractTest,RunContextTest,RunBudgetTest,DiagnosisHarnessCoreTest,HarnessRetryExecutorTest,ToolCallKeyFactoryTest,SpringAiRetryConfigurationTest,ChatControllerTest' test`:通过。
- 静态隔离:新 Harness 包仅 `RedisCanonicalInvocationStore` 引用 Redis;legacy recorder/JPA/Chat/AIOps/Controller/repository/resources diff 为空。
- OpenSpec:`openspec validate single-react-tool-invocation-store --strict`:通过。
@@ -0,0 +1,20 @@
# Evidence: single-react-tool-invocation-store
## 文档与代码证据
- ISS-014 阶段 3A 明确要求统一 ToolBoundary、canonical invocation、PROJECTING/READY/ERROR、TTL/容量、ID/Run 所有权,且不实现 3B/3C projector。
- 阶段 2 已提供 `RunContext`、Run bytes capacity 和 `ToolCallKeyFactory`,本阶段直接复用。
- 旧 `ToolInvocationRecorder` 使用 JPA preview 和 ThreadLocal fallback;新 canonical record 独立保存完整 request/raw/agent,不修改旧 recorder/JPA。
- 既有 `SessionConfiguration` 提供 `RedisTemplate<String,Object>` JSON bean;`RedisCanonicalInvocationStore` 是新 Harness 包唯一 Redis 引用。
## Evidence-driven 结论
- `setIfAbsent` 确保同一 `runId+toolCallId` 不覆盖;读取不调用 expire;更新使用剩余 TTL。
- canonical record transition 只允许 PROJECTING -> READY/ERROR;READY 只接受 FOUND/NO_EVIDENCE,ERROR 不可引用。
- ToolBoundary 在执行前校验 Run、ID、JSON、授权、只读和预算;raw 只在可信 store 保存,不返回 Agent。
- UTF-8 record/Agent limits 与 Run capacity 双门禁;raw oversize 跳过 projector,Agent oversize 不返回,均产生 RESULT_TOO_LARGE。
- Fake store/Redis mock tests 已覆盖 duplicate、cross-run、unauthorized、writable、execution/projection error、NO_EVIDENCE、TTL、raw/agent oversize。
## 工具限制
- 当前无 `codebase-retrieval`/LSP;使用 `rg`、源码、Maven 编译、Mockito Redis API 和 in-memory fake 完成等价验证。