diff --git a/devflow/index.md b/devflow/index.md index 9349e17..1016a1e 100644 --- a/devflow/index.md +++ b/devflow/index.md @@ -4,6 +4,7 @@ | 日期 | slug | 说明 | 领域 | 关键词 | 关联 OpenSpec | 状态 | |---|---|---|---|---|---|---| +| 2026-07-21 | single-react-aci-tool-contracts | 冻结 RAG、日志和 MySQL evidence Tool 的 Agent-facing ACI Schema、状态、框架调用引用和描述边界。 | Harness/Agent Tool contract | ISS-014, ACI, tool_call_id, evidence_status, RAG, query_logs, query_mysql, MOCK | openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts | archived | | 2026-07-21 | single-react-design-freeze | 冻结单体 Diagnosis Agent、Harness、Guard、工具证据与阶段门禁契约。 | Chat/Harness/Agent contract | ISS-014, single ReactAgent, Harness, EvidenceGuard, SemanticGuard, tool_call_id, evidence_status | openspec/changes/archive/2026-07-21-single-react-design-freeze | archived | | 2026-07-10 | session-run-trace-isolation | 拆分会话态和运行态,引入 runId 隔离 Trace、Feedback、AIOps 和 demo 链路。 | Trace/session/run isolation | chat_session, diagnosis_run, runId, trace exact run, feedback fallback, AIOps SSE metadata, baseline drift | openspec/changes/archive/2026-07-10-session-run-trace-isolation | archived | | 2026-07-09 | interview-demo-quality-audit | 增加面试演示前置质量审计,覆盖 prompt、Gatekeeper 和评测基线。 | Agent eval/demo/Prompt audit | interview demo preflight, prompt_audit, gatekeeper rules, diagnosis baseline, 12 fixtures | openspec/changes/archive/2026-07-09-interview-demo-quality-audit | archived | diff --git a/devflow/projects/2026-07-21-single-react-aci-tool-contracts/acceptance.md b/devflow/projects/2026-07-21-single-react-aci-tool-contracts/acceptance.md new file mode 100644 index 0000000..4286ab3 --- /dev/null +++ b/devflow/projects/2026-07-21-single-react-aci-tool-contracts/acceptance.md @@ -0,0 +1,44 @@ +# Acceptance: single-react-aci-tool-contracts + +## 实现结果 + +- RAG Contract:最小 query Request、bounded document evidence Result。 +- Log Contract:逻辑 Topic/Lookback Request、Mock provenance、Scope/Pattern/Event Result。 +- MySQL Contract:逻辑 data source、参数化 SQL Request、bounded structured rows Result。 +- 共享 Contract:snake_case Tool 名称、ACI 描述、不可变集合 helper,复用阶段 0 两套状态枚举。 +- 旧 Tool、Chat/AIOps、Controller、持久化、数据源和公开协议未修改。 + +## 静态验证 + +- `openspec validate single-react-aci-tool-contracts --strict`:通过。 +- `openspec instructions apply --change single-react-aci-tool-contracts --json`:12/12 tasks complete。 +- `rg` 引用检查:新 contract 生产包未接入旧运行链路。 +- 受保护文件 diff scope:旧 Tool、Chat/AIOps、Controller、Repository、resources 均为空。 + +## 脚本验证 + +- `mvn -q -DskipTests compile`:通过。 +- `mvn -q '-Dtest=RagToolContractTest,QueryLogsToolContractTest,MysqlToolContractTest' test`:通过。 +- `mvn -q '-Dtest=HarnessContractTest,LookupKnowledgeToolTest,QueryLogsToolsTest' test`:通过。 + +## 浏览器/人工验证 + +- 不适用。本阶段无 UI、Controller、SSE 或公开运行行为变化。 + +## 未验证 + +- 未执行 live LLM/Redis/CLS/MySQL E2E;本阶段没有接入这些运行路径,最终 live E2E 按 ISS-014 门禁留到阶段 7。 +- 未验证 ToolInterceptor 的真实 ID 传播;阶段 2/3A 必须以 `ToolCallRequest.getToolCallId()` 添加集成测试。 +- 未验证真实日志 adapter 的 `SourceKind` 扩展;本 Issue 首版明确只使用 Mock。 +- Provider 侧旧凭据轮换仍需凭据所有者完成,仓库只能证明明文已移除。 + +## 剩余风险与后续门禁 + +- 新旧 Contract 短期并存,阶段 3B/3C 接入前不得声称旧 Tool 已符合新 ACI 输出。 +- 下一阶段只能在本 change OpenSpec Archive 和 Git commit 完成后开始。 + +## 状态 + +- Stage acceptance: accepted +- OpenSpec archive: archived at `openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts` +- Main spec sync: `openspec/specs/aci-evidence-tool-contracts/spec.md`(7 added requirements) diff --git a/devflow/projects/2026-07-21-single-react-aci-tool-contracts/brief.md b/devflow/projects/2026-07-21-single-react-aci-tool-contracts/brief.md new file mode 100644 index 0000000..75945dd --- /dev/null +++ b/devflow/projects/2026-07-21-single-react-aci-tool-contracts/brief.md @@ -0,0 +1,32 @@ +# Brief: single-react-aci-tool-contracts + +## 背景 + +旧 RAG 和日志 Tool 暴露检索/基础设施细节与不一致状态,MySQL Tool 尚无 Agent-facing 类型。Harness 实现前需要先冻结三类最小 ACI 契约。 + +## 目标 + +- 冻结 RAG、日志、MySQL 的 Request/Result JSON Schema。 +- 统一 `evidence_status`,并保持其与 invocation lifecycle 独立。 +- 统一使用框架 `tool_call_id`,禁止模型传入或 Harness 生成第二套 ID。 +- 冻结简短 Tool 名称/描述和 Mock 日志来源边界。 +- 用三个独立契约测试锁定行为。 + +## 范围 + +- 新增 `com.superbiz.agent.harness.tool.contract` 值对象、枚举和描述常量。 +- 复用阶段 0 的 `InvocationStatus` 与 `EvidenceStatus`。 +- 验证 JSON、不可变集合、描述泄漏和新旧边界。 + +## 非目标 + +- 不切换旧 RAG/日志运行方法或 Chat/AIOps 注册。 +- 不实现投影、Redis store、真实日志适配器或 MySQL 执行。 +- 不修改 Controller/SSE 或公开协议。 + +## 元数据 + +- 分档:standard +- 接口影响:L2 前置内部契约;当前运行行为无变化 +- 关联 Issue:ISS-014 阶段 1 +- 关联 OpenSpec:`openspec/changes/single-react-aci-tool-contracts` diff --git a/devflow/projects/2026-07-21-single-react-aci-tool-contracts/decisions.md b/devflow/projects/2026-07-21-single-react-aci-tool-contracts/decisions.md new file mode 100644 index 0000000..072bf51 --- /dev/null +++ b/devflow/projects/2026-07-21-single-react-aci-tool-contracts/decisions.md @@ -0,0 +1,116 @@ +# Decisions: single-react-aci-tool-contracts + +## 规模与入口 + +- 分档:standard。 +- 入口:ISS-014 阶段 1,前置 `single-react-design-freeze` 已 Archive 并由 Git commit `58c3910` 固化。 +- 目标:冻结三类 evidence Tool 的 Agent-facing ACI 契约,不接入新运行链路。 + +## Context + +- `devflow/index.md` 命中 `single-react-design-freeze`、`modular-rag-pipeline` 和 `evidence-trace-hardening`。 +- `devflow/glossary/CONTEXT.md` 已定义 Diagnosis Harness、Invocation Status、Evidence Status 与 Evidence Tools。 +- 阶段 0 已确认 `tool_call_id` 使用框架 ID、两套状态语义分离、阶段串行门禁和阶段 6B 才公开切换。 +- 未发现根目录旧 `CONTEXT.md` 与 glossary 冲突。 + +## Question Pool + +| 维度 | 问题 | 模式 | 证据与结论 | 状态 | +|---|---|---|---|---| +| 术语 | invocation lifecycle 与 evidence result 是否使用同一状态? | evidence-driven | ISS-014 5.2、阶段 0 contract 明确分离;分别复用 `InvocationStatus` 与 `EvidenceStatus`。 | 已解决并汇报 | +| 术语 | `tool_call_id` 由谁生成、从哪里取得? | evidence-driven | Spring AI `AssistantMessage.ToolCall.id()` 与 Alibaba `ToolCallRequest.getToolCallId()` 提供框架 ID;普通 `ToolContext` 不自动加入该 ID。Harness 不生成第二套 ID。 | 已解决并汇报 | +| 边界 | 阶段 1 是否直接改旧 RAG/日志执行签名与返回值? | evidence-driven | ISS-014 阶段 1 只冻结 Contract,投影在阶段 3B、公开切换在阶段 6B;本阶段只新增契约代码和测试。 | 已解决并汇报 | +| 边界 | 日志阶段是否实现真实 CLS/MCP 或保留 Topic discovery? | evidence-driven | ISS-014 8.1 明确继续 Mock、删除 Agent 侧 discovery、真实适配器不在本 Issue 提前设计。 | 已解决并汇报 | +| 验收 | 如何证明契约已冻结且有界? | evidence-driven | 对三类独立 DTO 做精确 JSON、不可变集合、状态和描述泄漏测试;不以旧 Tool 集成测试代替。 | 已解决并汇报 | +| 技术 | 框架 ID 能否在后续 Harness 边界取得? | evidence-driven | 本地依赖 Spring AI Alibaba 1.1.2.0 暴露 `ToolInterceptor.interceptToolCall(ToolCallRequest, ToolCallHandler)`,request 含 `toolCallId`。 | 已解决并汇报 | + +## Grill 结论 + +- 术语、边界、验收三类问题均已由代码、依赖 API、阶段 0 档案和 ISS-014 证明。 +- 没有需要新增用户偏好或风险取舍的 `user-interview` 问题;不代理确认任何新方向。 +- `grill-with-docs` 要求的代码可证问题已先查证;结论已向用户汇报。 +- Proposal 已回写框架 ID 接入点、旧运行链路不切换、Mock 边界和 L2 接口影响。 + +## 已确认决策 + +- DTO 放在 Harness 的 Tool Contract 边界,复用阶段 0 的共享状态枚举,不在旧 `dto` 包继续堆叠协议。 +- 三类结果只携带 `evidence_status`,canonical invocation 的 `status` 保持独立;阶段 1 通过测试冻结枚举,不提前定义存储实现。 +- Tool description 使用代码常量冻结,后续 Tool adapter 注册时复用;旧 `@Tool` 注解本阶段不改,避免提前改变运行行为。 +- RAG 输入仅保留 `query`;日志输入仅保留逻辑 `topic/query/lookback_minutes`;MySQL 输入仅保留逻辑 `data_source/sql/params`。 +- 日志 `source_kind` 首版固定支持 `MOCK` 契约值,但保留 enum 扩展位置给后续真实适配器 change 审查。 + +## 能力与工具限制 + +- Discover 能力来源:`sm-flow` + `grill-with-docs`。 +- 仓库要求的 `codebase-retrieval` 和 LSP 工具在当前工具集中不可用;已用 `rg` 引用搜索、源码阅读和本地依赖 `javap` 补足事实核对。该限制不改变契约方向,但后续 Apply 仍需通过编译和引用测试验证。 + +## Cross-artifact 对齐 + +| 链路 | 状态 | 结论 | +|---|---|---| +| brief 目标/范围/非目标 -> proposal | 已对齐 | 三类 DTO、状态、框架 ID、短描述和不切旧运行链路均有对应。 | +| proposal 范围/约束/承诺 -> design | 已对齐 | 包边界、ID 来源、record/defensive copy、三类 Schema 和迁移顺序均已设计。 | +| design 决策/接口影响/风险 -> specs/tasks | 已对齐 | L2 边界、字段、状态、描述、Mock provenance 和运行不切换均有可验证 requirement 与任务。 | +| specs 可观察行为 -> tasks | 已对齐 | 每类 Contract 都有实现与独立测试,另有状态、回归和 diff scope 验证。 | + +## Architecture Audit + +- 能力来源:`zoom-out`,以项目 glossary 的 Diagnosis Harness、Evidence Tools、Invocation Status 和 Evidence Status 术语审计。 +- 当前输入到输出链路仍为 `ChatController/ChatService` 或 `AiOpsService -> ReactAgent -> LookupKnowledgeTool/QueryLogsTools -> 旧结果`,新契约没有运行消费者。 +- 未来链路为 `Diagnosis Agent -> Alibaba ToolInterceptor/Harness -> typed Request -> adapter/store/projector -> bounded Result -> Agent observation`,阶段 1 只占有 typed contract 边界。 +- 数据所有权保持明确:框架拥有 `tool_call_id`,canonical invocation 拥有生命周期,Tool-specific result 拥有证据语义与有界内容。 +- 主要耦合风险是新旧契约短期并存被误当成已迁移;通过独立包、无旧调用方修改和后续阶段门禁控制,无 ADR 冲突。 + +## Commit Gate Preflight + +- proposal、design、specs、tasks 文件完整,OpenSpec CLI 状态为 complete。 +- strict validation:`openspec validate single-react-aci-tool-contracts --strict` 通过。 +- question pool 中没有未汇报的 evidence-driven 结论或未确认的 user-interview 问题。 +- 接口影响为 L2,当前运行消费者零变更;阶段 6B 的 L4 切换保持独立。 +- cross-artifact 四段对齐无 gap,架构审计未发现需要回写的新实现约束。 +- Apply 已由用户对 ISS-014 全阶段的持续授权覆盖;仍严格限制在本 Committed OpenSpec tasks 内。 + +## Pre-apply Research + +### 参考实现 + +- `src/main/java/com/superbiz/agent/tool/LookupKnowledgeTool.java`:确认旧 RAG 暴露 `LookupResult`、ContextPack 和检索 Trace,本阶段不改。 +- `src/main/java/com/superbiz/agent/agent/tool/QueryLogsTools.java`:确认旧日志输入包含 region/logTopic/limit、存在 Topic discovery 和 mutable nested DTO,本阶段不改。 +- `src/test/java/com/superbiz/agent/tool/LookupKnowledgeToolTest.java`:现有 RAG 行为回归基线。 +- `src/test/java/com/superbiz/agent/agent/tool/QueryLogsToolsTest.java`:现有 Mock 日志行为回归基线。 +- `src/main/java/com/superbiz/agent/harness/contract/*.java`:Java 17 record、Jackson snake_case 和共享状态枚举风格。 + +### 技术栈清单 + +- 请求/响应标准:Java 17 record + Jackson `@JsonProperty`,集合构造时 defensive copy。 +- Tool 定义:当前使用 Spring AI `@Tool`,新名称/描述先以常量冻结,后续 adapter 注册复用。 +- 框架调用 ID:Spring AI Alibaba `ToolCallRequest.getToolCallId()`;普通 `ToolContext` 不作为 ID 来源。 +- 异常与校验:本阶段只冻结结构;Schema、ID、权限、范围和状态组合错误由后续 Pre-Tool/Projector 显式返回安全 `ERROR`。 +- MQ/Consumer/加密验签:本 change 不涉及。 + +### 新建类型 + +- `AgentToolContracts` 与 contract defensive-copy helper。 +- RAG Request/Result/Evidence。 +- Log Topic/SourceKind/Request/Result/Scope/Pattern/Event。 +- MySQL Request/Result。 +- 三个独立 contract test classes。 + +### 影响半径 + +- 新增包当前应无生产调用方;旧 Chat/AIOps、Tool、Controller、Repository 和配置文件均不修改。 +- 通过 focused compile/tests 和 `rg`/diff scope 证明边界。 + +## Apply 结果 + +- 冲突分类:未发现 OpenSpec 遗漏、代码偏离或方向不确定项。 +- 新增共享 ACI Tool 名称/描述、defensive-copy helper 和三类 typed Request/Result records。 +- 新增三个独立契约测试,覆盖精确 JSON、状态分离、框架 ID 原样保留、不可变集合、Mock provenance 和基础设施字段排除。 +- 首模块对齐:共享/RAG/日志/MySQL contract 与 design/tasks 全部完成;旧 runtime 接入保持 TODO,归属后续 3B/3C/6B changes。 + +## Apply 验证 + +- 编译:`mvn -q -DskipTests compile` 通过。 +- 新契约:`mvn -q '-Dtest=RagToolContractTest,QueryLogsToolContractTest,MysqlToolContractTest' test` 通过。 +- 旧行为回归:`mvn -q '-Dtest=HarnessContractTest,LookupKnowledgeToolTest,QueryLogsToolsTest' test` 通过。 +- 静态 scope:新 contract 生产类型当前无旧运行消费者;受保护的 Tool、Chat/AIOps、Controller、Repository 和配置文件 diff 为空。 diff --git a/devflow/projects/2026-07-21-single-react-aci-tool-contracts/evidence.md b/devflow/projects/2026-07-21-single-react-aci-tool-contracts/evidence.md new file mode 100644 index 0000000..0032fb1 --- /dev/null +++ b/devflow/projects/2026-07-21-single-react-aci-tool-contracts/evidence.md @@ -0,0 +1,27 @@ +# Evidence: single-react-aci-tool-contracts + +## 文档证据 + +- ISS-014 5.2/5.3 冻结 `evidence_status` 与框架 `tool_call_id`;6-9 节冻结 RAG、日志和 MySQL Agent-facing Schema。 +- ISS-014 阶段 1 明确只冻结 ACI Contract,不实现 Agent 架构切换、真实 CLS/MCP 或 MySQL 执行。 +- 阶段 0 OpenSpec 与 devflow 已冻结 `InvocationStatus`、`EvidenceStatus`、框架 ID 真理源和阶段 6B 才公开切换。 + +## 代码证据 + +- `LookupKnowledgeTool` 仍返回包含 ContextPack/Trace 的旧 `LookupResult`,证明需要新的 bounded RAG Contract,也证明本阶段未提前切换。 +- `QueryLogsTools` 仍暴露 region/logTopic/limit、Topic discovery 和旧 mutable DTO,证明逻辑 Topic/Scope/Mock provenance 契约的必要性。 +- `ChatService` 与 `AiOpsService` 仍引用旧 `LookupKnowledgeTool`/`QueryLogsTools`,新 contract 生产包当前没有旧运行消费者。 +- 本地 Spring AI 1.1.7 `AssistantMessage.ToolCall` 提供 `id()`;Spring AI Alibaba 1.1.2.0 `ToolCallRequest` 提供 `getToolCallId()` 与 `ToolInterceptor` 边界。 +- Spring AI 1.1.7 普通 `ToolContext` 只传递调用方 context/history,不自动提供当前 Tool Call ID,因此后续必须从 Alibaba interceptor request 接入。 + +## Evidence-driven 结论 + +- lifecycle 与 evidence result 必须保持两套正交状态;已汇报并进入 OpenSpec/代码测试。 +- `tool_call_id` 可以从当前框架 API 取得,Harness 无需也不得生成第二套 ID;已汇报并进入 OpenSpec。 +- 阶段 1 不改旧运行签名/返回;已通过引用和 diff scope 验证。 +- 日志首版保留 Mock 数据源但必须显式 `source_kind=MOCK`,不实现真实适配器;已进入 Log Contract。 +- 三类 Contract 的可验证口径是精确 JSON、不可变结果、短描述和基础设施字段排除;三个独立测试均通过。 + +## 工具限制 + +- 当前会话未提供 `codebase-retrieval` 或 LSP;使用 `rg` 引用搜索、源码阅读、本地依赖 `javap`、Maven 编译和 focused tests 完成等价核对。 diff --git a/mvp/issues/active/ISS-014-single-react-agent-harness-aci-ptk-refactor.md b/mvp/issues/active/ISS-014-single-react-agent-harness-aci-ptk-refactor.md index 360ee5e..081c296 100644 --- a/mvp/issues/active/ISS-014-single-react-agent-harness-aci-ptk-refactor.md +++ b/mvp/issues/active/ISS-014-single-react-agent-harness-aci-ptk-refactor.md @@ -1,6 +1,6 @@ # ISS-014 单体 ReAct Agent、Harness 与 ACI 工具瘦身 -**状态**:设计决策已确认,待阶段 0 冻结 +**状态**:实施中(阶段 0-1 已归档,下一阶段 2) **严重程度**:高 **发现时间**:2026-07-20 **目标分支**:`refactor/chat-single-react-harness` diff --git a/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/.archive-ready b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/.archive-ready new file mode 100644 index 0000000..746db3b --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/.archive-ready @@ -0,0 +1 @@ +Archive-ready after implementation and focused verification on 2026-07-21. diff --git a/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/.committed b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/.committed new file mode 100644 index 0000000..c501e20 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/.committed @@ -0,0 +1 @@ +Committed after strict validation on 2026-07-21. diff --git a/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/.openspec.yaml b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/.openspec.yaml new file mode 100644 index 0000000..c0a8162 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-21 diff --git a/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/design.md b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/design.md new file mode 100644 index 0000000..2817899 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/design.md @@ -0,0 +1,89 @@ +## Context + +当前 `LookupKnowledgeTool` 返回 `LookupResult`,包含 `ContextPack`、`RetrievalTrace`、`RerankTrace` 等内部检索信息;`QueryLogsTools` 暴露 region、TopicId、limit、Topic discovery 和旧 `success/total/logs` 结构。MySQL evidence Tool 尚不存在。阶段 0 已创建共享 `InvocationStatus` 与 `EvidenceStatus`,但尚无三类 Agent-facing DTO 或稳定 Tool 描述。 + +本地依赖验证显示,Spring AI 1.1.7 的 `AssistantMessage.ToolCall` 持有 `id`,Spring AI Alibaba 1.1.2.0 的 `ToolCallRequest` 将其暴露为 `getToolCallId()` 并允许通过 `ToolInterceptor` 包装调用;普通 Spring AI `ToolContext` 只包含调用方传入的 context 和 history,不自动提供当前 Tool Call ID。因此后续 Harness 必须以 Alibaba interceptor request 为 ID 接入边界。 + +## Goals / Non-Goals + +**Goals:** + +- 用 Java 类型冻结 RAG、日志和 MySQL 的最小 Agent Request/Result JSON Schema。 +- 复用统一证据状态,并保持 invocation lifecycle 与 evidence result 两套语义正交。 +- 保证所有可引用结果只携带框架 Tool Call ID,不生成替代 ID。 +- 冻结简短、面向行动的 Tool 名称和描述,禁止基础设施/审计实现泄漏。 +- 通过三个独立契约测试锁定字段、集合不可变性、描述边界和 Mock 来源标识。 + +**Non-Goals:** + +- 不修改旧 `@Tool` 方法、当前 Chat/AIOps 工具注册或返回行为。 +- 不实现 `ToolInterceptor`、Pre/Post Tool、Redis invocation store 或 ToolResultProjector。 +- 不实现真实 CLS/MCP、MySQL 连接、JSqlParser 校验、allowlist 或脱敏。 +- 不删除旧 DTO、Topic discovery 或审计服务;这些在后续切片接入和清理。 + +## Decisions + +### 1. 契约放在独立 Harness Tool 包 + +新增类型放在 `com.superbiz.agent.harness.tool.contract`,共享状态继续引用 `com.superbiz.agent.harness.contract`。这使新 Harness 边界与旧 `dto`、旧工具内部模型明确隔离。 + +替代方案是在旧工具类内新增嵌套 DTO;这会继续把运行实现与 Agent Contract 绑定,并妨碍三个 projector 复用,故不采用。 + +### 2. Tool Call ID 是结果字段,不是 Agent 输入字段 + +三类 Request 均不包含 `tool_call_id`。后续 Harness 从 `ToolCallRequest.getToolCallId()` 取得 ID,在投影 Result 时写入;模型不能选择或覆盖该值。`EVIDENCE_FOUND` 和 `NO_EVIDENCE` 结果必须具备合法 ID;如果 Pre-Tool 失败原因就是 ID 缺失/非法,`ERROR` 可以没有可引用 ID。 + +替代方案是让模型把 ID 作为参数回传,或由 Harness 生成 UUID;两者都会形成不可信输入或第二套标识,违反阶段 0 决策。 + +### 3. Result 直接携带 evidence status,不携带 invocation status + +`RagToolResult`、`QueryLogsToolResult` 和 `MysqlToolResult` 只包含 `EvidenceStatus`。`InvocationStatus` 继续描述 Redis canonical invocation 的 `PROJECTING/READY/ERROR` 生命周期,由阶段 3A 的存储模型承载。契约测试分别锁定两个枚举,防止把 `READY` 当成“找到证据”或把 `NO_EVIDENCE` 当成生命周期状态。 + +### 4. 使用 record 和 defensive copy + +Request/Result 使用 Java 17 record,并用 Jackson `@JsonProperty` 固定 snake_case。所有 list 和 row map 在构造时 defensive copy,避免后续投影、存储或测试在对象创建后改变 Agent-visible 结果。 + +替代方案是沿用 Lombok mutable bean;它更贴近旧代码,但无法天然表达冻结后的值对象边界。 + +### 5. 三类最小 Schema + +- RAG Request 只有 `query`;Result 只有查询回显、有界 evidence、计数和截断标记。 +- Logs Request 只有逻辑 `topic`、`query` 和可选 `lookback_minutes`;Result 保留 `source_kind`、完整 scope、聚合 patterns、少量 timeline events、计数和截断标记。 +- MySQL Request 只有逻辑 `data_source`、参数化 `sql` 和 `params`;Result 保留 columns、结构化 rows、计数和截断标记。 + +日志逻辑 Topic 首版冻结为 `APPLICATION`、`DATABASE_SLOW_QUERY`、`SYSTEM_EVENTS`。旧 `system-metrics` 不纳入日志 Contract;指标 Tool 不属于本 Issue。`SourceKind` 首版只有 `MOCK`,真实适配器以后在保持字段语义的前提下扩展。 + +### 6. Tool 描述作为常量契约 + +使用单一 `AgentToolContracts` 定义三个 snake_case Tool 名称和 ACI 描述。测试禁止描述中出现 Milvus、L0/L1、rerank、CLS region/TopicId、连接、凭据、topK、limit、Redis、Trace 等实现词。旧 `@Tool` 注解暂不引用这些常量,以免本阶段提前改变模型可观察行为。 + +## Module Flow + +```text +ReactAgent tool call + -> [later] Alibaba ToolInterceptor obtains framework tool_call_id + -> typed Request contract + -> [later] data adapter + canonical persistence + ToolResultProjector + -> typed bounded Result contract + -> Agent observation +``` + +阶段 1 只实现图中的 typed contracts 和描述常量。旧 `ChatService/AiOpsService -> LookupKnowledgeTool/QueryLogsTools` 调用链保持不变。 + +## Risks / Trade-offs + +- [新旧 Contract 短期并存,容易误判迁移已完成] -> 契约放独立包,proposal/spec/tasks 明确禁止本阶段改旧工具,并用 `rg` 验证旧注册路径未切换。 +- [DTO 不能自行保证 ID 来自框架] -> spec 明确 provenance,阶段 2/3 在 `ToolInterceptor` 测试真实 `ToolCallRequest.getToolCallId()` 传播。 +- [record 只做结构冻结,不做业务校验] -> 参数范围、SQL allowlist、ID 格式和状态组合由后续 Pre-Tool/Projector validator 实现;本阶段不在构造器复制安全策略。 +- [逻辑 Topic 与旧 TopicId 需要映射] -> mapping 属于日志 adapter/projector 阶段,Agent Contract 不接受 region 或物理 TopicId。 + +## Migration Plan + +1. 本阶段新增契约和测试,不切换消费者;回滚只需删除新增类型。 +2. 阶段 2/3A 从 Alibaba `ToolInterceptor` 接入框架 ID 和 invocation lifecycle。 +3. 阶段 3B/3C 分别让 RAG/日志/MySQL adapter 输出本契约。 +4. 阶段 6B 原子切换公开 Chat;旧 Contract 最终由阶段 7 清理。 + +## Open Questions + +无。真实日志 adapter 的 `SourceKind` 扩展和值映射在后续接入 change 决策,不阻塞本阶段。 diff --git a/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/proposal.md b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/proposal.md new file mode 100644 index 0000000..fbee4af --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/proposal.md @@ -0,0 +1,43 @@ +## Why + +现有 `lookup_knowledge` 和 `query_logs` 将检索实现、基础设施参数、审计数据和不一致的成功语义暴露给 Agent;新增 MySQL Tool 也缺少可复用的 Agent-facing 类型。进入 Harness 和投影实现前,需要先把三类 evidence Tool 的最小输入、稳定有界输出、状态语义与框架 Tool Call 引用冻结为代码契约,避免后续阶段继续依赖字符串和旧 DTO。 + +## What Changes + +- 新增 RAG、日志和只读 MySQL 三类 Agent-facing Request/Result 契约,JSON 字段严格使用 ISS-014 已确认的 snake_case Schema。 +- 三类结果统一复用 `EvidenceStatus`,并携带框架提供的 `tool_call_id`;契约代码不生成、替换或推导第二套调用 ID。 +- 冻结 `InvocationStatus=PROJECTING/READY/ERROR` 与 `EvidenceStatus=EVIDENCE_FOUND/NO_EVIDENCE/ERROR` 的独立语义,并通过测试禁止混用。 +- 冻结三类 Tool 的短名称与 ACI 描述,描述只说明用途、输入和禁用场景,不泄露 L0/L1、Milvus、CLS region/TopicId、连接、凭据、topK、limit、rerank 或审计实现。 +- 冻结日志 `source_kind=MOCK` 及逻辑 Topic 边界,为后续真实适配器保留同一 Contract;本阶段不新增 CLS/MCP 适配器。 +- 添加三类独立契约测试,覆盖序列化字段、不可变集合、状态与描述边界。 +- 本阶段不修改旧 Tool 的执行签名、返回值或 Chat/AIOps 注册路径,不实现 ResultProjector、Harness invocation store、MySQL SQL 校验或数据库访问。 + +## Capabilities + +### New Capabilities + +- `aci-evidence-tool-contracts`: 定义 RAG、日志和 MySQL evidence Tool 的 Agent-facing ACI Schema、状态语义、框架调用引用和描述边界。 + +### Modified Capabilities + +- None. 当前公开运行链路仍使用旧 Tool Contract;新契约将在后续 Harness/Tool 投影和 SSE 切换 change 中接入。 + +## Context Constraints + +- `tool_call_id` 的真理源是 Spring AI Alibaba `ToolCallRequest.getToolCallId()`;普通 Spring AI `ToolContext` 不保证包含该 ID。 +- `NO_EVIDENCE` 只表示当前查询范围没有匹配证据,只能支持 `NEGATIVE_OBSERVATION`,不能表达工具失败或系统健康。 +- 生命周期 `status` 属于 canonical invocation,`evidence_status` 属于 Agent-facing 查询结果,两者不得互相替代。 +- Agent 不可控制日志 region/TopicId/limit、RAG topK/filter 或 MySQL 连接与资源上限。 +- Mock 日志必须显式保留 `source_kind=MOCK`,不得被后续 Harness 表述为生产实时事实。 + +## Interface Impact + +- 等级:L2(内部接口,前置冻结)。本 change 新增未来 Harness 内部使用的 Agent-facing DTO 和描述常量,所有消费者均在 ISS-014 后续实施范围内。 +- 当前运行中的旧 Tool 方法、Controller、ChatService 和 AiOpsService 不切换,因此本阶段没有对外可观察行为变化。 +- 阶段 6B 切换公开 `/api/chat` 时属于独立的 L4 破坏性变更,必须使用该阶段自己的迁移和回滚规格。 + +## Risks + +- 仅有 DTO 不能证明框架 ID 已贯穿执行;阶段 2/3 必须在 Alibaba `ToolInterceptor` 边界接收并校验 `ToolCallRequest.getToolCallId()`。 +- 新旧 Contract 会短期并存;旧运行工具不得被误认为已符合新 ACI 输出,真正接入留给 RAG/日志投影和 MySQL Tool 阶段。 +- Provider 侧旧凭据轮换仍是外部安全前置,不因本阶段契约完成而视为关闭。 diff --git a/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/specs/aci-evidence-tool-contracts/spec.md b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/specs/aci-evidence-tool-contracts/spec.md new file mode 100644 index 0000000..d11ac23 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/specs/aci-evidence-tool-contracts/spec.md @@ -0,0 +1,70 @@ +## ADDED Requirements + +### Requirement: Evidence result semantics SHALL be distinct from invocation lifecycle +The system SHALL expose `EVIDENCE_FOUND`, `NO_EVIDENCE`, and `ERROR` as Agent-facing evidence result semantics while retaining `PROJECTING`, `READY`, and `ERROR` only for canonical invocation lifecycle. `NO_EVIDENCE` SHALL mean that the executed query found no evidence within its recorded scope and SHALL NOT mean tool failure or system health. + +#### Scenario: Successful empty query result +- **WHEN** an evidence Tool completes successfully with no matching evidence in its recorded scope +- **THEN** its Agent-facing result uses `evidence_status=NO_EVIDENCE` and the invocation lifecycle may independently reach `status=READY` + +#### Scenario: Tool execution fails +- **WHEN** schema, authorization, execution, or projection fails +- **THEN** the Agent-facing result uses `evidence_status=ERROR` and SHALL NOT report `NO_EVIDENCE` + +### Requirement: Referencable Tool results SHALL use the framework Tool Call ID +Each `EVIDENCE_FOUND` or `NO_EVIDENCE` result SHALL contain the non-blank `tool_call_id` supplied by the framework Tool Call request. Agent inputs SHALL NOT contain `tool_call_id`, and contract code SHALL NOT generate, replace, or derive a second call ID. + +#### Scenario: Framework requests a Tool call +- **WHEN** Spring AI Alibaba exposes an `AssistantMessage.ToolCall.id` through `ToolCallRequest.getToolCallId()` +- **THEN** the bounded Agent result carries exactly that ID as `tool_call_id` + +#### Scenario: Framework ID is invalid +- **WHEN** the Tool request has a missing or invalid framework Tool Call ID +- **THEN** the call returns an `ERROR` result without inventing a referencable ID + +### Requirement: RAG Tool contract SHALL expose only bounded document evidence +The RAG Request SHALL contain only `query`. The RAG Result SHALL contain `evidence_status`, `tool_call_id`, `query`, bounded `evidence`, `returned_count`, and `truncated`; each evidence item SHALL contain only `document_id`, `source`, `title`, `breadcrumb`, and an exact `excerpt`. + +#### Scenario: RAG evidence is serialized +- **WHEN** a RAG result contains a matching document excerpt +- **THEN** its JSON matches the frozen snake_case fields and excludes ContextPack, RetrievalTrace, RerankTrace, raw scores, fallback attempts, metadata, and full document bodies + +#### Scenario: RAG query has no evidence +- **WHEN** RAG executes successfully without a usable document excerpt +- **THEN** it returns `NO_EVIDENCE`, preserves the original query and framework Tool Call ID, and returns an empty evidence list + +### Requirement: Log Tool contract SHALL use logical scope and retain Mock provenance +The log Request SHALL contain logical `topic`, `query`, and optional `lookback_minutes` only. The log Result SHALL contain `evidence_status`, `tool_call_id`, `source_kind`, complete query `scope`, `match_count`, `returned_count`, bounded `patterns`, bounded timeline `events`, and `truncated`. The initial logical topics SHALL be `APPLICATION`, `DATABASE_SLOW_QUERY`, and `SYSTEM_EVENTS`, and the initial source kind SHALL be `MOCK`. + +#### Scenario: Mock log evidence is serialized +- **WHEN** the existing Mock source returns matching application events +- **THEN** the Agent result records `source_kind=MOCK`, the logical query scope, aggregate patterns, bounded timeline events, and distinct match and returned counts + +#### Scenario: Agent creates a log request +- **WHEN** the Agent requests log evidence +- **THEN** it selects a logical topic and lookback window without supplying region, physical TopicId, credentials, or result limit and without calling a Topic discovery Tool first + +### Requirement: MySQL Tool contract SHALL expose a logical read-only query interface +The MySQL Request SHALL contain only logical `data_source`, parameterized `sql`, and `params`. The MySQL Result SHALL contain `evidence_status`, `tool_call_id`, `columns`, bounded structured `rows`, `returned_count`, and `truncated`; it SHALL NOT expose connection details, credentials, internal stack traces, or resource-limit controls. + +#### Scenario: MySQL evidence is serialized +- **WHEN** a future read-only adapter returns authorized rows +- **THEN** the Agent result preserves column order, structured row values, the framework Tool Call ID, returned count, and truncation state using the frozen JSON fields + +#### Scenario: Agent creates a MySQL request +- **WHEN** the Agent requests business database evidence +- **THEN** it supplies a logical data source, SQL placeholders, and parameter values without supplying JDBC connection information or security policy + +### Requirement: Tool descriptions SHALL be concise and implementation-neutral +The system SHALL define stable snake_case names and concise descriptions for `lookup_knowledge`, `query_logs`, and `query_mysql`. Each description SHALL state when to call the Tool, its minimal input, and what it cannot query, and SHALL NOT describe retrieval internals, infrastructure coordinates, credentials, audit storage, retries, result limits, or ranking implementation. + +#### Scenario: Agent receives Tool definitions +- **WHEN** a future Agent adapter registers the frozen Tool definitions +- **THEN** the definitions describe available actions and boundaries without exposing Milvus, L0/L1, rerank, CLS region/TopicId, Redis, JDBC credentials, topK, limit, or Trace internals + +### Requirement: Contract freeze SHALL NOT cut over the current runtime +This change SHALL add contract types, descriptions, and tests without changing the current `LookupKnowledgeTool`, `QueryLogsTools`, ChatService, AiOpsService, Controller, Tool registration, or Agent-visible runtime results. + +#### Scenario: Stage 1 tests pass +- **WHEN** all ACI contract tests pass +- **THEN** the current public Chat and AIOps paths still execute the old Tool implementations until their later projector and cutover changes diff --git a/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/tasks.md b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/tasks.md new file mode 100644 index 0000000..408cd17 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts/tasks.md @@ -0,0 +1,26 @@ +## 1. Shared ACI Contract + +- [x] 1.1 Add stable snake_case Tool names and concise implementation-neutral descriptions for RAG, logs, and MySQL. +- [x] 1.2 Reuse and lock the independent invocation lifecycle and evidence result enums without adding a second Tool Call ID type. +- [x] 1.3 Add a shared defensive-copy helper for immutable Agent-facing list and row values. + +## 2. RAG Contract + +- [x] 2.1 Implement the RAG Request, Result, and document evidence records with the frozen snake_case fields. +- [x] 2.2 Add an independent RAG contract test covering exact JSON fields, framework ID preservation, immutability, and description leakage. + +## 3. Log Contract + +- [x] 3.1 Implement logical log Topic, source kind, Request, Result, Scope, Pattern, and Event records. +- [x] 3.2 Add an independent log contract test covering Mock provenance, scope/count semantics, exact JSON fields, immutability, and excluded infrastructure inputs. + +## 4. MySQL Contract + +- [x] 4.1 Implement the logical MySQL Request and bounded Result records with immutable params, columns, and rows. +- [x] 4.2 Add an independent MySQL contract test covering parameterized input, structured output, exact JSON fields, deep immutability, and secret/connection exclusion. + +## 5. Verification + +- [x] 5.1 Run the three ACI contract test classes together and confirm lifecycle/evidence status separation. +- [x] 5.2 Run the stage 0 Harness contract and existing RAG/log focused tests to prove the old runtime path remains unchanged. +- [x] 5.3 Verify references and diff scope show no changes to old Tool methods, Chat/AIOps registration, Controller, persistence, datasource, or public protocol. diff --git a/openspec/specs/aci-evidence-tool-contracts/spec.md b/openspec/specs/aci-evidence-tool-contracts/spec.md new file mode 100644 index 0000000..6e791ba --- /dev/null +++ b/openspec/specs/aci-evidence-tool-contracts/spec.md @@ -0,0 +1,74 @@ +# aci-evidence-tool-contracts Specification + +## Purpose +定义 Diagnosis Harness 中 RAG、日志和只读 MySQL evidence Tool 的最小 Agent-facing ACI 契约,包括状态语义、框架 Tool Call 引用、逻辑输入、有界输出、Mock 来源标识和实现中立的 Tool 描述。 + +## Requirements +### Requirement: Evidence result semantics SHALL be distinct from invocation lifecycle +The system SHALL expose `EVIDENCE_FOUND`, `NO_EVIDENCE`, and `ERROR` as Agent-facing evidence result semantics while retaining `PROJECTING`, `READY`, and `ERROR` only for canonical invocation lifecycle. `NO_EVIDENCE` SHALL mean that the executed query found no evidence within its recorded scope and SHALL NOT mean tool failure or system health. + +#### Scenario: Successful empty query result +- **WHEN** an evidence Tool completes successfully with no matching evidence in its recorded scope +- **THEN** its Agent-facing result uses `evidence_status=NO_EVIDENCE` and the invocation lifecycle may independently reach `status=READY` + +#### Scenario: Tool execution fails +- **WHEN** schema, authorization, execution, or projection fails +- **THEN** the Agent-facing result uses `evidence_status=ERROR` and SHALL NOT report `NO_EVIDENCE` + +### Requirement: Referencable Tool results SHALL use the framework Tool Call ID +Each `EVIDENCE_FOUND` or `NO_EVIDENCE` result SHALL contain the non-blank `tool_call_id` supplied by the framework Tool Call request. Agent inputs SHALL NOT contain `tool_call_id`, and contract code SHALL NOT generate, replace, or derive a second call ID. + +#### Scenario: Framework requests a Tool call +- **WHEN** Spring AI Alibaba exposes an `AssistantMessage.ToolCall.id` through `ToolCallRequest.getToolCallId()` +- **THEN** the bounded Agent result carries exactly that ID as `tool_call_id` + +#### Scenario: Framework ID is invalid +- **WHEN** the Tool request has a missing or invalid framework Tool Call ID +- **THEN** the call returns an `ERROR` result without inventing a referencable ID + +### Requirement: RAG Tool contract SHALL expose only bounded document evidence +The RAG Request SHALL contain only `query`. The RAG Result SHALL contain `evidence_status`, `tool_call_id`, `query`, bounded `evidence`, `returned_count`, and `truncated`; each evidence item SHALL contain only `document_id`, `source`, `title`, `breadcrumb`, and an exact `excerpt`. + +#### Scenario: RAG evidence is serialized +- **WHEN** a RAG result contains a matching document excerpt +- **THEN** its JSON matches the frozen snake_case fields and excludes ContextPack, RetrievalTrace, RerankTrace, raw scores, fallback attempts, metadata, and full document bodies + +#### Scenario: RAG query has no evidence +- **WHEN** RAG executes successfully without a usable document excerpt +- **THEN** it returns `NO_EVIDENCE`, preserves the original query and framework Tool Call ID, and returns an empty evidence list + +### Requirement: Log Tool contract SHALL use logical scope and retain Mock provenance +The log Request SHALL contain logical `topic`, `query`, and optional `lookback_minutes` only. The log Result SHALL contain `evidence_status`, `tool_call_id`, `source_kind`, complete query `scope`, `match_count`, `returned_count`, bounded `patterns`, bounded timeline `events`, and `truncated`. The initial logical topics SHALL be `APPLICATION`, `DATABASE_SLOW_QUERY`, and `SYSTEM_EVENTS`, and the initial source kind SHALL be `MOCK`. + +#### Scenario: Mock log evidence is serialized +- **WHEN** the existing Mock source returns matching application events +- **THEN** the Agent result records `source_kind=MOCK`, the logical query scope, aggregate patterns, bounded timeline events, and distinct match and returned counts + +#### Scenario: Agent creates a log request +- **WHEN** the Agent requests log evidence +- **THEN** it selects a logical topic and lookback window without supplying region, physical TopicId, credentials, or result limit and without calling a Topic discovery Tool first + +### Requirement: MySQL Tool contract SHALL expose a logical read-only query interface +The MySQL Request SHALL contain only logical `data_source`, parameterized `sql`, and `params`. The MySQL Result SHALL contain `evidence_status`, `tool_call_id`, `columns`, bounded structured `rows`, `returned_count`, and `truncated`; it SHALL NOT expose connection details, credentials, internal stack traces, or resource-limit controls. + +#### Scenario: MySQL evidence is serialized +- **WHEN** a future read-only adapter returns authorized rows +- **THEN** the Agent result preserves column order, structured row values, the framework Tool Call ID, returned count, and truncation state using the frozen JSON fields + +#### Scenario: Agent creates a MySQL request +- **WHEN** the Agent requests business database evidence +- **THEN** it supplies a logical data source, SQL placeholders, and parameter values without supplying JDBC connection information or security policy + +### Requirement: Tool descriptions SHALL be concise and implementation-neutral +The system SHALL define stable snake_case names and concise descriptions for `lookup_knowledge`, `query_logs`, and `query_mysql`. Each description SHALL state when to call the Tool, its minimal input, and what it cannot query, and SHALL NOT describe retrieval internals, infrastructure coordinates, credentials, audit storage, retries, result limits, or ranking implementation. + +#### Scenario: Agent receives Tool definitions +- **WHEN** a future Agent adapter registers the frozen Tool definitions +- **THEN** the definitions describe available actions and boundaries without exposing Milvus, L0/L1, rerank, CLS region/TopicId, Redis, JDBC credentials, topK, limit, or Trace internals + +### Requirement: Contract freeze SHALL NOT cut over the current runtime +This change SHALL add contract types, descriptions, and tests without changing the current `LookupKnowledgeTool`, `QueryLogsTools`, ChatService, AiOpsService, Controller, Tool registration, or Agent-visible runtime results. + +#### Scenario: Stage 1 tests pass +- **WHEN** all ACI contract tests pass +- **THEN** the current public Chat and AIOps paths still execute the old Tool implementations until their later projector and cutover changes diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/AgentToolContracts.java b/src/main/java/com/superbiz/agent/harness/tool/contract/AgentToolContracts.java new file mode 100644 index 0000000..b641ffa --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/AgentToolContracts.java @@ -0,0 +1,26 @@ +package com.superbiz.agent.harness.tool.contract; + +public final class AgentToolContracts { + + public static final String LOOKUP_KNOWLEDGE = "lookup_knowledge"; + public static final String QUERY_LOGS = "query_logs"; + public static final String QUERY_MYSQL = "query_mysql"; + + public static final String LOOKUP_KNOWLEDGE_DESCRIPTION = + "查询内部知识库中的文档、接口说明、错误码和排障手册。" + + "适用于稳定背景知识,不用于查询实时日志、指标或数据库状态。" + + "输入 query:需要查询的问题或关键词。"; + + public static final String QUERY_LOGS_DESCRIPTION = + "查询指定逻辑日志主题在时间窗口内与目标相关的日志证据。" + + "适用于应用错误、慢查询和系统事件,不用于查询指标或数据库表。" + + "输入 topic、query、lookback_minutes。"; + + public static final String QUERY_MYSQL_DESCRIPTION = + "在授权的逻辑数据源上执行参数化只读查询,获取业务数据库事实。" + + "只用于已知库表字段的 SELECT,不用于发现表结构或执行写操作。" + + "输入 data_source、sql、params。"; + + private AgentToolContracts() { + } +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/LogEvent.java b/src/main/java/com/superbiz/agent/harness/tool/contract/LogEvent.java new file mode 100644 index 0000000..5e5f3a1 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/LogEvent.java @@ -0,0 +1,10 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record LogEvent( + @JsonProperty("timestamp") String timestamp, + @JsonProperty("level") String level, + @JsonProperty("service") String service, + @JsonProperty("message") String message) { +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/LogPattern.java b/src/main/java/com/superbiz/agent/harness/tool/contract/LogPattern.java new file mode 100644 index 0000000..53440d2 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/LogPattern.java @@ -0,0 +1,12 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record LogPattern( + @JsonProperty("count") long count, + @JsonProperty("first_seen") String firstSeen, + @JsonProperty("last_seen") String lastSeen, + @JsonProperty("level") String level, + @JsonProperty("service") String service, + @JsonProperty("example") String example) { +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/LogQueryScope.java b/src/main/java/com/superbiz/agent/harness/tool/contract/LogQueryScope.java new file mode 100644 index 0000000..5e73a05 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/LogQueryScope.java @@ -0,0 +1,10 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record LogQueryScope( + @JsonProperty("topic") LogTopic topic, + @JsonProperty("query") String query, + @JsonProperty("start_time") String startTime, + @JsonProperty("end_time") String endTime) { +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/LogSourceKind.java b/src/main/java/com/superbiz/agent/harness/tool/contract/LogSourceKind.java new file mode 100644 index 0000000..bf304d2 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/LogSourceKind.java @@ -0,0 +1,5 @@ +package com.superbiz.agent.harness.tool.contract; + +public enum LogSourceKind { + MOCK +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/LogTopic.java b/src/main/java/com/superbiz/agent/harness/tool/contract/LogTopic.java new file mode 100644 index 0000000..f828426 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/LogTopic.java @@ -0,0 +1,7 @@ +package com.superbiz.agent.harness.tool.contract; + +public enum LogTopic { + APPLICATION, + DATABASE_SLOW_QUERY, + SYSTEM_EVENTS +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/MysqlToolRequest.java b/src/main/java/com/superbiz/agent/harness/tool/contract/MysqlToolRequest.java new file mode 100644 index 0000000..7bae46a --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/MysqlToolRequest.java @@ -0,0 +1,15 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record MysqlToolRequest( + @JsonProperty("data_source") String dataSource, + @JsonProperty("sql") String sql, + @JsonProperty("params") List params) { + + public MysqlToolRequest { + params = ToolContractCollections.immutable(params); + } +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/MysqlToolResult.java b/src/main/java/com/superbiz/agent/harness/tool/contract/MysqlToolResult.java new file mode 100644 index 0000000..4611a9f --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/MysqlToolResult.java @@ -0,0 +1,21 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.superbiz.agent.harness.contract.EvidenceStatus; + +import java.util.List; +import java.util.Map; + +public record MysqlToolResult( + @JsonProperty("evidence_status") EvidenceStatus evidenceStatus, + @JsonProperty("tool_call_id") String toolCallId, + @JsonProperty("columns") List columns, + @JsonProperty("rows") List> rows, + @JsonProperty("returned_count") int returnedCount, + @JsonProperty("truncated") boolean truncated) { + + public MysqlToolResult { + columns = ToolContractCollections.immutable(columns); + rows = ToolContractCollections.immutableRows(rows); + } +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/QueryLogsRequest.java b/src/main/java/com/superbiz/agent/harness/tool/contract/QueryLogsRequest.java new file mode 100644 index 0000000..18f5531 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/QueryLogsRequest.java @@ -0,0 +1,9 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record QueryLogsRequest( + @JsonProperty("topic") LogTopic topic, + @JsonProperty("query") String query, + @JsonProperty("lookback_minutes") Integer lookbackMinutes) { +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/QueryLogsToolResult.java b/src/main/java/com/superbiz/agent/harness/tool/contract/QueryLogsToolResult.java new file mode 100644 index 0000000..e5d6b0c --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/QueryLogsToolResult.java @@ -0,0 +1,23 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.superbiz.agent.harness.contract.EvidenceStatus; + +import java.util.List; + +public record QueryLogsToolResult( + @JsonProperty("evidence_status") EvidenceStatus evidenceStatus, + @JsonProperty("tool_call_id") String toolCallId, + @JsonProperty("source_kind") LogSourceKind sourceKind, + @JsonProperty("scope") LogQueryScope scope, + @JsonProperty("match_count") long matchCount, + @JsonProperty("returned_count") int returnedCount, + @JsonProperty("patterns") List patterns, + @JsonProperty("events") List events, + @JsonProperty("truncated") boolean truncated) { + + public QueryLogsToolResult { + patterns = ToolContractCollections.immutable(patterns); + events = ToolContractCollections.immutable(events); + } +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/RagEvidence.java b/src/main/java/com/superbiz/agent/harness/tool/contract/RagEvidence.java new file mode 100644 index 0000000..972e195 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/RagEvidence.java @@ -0,0 +1,11 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record RagEvidence( + @JsonProperty("document_id") String documentId, + @JsonProperty("source") String source, + @JsonProperty("title") String title, + @JsonProperty("breadcrumb") String breadcrumb, + @JsonProperty("excerpt") String excerpt) { +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/RagToolRequest.java b/src/main/java/com/superbiz/agent/harness/tool/contract/RagToolRequest.java new file mode 100644 index 0000000..9e8794b --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/RagToolRequest.java @@ -0,0 +1,7 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record RagToolRequest( + @JsonProperty("query") String query) { +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/RagToolResult.java b/src/main/java/com/superbiz/agent/harness/tool/contract/RagToolResult.java new file mode 100644 index 0000000..e608e5f --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/RagToolResult.java @@ -0,0 +1,19 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.superbiz.agent.harness.contract.EvidenceStatus; + +import java.util.List; + +public record RagToolResult( + @JsonProperty("evidence_status") EvidenceStatus evidenceStatus, + @JsonProperty("tool_call_id") String toolCallId, + @JsonProperty("query") String query, + @JsonProperty("evidence") List evidence, + @JsonProperty("returned_count") int returnedCount, + @JsonProperty("truncated") boolean truncated) { + + public RagToolResult { + evidence = ToolContractCollections.immutable(evidence); + } +} diff --git a/src/main/java/com/superbiz/agent/harness/tool/contract/ToolContractCollections.java b/src/main/java/com/superbiz/agent/harness/tool/contract/ToolContractCollections.java new file mode 100644 index 0000000..0a4967b --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/tool/contract/ToolContractCollections.java @@ -0,0 +1,32 @@ +package com.superbiz.agent.harness.tool.contract; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class ToolContractCollections { + + private ToolContractCollections() { + } + + static List immutable(List values) { + return values == null ? List.of() : List.copyOf(values); + } + + static List> immutableRows(List> rows) { + if (rows == null) { + return List.of(); + } + return rows.stream() + .map(ToolContractCollections::immutableRow) + .toList(); + } + + private static Map immutableRow(Map row) { + if (row == null) { + return Map.of(); + } + return Collections.unmodifiableMap(new LinkedHashMap<>(row)); + } +} diff --git a/src/test/java/com/superbiz/agent/harness/tool/contract/MysqlToolContractTest.java b/src/test/java/com/superbiz/agent/harness/tool/contract/MysqlToolContractTest.java new file mode 100644 index 0000000..c78d202 --- /dev/null +++ b/src/test/java/com/superbiz/agent/harness/tool/contract/MysqlToolContractTest.java @@ -0,0 +1,73 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.superbiz.agent.harness.contract.EvidenceStatus; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MysqlToolContractTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void freezesLogicalParameterizedRequestWithoutConnectionData() { + List params = new ArrayList<>(List.of("order-123")); + MysqlToolRequest request = new MysqlToolRequest( + "order_readonly", + "SELECT order_id, payment_status FROM biz_order WHERE order_id = ?", + params); + params.clear(); + + JsonNode json = objectMapper.valueToTree(request); + assertEquals(Set.of("data_source", "sql", "params"), ToolContractAssertions.fieldNames(json)); + assertEquals(1, json.path("params").size()); + assertFalse(json.has("tool_call_id")); + assertFalse(json.has("jdbc_url")); + assertFalse(json.has("username")); + assertFalse(json.has("password")); + assertFalse(json.has("timeout")); + assertThrows(UnsupportedOperationException.class, () -> request.params().add("other")); + } + + @Test + void freezesBoundedStructuredRowsWithDeepImmutability() { + Map row = new LinkedHashMap<>(); + row.put("order_id", "order-123"); + row.put("payment_status", "FAILED"); + List> rows = new ArrayList<>(List.of(row)); + List columns = new ArrayList<>(List.of("order_id", "payment_status")); + MysqlToolResult result = new MysqlToolResult( + EvidenceStatus.EVIDENCE_FOUND, + "framework-call-mysql-1", + columns, + rows, + 1, + false); + row.put("secret", "must-not-appear"); + rows.clear(); + columns.clear(); + + JsonNode json = objectMapper.valueToTree(result); + assertEquals(Set.of("evidence_status", "tool_call_id", "columns", "rows", + "returned_count", "truncated"), ToolContractAssertions.fieldNames(json)); + assertEquals("framework-call-mysql-1", json.path("tool_call_id").asText()); + assertEquals(List.of("order_id", "payment_status"), result.columns()); + assertEquals(1, json.path("rows").size()); + assertFalse(json.path("rows").get(0).has("secret")); + assertThrows(UnsupportedOperationException.class, + () -> result.rows().get(0).put("other", "value")); + + assertEquals("query_mysql", AgentToolContracts.QUERY_MYSQL); + ToolContractAssertions.assertImplementationNeutral(AgentToolContracts.QUERY_MYSQL_DESCRIPTION); + } +} diff --git a/src/test/java/com/superbiz/agent/harness/tool/contract/QueryLogsToolContractTest.java b/src/test/java/com/superbiz/agent/harness/tool/contract/QueryLogsToolContractTest.java new file mode 100644 index 0000000..3ee869e --- /dev/null +++ b/src/test/java/com/superbiz/agent/harness/tool/contract/QueryLogsToolContractTest.java @@ -0,0 +1,76 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.superbiz.agent.harness.contract.EvidenceStatus; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class QueryLogsToolContractTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void freezesLogicalRequestWithoutInfrastructureControls() { + JsonNode json = objectMapper.valueToTree( + new QueryLogsRequest(LogTopic.APPLICATION, "order-service HikariCP timeout", 30)); + + assertEquals(Set.of("topic", "query", "lookback_minutes"), ToolContractAssertions.fieldNames(json)); + assertEquals("APPLICATION", json.path("topic").asText()); + assertFalse(json.has("tool_call_id")); + assertFalse(json.has("region")); + assertFalse(json.has("log_topic")); + assertFalse(json.has("limit")); + assertEquals(Set.of("APPLICATION", "DATABASE_SLOW_QUERY", "SYSTEM_EVENTS"), + java.util.Arrays.stream(LogTopic.values()).map(Enum::name) + .collect(java.util.stream.Collectors.toSet())); + } + + @Test + void freezesMockProvenanceScopeAndBoundedTimeline() { + List patterns = new ArrayList<>(List.of(new LogPattern( + 84, "2026-07-21T10:00:00Z", "2026-07-21T10:29:00Z", + "ERROR", "order-service", "HikariPool connection is not available"))); + List events = new ArrayList<>(List.of(new LogEvent( + "2026-07-21T10:29:00Z", "ERROR", "order-service", + "HikariPool connection is not available"))); + QueryLogsToolResult result = new QueryLogsToolResult( + EvidenceStatus.EVIDENCE_FOUND, + "framework-call-log-1", + LogSourceKind.MOCK, + new LogQueryScope(LogTopic.APPLICATION, "order-service HikariCP timeout", + "2026-07-21T10:00:00Z", "2026-07-21T10:30:00Z"), + 126, + 1, + patterns, + events, + true); + patterns.clear(); + events.clear(); + + JsonNode json = objectMapper.valueToTree(result); + assertEquals(Set.of("evidence_status", "tool_call_id", "source_kind", "scope", + "match_count", "returned_count", "patterns", "events", "truncated"), + ToolContractAssertions.fieldNames(json)); + assertEquals("framework-call-log-1", json.path("tool_call_id").asText()); + assertEquals("MOCK", json.path("source_kind").asText()); + assertEquals(126, json.path("match_count").asLong()); + assertEquals(1, json.path("returned_count").asInt()); + assertEquals(1, json.path("patterns").size()); + assertEquals(1, json.path("events").size()); + assertEquals(Set.of("topic", "query", "start_time", "end_time"), + ToolContractAssertions.fieldNames(json.path("scope"))); + assertThrows(UnsupportedOperationException.class, + () -> result.events().add(new LogEvent(null, null, null, null))); + + assertEquals("query_logs", AgentToolContracts.QUERY_LOGS); + ToolContractAssertions.assertImplementationNeutral(AgentToolContracts.QUERY_LOGS_DESCRIPTION); + } +} diff --git a/src/test/java/com/superbiz/agent/harness/tool/contract/RagToolContractTest.java b/src/test/java/com/superbiz/agent/harness/tool/contract/RagToolContractTest.java new file mode 100644 index 0000000..a9e2ddb --- /dev/null +++ b/src/test/java/com/superbiz/agent/harness/tool/contract/RagToolContractTest.java @@ -0,0 +1,61 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.superbiz.agent.harness.contract.EvidenceStatus; +import com.superbiz.agent.harness.contract.InvocationStatus; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class RagToolContractTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void freezesMinimalRequestAndBoundedEvidenceResult() { + RagToolRequest request = new RagToolRequest("支付超时处理方式"); + JsonNode requestJson = objectMapper.valueToTree(request); + + assertEquals(Set.of("query"), ToolContractAssertions.fieldNames(requestJson)); + assertFalse(requestJson.has("tool_call_id")); + + List source = new ArrayList<>(); + source.add(new RagEvidence("payment-timeout-guide", "payment-timeout.md", + "支付超时排查", "支付系统 > 故障排查", "先检查 ERR_TIMEOUT 发生时间。")); + RagToolResult result = new RagToolResult(EvidenceStatus.EVIDENCE_FOUND, "framework-call-rag-1", + request.query(), source, 1, false); + source.clear(); + + JsonNode json = objectMapper.valueToTree(result); + assertEquals(Set.of("evidence_status", "tool_call_id", "query", "evidence", + "returned_count", "truncated"), ToolContractAssertions.fieldNames(json)); + assertEquals("EVIDENCE_FOUND", json.path("evidence_status").asText()); + assertEquals("framework-call-rag-1", json.path("tool_call_id").asText()); + assertEquals(1, json.path("evidence").size()); + assertEquals(Set.of("document_id", "source", "title", "breadcrumb", "excerpt"), + ToolContractAssertions.fieldNames(json.path("evidence").get(0))); + assertFalse(json.has("context_pack")); + assertFalse(json.has("retrieval_trace")); + assertThrows(UnsupportedOperationException.class, + () -> result.evidence().add(new RagEvidence("other", "other.md", null, null, "other"))); + } + + @Test + void keepsLifecycleAndEvidenceStatusNamespacesIndependent() { + assertEquals(Set.of("PROJECTING", "READY", "ERROR"), enumNames(InvocationStatus.values())); + assertEquals(Set.of("EVIDENCE_FOUND", "NO_EVIDENCE", "ERROR"), enumNames(EvidenceStatus.values())); + assertEquals("lookup_knowledge", AgentToolContracts.LOOKUP_KNOWLEDGE); + ToolContractAssertions.assertImplementationNeutral(AgentToolContracts.LOOKUP_KNOWLEDGE_DESCRIPTION); + } + + private Set enumNames(Enum[] values) { + return java.util.Arrays.stream(values).map(Enum::name).collect(java.util.stream.Collectors.toSet()); + } +} diff --git a/src/test/java/com/superbiz/agent/harness/tool/contract/ToolContractAssertions.java b/src/test/java/com/superbiz/agent/harness/tool/contract/ToolContractAssertions.java new file mode 100644 index 0000000..82e87b3 --- /dev/null +++ b/src/test/java/com/superbiz/agent/harness/tool/contract/ToolContractAssertions.java @@ -0,0 +1,33 @@ +package com.superbiz.agent.harness.tool.contract; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class ToolContractAssertions { + + private static final Set INTERNAL_TERMS = Set.of( + "milvus", "l0", "l1", "rerank", "region", "topicid", "redis", + "jdbc", "credential", "password", "topk", "limit", "trace"); + + private ToolContractAssertions() { + } + + static Set fieldNames(JsonNode node) { + Set names = new HashSet<>(); + node.fieldNames().forEachRemaining(names::add); + return names; + } + + static void assertImplementationNeutral(String description) { + assertTrue(description.length() < 180, "Tool description must remain concise"); + String normalized = description.toLowerCase(Locale.ROOT); + INTERNAL_TERMS.forEach(term -> assertFalse(normalized.contains(term), + () -> "Tool description leaks internal term: " + term)); + } +}