diff --git a/.claude/skills/handoff/handoff-phase1-openspec-fix.md b/.claude/skills/handoff/handoff-phase1-openspec-fix.md index 2d351df..10e6ce7 100644 --- a/.claude/skills/handoff/handoff-phase1-openspec-fix.md +++ b/.claude/skills/handoff/handoff-phase1-openspec-fix.md @@ -148,7 +148,7 @@ openspec/changes/phase-1-infrastructure/ ## 敏感信息(已编辑) -- MySQL 密码:已配置在 application.yml(`!Fucker123..`) +- MySQL 密码:已从仓库移除,使用环境变量注入 - Redis:无密码 --- diff --git a/.docs/phase1-config-test-final-report.md b/.docs/phase1-config-test-final-report.md index c38e845..c39dd17 100644 --- a/.docs/phase1-config-test-final-report.md +++ b/.docs/phase1-config-test-final-report.md @@ -97,7 +97,7 @@ spring: redis: host: 119.29.78.52 port: 6379 - password: '!Fucker123..' + password: ${SUPERBIZ_REDIS_PASSWORD} database: 0 timeout: 3000 ``` diff --git a/.docs/phase1-config-test-report.md b/.docs/phase1-config-test-report.md index 43d67a6..1acd625 100644 --- a/.docs/phase1-config-test-report.md +++ b/.docs/phase1-config-test-report.md @@ -140,7 +140,7 @@ Error Code: 1049 datasource: url: jdbc:mysql://119.29.78.52:33306/superbiz_agent?... username: root - password: '!Fucker123..' + password: ${SUPERBIZ_MYSQL_PASSWORD} ``` **Redis 配置**: @@ -149,7 +149,7 @@ data: redis: host: 119.29.78.52 port: 6379 - password: '!Fucker123..' + password: ${SUPERBIZ_REDIS_PASSWORD} ``` **Flyway 配置**: diff --git a/.gitignore b/.gitignore index dcb8b93..22b7a11 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,13 @@ build/ app.log logs/ +### Local Secrets ### +.env +.env.* +!.env.example +application-local.yml +application-*.local.yml + ### Upload Files ### uploads/ diff --git a/devflow/glossary/CONTEXT.md b/devflow/glossary/CONTEXT.md index 02d5796..c24a985 100644 --- a/devflow/glossary/CONTEXT.md +++ b/devflow/glossary/CONTEXT.md @@ -155,6 +155,26 @@ - 使用场景:Executor 按 skill workflow 调用 evidence tools 收集事实,`tool_invocation` 记录这些事实证据。 - 边界:最终诊断结论必须被 evidence tools 支撑,不能仅由 skill 正文支撑。 +### Diagnosis Harness +- 定义:围绕 Diagnosis Agent 提供确定性运行控制的边界,负责 Run、预算、取消、重试装配、Tool 调用记录、证据验真和最终释放,不承担业务诊断推理。 +- 边界:Harness 不是工作流引擎,不实现 Planner/Executor/Composer 节点或自行编写 ReAct 循环。 + +### EvidenceGuard +- 定义:Harness 内部的确定性证据验真能力,校验 Draft 引用、当前 Run 所有权、Tool 调用状态和有界 Agent 投影。 +- 边界:EvidenceGuard 不调用 LLM,也不判断证据是否足以推出业务结论。 + +### SemanticGuard +- 定义:使用隔离上下文对完整诊断 Draft 与已验真证据做报告级语义审查的单轮 Agent。 +- 边界:无工具、无记忆、无 ReAct 循环,不访问 Redis,不生成或改写用户报告。 + +### Invocation Status +- 定义:Tool 调用及结果投影的生命周期状态,固定为 `PROJECTING`、`READY`、`ERROR`。 +- 边界:它只说明调用记录是否完成,不说明结果是否包含证据。 + +### Evidence Status +- 定义:证据 Tool 的结果语义,固定为 `EVIDENCE_FOUND`、`NO_EVIDENCE`、`ERROR`。 +- 边界:`NO_EVIDENCE` 只表示当前查询范围内没有匹配结果,不能解释为问题不存在、根因被排除或系统健康。 + ### Verifier Skill Isolation - 定义:Chat Verifier 与 skill 系统隔离,只校验 Executor 答案和 `tool_trace_summary`。 - 使用场景:防止 Verifier 把 playbook 指令当作事实证据;Verifier 只判断已有证据是否支持结论。 diff --git a/devflow/index.md b/devflow/index.md index a1842fb..9349e17 100644 --- a/devflow/index.md +++ b/devflow/index.md @@ -4,6 +4,7 @@ | 日期 | slug | 说明 | 领域 | 关键词 | 关联 OpenSpec | 状态 | |---|---|---|---|---|---|---| +| 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 | | 2026-07-08 | executor-composer-final-answer | 引入 Composer 生成最终回答,只使用 Verifier 允许的结论材料。 | Chat quality gate/evidence attribution | chat_composer, final answer, allowed_claims, allowed_hypotheses, safe fallback, composer_output | openspec/changes/archive/2026-07-08-executor-composer-final-answer | archived | diff --git a/devflow/projects/2026-07-21-single-react-design-freeze/acceptance.md b/devflow/projects/2026-07-21-single-react-design-freeze/acceptance.md new file mode 100644 index 0000000..d4db42c --- /dev/null +++ b/devflow/projects/2026-07-21-single-react-design-freeze/acceptance.md @@ -0,0 +1,36 @@ +# Acceptance: single-react-design-freeze + +## 实现结果 + +- 新增类型化 Harness contracts 和序列化契约测试。 +- ISS-014 已调整为 11 个串行 changes,并补齐 Tool ID、双状态、previous turn 和安全切换决策。 +- Python 查询脚本和 Spring 主配置改为环境变量 Secret。 +- 集成测试和历史 handoff 中的旧 Secret 副本已删除或脱敏。 +- devflow glossary 新增 Harness、EvidenceGuard、SemanticGuard、Invocation Status 和 Evidence Status。 + +## 静态验证 + +- OpenSpec strict validation:通过。 +- Secret 扫描:通过,已知真实 Secret 模式零匹配。 +- Diff whitespace 检查:通过。 + +## 脚本验证 + +- 4 个 contract tests:通过。 +- ToolInvocationRecorder、ExecutorGatekeeperService、ChatController focused baseline:通过。 +- 查询脚本缺失密码环境变量时 fail fast:通过。 + +## 浏览器/人工验证 + +- 不适用;阶段 0 未切换任何公开 UI/API。 + +## 未验证与后续门禁 + +- Provider 侧旧凭据轮换待凭据所有者完成。 +- Live E2E 留到阶段 7。 +- 阶段 1 只有在本 change Archive 和 Git commit 后才能开始。 + +## 状态 + +- Stage acceptance: accepted +- OpenSpec archive: authorized by standing user instruction diff --git a/devflow/projects/2026-07-21-single-react-design-freeze/brief.md b/devflow/projects/2026-07-21-single-react-design-freeze/brief.md new file mode 100644 index 0000000..7e39fe5 --- /dev/null +++ b/devflow/projects/2026-07-21-single-react-design-freeze/brief.md @@ -0,0 +1,31 @@ +# Brief: single-react-design-freeze + +## 背景 + +ISS-014 将 Chat 诊断从多 Agent、Hook、ThreadLocal 和外层重试编排收敛为单体 Diagnosis ReAct Agent、确定性 Harness 和隔离 SemanticGuard。阶段 0 先冻结后续实施共同依赖的契约和安全边界。 + +## 目标 + +- 提供可复用的 Diagnosis Draft、Knowledge Answer、Fallback、published result 和 previous turn 类型。 +- 固定 framework Tool Call ID、调用生命周期和证据结果双状态。 +- 固定取消、重试、no-evidence、阶段门禁和安全凭据策略。 +- 保持当前公开 Chat 运行行为不变。 + +## 范围 + +- `com.superbiz.agent.harness.contract` 类型层和 focused tests。 +- ISS-014 的 11 个串行 OpenSpec changes 台账。 +- 主配置、查询脚本和集成测试中的明文 Secret 清理。 +- OpenSpec、devflow glossary 和阶段验收基线。 + +## 非目标 + +- 不接入新 Harness、Agent 或 Guards。 +- 不切换 `/api/chat`,不删除旧运行链。 +- 不执行 live E2E。 + +## 元数据 + +- Scale: complex +- Parent issue: `ISS-014` +- OpenSpec: `openspec/changes/single-react-design-freeze` diff --git a/devflow/projects/2026-07-21-single-react-design-freeze/decisions.md b/devflow/projects/2026-07-21-single-react-design-freeze/decisions.md new file mode 100644 index 0000000..3bda541 --- /dev/null +++ b/devflow/projects/2026-07-21-single-react-design-freeze/decisions.md @@ -0,0 +1,20 @@ +# Decisions: single-react-design-freeze + +## 已确认决策 + +- ISS-014 保留为总 Issue,实施拆成 11 个独立、串行 sm-flow changes。 +- 每个 change 必须完成 Apply、阶段验收、OpenSpec Archive 和 Git commit 后才能进入下一项。 +- Apply、Archive 和阶段 Git commit 已获得用户对整个目标的持续授权;仅方向性决策需要暂停。 +- `tool_call_id` 使用框架协议 ID,Harness 不生成第二套 ID。 +- `status=PROJECTING/READY/ERROR` 表示 invocation lifecycle;`evidence_status=EVIDENCE_FOUND/NO_EVIDENCE/ERROR` 表示结果语义。 +- `NO_EVIDENCE` 只能支持限定范围的 `NEGATIVE_OBSERVATION`。 +- KNOWLEDGE_QUERY 使用 answer items 绑定 `tool_call_id + document_id`,首版不进入 SemanticGuard。 +- `diagnosis_run` 是 previous turn 的持久化真理源;只有最近的 `DIAGNOSIS + SUCCESS` 安全发布结果可用。 +- 取消采用分层、可观测语义;同步模型调用不承诺无法证明的立即硬取消。 +- 底层重试压为一次 attempt,Router/SemanticGuard 的允许重试只由 Harness 执行。 +- 阶段 4 和 6A 不接管公开入口,阶段 6B 才执行原子 SSE 切换。 + +## 权衡 + +- contract types 提前落地会增加少量文件,但后续 output type、validator、持久化和 SSE 可以复用,避免多阶段字符串协议漂移。 +- Provider 侧凭据轮换是外部前置,阶段 0 只证明工作树已清理,不伪造外部完成状态。 diff --git a/devflow/projects/2026-07-21-single-react-design-freeze/evidence.md b/devflow/projects/2026-07-21-single-react-design-freeze/evidence.md new file mode 100644 index 0000000..5e4a863 --- /dev/null +++ b/devflow/projects/2026-07-21-single-react-design-freeze/evidence.md @@ -0,0 +1,25 @@ +# Evidence: single-react-design-freeze + +## 代码与框架证据 + +- `ChatController` 当前直接管理会话、模型、工具、执行和伪流式 SSE,证明公开切换必须延后到阶段 6B。 +- `ChatService` 当前构建 Planner/Executor/Verifier/Composer 并依赖 ThreadLocal,证明阶段 0 只能创建无运行依赖的 contract types。 +- Spring AI Alibaba `Builder` 提供 ToolInterceptor、output type、tool timeout 和调用限制扩展点;`ReactAgent` 提供 interrupt。 +- Spring AI 1.1.7 `SpringAiRetryProperties` 构造器默认 `maxAttempts=10`,与 Harness 单一重试所有权冲突,阶段 2 必须压为一次底层 attempt。 +- `DiagnosisRun` 当前只有通用 status 和文本 answer,不足以确定性恢复安全 previous turn,因此确认后续保存 `intent/release_outcome/published_result`。 +- 历史 ISS-009 和 verifier evidence 归档证明 `NO_EVIDENCE` 只能表达限定范围内无匹配结果。 + +## 验证证据 + +- `mvn -q '-Dtest=HarnessContractTest' test`:通过。 +- `mvn -q '-Dtest=ToolInvocationRecorderTest,ExecutorGatekeeperServiceTest,ChatControllerTest' test`:通过。 +- `mvn -q '-Dtest=HarnessContractTest,ToolInvocationRecorderTest,ExecutorGatekeeperServiceTest,ChatControllerTest' test`:通过。 +- `openspec validate single-react-design-freeze --strict`:通过。 +- `git diff --check`:通过,仅有 Windows line-ending warning。 +- 已知旧 Secret 模式扫描:零匹配。 +- 缺少 `SUPERBIZ_MYSQL_PASSWORD` 时运行查询脚本:在连接前 fail fast,符合预期。 + +## 未验证 + +- 未运行模型、Redis、MySQL 或 Milvus live E2E;按阶段门禁留到阶段 7。 +- Provider 侧旧凭据是否已经轮换无法由仓库证明,必须由凭据所有者完成,并在阶段 3C/7 前核验。 diff --git a/handoff/2026-06-23-phase1-openspec-fix.md b/handoff/2026-06-23-phase1-openspec-fix.md index f4dc07f..ccb13ca 100644 --- a/handoff/2026-06-23-phase1-openspec-fix.md +++ b/handoff/2026-06-23-phase1-openspec-fix.md @@ -31,7 +31,7 @@ **配置信息**(已完成): - **MySQL**: 119.29.78.52:33306/superbiz_agent - 用户: root - - 密码: !Fucker123.. + - 密码: 已从仓库移除,使用环境变量注入 - driver: com.mysql.cj.jdbc.Driver - URL参数: useUnicode=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true - **Redis**: 119.29.78.52:6379 @@ -165,7 +165,7 @@ openspec/changes/phase-1-infrastructure/ ## 敏感信息(已编辑) -- MySQL 密码:已配置在 application.yml(`!Fucker123..`) +- MySQL 密码:已从仓库移除,使用环境变量注入 - Redis:无密码 --- diff --git a/mvp/disscus/plan.md b/mvp/disscus/plan.md new file mode 100644 index 0000000..839e11a --- /dev/null +++ b/mvp/disscus/plan.md @@ -0,0 +1,56 @@ +结合你在前几轮对话中梳理出的“过度设计”痛点,既然你已经决定回归单 Agent(ReAct)+ 强 Harness 架构,接下来你需要做一次彻底的“架构物理重构”。 +以下是为你量身定制的5步落地行动指南,按优先级从高到低执行: +第一步:链路合并,砍掉“伪 Graph”节点 +目标:把被拆散的推理逻辑还给单一的 ReAct 循环。 + +删除节点:直接在 Graph/状态机中抹掉 Planner、Executor、Composer 以及串联它们的边。 +合并为单一 Agent:创建一个 DiagnosisAgent。在它的 System Prompt 中明确:“你需要自行规划排查路径,调用工具获取证据,并在证据充分后输出最终的结构化诊断报告。” +保留机制:该 Agent 内部运行一个带最大步数限制的 While 循环(如 max_iterations=10),防止死循环。 + +第二步:实施“上下文卸载”与“工具清洗”(解决7次调用膨胀问题) +目标:确保单 Agent 在多轮工具调用后,上下文依然干净,从源头切断幻觉。 + +工具输出清洗(RTK 机制):改造你的所有工具(如 queryLogs, queryDB)。在工具的 Callback 层加代码,将原始的大段返回结果(如 1000 行日志)强制过滤、聚合,只把最核心的 5-10 行 ERROR 或统计指标返回给 Agent。 +上下文卸载:如果某些原始数据必须保留,在工具返回时,将全量数据写入本地文件(如 refs/log_001.md),上下文里只注入一行:[发现5个504错误,详见 refs/log_001.md]。 + +第三步:下沉确定性逻辑,用代码替代 LLM 节点 +目标:将你之前用 Gatekeeper 和 VerifiedInput 做的事,降级为零 LLM 调用的代码拦截器。 + +前置拦截(Pre-Tool Hook):Agent 发起工具调用时,Harness 代码用 JSON Schema 校验参数格式。不合法直接报错打回,不执行工具。 +后置断言(Post-Tool Hook):Agent 输出最终诊断报告时,代码层强制校验报告中引用的 evidence_id 和 raw_path 是否真实存在于历史记录或文件系统中。不合法直接拒绝输出,发回重试。 + +第四步:锁死输出契约 +目标:防止 Executor 过度输出和发散。 + +在 System Prompt 中强制规定 Agent 的中间思考步和最终输出步必须符合严格的 JSON 结构。 +例如,中间步必须是 {"thought": "<不超过50字>", "action": "queryLogs", "parameters": {...}}。Harness 代码检查字数,超长直接打回。 + +第五步:剥离异步验证(保留你最初的“防幻觉”初衷) +目标:在不增加主链路复杂度的前提下,保留交叉验证能力。 + +主 ReAct Agent 输出报告后,不要在主 Graph 里串行接一个 Verifier 节点。 +改为异步触发一个轻量级 LLM(或小模型),只传入“压缩后的证据摘要 + 草稿结论”。让它判断时间线与逻辑是否一致。如果不一致,在最终输出上加“低置信度警告”;如果一致,直接放行。 + +总结:你的重构后架构全景图 +重构后,你的代码结构应该极其清爽,大致如下: +[用户输入] + │ + ▼ +[Diagnosis ReAct Agent] (唯一的 LLM 推理节点,自带规划、执行、总结) + │ + ├── Tool: queryLogs + │ └── [Harness 代码]: 过滤 INFO,提取 ERROR,写入 refs,返回摘要 + ├── Tool: queryMetrics + │ └── [Harness 代码]: 聚合统计值,返回 3 行核心指标 + │ + ▼ (Agent 输出最终 JSON 报告) +[Output Schema Linter] (纯代码层,0 LLM) + │ + ├─ 校验失败 ──> 返回错误给 [Agent] 重新生成 + │ + ▼ (校验通过) +[Async Verifier] (异步轻量 LLM,只做时间线/逻辑一致性校验) + │ + ▼ +[最终输出 / 带警告输出] +现在你应该做的第一件事:打开你的代码,把主链路上除 Diagnosis Agent 以外的所有 LLM 编排节点全部注释掉,然后按照上面的结构,给工具加上 Pre-Tool 和 Post-Tool 的代码拦截器。把精力从“画 Graph”转移到“写工具清洗代码”上。 diff --git a/mvp/issues/README.md b/mvp/issues/README.md index b7cbe2d..8638c16 100644 --- a/mvp/issues/README.md +++ b/mvp/issues/README.md @@ -1,6 +1,6 @@ # MVP Issues 索引 -**更新日期**:2026-07-10 +**更新日期**:2026-07-20 **状态**:按活跃问题、设计笔记、RAG 问题集和已归档问题整理 ## 目录约定 @@ -18,6 +18,9 @@ |---|---|---|---|---| | ISS-003 | MVP 设计与实现 Review 收敛 | 高 | 待规划 | [active/ISS-003-mvp-design-implementation-review.md](active/ISS-003-mvp-design-implementation-review.md) | | ISS-004 | Executor 域级检索水位控制 | 低 | 待规划 | [active/ISS-004-executor-domain-hard-limit.md](active/ISS-004-executor-domain-hard-limit.md) | +| ISS-012 | Executor Token 预算与上下文膨胀 | 高 | 待规划 | [active/ISS-012-executor-token-budget-and-context-growth.md](active/ISS-012-executor-token-budget-and-context-growth.md) | +| ISS-013 | Chat 入口解耦与真正 SSE 收敛 | 高 | 待规划 | [active/ISS-013-chat-entry-decoupling-and-sse.md](active/ISS-013-chat-entry-decoupling-and-sse.md) | +| ISS-014 | 单体 ReAct Agent、Harness 与 ACI 工具瘦身 | 高 | 待阶段 0 冻结 | [active/ISS-014-single-react-agent-harness-aci-ptk-refactor.md](active/ISS-014-single-react-agent-harness-aci-ptk-refactor.md) | | executor-evidence-attribution-hallucination | Executor 证据归因幻觉 | 高 | 待规划 | [active/executor-evidence-attribution-hallucination.md](active/executor-evidence-attribution-hallucination.md) | | rag-refactor-plan | RAG 检索重构计划 | 高 | 待规划 | [active/rag-refactor-plan.md](active/rag-refactor-plan.md) | diff --git a/mvp/issues/active/ISS-012-executor-token-budget-and-context-growth.md b/mvp/issues/active/ISS-012-executor-token-budget-and-context-growth.md new file mode 100644 index 0000000..d73d53f --- /dev/null +++ b/mvp/issues/active/ISS-012-executor-token-budget-and-context-growth.md @@ -0,0 +1,119 @@ +# ISS-012 Executor Token 预算与上下文膨胀 + +**状态**:待规划 +**严重程度**:高 +**发现时间**:2026-07-20 +**关联**:ISS-002、ISS-004、ISS-011 + +--- + +## 背景 + +ISS-011 完成 StateGraph 切换后,复杂 Chat 链路已经具备显式 Node、Gatekeeper、Verifier、Composer 和 Run 级 Trace。但最终 live E2E 暴露出 Executor 的 Token 成本和上下文增长问题:一次只返回安全 Fallback 的请求消耗了超过 11 万 Token。 + +## 现象 + +最终验收 Run: + +- `runId`:`run-808ac38f-3ad0-4462-a6d0-ed50d8686473` +- 总耗时:`75964ms` +- 最终答案:109 字符,Run 为 `CHAT/SUCCESS`,`degraded=true` +- AgentStep:8 条,其中 Planner 1 次、Executor 7 次 +- ToolInvocation:12 次 +- Run `total_token_count`:`111802` + +Executor 每次模型调用的 Token 逐步上升: + +```text +6396 -> 11265 -> 12786 -> 16774 -> 17965 -> 19340 -> 24236 +``` + +工具调用包括 4 次 `lookup_knowledge`、6 次 `query_logs`、1 次 `query_metrics` 和 1 次 `get_available_log_topics`。最终因模型输出缺少 `source_invocation_id`,Gatekeeper 将结果降为 `LOW_CONFID` 并进入安全 Fallback。 + +## 已确认事实 + +1. 数据库中 8 条 `agent_step` 均为不同记录,不存在重复插入;`111802` 等于各步骤 `token_count` 的求和。 +2. `TokenTrackingChatModel` 当前只保存供应商返回的 `usage.totalTokens`,没有拆分输入、输出、缓存和推理 Token。 +3. `ChatService.backfillRunMetrics` 直接累加每条 AgentStep 的 `token_count`。 +4. `AgentLoggingHook` 只把模型输入截断为 500 字符写入审计表,无法从当前 Trace 还原模型实际发送的完整 Prompt。 +5. Executor 当前没有独立的模型调用次数、工具调用次数或 Token 预算;Graph recursion limit 不能限制 ReactAgent 内部工具循环。 + +## 初步根因假设 + +- 每次 Executor 模型调用都会重新携带 Planner 结果、历史消息和之前的工具返回,导致输入上下文随工具循环增长。 +- 工具返回内容包含较多日志、知识库结果和检索明细,完整结果被反复带入后续模型请求。 +- 工具结果没有稳定返回 `source_invocation_id`,模型无法可靠生成精确证据引用,导致高成本检索后仍然进入 Fallback。 +- 当前只能看到 `totalTokens`,尚未确认供应商 usage 中 input/output/cached/reasoning 的精确占比。 + +## 影响 + +- 单次诊断成本和延迟不可控,复杂问题可能继续超过模型上下文窗口。 +- Token 消耗与最终答案质量不匹配,出现“高成本检索 + 安全降级”的低收益路径。 +- 缺少 Token 分项指标,无法建立成本预算、P95 延迟和 degraded rate 门禁。 +- Executor 可能重复查询相同或相近的知识域、日志主题和指标。 + +## 目标 + +1. 建立按 Run/AgentStep 的 input、output、cached、reasoning Token 可观测性。 +2. 为 Executor 增加硬性模型轮数、工具调用和 Token 预算。 +3. 将完整工具结果留在 Run Trace/数据库中,模型上下文只接收有界证据投影。 +4. 让工具结果直接携带可引用的 `source_invocation_id` 和紧凑 `evidence_refs`。 +5. 在预算耗尽时安全结束并明确记录原因,不绕过 Gatekeeper、Verifier 或 Run Trace。 + +## 建议方案 + +### 1. Token 统计拆分 + +- 从 ChatModel usage 中记录 `input_tokens`、`output_tokens`、`cached_tokens`、`reasoning_tokens`(供应商提供时)。 +- 保留 `total_token_count` 作为汇总字段,但明确其计算口径。 +- 在 `orchestration_trace` 中记录每个 Agent 的累计 Token 和预算命中情况。 + +### 2. Executor 硬预算 + +初版建议从以下上限开始,并通过固定 E2E 调整: + +- Executor 模型调用最多 4 次。 +- 工具调用最多 8 次。 +- `lookup_knowledge` 最多 2 次。 +- `query_logs` 默认最多返回 5 条日志,并限制单次输出长度。 +- 达到预算后停止扩展检索,基于已验真证据输出,或进入带原因的安全 Fallback。 + +### 3. 有界证据上下文 + +- 工具完整原始结果继续写入 `tool_invocation`,不直接作为下一轮完整上下文。 +- 返回模型的工具视图只保留 invocation ID、工具名、查询条件、有限 evidence refs、excerpt 和 no-evidence 状态。 +- 同一工具数组项禁止重复绑定;相同知识域和日志主题不重复查询。 + +### 4. 证据引用闭环 + +- 每次 evidence tool 返回结果时直接包含 `source_invocation_id`。 +- Executor 输出必须引用该 ID;Gatekeeper 不再依赖事后猜测或唯一候选补全。 +- 由于引用失败进入 Fallback 时,Trace 必须记录具体缺失字段和预算消耗。 + +## 验收标准 + +- [ ] 每个 AgentStep 可查看 input/output/total Token,供应商支持时可查看 cached/reasoning Token。 +- [ ] 固定 `payment-timeout` E2E 的 Token 上限、工具调用上限和最大延迟已定义并通过回归。 +- [ ] 连续至少 10 次相同 fixture 运行,Token 和延迟 P95 不超过定义的预算。 +- [ ] Executor 预算耗尽时只走安全 Fallback,不绕过 Gatekeeper、Verifier 或 Trace 持久化。 +- [ ] 工具返回包含真实 `source_invocation_id`;正常证据链不再因缺少该字段而无谓降级。 +- [ ] 12 个 diagnosis eval fixture、Graph workflow/node contract、Trace ownership 回归全部通过。 +- [ ] E2E 日志和数据库能按 exact `sessionId + runId` 对齐 Token、工具调用、Fallback 原因和最终状态。 + +## 非目标 + +- 不删除 Gatekeeper、Verified Input 或 Verifier。 +- 不以降低模型 `maxTokens` 代替上下文治理。 +- 不恢复 Sequential/StateGraph 双轨或旧兼容协议。 +- 不在本 Issue 中物理删除数据库中的历史 `diagnosis_session` 表。 + +## 相关文件 + +- `src/main/java/com/superbiz/agent/hook/TokenTrackingChatModel.java` +- `src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java` +- `src/main/java/com/superbiz/agent/service/ChatService.java` +- `src/main/java/com/superbiz/agent/graph/diagnosis/ReactAgentDiagnosisInvoker.java` +- `src/main/java/com/superbiz/agent/service/ToolInvocationRecorder.java` +- `src/main/resources/prompts/chat-executor-prompt.md` +- `mvp/architecture/stategraph-runtime-architecture.md` +- `devflow/projects/2026-07-17-chat-diagnosis-stategraph-cleanup-docs/evidence.md` diff --git a/mvp/issues/active/ISS-013-chat-entry-decoupling-and-sse.md b/mvp/issues/active/ISS-013-chat-entry-decoupling-and-sse.md new file mode 100644 index 0000000..f32f5d3 --- /dev/null +++ b/mvp/issues/active/ISS-013-chat-entry-decoupling-and-sse.md @@ -0,0 +1,81 @@ +# ISS-013 Chat 入口解耦与真正 SSE 收敛 + +**状态**:待规划 +**严重程度**:高 +**发现时间**:2026-07-20 +**关联**:ISS-011、ISS-012 + +--- + +## 背景 + +当前 Chat 入口同时提供 `/api/chat` 和 `/api/chat_stream`。`ChatController` 不仅处理 HTTP/SSE 协议,还直接承担会话创建、历史读取与回写、模型和工具获取、执行策略调用以及异常响应组装,入口职责已经明显超出协议适配层。 + +现有 `/api/chat_stream` 会等待完整答案生成后再按固定长度切片发送,并不是真正的流式生成。同步和伪流式入口还复制了大部分业务流程,增加了维护成本和行为不一致风险。 + +## 已确认问题 + +1. Chat 的会话、历史、模型、工具、执行和结果回写功能耦合在 `ChatController` 中,HTTP 层与应用用例边界不清晰。 +2. `/api/chat` 与 `/api/chat_stream` 重复编排同一套 Chat 流程。 +3. `/api/chat_stream` 只是对完整答案做事后分块,不具备模型生成过程中的真实增量输出能力。 +4. Controller 直接获取 `ChatModel` 和 `ToolCallbackProvider`,将模型基础设施细节暴露到入口层。 +5. SSE 使用 Controller 自建的无界缓存线程池,缺少统一生命周期和容量治理。 +6. 当前入口错误响应存在 HTTP 状态、外层 `ApiResponse` 与内层 `ChatResponse` 状态不一致的问题。 + +## 目标 + +1. 将会话生命周期、历史管理、执行调用和结果回写从 Controller 分离,形成单一 Chat 应用用例入口。 +2. 只保留一个 `/api/chat` 接口,并将其协议改为真正的 SSE。 +3. SSE 在模型或诊断链路产生内容时增量发送,而不是等待完整答案后再切片。 +4. 保留 `sessionId + runId` 作为一次 Chat Run 的稳定关联契约。 +5. 在满足入口职责分离的前提下使用最少组件,不引入没有实际职责的接口、工厂或适配层。 + +## 设计约束 + +- Controller 只负责请求校验、协议转换和 SSE 生命周期,不负责选择模型、组装工具、管理历史或编排诊断流程。 +- 同一次请求只能进入一个应用用例入口,禁止同步和流式路径各自维护一套业务逻辑。 +- 真正 SSE 至少需要区分元数据、内容增量、完成和错误事件。 +- `sessionId`、`runId` 必须在内容事件之前可获得,并用于日志、数据库和 Trace 对齐。 +- 客户端断开、超时和执行失败必须显式终止后台执行并完成 Run 状态记录。 +- 不保留旧 `/api/chat_stream` 或同步 `/api/chat` 的兼容分支,直接以新协议为准。 +- 优先使用 Spring 管理的执行设施和现有服务能力,不创建无界线程池。 + +## 建议的最小边界 + +```text +POST /api/chat (SSE) + -> ChatController:请求与 SSE 协议 + -> Chat 应用用例:会话、Run、历史和执行生命周期 + -> 现有 Chat 执行能力:简单回答或诊断编排 +``` + +这里的“应用用例”是职责边界,不要求预先拆出多层接口。只有出现独立变化原因或明确复用需求时才增加新组件。 + +## 验收标准 + +- [ ] 对外只保留一个 `POST /api/chat`,响应类型为 `text/event-stream`。 +- [ ] 删除 `/api/chat_stream` 及同步 Chat 兼容路径。 +- [ ] 首个内容事件在完整答案生成完成前发送,禁止通过固定字符切片伪造流式输出。 +- [ ] SSE 事件包含稳定的 metadata、content、error、done 契约。 +- [ ] Controller 不再直接依赖 `ChatModel`、`ToolCallbackProvider`,也不管理会话历史和 Run 持久化。 +- [ ] 同一请求的 `sessionId + runId` 在 SSE、应用日志、`diagnosis_run`、`agent_step` 和 `tool_invocation` 中一致。 +- [ ] 客户端断开、超时、模型失败和工具失败都有明确的资源清理与 Run 终态。 +- [ ] 不存在 Controller 自建的无界线程池。 +- [ ] 单元测试覆盖入口校验和 SSE 事件契约;端到端测试验证真实增量输出、断开清理及 Trace 对齐。 + +## 非目标 + +- 不在本 Issue 中重新设计 StateGraph 节点、Gatekeeper、Verifier 或证据协议。 +- 不为未来可能出现的其他传输协议预建通用框架。 +- 不引入多套 Command、Handler、Adapter、Factory 只为形式上的分层。 +- 不保留旧同步接口或 `/api/chat_stream` 的兼容逻辑。 +- 不以“完整答案分块发送”作为 SSE 验收通过条件。 + +## 相关文件 + +- `src/main/java/com/superbiz/agent/controller/ChatController.java` +- `src/main/java/com/superbiz/agent/service/ChatService.java` +- `src/main/java/com/superbiz/agent/service/session/SessionManager.java` +- `src/test/java/com/superbiz/agent/controller/ChatControllerTest.java` +- `src/test/java/com/superbiz/agent/service/ChatServiceGraphIntegrationTest.java` +- `mvp/architecture/current-mvp-architecture.md` 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 new file mode 100644 index 0000000..360ee5e --- /dev/null +++ b/mvp/issues/active/ISS-014-single-react-agent-harness-aci-ptk-refactor.md @@ -0,0 +1,1408 @@ +# ISS-014 单体 ReAct Agent、Harness 与 ACI 工具瘦身 + +**状态**:设计决策已确认,待阶段 0 冻结 +**严重程度**:高 +**发现时间**:2026-07-20 +**目标分支**:`refactor/chat-single-react-harness` +**基线分支**:`refactor/mvp1.0` +**关联**:ISS-003、ISS-012、ISS-013、executor-evidence-attribution-hallucination + +--- + +## 1. 摘要 + +当前 Chat 诊断链路将一次完整 ReAct 生命周期拆成 Planner、Executor、Verifier、Composer 等多个 LLM 角色,再通过服务方法、Hook、ThreadLocal、结构化协议和多轮重试串联。该设计把 Agent 本身已经具备的“思考、行动、观察、继续行动、最终回答”重复提升为外层编排,造成职责分散、上下文重复、失败语义不一致和 Token 成本失控。 + +本 Issue 将后续实现收敛为: + +```text +一个负责实际工作的 Diagnosis ReAct Agent + + 一个负责确定性控制的 Harness + + 一个上下文隔离的 SemanticGuard Agent +``` + +Diagnosis ReAct Agent 和 SemanticGuard Agent 都通过同一个 Harness 执行边界运行;不为 SemanticGuard 再复制一套 Harness,也不引入新的 Graph 或编排器。 + +同时重构 Agent 可见 Tool Contract,使 RAG、日志和新增 MySQL Tool 遵守 ACI 原则,并通过工具结果清洗与投影层控制上下文体积、敏感数据和证据引用。 + +本 Issue 是后续实现的总设计来源。ISS-012 继续保留 Token/预算问题背景,ISS-013 继续保留入口/SSE 问题背景;实现范围和阶段顺序以本 Issue 最终确认版本为准。 + +## 2. 根因判断 + +### 2.1 ReAct 被重复实现 + +原有角色实际对应一个完整 ReAct 生命周期: + +```text +Planner -> 内部规划与推理 +Executor -> 工具调用与观察循环 +Verifier -> 证据充分性自检 +Composer -> 最终用户回答 +``` + +将这些职责拆成多个 Agent 后,需要额外维护: + +- 多套 Prompt 和输出 Schema; +- Agent 间 JSON 转换; +- 多套重试状态; +- 证据和上下文在不同角色之间重复搬运; +- 失败、低置信度和 Fallback 的分支组合; +- 主链路与审计链路之间的隐式 ThreadLocal 状态。 + +### 2.2 工具返回面向开发者而不是面向 Agent + +当前 Tool 结果混合了: + +- Agent 真正需要的证据; +- 检索/查询内部实现细节; +- 调试 Trace 和 Rerank 数据; +- 重复正文和 ContextPack; +- 原始大结果; +- Session 隐式记忆; +- 无结果、错误和去重等含义不一致的状态。 + +这违反 ACI 的清晰、稳定、有界、可恢复和可继续操作原则,也是 Agent 上下文膨胀的重要来源。 + +### 2.3 安全约束与业务编排耦合 + +证据真实性校验属于确定性执行约束,不需要成为独立 LLM 节点或业务编排角色。语义推导校验确实需要独立上下文,但它不应拥有工具、记忆、ReAct 循环或回调主 Agent 的能力。 + +## 3. 目标架构 + +```text +POST /api/chat (SSE) + -> Chat Application Use Case + -> Intent Router(一次轻量 Chat) + -> SYSTEM_CHAT + -> KNOWLEDGE_QUERY + -> DIAGNOSIS + +SYSTEM_CHAT + -> 无 Tool 普通 Chat + +KNOWLEDGE_QUERY + -> 单次 lookup_knowledge + -> 普通 Chat 基于 RAG Evidence 回答 + -> Harness 校验证据引用 + +DIAGNOSIS + -> Harness 启动 Diagnosis ReAct Agent + -> Agent 接收当前原始 Query 和可选 previous_turn + -> Agent 内部规划 + -> Agent 自主调用受控 Tool + -> Harness 在 Tool 边界校验、记录、清洗和投影 + -> Agent 内部检查证据是否足够 + -> Agent 输出结构化诊断草稿、Analysis Items、Conclusion 和 Tool Call Refs + + -> Harness EvidenceGuard(0 LLM) + -> Schema 校验 + -> 当前 Run 证据所有权校验 + -> tool_call_id 当前 Run 归属和调用结果校验 + + -> SemanticGuard Agent(独立隔离上下文) + -> 接收原始 Query、完整 Draft 和已物理验真的精确证据 + -> 判断整份用户报告是否语义成立且没有超出证据范围 + + -> Harness Release Policy + -> SUPPORTED:释放完整答案 + -> UNSUPPORTED:安全降级 + -> TIMEOUT/ERROR:不释放未验证草稿 +``` + +业务层不使用 StateGraph。底层 ReactAgent 框架内部是否使用 Graph 属于框架实现细节,不构成项目业务编排。 + +## 4. 已确认设计决策 + +### 4.1 单体 Diagnosis ReAct Agent + +主 Agent 负责: + +1. 接收当前原始 Query 和 Harness 组装的可选极简 previous_turn; +2. 理解诊断目标; +3. 在内部形成排查方向; +4. 选择并调用只读证据 Tool; +5. 观察 Tool 结果并决定是否继续; +6. 检查证据是否足以支持准备输出的结论; +7. 形成结论先行的结构化诊断草稿、Analysis Items 和 Tool Call Refs; +8. 证据不足时明确输出无法确认,不补造事实。 + +主 Agent 不负责: + +- 意图路由; +- Session/Run 生命周期; +- Token、工具、超时和取消预算; +- 数据库存储和 Trace; +- HTTP/SSE 协议; +- 证据物理真实性校验; +- 调用其他 Agent; +- 外层重试状态机。 + +不要求 Agent 输出 Thought 或 Chain of Thought。内部规划不形成对外协议,也不持久化为业务事实。 + +### 4.2 Harness + +Harness 负责确定性执行控制,而不是业务推理: + +#### Run 控制 + +- 创建和传播 `sessionId + runId`; +- 建立请求 Deadline 和取消信号; +- 限制模型调用、工具调用、Token 和总耗时; +- 客户端断开时取消后续执行; +- 保证 Run 进入明确终态。 + +#### Model 边界 + +- 包装 ChatModel; +- 记录 input/output/total Token; +- 应用调用次数和上下文预算; +- 不修改 Agent 的业务判断。 + +#### Tool 边界 + +- Pre-Tool 参数 Schema、权限、只读和预算校验; +- 执行真实 Tool; +- 接收框架在 Tool 执行请求中提供的 `tool_call_id`,并在执行前完成校验; +- Tool 返回后将请求、原始响应、状态和时间写入 Redis;当前版本暂不做持久化脱敏; +- 调用对应 `ToolResultProjector` 生成规范化、聚合、截断后的 `agent_result`,并写回同一 Redis 调用记录; +- 只把有界 `agent_result` 返回 Agent,禁止透传 `raw_response`; +- 清洗失败时返回 Tool ERROR,禁止透传未清洗原始结果。 + +Redis 中的完整调用记录至少包含:`tool_call_id`、`run_id`、`tool_name`、`request`、`raw_response`、`agent_result`、调用状态和起止时间。`tool_call_id` 使用框架提供的 Tool Call 协议 ID,是一次 Tool 调用在当前 Run 内的唯一引用;Harness 不另造第二套调用 ID,并可按 `runId + toolCallId` 取得完整记录。 + +Redis 是 Harness 内部基础设施,只允许 Harness 访问。任何 Agent 都不能获得 Redis Client、Redis Tool、key、连接信息或完整调用记录。本 Issue 不提供历史来源召回 Tool;Redis 只用于 Harness 的证据验真、短期调用追踪和最终引用生成,不作为 Agent 记忆。当前诊断若需要新的日志或数据库事实,由 Diagnosis Agent 通过当前受控 Tool 重新查询。 + +Redis 存储契约固定为: + +```text +key = superbiz:harness:tool-call:{runId}:{toolCallId} +status = PROJECTING | READY | ERROR +evidence_status = EVIDENCE_FOUND | NO_EVIDENCE | ERROR +``` + +- 一次 Tool Call 对应一个 Key,不建立额外 Evidence Index;Key 不包含 Query、Session 内容或 Tool 参数; +- `status` 表示调用/投影生命周期:Tool 原始响应写入后为 `PROJECTING`,`ToolResultProjector` 成功并写入 `agent_result` 后为 `READY`,执行或清洗失败为 `ERROR`; +- `evidence_status` 表示结果语义,只有 `status=READY` 时才允许为 `EVIDENCE_FOUND` 或 `NO_EVIDENCE`;失败记录为 `ERROR`; +- EvidenceGuard 只接受当前 Run 下 `status=READY` 且存在 `agent_result` 的记录;`evidence_status=ERROR` 不可引用,`NO_EVIDENCE` 只能用于负向观察; +- TTL 默认 2 小时并可配置,必须大于最大 Run 耗时、SemanticGuard 总耗时和安全缓冲; +- TTL 在创建时设置,后续读取或更新不得续期;取消和失败记录保留到 TTL,首版不永久归档; +- 首版默认 `max-record-bytes=1MB`、`max-agent-result-bytes=64KB`、`max-run-bytes=5MB`,均由 Harness 配置并根据运行数据校准; +- `raw_response` 超过单记录上限时不静默截断,调用进入 `ERROR/RESULT_TOO_LARGE`;`agent_result` 可按投影预算截断并标记 `truncated=true`; +- 使用 UTF-8 JSON,首版不压缩、不分片、不使用 Java 原生序列化或对象存储; +- Redis 使用独立 Key 前缀和 ACL,应用日志不得输出 `request/raw_response`。 + +数据分层术语固定为: + +- **Redis canonical invocation**:短期保存 `request/raw_response/agent_result` 的完整调用记录;当前版本允许未脱敏,但受 Harness-only ACL、TTL 和容量上限保护; +- **Agent projection**:`ToolResultProjector` 生成的有界 `agent_result`,经过 Tool-specific 聚合、截断和必要敏感字段清洗,只向 Diagnosis Agent、EvidenceGuard 和 SemanticGuard 的隔离快照开放; +- **Durable audit**:应用日志或业务审计表中的长期记录,只保存调用元数据、脱敏参数、耗时、状态和有界结果摘要,不保存 Redis `raw_response`。 + +```yaml +harness: + evidence: + key-prefix: "superbiz:harness:tool-call" + ttl: 2h + max-record-bytes: 1MB + max-agent-result-bytes: 64KB + max-run-bytes: 5MB +``` + +#### Output 边界 + +- 校验主 Agent 输出 Schema; +- 校验 Analysis Items 与 Tool Call IDs 的绑定; +- 执行 EvidenceGuard; +- 组装 SemanticGuard 的隔离输入; +- 根据验证结果决定最终释放或降级。 + +#### SSE 与审计 + +- 按最小 SSE 契约发送 metadata、脱敏 status/tool progress、最终 content 或 failure,以及 done; +- 记录模型调用、工具调用、验证结果、预算消耗和降级原因; +- 不输出内部 Prompt、Thought、原始 Tool 载荷和内部堆栈。 + +#### 预算配置 + +所有暂时无法准确估算的预算先集中提取为 Harness 配置,不在本 Issue 中伪造固定数值: + +- 主 Agent 最大模型调用次数、总 Tool 次数、单 Tool 次数、上下文/Token 和总耗时; +- SemanticGuard 单次超时、总耗时和输入大小; +- RAG 最大证据数、单条摘录和总字符数; +- 日志最大 Pattern/Event 数、消息长度、lookback 和总字符数; +- MySQL 最大行数、单元格长度、总字节数和查询超时; +- Redis Tool 调用记录的 TTL、单记录/Agent 投影/单 Run 大小上限。 + +首版使用静态应用配置和可调默认值,不建设配置中心、热更新、按租户覆盖或按模型路由配置。Harness 必须校验配置合法性、记录实际消耗和触发的预算原因;后续根据 Trace、Token、延迟和截断数据校准默认值。已经确认的“最多重试一次”属于失败语义,不允许通过配置放大为隐式重试循环。 + +#### 重试策略装配 + +重试策略从业务流程中提取为小型类型化配置,由 Harness 统一装配,不分散在 Agent、Tool、Controller 或 SDK 中: + +```text +RetryPolicy + -> max_attempts + -> retryable_failures + +HarnessRetryPolicies + -> intent_router: max_attempts=2 + -> diagnosis_agent: max_attempts=1 + -> tool_call: max_attempts=1 + -> semantic_guard: max_attempts=2 + -> evidence_repair: max_attempts=1 +``` + +- `HarnessRetryExecutor` 只承载“同一操作再次执行”的技术重试,首版用于 Intent Router 和 SemanticGuard; +- Intent Router 只在超时、传输失败或非法枚举输出时重试一次;第二次失败返回安全入口错误,不默认路由到 Diagnosis; +- Diagnosis Agent 整体和其单次模型调用不自动重试,ReAct 的正常模型/Tool 轮次不算重试; +- Tool 调用不自动重试;Tool `ERROR` 作为 Observation 返回,Agent 再次调用属于新的 Tool Action,必须由框架生成新的 `tool_call_id` 并计入预算; +- EvidenceGuard 的一次无 Tool 结构修复不是同操作重试,由 Harness 按显式流程执行; +- SemanticGuard 只在超时、传输失败、解析失败或 Schema 无效时使用同一输入重试一次;`UNSUPPORTED` 是有效业务结果,不重试; +- `NO_EVIDENCE`、取消、预算耗尽和业务拒绝均不可重试; +- 关闭或限制 SDK、HTTP Client 和数据库驱动的隐藏重试,所有实际 attempt 必须由 Harness 记录; +- 首版不引入 Retry DSL、插件注册中心、动态策略脚本或多级退避状态机。 + +Harness 不应包含: + +- StateGraph; +- Planner/Executor/Composer 节点; +- Agent 间消息路由; +- 通用工作流 DSL; +- HarnessPlugin 注册中心; +- 多级业务重试状态机; +- 自行实现的 ReAct While 循环。 + +### 4.3 EvidenceGuard + +原 Gatekeeper 不再作为独立编排组件存在。其能力进入 Diagnosis ReAct Agent 的 Harness: + +```text +Post-Tool + -> 按 tool_call_id 保存完整调用记录和 agent_result + +Pre-Release + -> 校验 Draft Schema + -> 校验 analysis_id 唯一 + -> 校验每条 Analysis 至少引用一个 tool_call_id + -> 校验 Conclusion/Action Plan/Recommendation 引用的 analysis_id 均存在 + -> 校验 tool_call_id 是否存在且属于当前 Run + -> 校验 Tool 调用返回可引用证据状态 + -> 校验 agent_result 存在 + -> 生成带真实性保证的 verified evidence snapshot +``` + +EvidenceGuard 使用确定性代码,不调用 LLM,不决定语义推导是否成立。它是原 Gatekeeper 的 Harness 内能力,而不是独立 Agent 或编排节点。具体规则包括: + +- `analysis_id` 在当前 Draft 内唯一; +- 每条 Analysis 的 `tool_call_ids` 非空,且每个 ID 对应当前 Run 中 `status=READY`、存在 `agent_result`、`evidence_status` 可引用的 Tool Call; +- `kind=NORMAL` 的 Analysis 只能引用 `EVIDENCE_FOUND`;`kind=NEGATIVE_OBSERVATION` 可以引用 `NO_EVIDENCE`,其 `agent_result` 必须保留原始查询条件、时间/数据范围和零匹配信息; +- `NO_EVIDENCE` 只证明当前查询范围内无匹配结果,不能被 EvidenceGuard 当作问题不存在、根因排除或系统健康的正向证据;`evidence_status=ERROR` 一律不可引用; +- 非空 Conclusion 的 `based_on_analysis_ids` 非空且全部指向已有 Analysis; +- 每条 Action Plan 和 Recommendation 的 `based_on_analysis_ids` 非空且全部指向已有 Analysis; +- `conclusion=null` 时允许保留有真实证据支撑、但尚不能形成结论的 Analysis;不要求每条 Analysis 都必须被 Conclusion 引用; +- 最终引用清单只能从 Draft 实际引用且通过上述校验的 Tool Calls 确定性展开。 + +通过后,所引用 Tool 调用的 Run 归属、执行状态和 `agent_result` 被视为可信;`ToolResultProjector` 的投影正确性由代码和单元测试保证。EvidenceGuard 不判断 Analysis 是否正确解释证据,也不判断 Analysis 是否足以推出 Conclusion,这些属于 SemanticGuard。SemanticGuard 不再重复访问 Redis 或重新验证真实性。 + +### 4.4 SemanticGuard Agent + +SemanticGuard 是独立审查 Agent,不加入主 ReAct Agent 上下文。 + +SemanticGuard 复用系统当前唯一配置的 ChatModel,不引入模型路由。它由 Harness 使用受限执行参数启动,Harness 负责隔离上下文、输入预算、调用超时、取消、最多一次重试、结构化输出校验、审计和最终释放策略。 + +必须满足: + +- 使用全新的隔离上下文; +- 没有工具; +- 没有隐式记忆; +- 没有 ReAct 循环; +- 不读取主 Agent Thought、完整历史或 System Prompt; +- 不读取未通过 EvidenceGuard 的数据; +- 不访问 Redis,不重新校验 tool_call_id 或 agent_result 真实性; +- 不重新规划、查询、总结、润色、改写或补充用户答案; +- 不回调主 Agent 形成循环; +- 每次尝试只执行单轮语义和时间线审查,不允许 Agent 自身循环;仅 Harness 可在超时、调用失败或结构化输出无效时重试一次。 + +输入只包含: + +- 原始 Query; +- 主 Agent Draft 的完整用户可见语义内容:Conclusion、Analysis、Action Plan、Recommendations 和 Limitations,但不包含内部 `tool_call_id`; +- 经过 EvidenceGuard 验真的 Evidence snapshot,包括精确 excerpt/value 和必要的可读来源信息:RAG 的 document_id/title/breadcrumb,或日志/MySQL 的 source、timestamp、scope。 + +`tool_call_id` 只在主 Agent 与 Harness、EvidenceGuard 之间使用,不注入 SemanticGuard 或其他 Agent。Harness 在组装 snapshot 时取出所引用调用的完整有界 `agent_result`,不读取或注入 `raw_response`,并将其中的来源信息、精确摘录和结构化值按 `analysis_id` 分组。该转换使用确定性代码,不调用 LLM 重新摘要;Harness 在模型调用之外保留 Tool 调用映射。 + +```json +{ + "analysis_id": "a1", + "analysis_text": "连接池活跃连接达到 50/50", + "verified_evidence": [ + { + "source_type": "LOG", + "source": "APPLICATION", + "scope": "order-service,最近 30 分钟", + "timestamp": "2026-07-21T10:30:00+08:00", + "excerpt": "HikariPool active=50, max=50, pending=23" + } + ] +} +``` + +RAG 使用稳定 `document_id/title/breadcrumb/excerpt`,日志使用 `source/scope/timestamp/pattern/event`,MySQL 使用逻辑数据源、查询范围、列名和有界行/值。SemanticGuard 依靠 `analysis_id -> verified_evidence` 关系校验每条 Analysis,不需要理解 Tool ID,也不能访问完整工具正文。 + +不向 SemanticGuard 传递 Diagnosis Plan、Tool 调用过程或其他审计数据。SemanticGuard 对最终将展示给用户的完整 Draft 做报告级校验: + +- Evidence 是否支持 Analysis; +- Analysis 是否能够推出 Conclusion; +- Action Plan 是否与已支持的分析/结论相关,危险行动是否标记人工确认; +- Recommendations 是否与诊断结果及 RAG/分析依据一致; +- Limitations 是否准确覆盖实际查询范围和缺失信息; +- 所有内容是否超出证据的服务、时间和数据范围。 + +SemanticGuard 只是审稿人,不是报告作者:它不能输出 corrected report、new conclusion、new action、new recommendation 或 user answer。真实性并不自动代表相关性、充分性或推导成立。 + +输出使用报告级二元状态,不使用未校准数字作为在线释放门禁,也不返回逐条 Analysis 状态: + +```json +{ + "verdict": "SUPPORTED", + "reason": "已验证证据能够支持诊断草稿中的分析并推出结论" +} +``` + +`verdict` 只允许 `SUPPORTED` 或 `UNSUPPORTED`。Diagnosis Agent 是唯一报告作者;SemanticGuard 不生成、改写或裁剪用户报告;Harness 不总结报告,只发布原 Draft 或固定 Fallback。`reason` 只进入审计,不直接作为用户答案。 + +第一次超时、调用失败、解析失败或 Schema 校验失败时,Harness 使用相同的已验真证据快照重试一次,不重新运行主 Agent 或 Tool。第二次仍失败则按未通过处理:不释放未验证草稿,只返回安全降级报告并正常结束 SSE。单次与总超时由 Harness 配置提供,初期不冻结具体数值。 + +### 4.5 意图识别 + +使用一次轻量 Chat 对当前 Query 做三分类: + +```text +SYSTEM_CHAT +KNOWLEDGE_QUERY +DIAGNOSIS +``` + +约束: + +- 输入只包含当前原始 `query`、可选 `last_intent` 和可选 `last_user_query`; +- 当前 Query 明确表达新主题时以当前 Query 为准;只有“继续”“那另一个服务呢”等追问才参考前一意图和问题; +- 不改写 Query; +- 不传完整历史、来源文档、evidence refs、Tool 结果或已发布答案; +- 不配置 Tool、Skill、记忆和 ReAct 循环; +- 输出只允许三个固定路由值; +- 原关键词 `QuestionComplexity` 不再作为主路由。 + +```json +{ + "query": "那退款服务呢?", + "last_intent": "DIAGNOSIS", + "last_user_query": "订单 order-123 支付超时,帮我排查" +} +``` + +三个意图使用以下固定执行器: + +#### SYSTEM_CHAT + +- 用于系统定位、能力说明、使用方式、简单问候和非业务闲聊; +- 使用无 Tool 的普通 ChatModel; +- 系统能力来自简短、受控的能力说明,不依赖模型隐式知识; +- 不进入 ReAct、EvidenceGuard 或 SemanticGuard; +- 输出普通用户答案。 + +#### KNOWLEDGE_QUERY + +- 用于内部文档、流程、API、错误码、配置规范和排障手册; +- 原始 Query 只执行一次有界 `lookup_knowledge`; +- 使用普通 Chat 基于 RAG Evidence 生成答案; +- Harness 校验证据引用; +- 不开放 query_logs、query_metrics 或 query_mysql; +- 不进入完整 Diagnosis ReAct 循环; +- 输出 `answer + references + limitations`。 + +#### DIAGNOSIS + +- 用于故障排查、根因定位、实时状态分析和多证据组合; +- 进入完整 Diagnosis ReAct Agent; +- 可使用 RAG、日志和 MySQL 等受控证据 Tool;本 Issue 不提供 `query_metrics`; +- 经过 Harness EvidenceGuard 和独立 SemanticGuard; +- 输出完整诊断报告。 + +三个执行路径都接收同一个原始 Query,不做 Query 改写,不相互调用。原 `DATABASE_QUERY` 路由删除;MySQL Tool 只作为 DIAGNOSIS 的证据工具,不对应独立意图。 + +### 4.6 Diagnosis Agent 极简上下文 + +Diagnosis Agent 不接收完整 Session 历史,只接收当前 Query 和可选的上一个已完成回合: + +```json +{ + "query": "那退款服务呢?", + "previous_turn": { + "user_query": "订单 order-123 支付超时,帮我排查", + "published_conclusion": "支付超时与连接池耗尽有关", + "scope": "order-service,最近 30 分钟", + "limitations": ["尚未取得长事务数据"], + "source_documents": [ + { + "document_id": "payment-timeout-guide", + "title": "支付超时排查手册" + } + ] + } +} +``` + +- `previous_turn` 只取同一 Session 上一个已完成回合;无上一回合或上一回合未安全发布时为 `null`; +- 只读取已安全发布的结构化 Conclusion、Scope、Limitations 和 RAG 来源文档元数据; +- `source_documents` 只包含稳定 document ID 和 title,不包含正文、Tool Call ID、Tool 结果或内部引用; +- 不复用历史日志或 MySQL 数据,需要当前运行事实时重新调用受控 Tool; +- Harness 从上一回合结构化最终结果确定性组装,不调用 LLM 生成摘要; +- 字段长度和来源文档数量使用集中配置,超限时按字段边界截断或删除末尾来源; +- 当前 Query 始终优先,`previous_turn` 只辅助理解追问,不作为当前事实证据。 + +`previous_turn` 的持久化真理源固定为 `diagnosis_run`。Run 记录保存 `intent`、`release_outcome` 和 `published_result`;`published_result` 只包含 `user_query/published_conclusion/scope/limitations/source_documents`,不包含 Tool Call ID、原始证据、完整 Draft 或 SemanticGuard 审计原因。只允许读取同一 Session 最近一个 `intent=DIAGNOSIS`、`release_outcome=SUCCESS` 且 `published_result` 非空的 Run;`FALLBACK/FAILED/CANCELLED` 只用于审计,不进入下一轮上下文。Redis SessionContext 只作为短期会话缓存,不是该契约的真理源。 + +### 4.7 SSE 用户交互 + +SSE 采用“过程事件实时发送、最终报告安全释放”的口径: + +- SSE 连接在整个诊断期间保持打开; +- 计划、Tool 进度、清洗、EvidenceGuard 和 SemanticGuard 状态实时发送; +- SemanticGuard 完成前不发送最终结论; +- 校验通过后以一个 `content` 事件释放结构化最终报告; +- 不将已生成的完整答案切片伪装成 Token 流; +- SemanticGuard 超时/失败时发送安全降级内容并结束; +- 不引入 WebSocket、轮询或验证后的第二个 Composer Agent。 + +首版只定义五种业务事件,固定顺序为: + +```text +metadata -> status* -> content|failure -> done +``` + +- `metadata`:固定第一个且只发送一次,包含 `session_id` 和 `run_id`; +- `status`:可发送零到多次,只包含固定阶段码和安全的用户可见说明; +- `content`:最多一次,承载正常答案或固定安全 Fallback,不做 Token 切片; +- `failure`:最多一次,只用于 Router/Harness 等无法生成任何安全响应的技术失败,不能与 `content` 同时发送; +- `done`:固定最后一个且只发送一次,`outcome` 只允许 `SUCCESS/FALLBACK/FAILED`。 + +最小事件结构: + +```text +event: metadata +data: {"session_id":"...","run_id":"..."} + +event: status +data: {"code":"SEMANTIC_VALIDATING","message":"正在进行安全校验"} + +event: content +data: {"content_type":"DIAGNOSIS_REPORT","payload":{...}} + +event: failure +data: {"code":"ROUTING_UNAVAILABLE","message":"当前暂时无法处理该请求,请稍后重试"} + +event: done +data: {"outcome":"SUCCESS"} +``` + +EvidenceGuard 或 SemanticGuard 降级使用 `content + done(FALLBACK)`;Router 重试后仍失败等无法形成安全内容的技术错误使用 `failure + done(FAILED)`。客户端主动断开后不再尝试发送终态事件,Harness 必须取消当前模型和 Tool 调用,并只在内部将 Run 记录为 `CANCELLED`。首版不实现断线续传、事件重放、事件序号恢复、轮询或 WebSocket。 + +禁止输出: + +- Chain of Thought 和内部规划; +- System Prompt、模型 Prompt 和完整上下文; +- 未脱敏 Tool 参数; +- 原始日志、完整数据库行和完整检索 Trace; +- 内部异常、供应商错误和 Java 堆栈; +- 尚未通过释放门禁的强结论; +- Planner plan、Verifier verdict、Graph retry 等内部实现状态。 + +只保留一个 `POST /api/chat` SSE 接口,不保留同步 Chat 或 `/api/chat_stream` 兼容路径。 + +## 5. ACI Tool Contract + +### 5.1 原则 + +Agent-facing Tool 必须满足: + +1. 名称说明 Agent 能做什么,不暴露内部实现; +2. 描述说明什么时候调用、输入什么、不能用于什么; +3. 参数数量最小、含义明确、具备 Schema; +4. Agent 不控制连接、凭据、region、topK、limit 等基础设施参数,除非业务确有必要; +5. 输出稳定、结构化、有界; +6. 明确区分成功有证据、成功无证据和工具失败; +7. 输出包含可继续使用的稳定 Tool Call Reference; +8. 错误信息安全、可恢复,不泄露内部细节; +9. 相同输入不因隐藏 Session 记忆产生不可解释的语义变化; +10. Agent 只看到执行结果,不看到审计/调试实现。 + +### 5.2 通用状态语义 + +Agent-facing evidence Tool 统一使用 `evidence_status`: + +```text +evidence_status = EVIDENCE_FOUND 查询成功且返回可使用证据 +evidence_status = NO_EVIDENCE 查询成功但范围内没有证据 +evidence_status = ERROR Tool 执行、参数、权限或清洗失败 +``` + +`EVIDENCE_FOUND` 只表达 Tool 找到了证据,不声明证据支持任何分析或结论。`NO_EVIDENCE` 不是异常,只能支持明确标记为负向观察的 Analysis,不能推出问题不存在、问题已排除或系统健康。`ERROR` 不能伪装成无结果。`SUPPORTED/UNSUPPORTED` 只保留给 SemanticGuard。 + +### 5.3 通用 Tool Call Reference + +每次可引用 Tool 调用只需要框架提供的一个稳定 ID: + +```text +tool_call_id +``` + +`tool_call_id` 由框架随 Tool Call 请求提供,Harness 不替换或重新生成该 ID,只负责在 Tool 执行前校验并贯穿 Agent response、Redis 调用记录、EvidenceGuard 和最终 Trace。该 ID 必须非空、满足长度/字符集约束,并在同一 Run 内唯一;重复、非法或缺失 ID 不得覆盖既有记录,直接进入 Tool ERROR。Redis Key 使用 `runId + toolCallId` 隔离不同 Run。SemanticGuard 不接收该 ID,只接收 Harness 从对应调用的 `agent_result` 组装的 verified evidence snapshot。 + +## 6. 工具结果清洗与投影层 + +本 Issue 使用中文“工具结果清洗与投影层”作为架构名称,不再使用 PTK/RTK 缩写。代码统一使用 `ToolResultProjector`,首版只有三个 Tool-specific 实现:`RagResultProjector`、`QueryLogsResultProjector` 和 `MysqlResultProjector`。不再拆分 Cleaner、Sanitizer、ContextBuilder 或通用转换 Pipeline。 + +统一处理顺序: + +```text +Agent Tool Request + -> Pre-Tool Guard + -> Mock Tool / MCP / DataSource + -> Raw Invocation Persistence + -> Tool-specific ToolResultProjector + -> Agent Result Persistence + -> Bounded Agent-facing Result +``` + +原则: + +- 完整结果只在可信边界内保存; +- 当前版本不在 canonical evidence 持久化阶段做脱敏;Agent 仍只能接收有界的 Tool-specific 投影,原始 Redis 结果只能由 Harness 读取; +- Agent 只接收有界投影; +- Agent 看到的证据必须可回到 canonical result; +- `ToolResultProjector` 使用确定性代码,不调用 LLM 做二次摘要; +- 不建立万能清洗算法;统一 Envelope,下沉 Tool-specific projection; +- 不把完整结果写入进程本地 `refs/*.md` 作为主存储; +- 清洗失败不能绕过 Harness。 + +## 7. RAG Tool 设计 + +### 7.1 ACI 描述 + +Tool 只说明: + +```text +查询内部知识库中的文档、接口说明、错误码和排障手册。 +适用于稳定背景知识,不用于查询实时日志、指标或数据库状态。 +输入 query:需要查询的问题或关键词。 +``` + +不向 Agent 解释 L0/L1、category filter、fallback attempt、rerank 和 VectorStore/Milvus 实现。 + +### 7.2 Agent-facing 输出 + +```json +{ + "evidence_status": "EVIDENCE_FOUND", + "tool_call_id": "tool-123", + "query": "支付超时处理方式", + "evidence": [ + { + "document_id": "payment-timeout-guide", + "source": "payment-timeout.md", + "title": "支付超时排查", + "breadcrumb": "支付系统 > 故障排查", + "excerpt": "当支付请求出现 ERR_TIMEOUT 时,应先检查……" + } + ], + "returned_count": 1, + "truncated": false +} +``` + +### 7.3 Agent 不可见内容 + +- ContextPack; +- RetrievalTrace; +- RerankTrace; +- L0 hints; +- 原始候选列表; +- 向量原始分数; +- hit reasons; +- fallback attempts; +- Session 级 retrieved domains; +- 完整 Metadata; +- 完整文档正文。 + +这些内容按审计需要保存或删除,但不进入 Agent 上下文。 + +### 7.4 RAG 结果投影 + +- 删除 EvidenceBlock 与 ContextPack 的重复正文; +- 返回精确文档摘录,不使用 LLM 摘要; +- 相同文档/段落去重; +- 限制 evidence 数量、单条 excerpt 和整体字符数; +- 文档内容作为不可信证据数据,不执行其中的指令; +- 明确 `NO_EVIDENCE` 和 `ERROR`; +- 删除 Session 隐式去重或改为显式 Run 内重复查询控制。 + +## 8. query_logs Tool 设计 + +### 8.1 ACI 输入 + +删除 Agent 可见的 `getAvailableLogTopics`。固定/动态 Topic 目录由 Harness 或 Tool Adapter 管理。 + +当前环境没有真实日志数据,本 Issue 首版继续使用现有 Mock 日志实现;不新增 MCP 或 Java CLS 适配器。Mock 只作为数据源实现,仍必须经过相同的 ACI、Harness、canonical evidence 和 Agent projection 链路。后续真实适配器若接入,必须复用本节 Contract,不在本 Issue 提前设计适配平台。 + +```json +{ + "topic": "APPLICATION", + "query": "order-service HikariCP connection timeout", + "lookback_minutes": 30 +} +``` + +约束: + +- topic 使用稳定逻辑枚举; +- query 表达日志检索目标; +- lookback 有默认值和最大值; +- region、TopicId、limit 由系统控制; +- Agent 不需要先调用发现工具; +- 当前仅验证本地 Mock 返回 Agent-facing Contract;真实 CLS/MCP 的兼容性留待后续接入时验证。 + +### 8.2 Agent-facing 输出 + +```json +{ + "evidence_status": "EVIDENCE_FOUND", + "tool_call_id": "tool-456", + "source_kind": "MOCK", + "scope": { + "topic": "APPLICATION", + "query": "order-service HikariCP connection timeout", + "start_time": "...", + "end_time": "..." + }, + "match_count": 126, + "returned_count": 1, + "patterns": [ + { + "count": 84, + "first_seen": "...", + "last_seen": "...", + "level": "ERROR", + "service": "order-service", + "example": "HikariPool connection is not available" + } + ], + "events": [ + { + "timestamp": "...", + "level": "ERROR", + "service": "order-service", + "message": "HikariPool connection is not available" + } + ], + "truncated": true +} +``` + +### 8.3 日志结果投影 + +- 按稳定日志模板聚合重复事件; +- Pattern 保存 count、first_seen、last_seen 和代表样本; +- 额外保留少量按时间排序的事件,防止丢失时间线; +- 不固定删除 INFO,只根据 Query 和有界投影选择证据; +- 脱敏 Token、密码、Authorization Header 等敏感数据; +- 限制单条消息、Pattern 数、Event 数和整体字符数; +- 返回完整查询范围,防止将局部结果扩大为全局结论; +- `match_count` 表示查询范围内的原始匹配事件总数,`returned_count` 表示 `events` 中实际返回的有界时间线事件数量;Pattern 数量直接由 `patterns` 数组长度表示; +- Redis `raw_response` 不进入 Agent 上下文;持久化审计只保存脱敏参数、聚合元数据和有界结果摘要。 +- `source_kind=MOCK` 必须保留在 `agent_result` 和诊断范围中;Harness 不得把 Mock 数据表述为生产实时事实。 + +## 9. 新增 MySQL Tool 设计 + +### 9.1 定位 + +新增一个只读、受限、可审计的外部 MySQL 查询 Tool。该 Tool 查询独立配置的业务/诊断数据源,不连接 Agent 自身的持久化数据库。 + +连接和安全策略由后续新增的独立配置文件提供。配置只暴露逻辑数据源 ID,不把真实连接信息交给 Agent: + +- 数据源 URL、账号和密码不写入 Prompt、Skill 或 RAG 文档; +- 密码等敏感值使用环境变量或受控 Secret 注入,不在配置文件中硬编码; +- 配置定义允许的 schema/table/column、超时、最大行数和最大结果大小; +- Tool 通过逻辑数据源 ID 选择连接,不接受 Agent 传入 URL、账号、密码或任意 JDBC 参数; +- 不复用现有 Python 查询脚本的连接和执行方式;现有脚本中的硬编码凭据和可写执行分支必须清理。 + +表名、字段名、关联关系和业务语义后续从 Skill 或 RAG 召回的数据字典/查询知识中获得,MySQL Tool 本身不负责发现表结构。Skill 或 RAG 文档可以帮助 Agent 生成 SQL,但它们只是候选查询知识,不是授权来源。Harness 必须将 SQL AST 中实际使用的 schema/table/column 与独立配置的 allowlist 做最终校验;文档中出现但配置未授权的对象一律拒绝。 + +首版只提供静态“库、表、字段”三层白名单,不在本 Issue 设计租户或行级权限: + +```yaml +harness: + mysql-tools: + data-sources: + order_readonly: + jdbc-url: ${ORDER_MYSQL_JDBC_URL} + username: ${ORDER_MYSQL_USERNAME} + password: ${ORDER_MYSQL_PASSWORD} + default-schema: order_db + allowed-schemas: + order_db: + tables: + biz_order: + columns: + - order_id + - user_id + - payment_status + - created_at + - updated_at + payment_record: + columns: + - payment_id + - order_id + - channel + - status + - created_at + - updated_at +``` + +配置语义: + +- `data-sources` Key 是 Agent 可传入的逻辑数据源 ID,不是真实连接名; +- `allowed-schemas` 下每个 Key 是允许访问的 MySQL 库/schema;未配置的库一律拒绝; +- 每个库只允许 `tables` 中显式列出的表;每张表只允许 `columns` 中显式列出的字段; +- 首版不支持 `*`、正则、前缀匹配、继承或动态白名单;库、表、字段均为精确匹配; +- SQL 未显式写库名时只能使用该数据源的 `default-schema`,且 default schema 必须同时存在于 `allowed-schemas`; +- JOIN 中每个表及每个投影、过滤、关联、分组、排序字段都必须通过对应库/表/字段校验; +- 未限定字段通过 JSqlParser 的表别名和作用域解析到唯一表;无法唯一解析时拒绝; +- 函数白名单、超时、行数和字节预算继续使用 Harness 的独立配置,不混入库表字段结构; +- 凭据只引用环境变量/Secret,不允许在配置文件中写明文。 + +固定调用链: + +```text +Skill/RAG 召回候选数据字典 + -> Agent 使用明确表名和列名生成参数化 SQL + -> Harness 解析 SQL AST 并用独立配置做授权裁决 + -> MySQL Tool 使用配置映射的只读数据源执行 + -> Harness 清洗、持久化 canonical evidence 并投影给 Agent +``` + +MySQL Tool 不向 Agent 开放 `SHOW TABLES`、`SHOW COLUMNS`、`DESCRIBE` 或直接查询 `information_schema` 的元数据发现能力,避免绕过 Skill/RAG 知识边界和配置授权。 + +### 9.2 ACI 输入 + +初步推荐采用参数绑定的受限 SQL: + +```json +{ + "data_source": "order_readonly", + "sql": "SELECT order_id, payment_status, updated_at FROM biz_order WHERE order_id = ?", + "params": ["order-123"] +} +``` + +只允许: + +- 单条 `SELECT`; +- 显式列名; +- `INNER JOIN` 和 `LEFT JOIN`; +- `WHERE`、`GROUP BY`、`HAVING`、`ORDER BY` 和受控 `LIMIT`; +- allowlist 中的聚合/标量函数,首版至少覆盖 `COUNT`、`SUM`、`AVG`、`MIN` 和 `MAX`; +- 参数绑定; +- 独立配置中存在的逻辑数据源 ID; +- 上述静态 allowlist 内的 schema/table/column 精确匹配; +- Harness 注入或限制的行数、超时和字节预算。 + +必须阻止: + +- INSERT/UPDATE/DELETE/REPLACE; +- CREATE/ALTER/DROP/TRUNCATE; +- 投影通配符 `SELECT *` 和 `table.*`,包括子查询中的通配符; +- `WITH`、子查询、`UNION` 和窗口函数; +- `CROSS JOIN`、未在 allowlist 中的函数和无法识别的 AST 节点; +- `SHOW TABLES`、`SHOW COLUMNS`、`DESCRIBE` 和 `information_schema` 元数据查询; +- CALL; +- 多语句; +- SELECT INTO OUTFILE/DUMPFILE; +- FOR UPDATE; +- SLEEP/BENCHMARK/LOAD_FILE 等危险函数; +- 任意事务控制和写入路径。 + +`COUNT(*)` 作为聚合函数例外允许,因为它不返回全部字段;禁止的是字段投影通配符。若后续需要完全禁止星号,可将查询知识统一改为 `COUNT(1)`,不影响整体校验结构。 + +### 9.3 JSqlParser 校验 + +首版引入 JSqlParser 将 Agent 生成的 SQL 解析为 AST。JSqlParser 只负责理解 SQL 结构,不连接或执行数据库;Harness 使用 AST visitor 执行 fail-closed 白名单校验: + +```text +解析且确认只有一个 Statement + -> 确认根节点是允许的 SELECT + -> 遍历所有 table/column/join/function/子表达式 + -> 校验逻辑数据源和 schema/table/column/function allowlist + -> 校验占位符数量与 params 一致 + -> 注入或压低 LIMIT + -> 通过 PreparedStatement 执行 +``` + +解析失败、遍历到未知节点或无法证明符合允许子集时直接拒绝,不回退到正则、`startsWith` 或容错执行。具体依赖版本在实施阶段选择兼容 Java 17 的稳定版本,并通过固定安全用例验证后锁定。 + +安全边界必须同时包含: + +- MySQL 专用只读账号; +- JSqlParser AST 校验,而不是 startsWith/正则; +- 独立配置提供的 schema/table/column allowlist; +- JDBC/数据库侧 read-only; +- JDBC `PreparedStatement` 和 `setMaxRows`; +- 查询超时; +- 最大行数、单元格长度和总字节数; +- 客户端断开时取消查询; +- 独立连接池或明确资源隔离。 + +### 9.4 Agent-facing 输出 + +```json +{ + "evidence_status": "EVIDENCE_FOUND", + "tool_call_id": "tool-789", + "columns": ["order_id", "payment_status", "updated_at"], + "rows": [ + { + "order_id": "order-123", + "payment_status": "FAILED", + "updated_at": "2026-07-20 10:30:00" + } + ], + "returned_count": 1, + "truncated": false +} +``` + +### 9.5 MySQL 结果投影 + +- 限制返回行数; +- 限制单元格、整行和整体结果大小; +- Blob、长文本和长 JSON 截断并标记; +- 按字段策略脱敏密码、Token、个人敏感信息; +- 每行保留结构化字段,整次调用通过 `tool_call_id` 引用; +- SQL 模板、脱敏参数、耗时、状态和有界结果摘要进入持久化审计;完整 `raw_response` 只按 Redis canonical invocation 契约短期保存; +- 连接字符串、账号、密码和内部堆栈不得进入 Agent 或审计明文。 + +## 10. 主 Agent 输出契约与低置信度处理 + +### 10.1 两类 Plan 必须分离 + +- **Diagnosis Plan**:主 Agent 开始排查时产生的 3–5 个高层步骤,通过 SSE status 提前输出;只描述准备查什么,不输出 Thought、完整 SQL、敏感参数或内部推理。 +- **Action Plan**:诊断完成后给用户的下一步操作;与 Diagnosis Plan 不是同一个字段。 + +Diagnosis Plan 不参与 EvidenceGuard 或 SemanticGuard,因为它不是事实结论。执行中需要补充查询时只发送脱敏状态事件,不建立复杂 Plan 版本状态机。 + +### 10.2 已确认的 Agent Draft Schema + +Diagnosis Agent 是诊断报告唯一的语义作者,负责生成结论、分析支撑、行动计划、长期建议、诊断范围与限制,并通过 Tool Call ID 选择各项分析所引用的证据。Harness 不生成、补充、删改或覆盖这些语义字段。 + +主 Agent 不输出不可安全裁剪的自由文本 answer,而是输出以下结构: + +```json +{ + "conclusion": { + "text": "支付超时的主要原因是数据库连接池耗尽", + "based_on_analysis_ids": ["a1"] + }, + "analysis": [ + { + "analysis_id": "a1", + "kind": "NORMAL", + "text": "连接池活跃连接达到 50/50,并存在等待线程", + "tool_call_ids": ["tool-456"] + } + ], + "action_plan": [ + { + "action": "查询连接泄漏和长事务", + "based_on_analysis_ids": ["a1"], + "requires_human_confirmation": false + }, + { + "action": "临时扩大连接池容量", + "based_on_analysis_ids": ["a1"], + "requires_human_confirmation": true + } + ], + "recommendations": [ + { + "text": "增加连接池等待线程和获取耗时告警", + "based_on_analysis_ids": ["a1"] + } + ], + "limitations": { + "scope": "order-service,最近 30 分钟", + "missing_info": ["尚未取得慢 SQL 和长事务数据"] + } +} +``` + +字段语义: + +- `conclusion`:结论先行;引用支撑它的 analysis IDs,不重复绑定 Evidence;证据不足时允许为 `null`。 +- `analysis`:可独立验证的分析支撑。每一项必须有唯一 analysis ID、固定 `kind=NORMAL|NEGATIVE_OBSERVATION` 和至少一个 `tool_call_id`,拒绝无来源分析。负向观察只能绑定 `NO_EVIDENCE`,并严格限定为对应 Tool 的实际查询范围。 +- `action_plan`:立即执行的下一步,必须通过 `based_on_analysis_ids` 绑定分析。危险或有副作用的操作必须标记 `requires_human_confirmation=true`,Harness/HITL 策略拥有最终决定权。 +- `recommendations`:长期治理建议,必须通过 `based_on_analysis_ids` 绑定分析;来自知识库或外部规范时,支撑该分析的 Tool Call 应来自 RAG。 +- `limitations.scope`:由 Diagnosis Agent 根据本次实际查询范围生成;Harness 不改写或覆盖,SemanticGuard 负责校验它是否超出 verified evidence 中的真实 Tool scope。 +- `limitations.missing_info`:尚未取得、导致结论受限的关键材料。 + +主 Agent 只向 Harness 输出 Tool Call IDs,不输出可自行编造的完整 Reference 内容。Harness 在调用 SemanticGuard 前使用这些 IDs 读取对应的 `agent_result`,仅传递已解析的来源文档/来源工具和证据内容。Harness 只把 Agent 已选择的 Tool Call ID 确定性展开为最终引用清单,展示来源、scope/timestamp 和精确 evidence excerpt/value;它不能选择额外来源、生成引用摘要或改变报告语义。 + +最终用户报告顺序固定为: + +```text +1. 结论 +2. 分析支撑 +3. 行动计划 +4. 长期建议 +5. 诊断范围与限制 +6. 引用 +``` + +### 10.3 释放语义 + +不使用主 Agent 自报的 0–1 数值置信度作为释放依据。 + +```text +EvidenceGuard 失败 + -> 不进入 SemanticGuard + -> 首次失败执行一次无 Tool 的结构化输出修复 + -> 修复后重新执行 EvidenceGuard + -> 第二次仍失败则安全 Fallback + +SemanticGuard SUPPORTED + -> Harness 原样释放主 Agent 的语义报告,并附加由其 Tool Call IDs 确定性展开的引用清单 + +SemanticGuard UNSUPPORTED/TIMEOUT/ERROR + -> 不释放主 Agent 草稿、分析、结论、行动或建议 + -> Harness 返回固定安全 Fallback,只包含已验证来源、证据不足说明和诊断限制 +``` + +固定安全 Fallback 由 Harness 通过确定性模板生成,不调用 LLM 或 Composer: + +```json +{ + "type": "SEMANTIC_UNSUPPORTED", + "conclusion": null, + "message": "当前证据不足,无法确认根因", + "verified_sources": [ + { + "source_type": "LOG", + "source": "APPLICATION (MOCK)", + "scope": "order-service,最近 30 分钟" + } + ], + "limitations": ["语义校验未通过或暂不可用"], + "next_steps": ["补充当前缺失的数据后重新发起诊断"] +} +``` + +- `type` 是稳定的用户可见降级分类,只允许 `EVIDENCE_VALIDATION_FAILED`、`SEMANTIC_UNSUPPORTED` 和 `SEMANTIC_UNAVAILABLE`; +- `EVIDENCE_VALIDATION_FAILED` 表示 EvidenceGuard 修复后仍未通过,此时 `verified_sources` 必须为空; +- `SEMANTIC_UNSUPPORTED` 表示证据已通过 EvidenceGuard,但不支持 Agent 报告;`SEMANTIC_UNAVAILABLE` 表示 SemanticGuard 第二次超时、调用失败或输出无效;这两类可以返回本次已通过 EvidenceGuard 的来源; +- `verified_sources` 只列出 EvidenceGuard 已通过的来源名称和查询范围,不包含主 Agent 的 Analysis、Conclusion 或推导文本; +- `limitations` 使用稳定的用户可见原因分类,不直接展示 SemanticGuard `reason`、供应商错误或内部异常; +- `next_steps` 只列出补充数据、缩小范围或稍后重试等安全动作,不包含主 Agent 未校验的 Action Plan/Recommendations; +- 三种降级使用同一 Fallback Schema;内部可使用比 `type` 更细的 `fallback_reason` 审计码,但不得将供应商错误或异常细节暴露给用户; +- Fallback 通过最终 `content` 事件发送并正常 `done`,不作为 HTTP/SSE 系统错误。 + +## 11. 行为与协议变化 + +这是有意的不兼容重构: + +- 删除同步 `/api/chat` 响应模式; +- 删除 `/api/chat_stream`; +- `POST /api/chat` 改为唯一 SSE Chat 协议; +- 删除 `QuestionComplexity` 主路由; +- Planner/Executor/Verifier/Composer 不再作为主链路多 Agent 角色; +- 不引入业务 StateGraph; +- Tool 参数和返回协议发生不兼容变化; +- RAG Session 隐式检索记忆删除或改为显式 Run 控制; +- `getAvailableLogTopics` 不再对 Agent 暴露; +- 新增只读 MySQL Tool; +- 原 Gatekeeper 编排身份消失,能力进入 Harness EvidenceGuard; +- SemanticGuard 使用隔离上下文且不能形成 Agent 回路。 + +受影响的消费者包括前端 Chat、Controller tests、ChatService/Agent tests、Prompt contracts、Tool contract tests、Trace/Eval fixtures 和任何直接调用旧 Tool 方法的代码。 + +## 12. 分阶段实施计划 + +ISS-014 是总设计 Issue,不创建跨阶段共享的 OpenSpec change。以下 11 个实施切片各自执行一个完整、串行的 sm-flow: + +| 阶段 | OpenSpec change | 状态 | +|---|---|---| +| 0 | `single-react-design-freeze` | Completed;已归档,Git commit 见阶段历史 | +| 1 | `single-react-aci-tool-contracts` | Pending | +| 2 | `single-react-harness-run-context` | Pending | +| 3A | `single-react-tool-invocation-store` | Pending | +| 3B | `single-react-rag-log-projections` | Pending | +| 3C | `single-react-mysql-readonly-tool` | Pending | +| 4 | `single-react-diagnosis-agent` | Pending | +| 5 | `single-react-evidence-semantic-guards` | Pending | +| 6A | `single-react-chat-application-usecase` | Pending | +| 6B | `single-react-chat-sse-cutover` | Pending | +| 7 | `single-react-cleanup-e2e` | Pending | + +每个 change 独立完成 Discover、Commit、Apply、阶段验收、Archive 和 Git commit;前一阶段 Archive 且提交后才允许启动下一阶段,不并行实施相邻阶段。 + +### 阶段 0:设计冻结与安全前置 + +目标:冻结本 Issue 的未决契约,不修改业务执行链。 + +任务: + +- 将已确认的主 Agent Draft/Analysis/Conclusion Schema 固化为契约测试; +- 将 EvidenceGuard 一次无 Tool 输出修复和二次失败 Fallback 固化为契约测试; +- 冻结 SemanticGuard 报告级 `SUPPORTED/UNSUPPORTED` 二元契约和固定安全 Fallback; +- 将过程事件实时 SSE、最终报告门禁后释放的口径固化为协议; +- 将已确认的 SYSTEM_CHAT/KNOWLEDGE_QUERY/DIAGNOSIS 执行器映射固化为契约测试; +- 冻结 Harness 预算配置项、合法性校验和预算耗尽语义,不要求在本阶段冻结准确默认数值; +- 冻结 `RetryPolicy/HarnessRetryPolicies/HarnessRetryExecutor`、固定重试矩阵和隐藏重试禁用规则; +- 冻结工具结果清洗与投影层及 `ToolResultProjector` 代码命名; +- 确认 Redis 调用记录保存 `request + raw_response + agent_result`,冻结单 Key、状态、默认 TTL/容量、非续期和 Harness-only ACL;当前版本明确暂不做持久化脱敏,并记录为后续安全增强项; +- 确认 JSqlParser、首版 SQL 允许子集、MySQL allowlist 和只读账号方案; +- 移除并轮换现有脚本中的硬编码数据库凭据; +- 建立改造前 focused baseline。 + +验收: + +- 所有阻塞性问题有明确决策; +- 无业务 Java 行为变化时可跳过单元测试,并记录理由; +- Security prerequisite 完成后才能进入 MySQL Tool 实现。 + +### 阶段 1:ACI Tool Contract 冻结 + +目标:先定义 Agent 能看到的工具界面,不实现 Agent 架构切换。 + +任务: + +- 冻结 RAG、query_logs、query_mysql Request/Response Schema; +- 冻结调用生命周期 `status=PROJECTING/READY/ERROR` 与结果语义 `evidence_status=EVIDENCE_FOUND/NO_EVIDENCE/ERROR`,禁止混用; +- 冻结 `tool_call_id` 调用引用契约; +- 缩短 Tool descriptions,移除内部实现泄漏; +- 为三类 Tool 添加独立契约测试; +- 确认现有 Mock Tool 的 Agent-facing Contract 和后续适配边界;本阶段不实现真实 MCP/CLS 适配器。 + +验收: + +- Schema 和错误语义契约测试通过; +- Agent-facing 与 audit-facing 字段清单无交叉歧义; +- 不新增通用插件框架或工作流 DSL。 + +### 阶段 2:Harness Core 与 RunContext + +目标:先建立后续 Tool、Agent、Guard 和入口共同依赖的显式运行上下文,不提前切换 HTTP/SSE 协议。 + +任务: + +- 定义不可变 `RunContext`,显式携带 `sessionId/runId/deadline/cancellation/budget/retryPolicies`; +- 定义 Run 创建、预算计数、取消传播和终态管理的 Harness Core; +- 装配 `HarnessRetryPolicies/HarnessRetryExecutor`,关闭或纳管底层隐藏重试; +- 定义 Redis Tool Call Key Factory 和单 Run 容量计数器,但不实现具体 Tool 投影; +- 所有后续边界通过参数或受控上下文对象显式接收 `RunContext`,禁止 ThreadLocal; +- 使用 Fake Model/Tool 验证 deadline、取消、预算、重试记录和 Run 终态; +- 不修改 Controller 协议,不接入旧 ChatService 的临时适配层。 + +验收: + +- RunContext 无隐式线程状态,可在同步/异步边界显式传播; +- 预算耗尽、取消和异常均产生唯一 Run 终态; +- 阶段 3A 可直接依赖 Harness Core,不需要临时耦合旧 ChatService; +- focused Harness Core tests 通过。 + +### 阶段 3A:Harness Tool Boundary 与 Canonical Invocation Store + +目标:实现所有 Tool 共用的 Pre/Post 边界、canonical invocation store 和统一状态机,不在本阶段实现 Tool-specific 投影。 + +任务: + +- 接收、校验并传播框架提供的 `tool_call_id`,覆盖缺失、非法、重复和跨 Run 引用测试; +- 实现 Pre-Tool Schema/权限/只读/预算校验; +- 将 `request + raw_response + agent_result` 写入同一 Redis Tool 调用记录;禁止仅依赖截断 preview 做验真; +- 实现 `PROJECTING/READY/ERROR` 生命周期和独立 `evidence_status`,以及 TTL 不续期、单记录/单 Run 容量门禁和 `RESULT_TOO_LARGE`; +- 清洗失败显式 ERROR; +- 使用 Fake Tool/Projector 增加 invocation store、状态、TTL、容量、no-evidence 和 error tests。 + +验收: + +- 未清洗原始结果不会进入 Agent; +- 每条返回证据可以回到当前 Run 的 canonical result; +- 只有当前 Run 的 `READY` 记录可进入引用校验;`EVIDENCE_FOUND`、`NO_EVIDENCE` 和 `ERROR` 的引用边界有契约测试,读取不会刷新 TTL,超限原始响应不会被静默截断; +- 阶段 3B/3C 可复用同一 Tool boundary 和 invocation store,不复制状态机或 Redis 访问逻辑。 + +### 阶段 3B:RAG 与 query_logs 结果投影 + +目标:在阶段 3A 的统一边界上迁移两个现有证据 Tool,并完成 Agent-facing 有界投影。 + +任务: + +- RAG:审计分离、去重复正文、精确摘录和上下文预算; +- query_logs:在现有 Mock 上实现统一 Contract、模板聚合、时间线抽样和脱敏;不实现真实 MCP/CLS 适配器; +- 两个 Tool 统一接入阶段 3A 的 canonical invocation store、`evidence_status` 和 Tool Call ID 契约; +- 增加 Tool contract、truncation、no-evidence、负向观察和 error tests。 + +验收: + +- RAG/日志原始结果不会未经投影进入 Agent; +- RAG/日志的长度、数量、查询范围和脱敏预算有 focused tests; +- `NO_EVIDENCE` 保留查询范围并且不会被表述为问题不存在; +- 不实现真实 MCP/CLS 适配器。 + +### 阶段 3C:只读 MySQL Tool + +目标:独立实现安全敏感的只读 MySQL Tool,避免与现有 Tool 迁移共享归档边界。 + +任务: + +- 引入 JSqlParser,完成保守 SELECT 子集的 fail-closed AST 校验; +- 实现逻辑数据源、schema/table/column allowlist、参数绑定、只读执行、超时、取消和结果投影; +- 接入阶段 3A 的 canonical invocation store、预算、`evidence_status` 和 Tool Call ID 契约; +- 使用隔离测试数据源完成 security、truncation、no-evidence 和 error tests。 + +验收: + +- MySQL 的行数、单元格、总字节和查询超时预算有测试; +- MySQL write、WITH、子查询、UNION、投影通配符、未知 AST、allowlist bypass、timeout 和其他 security cases 被拒绝;合法显式列 SELECT 和 `COUNT(*)` 通过。 + +### 阶段 4:单体 Diagnosis ReAct Agent + +目标:将 Planner、Executor、内部 Verifier、自身 Composer 职责合并为一个完整 ReAct Agent;本阶段只通过内部应用用例和测试入口验证,不接管公开 Chat 入口。 + +任务: + +- 创建单一 Diagnosis Agent Prompt; +- Agent 接收当前原始 Query 和可选的固定 Schema `previous_turn`; +- 使用框架已有 ReAct/tool loop,不手写 While; +- 接入 Harness 包装后的 Tool; +- 输出结构化 Draft/Analysis/Conclusion/Tool Call IDs; +- 证据不足时明确停止; +- 新 Agent 通过独立内部应用用例接入 Harness,公开 `/api/chat` 暂不切换; +- 旧多 Agent 主链路在阶段 5/6A/6B 完成释放门禁和切换前继续保留,禁止阶段 4 直接删除导致公开入口失效; +- 保留 Run/AgentStep/ToolInvocation 基础审计。 + +验收: + +- 新诊断链路只有一个拥有工具循环的 Agent,且只在内部测试入口运行; +- Diagnosis Agent 整体、单次模型调用和 Tool 调用均无 Harness 自动重试; +- 无 Planner/Executor/Composer Agent 间 JSON 搬运; +- 无外层 Graph; +- focused Agent/tool-loop/output tests 通过; +- 工具、模型和上下文预算均有可配置且被 Harness 强制执行的确定性上限。 + +### 阶段 5:EvidenceGuard、SemanticGuard 与释放策略 + +目标:在不恢复 Graph 的前提下完成物理验真和独立语义审查。 + +任务: + +- EvidenceGuard 校验 Draft Schema、Analysis ID 唯一性、Analysis `kind`、报告内部引用完整性,以及当前 Run、Tool Call ID、调用生命周期、`evidence_status` 和 `agent_result`; +- 格式/物理验真首次失败只执行一次无 Tool 输出修复,禁止重新运行诊断或 Tool 循环; +- 修复后再次执行 EvidenceGuard,仍失败则直接安全 Fallback; +- 构造 SemanticGuard 最小隔离输入; +- 复用系统当前唯一 ChatModel,实现由同一 Harness 受限控制的无工具、无记忆、单轮 SemanticGuard Agent; +- Harness 控制 SemanticGuard 输入预算、超时、取消、一次重试、结构校验、审计和降级释放,禁止 Agent 自身重试或形成回路; +- 实现 SUPPORTED/UNSUPPORTED/TIMEOUT/ERROR release policy; +- 确保 SemanticGuard 不回调主 Agent; +- 增加 duplicate/missing Analysis ID、empty evidence reference、fabricated Tool ID、cross-run、failed call、missing agent_result、`NO_EVIDENCE` 负向观察、无证据过度推断、unsupported report、timeout、Fallback type 和 `verified_sources` 规则测试。 + +验收: + +- 重复/缺失 Analysis ID、空引用、伪造/跨 Run/失败调用或缺失 `agent_result` 不会进入 SemanticGuard; +- SemanticGuard 只看到 verified evidence; +- SemanticGuard 与主 Agent 复用同一模型配置,但不共享上下文、工具或记忆; +- EvidenceGuard 修复最多一次,且修复调用没有 Tool; +- 第二次物理验真失败直接 Fallback; +- 超时和失败不释放草稿; +- UNSUPPORTED 不会释放或局部裁剪主 Agent 草稿; +- Fallback 不包含主 Agent 草稿或 SemanticGuard 内部 reason,并以 `content + done` 正常结束; +- 无隐式 retry loop。 + +### 阶段 6A:Chat Application Use Case、意图路由与 previous turn + +目标:在不切换公开协议的前提下完成 Chat 应用用例、三类意图路由和上一安全回合组装。 + +任务: + +- 会话、历史和执行生命周期下沉应用用例,由应用用例创建并传播阶段 2 定义的 `RunContext`; +- 引入三分类轻量 Intent Router; +- Intent Router 技术失败或非法枚举只由 Harness 重试一次,第二次失败不默认进入 Diagnosis; +- 由应用用例确定性组装 Diagnosis Agent 的上一个已安全发布回合,不调用上下文摘要模型; +- 原始 Query 原样进入选定执行路径; +- 新应用用例只通过内部测试入口验证,公开 `/api/chat` 和 `/api/chat_stream` 暂不切换; + +验收: + +- SYSTEM_CHAT、KNOWLEDGE_QUERY 和 DIAGNOSIS 路由映射及失败语义通过 focused tests; +- previous turn 只来自同一 Session 上一个符合安全发布条件的结构化结果; +- 应用用例显式传播同一个 sessionId/runId,不依赖 Controller 提供模型和工具; +- 阶段 6B 可直接完成协议切换,不需要重写应用用例。 + +### 阶段 6B:唯一 `/api/chat` SSE 切换 + +目标:将公开 Chat 原子切换到阶段 6A 的应用用例,并完成破坏性 SSE 协议迁移。 + +任务: + +- Controller 只负责请求校验、HTTP/SSE 协议和连接生命周期; +- 仅在阶段 5 的 EvidenceGuard、SemanticGuard 和 Release Policy 全部通过后,原子切换 `/api/chat` 到新链路; +- 切换前后均不得出现未经过释放门禁的公开 Draft; +- 只保留 `POST /api/chat`; +- 删除 `/api/chat_stream` 和同步兼容路径; +- 同步迁移前端 Chat 消费者到固定 SSE 事件契约; +- 按固定顺序输出 `metadata -> status* -> content|failure -> done`,终态只允许 `SUCCESS/FALLBACK/FAILED`; +- 处理断开、取消、超时和 Run 终态; +- 不创建 Controller 自有无界线程池。 + +验收: + +- Controller 不依赖 ChatModel/ToolCallbackProvider; +- 同一请求 sessionId/runId 在 SSE、日志和数据库一致; +- 禁止输出 Thought、Prompt、raw Tool data 和内部错误; +- SSE 事件 Schema、顺序、互斥、唯一终态、客户端取消和错误单元/集成测试通过。 + +### 阶段 7:物理清理、文档与最终验收 + +目标:删除旧架构和完成一次最终端到端验收。 + +任务: + +- 物理删除被替代的旧 Agent、Hook、ThreadLocal、路由和死代码; +- 删除旧 Tool compatibility contract 和过时测试; +- 不保留注释旧代码或双轨开关; +- 更新 MVP 架构、接口、Tool、Trace 和安全文档; +- 审查 ISS-012/ISS-013 是否可归档或被本 Issue 吸收; +- 运行最终 Maven 启动 E2E; +- 核对 `logs/`; +- 使用安全的数据库查询方式核对 exact sessionId/runId; +- 验证 Token、工具次数、SSE、EvidenceGuard、SemanticGuard 和 Fallback。 +- query_logs 和外部 MySQL Tool 使用现有 Mock/测试数据完成契约、安全与编排验收;本阶段不声称完成真实 CLS 或生产业务 MySQL 数据源的 live E2E。 + +验收: + +- 旧多 Agent/Graph/伪流式入口不存在; +- 全部 focused tests 通过; +- 最终 E2E、日志和数据库证据归档; +- 无硬编码凭据、临时 refs 文件、未解释兼容层和死代码; +- 工作区改动范围与本 Issue 一致。 + +## 13. 阶段门禁 + +实施时每阶段必须满足: + +1. 当前阶段范围和行为变化已确认; +2. 需要的单元/契约测试已补充并通过;无必要时记录跳过理由; +3. 当前独立 OpenSpec change 已完成 Apply 和阶段验收,证据已归档; +4. 当前 change 已 Archive,OpenSpec 索引/规格同步完成; +5. `git diff` 精确审查完成,当前阶段已独立 Git commit; +6. 前一阶段 Archive 和 Git commit 完成后才进入下一阶段,不并行实施相邻阶段; +7. 阶段 0–6B 不运行完整 live E2E;阶段 7 全部完成后统一执行 Maven E2E、日志和数据库核验。 +8. 阶段 4 和 6A 不得接管公开入口;公开入口切换只能发生在阶段 6B,且必须依赖阶段 5 的释放门禁。 + +## 14. 总体验收标准 + +- [ ] 复杂诊断只存在一个拥有工具循环的 Diagnosis ReAct Agent。 +- [ ] 不存在业务 StateGraph 或 Planner/Executor/Composer 多 Agent 主链路。 +- [ ] Harness 不承担业务推理,不演变为工作流引擎。 +- [ ] EvidenceGuard 是 Harness 内的确定性能力,不是独立编排节点。 +- [ ] SemanticGuard 使用完全隔离上下文,无工具、无记忆、无回调循环。 +- [ ] SemanticGuard 不使用未校准数值置信度控制在线释放。 +- [ ] 所有 Agent-facing evidence Tool 符合 ACI 状态和 Tool Call ID 契约。 +- [ ] RAG 不再向 Agent 返回 ContextPack/Trace/Rerank 等审计数据。 +- [ ] query_logs 不要求 Agent 先调用 Topic discovery,且返回聚合、抽样、脱敏结果。 +- [ ] MySQL Tool 只读、安全解析、参数绑定、allowlist、超时和结果上限全部生效。 +- [ ] Tool 原始结果不会未经有界投影进入 Agent 上下文;Redis canonical evidence 当前可暂不脱敏,但不得被 Agent 直接读取。 +- [ ] Redis 每次 Tool Call 单 Key 保存,状态、TTL、容量、ACL 和日志禁泄漏规则均有测试。 +- [ ] 每个 Tool Call ID 可以按 exact runId 和调用记录状态验真,并取得对应 `agent_result`。 +- [ ] 只保留一个 `/api/chat` SSE 接口。 +- [ ] SSE 不输出 Thought、Prompt、原始 Tool 载荷和未验证结论。 +- [ ] 客户端断开、模型/Tool/SemanticGuard 超时均有明确取消和 Run 终态。 +- [ ] 所有重试由 Harness 按类型化策略装配和记录,无 SDK/HTTP/数据库隐藏重试或整个 Diagnosis Agent 重跑。 +- [ ] 最终 E2E 能按 sessionId/runId 对齐 SSE、日志、AgentStep、ToolInvocation 和最终答案。 +- [ ] Token、工具调用、Tool 投影、Redis TTL 和总延迟预算均来自集中配置,并有可验证的强制上限和耗尽原因。 +- [ ] 11 个 OpenSpec changes 均已独立 Archive,并分别对应一个范围清晰的 Git commit。 +- [ ] 不保留旧兼容分支、注释代码、本地 refs 卸载和硬编码凭据。 + +## 15. 非目标 + +- 不构建多 Agent 协作平台; +- 不引入业务 StateGraph; +- 不实现 Harness 插件市场、通用 Pipeline DSL 或策略语言; +- 不保留旧同步 Chat、`/chat_stream` 或旧 Tool Contract; +- 不输出或持久化 Chain of Thought; +- 不使用本地 `refs/*.md` 作为 Tool 原始结果主存储; +- 不向 Agent 暴露 Redis Client、Redis Tool、key、连接信息或完整调用记录; +- 不提供历史来源召回 Tool、通用 Memory Tool 或完整上下文恢复能力; +- 不让 `ToolResultProjector` 调用 LLM 生成证据摘要; +- 不允许 MySQL 写操作; +- 不允许 MySQL Tool 查询 Agent 自身的持久化数据库; +- 不允许 Skill/RAG 文档绕过独立配置授予数据库访问权限; +- 本 Issue 不实现 MySQL 租户、行级权限或动态数据授权;首版只落实库/表/字段白名单; +- 不提供 `query_metrics` Tool;指标查询和其 ACI/结果投影设计另立后续 Issue; +- 不用调低模型 maxTokens 代替上下文治理; +- 不因 SemanticGuard 超时而释放未验证草稿; +- 不以追加“人工复核”警告替代删除不受支持的强结论。 + +## 16. 设计 Review:已确认决策 + +以下决策已经完成讨论,作为阶段 0 契约冻结的输入: + +### 已确认项 + +1. **Canonical Evidence 存储**:已确认 Redis 每个 Tool Call 使用单 Key `superbiz:harness:tool-call:{runId}:{toolCallId}`,保存 `request/raw_response/agent_result/status/evidence_status/timestamps`。`status=PROJECTING/READY/ERROR` 表示调用生命周期,`evidence_status=EVIDENCE_FOUND/NO_EVIDENCE/ERROR` 表示结果语义;`NO_EVIDENCE` 只能支持限定查询范围的负向观察。默认 TTL 2 小时且不续期,默认单记录 1MB、Agent Result 64KB、单 Run 5MB;Harness-only ACL,原始超限进入 `RESULT_TOO_LARGE`,不静默截断。当前版本暂不做持久化脱敏,默认值后续按运行数据校准。 + +2. **SemanticGuard 模型与超时**:已确认复用当前唯一 ChatModel,由同一 Harness 控制;首次超时/失败/结构无效时使用相同证据快照重试一次,仍失败则安全降级。单次和总超时提取为集中配置,初期不冻结具体数值。 +3. **SemanticGuard 完整报告审查**:已确认接收原始 Query、Diagnosis Agent 完整 Draft 和 EvidenceGuard 生成的 verified evidence snapshot;校验结论、分析、行动计划、建议、限制和范围是否语义成立。它不访问 Redis、不重复验真、不接收 Diagnosis Plan/Tool 过程,也不生成、总结、改写或补充用户报告。Diagnosis Agent 是唯一报告作者,Harness 只发布原 Draft 或固定 Fallback。 +4. **预算数值**:已确认主 Agent、Tool、SemanticGuard 和 Redis 的预算全部集中配置,首版使用可调默认值,不在设计阶段冻结未经数据验证的数值;Harness 负责强制执行和记录耗尽原因。 +5. **RAG 投影预算**:最大 Evidence 数、单条 excerpt 和总字符预算提取为同一配置;默认值根据后续 Trace 和截断数据校准。 +6. **日志投影预算**:最大 Pattern/Event 数、消息长度、lookback 和总字符预算提取为同一配置;聚合指纹规则仍属于 Tool 行为契约,不作为数值配置问题处理。 +7. **真实日志适配**:已确认本 Issue 首版只沿用现有 Mock;真实 MCP/Java CLS 适配器和生产接入另立后续 Issue,当前只保留可复用的 Agent-facing Contract。 +8. **MySQL SQL Parser**:已确认使用 JSqlParser。首版只允许显式列的单条 SELECT、INNER/LEFT JOIN、常用筛选/分组/排序和函数 allowlist;允许 `COUNT(*)`,禁止 WITH、子查询、UNION、窗口函数、投影通配符和未知 AST;Harness 注入或压低 LIMIT,并以 PreparedStatement、只读账号、超时和 `setMaxRows` 兜底。 +9. **MySQL allowlist 配置**:已冻结为逻辑数据源下的静态 `allowed-schemas -> tables -> columns` 三层精确白名单,并配置 `default-schema`。不支持通配符、正则或动态授权;凭据通过环境变量/Secret 注入。本 Issue 暂不展开租户和行级权限。 +10. **对话历史**:已确认只使用显式极简上下文,不提供完整历史或 Redis 历史召回。Intent Router 接收当前 `query + last_intent + last_user_query`。`diagnosis_run` 持久化 `intent/release_outcome/published_result`,只有同 Session 最近一个 `DIAGNOSIS + SUCCESS` 的安全结构化结果可进入 `previous_turn`;Fallback、失败和取消均排除。Diagnosis Agent 接收当前 Query 和固定字段 `user_query/published_conclusion/scope/limitations/source_documents`,由 Harness 确定性组装并按配置截断,不调用 LLM 摘要,不复用旧日志/MySQL 结果。SemanticGuard 只接收当前诊断的 Query、完整 Draft 和 verified evidence snapshot。 +11. **重试边界**:已确认由 Harness 装配类型化重试策略。Router 与 SemanticGuard 的技术失败最多重试一次;Diagnosis Agent、主模型调用和 Tool 调用不自动重试;EvidenceGuard 只执行一次显式无 Tool 结构修复;禁止隐藏和嵌套重试。 +12. **阶段依赖顺序**:已调整为阶段 0 设计冻结、1 ACI Contract、2 Harness Core/RunContext、3A invocation store、3B RAG/日志投影、3C MySQL Tool、4 Diagnosis Agent、5 Guards、6A Chat 应用用例、6B SSE 切换、7 清理/E2E。阶段 3A 起只依赖显式 RunContext,不临时耦合旧 ChatService。 +13. **OpenSpec/sm-flow 映射**:已确认 ISS-014 作为总 Issue,11 个实施切片各自使用独立 OpenSpec change 和完整串行 sm-flow;每阶段 Archive 并 Git commit 后才能进入下一阶段,最终 E2E 只在阶段 7 执行。 +14. **EvidenceGuard 与报告作者边界**:Diagnosis Agent 是唯一语义作者;EvidenceGuard 作为 Harness 内的确定性能力校验 Draft Schema、Analysis 引用完整性和当前 Run Tool Call 真实性,不判断语义推导。Harness 只确定性展开 Agent 已选择的引用,不改写报告。 +15. **SemanticGuard evidence snapshot**:Harness 将已验证 Tool Calls 的有界 `agent_result` 按 `analysis_id` 分组,不注入 `tool_call_id`、Redis 或 `raw_response`,SemanticGuard 据此校验 Analysis、Conclusion、Action Plan、Recommendations、Scope 和 Limitations。 +16. **Fallback 与 SSE**:Fallback 使用统一 Schema 和三种稳定 `type`;EvidenceGuard 失败时 `verified_sources=[]`。SSE 首版固定为 `metadata -> status* -> content|failure -> done`,不实现断线续传、事件重放、轮询或 WebSocket。 + +### Review 结论 + +当前架构和关键契约已经足够进入阶段 0,不再继续增加设计组件。阶段 0 负责把上述决策固化为 OpenSpec、Schema、契约测试和安全基线;完成 Archive 与 Git commit 前不得进入阶段 1。 + +剩余风险属于实施期验证项,而不是继续扩展架构的理由: + +- SemanticGuard 是否能稳定执行报告级二元语义校验; +- canonical evidence 是否按 Redis TTL 和访问边界承载完整调用结果; +- Harness 是否能严格执行集中预算,并为耗尽和截断提供可观察原因; +- MySQL Tool 的 AST 白名单、只读、超时和容量边界能否通过 Mock/隔离测试数据源的安全用例;真实生产数据源接入另立后续 Issue 验证。 + +实现中不得为这些风险创建 HarnessPlugin、Graph、通用 Tool DSL 或大量预留接口;按各阶段最小范围实现并通过门禁验证。 + +## 17. 相关文件 + +- `mvp/issues/active/ISS-012-executor-token-budget-and-context-growth.md` +- `mvp/issues/active/ISS-013-chat-entry-decoupling-and-sse.md` +- `mvp/disscus/plan.md` +- `src/main/java/com/superbiz/agent/controller/ChatController.java` +- `src/main/java/com/superbiz/agent/service/ChatService.java` +- `src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java` +- `src/main/java/com/superbiz/agent/hook/TokenTrackingChatModel.java` +- `src/main/java/com/superbiz/agent/hook/VerifierInputHook.java` +- `src/main/java/com/superbiz/agent/service/ExecutorGatekeeperService.java` +- `src/main/java/com/superbiz/agent/service/ToolInvocationRecorder.java` +- `src/main/java/com/superbiz/agent/tool/LookupKnowledgeTool.java` +- `src/main/java/com/superbiz/agent/agent/tool/QueryLogsTools.java` +- `src/main/resources/prompts/chat-planner-prompt.md` +- `src/main/resources/prompts/chat-executor-prompt.md` +- `src/main/resources/prompts/chat-verifier-prompt.md` +- `src/main/resources/prompts/chat-composer-prompt.md` +- `scripts/query_mysql.py` +- `mvp/architecture/current-mvp-architecture.md` diff --git a/openspec/changes/archive/2026-07-21-single-react-design-freeze/.archive-ready b/openspec/changes/archive/2026-07-21-single-react-design-freeze/.archive-ready new file mode 100644 index 0000000..de82ad6 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-design-freeze/.archive-ready @@ -0,0 +1 @@ +Devflow archive prepared and stage verification passed on 2026-07-21. diff --git a/openspec/changes/archive/2026-07-21-single-react-design-freeze/.committed b/openspec/changes/archive/2026-07-21-single-react-design-freeze/.committed new file mode 100644 index 0000000..c501e20 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-design-freeze/.committed @@ -0,0 +1 @@ +Committed after strict validation on 2026-07-21. diff --git a/openspec/changes/archive/2026-07-21-single-react-design-freeze/.openspec.yaml b/openspec/changes/archive/2026-07-21-single-react-design-freeze/.openspec.yaml new file mode 100644 index 0000000..c0a8162 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-design-freeze/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-21 diff --git a/openspec/changes/archive/2026-07-21-single-react-design-freeze/acceptance.md b/openspec/changes/archive/2026-07-21-single-react-design-freeze/acceptance.md new file mode 100644 index 0000000..1ceecce --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-design-freeze/acceptance.md @@ -0,0 +1,29 @@ +# Stage 0 Acceptance Evidence + +## Static Verification + +- Contract package is dependency-free from Redis, JPA, Controller, Agent state and existing Hook classes. +- Repository secret scan covers tracked worktree files and reports no known plaintext credential matches. +- `scripts/query_mysql.py` requires `SUPERBIZ_MYSQL_PASSWORD` and exits before connecting when it is absent. +- Spring AI 1.1.7 `SpringAiRetryProperties` bytecode shows a default `maxAttempts` value of 10; stage 2 must set underlying retries to one attempt and keep retry ownership in Harness. + +## Script Verification + +- `mvn -q '-Dtest=HarnessContractTest' test` - passed. +- `mvn -q '-Dtest=ToolInvocationRecorderTest,ExecutorGatekeeperServiceTest,ChatControllerTest' test` - passed. +- `mvn -q '-Dtest=HarnessContractTest,ToolInvocationRecorderTest,ExecutorGatekeeperServiceTest,ChatControllerTest' test` - passed. +- `openspec validate single-react-design-freeze --strict` - passed. +- `git diff --check` - passed; only existing Windows line-ending warnings were reported. +- Secret scan for known committed key/password patterns - zero matches. +- `python scripts/query_mysql.py "SELECT 1"` without `SUPERBIZ_MYSQL_PASSWORD` - exited before connecting with the expected missing-variable error. + +## Runtime Behavior + +- Public Chat runtime was not switched in stage zero. +- No live model, Redis, MySQL or Milvus E2E was run; full live E2E remains stage 7 scope. + +## External Security Prerequisite + +- Plaintext credentials previously present in the repository must be rotated in their respective MySQL, Redis, DeepSeek, SiliconFlow and Milvus systems by the credential owner. +- Repository changes can prove removal but cannot prove provider-side rotation. +- Stage 3C and stage 7 must not claim live security/E2E acceptance until required environment variables contain rotated credentials. diff --git a/openspec/changes/archive/2026-07-21-single-react-design-freeze/brief.md b/openspec/changes/archive/2026-07-21-single-react-design-freeze/brief.md new file mode 100644 index 0000000..6109eb6 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-design-freeze/brief.md @@ -0,0 +1,25 @@ +# Brief: single-react-design-freeze + +## Background + +ISS-014 将当前 Chat 多 Agent/Hook/ThreadLocal 主链路重构为一个 Diagnosis ReAct Agent、一个确定性 Harness 和一个隔离 SemanticGuard。阶段 0 先冻结后续 10 个实施 change 共同依赖的契约和安全边界。 + +## Goal + +产出可执行、可测试、可归档的 contract types、失败语义、安全前置和阶段门禁,同时保持现有公开 Chat 运行行为不变。 + +## Scope + +- 类型化 Draft、Knowledge Answer、Fallback、previous turn 和状态枚举。 +- Tool ID、双状态、取消、重试、Redis canonical record 和 MySQL 安全设计冻结。 +- 明文脚本凭据清理和 focused baseline。 +- ISS-014、OpenSpec、devflow 对齐。 + +## Non-Goals + +- 不实现或接入新 Harness/Agent/Guard。 +- 不切换 `/api/chat`、不删除旧链路、不运行 live E2E。 + +## Source PRD + +`mvp/issues/active/ISS-014-single-react-agent-harness-aci-ptk-refactor.md` 是本 change 的完整 PRD 和总设计来源,不复制为第二份 PRD。 diff --git a/openspec/changes/archive/2026-07-21-single-react-design-freeze/decisions.md b/openspec/changes/archive/2026-07-21-single-react-design-freeze/decisions.md new file mode 100644 index 0000000..96bd101 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-design-freeze/decisions.md @@ -0,0 +1,80 @@ +# Decisions + +## Entry + +- Parent issue: `ISS-014` +- Change: `single-react-design-freeze` +- Scale: `complex` +- Interface impact: future L4; this change freezes contracts without switching runtime behavior. +- Capability sources: sm-flow built-in clarify/context/propose, `grill-with-docs`, `openspec-propose`, `zoom-out`, `openspec-apply-change`, `openspec-archive-change`. + +## Context Evidence + +- `session-run-trace-isolation` established Chat Session and Diagnosis Run as separate lifecycles and made `runId` the Trace ownership key. +- `verifier-evidence-reference-fidelity` established that no-evidence is a scoped negative observation, not proof that a problem does not exist. +- `executor-composer-final-answer` established deterministic safe fallback boundaries and prohibited unfiltered raw output from reaching users. +- `modular-rag-pipeline` established that retrieval trace and context packing are audit details rather than direct facts. +- Current Spring AI Alibaba `ToolCallRequest` already provides `tool_call_id`; Harness must validate and persist it rather than create a second identity. +- Current Spring AI `ChatModel.call(Prompt)` has no cancellation token, so cancellation must be expressed as layered, observable semantics rather than an unsupported absolute guarantee. + +## Question Pool + +| ID | Dimension | Question | Mode | Status | +|---|---|---|---|---| +| Q1 | Terminology | Does `tool_call_id` use the framework ID or a Harness-generated ID? | user-interview | confirmed: framework ID | +| Q2 | Terminology | Are invocation lifecycle and evidence outcome separate fields? | user-interview | confirmed: `status` + `evidence_status` | +| Q3 | Boundary | Is ISS-014 one umbrella Issue with independent OpenSpec changes? | user-interview | confirmed: one Issue, 11 changes | +| Q4 | Boundary | May stage 4 publish before Guards exist? | user-interview | confirmed: no; public cutover only in 6B | +| Q5 | Lifecycle | What is the durable source of truth for `previous_turn` and `last_intent`? | user-interview | confirmed: `diagnosis_run` safe published result | +| Q6 | Contract | How is KNOWLEDGE_QUERY citation validation represented? | evidence-driven | resolved: structured answer items with exact RAG bindings | +| Q7 | Cancellation | What cancellation guarantees are technically enforceable? | evidence-driven | resolved: layered cancellation, no false hard-cancel claim | +| Q8 | Acceptance | Are Apply, Archive and phase Git commit pre-authorized? | user-interview | confirmed: yes, for all phases | + +## Confirmed Decisions + +- `tool_call_id` is the framework Tool Call protocol ID. Harness validates non-empty, bounded, safe characters and Run-local uniqueness; duplicate/invalid/missing IDs fail closed. +- Redis `status=PROJECTING/READY/ERROR` represents invocation/projector lifecycle. +- `evidence_status=EVIDENCE_FOUND/NO_EVIDENCE/ERROR` represents result semantics. +- `NO_EVIDENCE` may only support `NEGATIVE_OBSERVATION` within the exact query scope; it cannot prove absence, exclusion or health. +- Stage 4 and 6A remain internal. Stage 6B performs the only public Chat cutover after stage 5 release gates pass. +- ISS-014 remains the umbrella Issue. Eleven independent changes run serially; each must complete sm-flow, OpenSpec archive and Git commit before the next starts. +- Full live E2E is deferred to stage 7; earlier stages run focused verification proportional to their change. +- KNOWLEDGE_QUERY uses a dedicated structured draft: each answer item binds the single lookup `tool_call_id` and one or more returned `document_id` values; Harness validates exact membership before rendering `answer + references + limitations`. It does not reuse the Diagnosis Analysis schema and does not enter SemanticGuard in the first version. +- Cancellation is layered: mark cancellation requested, prevent new model/Tool rounds and any final Draft release, invoke framework interruption, cancel owned Tool/JDBC work where supported, and rely on configured HTTP timeouts for an already-blocking synchronous model call. Run finalization is atomic and late results are discarded. +- Public SSE `done` is not emitted after the client has disconnected; internal Run state still reaches `CANCELLED`. +- `diagnosis_run` is the durable source for `intent`, `release_outcome` and `published_result`. Only the latest same-session `DIAGNOSIS + SUCCESS` record with a non-null safe published result may become `previous_turn`; `FALLBACK/FAILED/CANCELLED` remain auditable but are excluded. +- `published_result` stores only `user_query/published_conclusion/scope/limitations/source_documents`; it excludes Tool Call IDs, raw evidence, full Draft and SemanticGuard audit reasons. + +## Evidence-Driven Findings To Report + +- The framework already exposes `ToolInterceptor`, structured output types, tool execution timeout, model/tool call limit hooks and `ReactAgent.interrupt`; later Harness stages should reuse these extension points. +- Spring AI model dependencies include retry support, while current application configuration does not explicitly freeze all retry layers; stage 0 must define a retry inventory and stage 2 must enforce it. +- Current `DiagnosisRun` stores a text answer and generic status but has no explicit `intent`, `release_outcome` or structured published result; Q5 must be resolved before the previous-turn contract is executable. +- Current KNOWLEDGE_QUERY target behavior promises citation validation, but the issue only defines the Diagnosis Draft binding schema; the committed spec must add the dedicated answer-item contract described above. +- Spring AI retry auto-configuration defaults `maxAttempts` to 10. The target Harness retry matrix requires underlying model/HTTP retries to be set to one attempt, with Router and SemanticGuard retries performed only by Harness. + +## OpenSpec Backfill + +- All confirmed decisions above must appear in design/specs/tasks before `.committed` is created. +- All user-interview questions are confirmed; no pending decision blocks Commit. + +## Architecture Audit + +Current input flows from `ChatController` into `ChatService`, which owns routing, ReactAgent construction, multi-Agent orchestration and final rendering; tools persist evidence through `ToolInvocationRecorder`, while Hooks and ThreadLocal bridge Run and verifier state. Stage zero introduces only dependency-free contract types under `harness.contract`; those types must not depend on Controller, Redis, JPA, Spring Agent state or current Hook classes. Later stages move ownership in order: RunContext, invocation store, Tool-specific projection, Diagnosis Agent, Guards, application use case and finally the public SSE adapter. `diagnosis_run` remains durable Run ownership, Redis canonical invocation remains short-lived Harness ownership, and `agent_step/tool_invocation` remain durable audit detail. The principal risk is spec/runtime drift, mitigated by archiving only the stage-zero contract capability now and delaying modifications to existing runtime capabilities until their implementation changes. + +## Cross-Artifact Alignment + +| Chain | Status | Evidence | +|---|---|---| +| ISS-014/brief goals, scope and non-goals → proposal | aligned | Proposal limits stage zero to contracts, security and baseline with no public cutover. | +| proposal commitments → design | aligned | Design records every ID, status, Draft, fallback, previous-turn, retry, cancellation and phase-gate commitment. | +| design decisions → specs | aligned | The single stage-zero capability has testable requirements for every stable contract boundary. | +| specs observable behavior → tasks | aligned | Tasks create reusable types/tests, remove the secret, align artifacts and verify without switching runtime behavior. | + +## Commit Gate Result + +- Question pool covers terminology, boundary, lifecycle, contract, cancellation and acceptance. +- All user-interview items are explicitly confirmed. +- Evidence-driven conclusions were reported and written into design/spec/tasks. +- Interface impact is recorded as future L4; this change itself does not switch the public API. +- No devflow/OpenSpec conflict remains. diff --git a/openspec/changes/archive/2026-07-21-single-react-design-freeze/design.md b/openspec/changes/archive/2026-07-21-single-react-design-freeze/design.md new file mode 100644 index 0000000..84a4784 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-design-freeze/design.md @@ -0,0 +1,77 @@ +## Context + +ISS-014 是一次 L4 Chat 重构的总设计来源,但实施被拆成 11 个必须串行归档的 OpenSpec changes。阶段 0 不切换公开协议或 Agent 运行链,只创建后续阶段复用的类型化契约、安全前置、失败语义和 focused baseline。 + +现有代码已经具备 `runId` Trace、工具调用审计、no-evidence 精确引用、Verifier/Composer fallback 和模块化 RAG,但这些能力分散在 `ChatService`、Hook、ThreadLocal、Tool 和 JSON 字符串中。Spring AI Alibaba 已提供 `ToolInterceptor`、结构化输出类型、工具执行超时、调用限额 Hook 和 `ReactAgent.interrupt`;Spring AI 底层 retry 默认最多 10 次,不能直接满足 ISS-014 的显式 Harness 重试矩阵。 + +## Goals / Non-Goals + +**Goals:** + +- 生成后续阶段可直接复用的 Java contract types 和枚举,不实现新 Agent 执行链。 +- 冻结 Tool ID、双状态、Draft、Knowledge Answer、Fallback、previous turn、SSE、重试和取消语义。 +- 冻结 MySQL fail-closed 允许子集和安全前置。 +- 移除仓库脚本和主配置中的明文凭据并建立改造前 focused baseline。 +- 保证 ISS-014、OpenSpec、devflow 术语和 11 个阶段门禁一致。 + +**Non-Goals:** + +- 不接入 Harness、Diagnosis Agent、EvidenceGuard 或 SemanticGuard 运行时。 +- 不修改 Controller 协议、旧 ChatService 行为、Redis invocation store 或数据库表。 +- 不实现 RAG/日志/MySQL Tool 投影。 +- 不运行完整 live E2E。 + +## Decisions + +### Contract types are reusable runtime inputs + +阶段 0 创建位于 `com.superbiz.agent.harness.contract` 的轻量 record/enum,而不是只写文档或引入 JSON Schema 引擎。后续 ReactAgent `outputType`、Harness validator、持久化和 SSE DTO 可以直接复用这些类型,减少同一字段在多个阶段重复定义。 + +### Framework Tool Call ID is canonical + +`tool_call_id` 使用框架协议 ID。Harness 后续只校验非空、长度/字符安全和 Run 内唯一性,不生成第二套 ID。Redis Key 仍按 `runId + toolCallId` 隔离,真实性来自当前 Run 的 canonical record,而不是 ID 本身。 + +### Invocation lifecycle and evidence outcome are orthogonal + +`InvocationStatus=PROJECTING/READY/ERROR` 只描述调用与投影生命周期;`EvidenceStatus=EVIDENCE_FOUND/NO_EVIDENCE/ERROR` 描述结果语义。`NO_EVIDENCE` 只允许绑定 `AnalysisKind=NEGATIVE_OBSERVATION`,并必须保留查询范围和零匹配信息。 + +### Diagnosis and knowledge answer contracts stay separate + +Diagnosis Draft 使用 Analysis ID 与 Tool Call IDs;KNOWLEDGE_QUERY 使用 answer items,每项绑定一次 lookup 的 Tool Call ID 和返回的 document IDs。Harness 后续验证精确成员关系并确定性展开引用。首版 KNOWLEDGE_QUERY 不进入 SemanticGuard。 + +### Safe published context belongs to Diagnosis Run + +`diagnosis_run` 是 `intent/release_outcome/published_result` 的持久化真理源。只有同 Session 最近一个 `DIAGNOSIS + SUCCESS` 且 published result 非空的 Run 可形成 previous turn。Published result 只含用户查询、已发布结论、范围、限制和 RAG 文档元数据。 + +### Cancellation is observable and layered + +取消请求立即阻止新模型/Tool 轮次和最终 Draft 释放;框架中断、可控 Future/JDBC 取消尽力执行;已进入同步 `ChatModel.call` 的请求依靠底层 HTTP timeout。Run 终态通过原子状态转换保证唯一,晚到结果被丢弃,不宣称无法证明的底层硬取消。 + +### Harness owns all retries + +底层 SDK/HTTP/数据库 retry 必须关闭或压为一次 attempt。Intent Router 和 SemanticGuard 的第二次 attempt 由 Harness 显式执行并审计。阶段 0 只冻结矩阵;阶段 2 实现执行器和配置。 + +### Stage boundaries are release boundaries + +阶段 4 和 6A 仅内部运行;阶段 6B 在 Guards 已完成后原子切换公开入口。每个 change 必须 Archive 并 Git commit 后才能进入下一项,阶段 7 才执行统一 live E2E。 + +## Risks / Trade-offs + +- [Risk] Contract records过早绑定实现细节 → Mitigation:只包含跨阶段稳定字段,不包含 Redis、JPA 或框架对象。 +- [Risk] Spring AI provider 对 Tool Call ID 行为不同 → Mitigation:阶段 3A 使用 Fake Model 和当前 DeepSeek 路径验证,缺失或重复时 fail closed。 +- [Risk] 同步模型调用不能立即取消 → Mitigation:明确 layered semantics、HTTP timeout 和晚到结果丢弃,不把状态更新等同底层资源已终止。 +- [Risk] 设计冻结测试增加维护成本 → Mitigation:只保留 focused serialization/validation tests,不复制完整 E2E。 +- [Risk] 明文凭据可能已泄露 → Mitigation:仓库中移除并要求外部轮换;轮换证据记录在 acceptance。 + +## Migration Plan + +1. 创建 contract types、契约测试和阶段台账,不接运行链。 +2. 清理查询脚本凭据,改为环境变量注入。 +3. 记录 focused baseline;Archive 本 change。 +4. 后续 10 个 change 逐步实现,并在各自 Archive 时同步对应运行 capability specs。 + +Rollback:阶段 0 没有公开行为变化;可删除新增 contract package/tests 并恢复文档。已经轮换的凭据不得回滚为旧值。 + +## Open Questions + +无。所有影响实现的用户决策已经在 `decisions.md` 中确认。 diff --git a/openspec/changes/archive/2026-07-21-single-react-design-freeze/proposal.md b/openspec/changes/archive/2026-07-21-single-react-design-freeze/proposal.md new file mode 100644 index 0000000..3641b49 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-design-freeze/proposal.md @@ -0,0 +1,32 @@ +## Why + +当前 Chat 诊断把一个 ReAct 生命周期拆成多个 Agent、Hook、ThreadLocal 和重试分支,导致证据契约、失败语义、上下文预算和公开释放边界分散。进入分阶段重构前,需要先把 ISS-014 的跨阶段契约、安全前置和验收基线冻结为唯一可执行规格,避免后续 change 各自解释同一概念。 + +## What Changes + +- 冻结单体 Diagnosis ReAct Agent、确定性 Harness、EvidenceGuard 和隔离 SemanticGuard 的职责边界。 +- 冻结 Diagnosis Draft、Analysis、Conclusion、Fallback、Intent Router 和 SSE 事件契约。 +- 冻结 Tool Call ID、调用生命周期 `status`、结果语义 `evidence_status`、Redis canonical invocation 和 ToolResultProjector 命名。 +- 冻结预算、取消、重试、隐藏重试禁用和 Run 终态语义,但不在本阶段实现新运行链路。 +- 冻结只读 MySQL Tool 的 JSqlParser 允许子集、静态 allowlist 和安全前置。 +- 移除仓库脚本和主配置中的明文数据库、Redis、模型及向量服务凭据,并记录必须完成外部轮换。 +- 建立改造前 focused baseline 和后续 11 个串行 sm-flow change 的阶段台账。 +- **BREAKING(后续阶段实施)**:最终仅保留 `POST /api/chat` SSE、移除旧多 Agent/Graph Chat 主链路和旧 Tool Contract;本 change 不执行公开协议切换。 + +## Capabilities + +### New Capabilities + +- `single-react-diagnosis-harness`: 冻结单体诊断 Agent、Harness、证据状态、Guard、Fallback、预算、重试和阶段门禁的跨阶段基础契约。 + +### Modified Capabilities + +- None. 本阶段不声明旧运行能力已经迁移;后续 change 在实现对应行为时再修改现有 capability specs。 + +## Impact + +- 设计与规格:ISS-014、OpenSpec 主规格、devflow 词汇表和阶段归档台账。 +- 契约测试:Draft/Fallback、状态语义、SSE、Router、预算、重试和 MySQL 安全基线。 +- 安全:`scripts/query_mysql.py` 和 `application.yml` 中的明文凭据必须移除并在外部轮换。 +- 后续代码范围:ChatController、ChatService、Agent/Hook、Tool、Redis、JPA/Flyway、静态前端、Trace/Eval fixtures。 +- 本 change 不切换 Controller 协议、不接入新 Agent、不修改公开运行行为。 diff --git a/openspec/changes/archive/2026-07-21-single-react-design-freeze/specs/single-react-diagnosis-harness/spec.md b/openspec/changes/archive/2026-07-21-single-react-design-freeze/specs/single-react-diagnosis-harness/spec.md new file mode 100644 index 0000000..3aea82f --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-design-freeze/specs/single-react-diagnosis-harness/spec.md @@ -0,0 +1,75 @@ +## ADDED Requirements + +### Requirement: Design contracts SHALL separate deterministic control from diagnosis reasoning +The frozen contract set SHALL define Diagnosis Agent as the only diagnosis report author, Harness as deterministic execution control, EvidenceGuard as deterministic evidence validation, and SemanticGuard as an isolated single-turn semantic reviewer. + +#### Scenario: Contract ownership is inspected +- **WHEN** a later phase reads the stage-zero contracts +- **THEN** no Harness contract assigns Planner, Executor, Composer, workflow routing, or diagnosis reasoning responsibilities to Harness + +### Requirement: Tool invocation identity SHALL use the framework Tool Call ID +The contract SHALL use the framework-provided `tool_call_id` as the sole Tool invocation reference and SHALL require later Harness implementations to reject missing, invalid, or duplicate IDs within a Run. + +#### Scenario: Duplicate Tool Call ID is proposed +- **WHEN** two Tool actions in one Run present the same framework Tool Call ID +- **THEN** the contract classifies the second action as an error and prohibits overwriting the first canonical invocation + +### Requirement: Invocation status and evidence status SHALL be independent +The contract SHALL define `PROJECTING/READY/ERROR` as invocation lifecycle states and `EVIDENCE_FOUND/NO_EVIDENCE/ERROR` as evidence result states. + +#### Scenario: Successful query returns no evidence +- **WHEN** a Tool executes successfully and its bounded projection contains zero matching evidence +- **THEN** invocation status is `READY` and evidence status is `NO_EVIDENCE` + +#### Scenario: No-evidence result is cited +- **WHEN** a Diagnosis Draft cites a `NO_EVIDENCE` Tool result +- **THEN** the Analysis kind MUST be `NEGATIVE_OBSERVATION` and MUST remain bounded to the Tool query scope + +### Requirement: Diagnosis Draft SHALL expose typed report structure +The contract SHALL define conclusion, analysis items, action plan, recommendations, limitations and Tool Call bindings without exposing chain-of-thought or raw Tool payloads. + +#### Scenario: Draft contains an analysis item +- **WHEN** the Diagnosis Agent emits a structured Draft +- **THEN** every Analysis has a unique analysis ID, a fixed Analysis kind and at least one Tool Call ID + +### Requirement: Knowledge answers SHALL use exact RAG bindings +The KNOWLEDGE_QUERY contract SHALL represent the answer as bounded answer items whose references identify the single lookup Tool Call and returned document IDs. + +#### Scenario: Knowledge answer cites an unknown document +- **WHEN** an answer item references a document ID absent from the bounded lookup result +- **THEN** later Harness validation rejects the answer instead of publishing the fabricated citation + +### Requirement: Safe fallback SHALL use a fixed schema +The contract SHALL define stable fallback types for evidence validation failure, semantic unsupported and semantic unavailable outcomes, and SHALL exclude unvalidated Draft content and internal errors. + +#### Scenario: Evidence validation fails twice +- **WHEN** initial validation and the single no-Tool structural repair both fail +- **THEN** the fallback type is `EVIDENCE_VALIDATION_FAILED` and verified sources are empty + +### Requirement: Previous turn SHALL come from a safe durable Run result +The contract SHALL define `diagnosis_run` as the durable source of intent, release outcome and safe published result, and SHALL exclude fallback, failed and cancelled Runs from Diagnosis previous-turn selection. + +#### Scenario: Latest Run is a fallback +- **WHEN** the latest same-session Run ended with `FALLBACK` +- **THEN** it is not used as Diagnosis previous turn and selection continues to the latest eligible `DIAGNOSIS + SUCCESS` Run + +### Requirement: Cancellation SHALL be layered and observable +The contract SHALL distinguish cancellation request, prevention of new work, framework interruption, cancellable Tool work and HTTP timeout for already-blocking synchronous model calls. + +#### Scenario: Client disconnects during a model call +- **WHEN** an SSE client disconnects while a synchronous model call is in flight +- **THEN** the system prevents later Draft release, requests interruption, records an internal cancelled terminal state and discards any late model result + +### Requirement: Retry attempts SHALL be owned by Harness +The contract SHALL require underlying SDK, HTTP and database retry layers to execute one attempt, while Harness explicitly owns any allowed Router or SemanticGuard retry. + +#### Scenario: SemanticGuard returns an invalid schema +- **WHEN** the first SemanticGuard attempt returns an invalid structured result +- **THEN** Harness may execute one second attempt with the same verified snapshot and records both attempts + +### Requirement: Phase gates SHALL remain serial +The implementation plan SHALL contain eleven independent OpenSpec changes and SHALL prohibit starting a change before its predecessor is archived and committed. + +#### Scenario: Stage 4 completes internal Agent tests +- **WHEN** stage 4 passes its focused tests but stage 5 Guards are not implemented +- **THEN** the public Chat entry remains on the old path and stage 6B cutover is prohibited diff --git a/openspec/changes/archive/2026-07-21-single-react-design-freeze/tasks.md b/openspec/changes/archive/2026-07-21-single-react-design-freeze/tasks.md new file mode 100644 index 0000000..34ec3a8 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-single-react-design-freeze/tasks.md @@ -0,0 +1,21 @@ +## 1. Contract Model + +- [x] 1.1 Add typed enums for intent, release outcome, invocation status, evidence status, analysis kind, semantic verdict, fallback type and SSE outcome. +- [x] 1.2 Add reusable records for Diagnosis Draft, Knowledge Answer Draft, safe fallback, published result and previous turn. +- [x] 1.3 Add focused serialization and contract-shape tests covering positive evidence, no-evidence negative observation and forbidden raw/internal fields. + +## 2. Security And Configuration Baseline + +- [x] 2.1 Remove tracked plaintext credentials from `scripts/query_mysql.py` and `application.yml`, require environment-based secrets, and ignore local secret files. +- [x] 2.2 Record the required external credential rotation and the Spring AI hidden-retry baseline without changing the public Chat runtime in this stage. + +## 3. Design Freeze Alignment + +- [x] 3.1 Keep ISS-014, OpenSpec artifacts and the devflow glossary aligned on the 11 serial changes, framework Tool Call ID, dual status fields and safe previous-turn source. +- [x] 3.2 Add an architecture audit and cross-artifact alignment result to `decisions.md`. +- [x] 3.3 Create the sm-flow `.committed` marker after proposal/design/specs/tasks and all decision gates pass. + +## 4. Verification + +- [x] 4.1 Run focused contract tests and the smallest existing Chat/evidence baseline needed to prove stage zero did not switch runtime behavior. +- [x] 4.2 Run strict OpenSpec validation and record commands, results and unverified external rotation in acceptance evidence. diff --git a/openspec/specs/single-react-diagnosis-harness/spec.md b/openspec/specs/single-react-diagnosis-harness/spec.md new file mode 100644 index 0000000..ac4459c --- /dev/null +++ b/openspec/specs/single-react-diagnosis-harness/spec.md @@ -0,0 +1,78 @@ +# single-react-diagnosis-harness Specification + +## Purpose +TBD - created by archiving change single-react-design-freeze. Update Purpose after archive. +## Requirements +### Requirement: Design contracts SHALL separate deterministic control from diagnosis reasoning +The frozen contract set SHALL define Diagnosis Agent as the only diagnosis report author, Harness as deterministic execution control, EvidenceGuard as deterministic evidence validation, and SemanticGuard as an isolated single-turn semantic reviewer. + +#### Scenario: Contract ownership is inspected +- **WHEN** a later phase reads the stage-zero contracts +- **THEN** no Harness contract assigns Planner, Executor, Composer, workflow routing, or diagnosis reasoning responsibilities to Harness + +### Requirement: Tool invocation identity SHALL use the framework Tool Call ID +The contract SHALL use the framework-provided `tool_call_id` as the sole Tool invocation reference and SHALL require later Harness implementations to reject missing, invalid, or duplicate IDs within a Run. + +#### Scenario: Duplicate Tool Call ID is proposed +- **WHEN** two Tool actions in one Run present the same framework Tool Call ID +- **THEN** the contract classifies the second action as an error and prohibits overwriting the first canonical invocation + +### Requirement: Invocation status and evidence status SHALL be independent +The contract SHALL define `PROJECTING/READY/ERROR` as invocation lifecycle states and `EVIDENCE_FOUND/NO_EVIDENCE/ERROR` as evidence result states. + +#### Scenario: Successful query returns no evidence +- **WHEN** a Tool executes successfully and its bounded projection contains zero matching evidence +- **THEN** invocation status is `READY` and evidence status is `NO_EVIDENCE` + +#### Scenario: No-evidence result is cited +- **WHEN** a Diagnosis Draft cites a `NO_EVIDENCE` Tool result +- **THEN** the Analysis kind MUST be `NEGATIVE_OBSERVATION` and MUST remain bounded to the Tool query scope + +### Requirement: Diagnosis Draft SHALL expose typed report structure +The contract SHALL define conclusion, analysis items, action plan, recommendations, limitations and Tool Call bindings without exposing chain-of-thought or raw Tool payloads. + +#### Scenario: Draft contains an analysis item +- **WHEN** the Diagnosis Agent emits a structured Draft +- **THEN** every Analysis has a unique analysis ID, a fixed Analysis kind and at least one Tool Call ID + +### Requirement: Knowledge answers SHALL use exact RAG bindings +The KNOWLEDGE_QUERY contract SHALL represent the answer as bounded answer items whose references identify the single lookup Tool Call and returned document IDs. + +#### Scenario: Knowledge answer cites an unknown document +- **WHEN** an answer item references a document ID absent from the bounded lookup result +- **THEN** later Harness validation rejects the answer instead of publishing the fabricated citation + +### Requirement: Safe fallback SHALL use a fixed schema +The contract SHALL define stable fallback types for evidence validation failure, semantic unsupported and semantic unavailable outcomes, and SHALL exclude unvalidated Draft content and internal errors. + +#### Scenario: Evidence validation fails twice +- **WHEN** initial validation and the single no-Tool structural repair both fail +- **THEN** the fallback type is `EVIDENCE_VALIDATION_FAILED` and verified sources are empty + +### Requirement: Previous turn SHALL come from a safe durable Run result +The contract SHALL define `diagnosis_run` as the durable source of intent, release outcome and safe published result, and SHALL exclude fallback, failed and cancelled Runs from Diagnosis previous-turn selection. + +#### Scenario: Latest Run is a fallback +- **WHEN** the latest same-session Run ended with `FALLBACK` +- **THEN** it is not used as Diagnosis previous turn and selection continues to the latest eligible `DIAGNOSIS + SUCCESS` Run + +### Requirement: Cancellation SHALL be layered and observable +The contract SHALL distinguish cancellation request, prevention of new work, framework interruption, cancellable Tool work and HTTP timeout for already-blocking synchronous model calls. + +#### Scenario: Client disconnects during a model call +- **WHEN** an SSE client disconnects while a synchronous model call is in flight +- **THEN** the system prevents later Draft release, requests interruption, records an internal cancelled terminal state and discards any late model result + +### Requirement: Retry attempts SHALL be owned by Harness +The contract SHALL require underlying SDK, HTTP and database retry layers to execute one attempt, while Harness explicitly owns any allowed Router or SemanticGuard retry. + +#### Scenario: SemanticGuard returns an invalid schema +- **WHEN** the first SemanticGuard attempt returns an invalid structured result +- **THEN** Harness may execute one second attempt with the same verified snapshot and records both attempts + +### Requirement: Phase gates SHALL remain serial +The implementation plan SHALL contain eleven independent OpenSpec changes and SHALL prohibit starting a change before its predecessor is archived and committed. + +#### Scenario: Stage 4 completes internal Agent tests +- **WHEN** stage 4 passes its focused tests but stage 5 Guards are not implemented +- **THEN** the public Chat entry remains on the old path and stage 6B cutover is prohibited diff --git a/scripts/query_mysql.py b/scripts/query_mysql.py index 157939f..1f12aac 100644 --- a/scripts/query_mysql.py +++ b/scripts/query_mysql.py @@ -22,13 +22,20 @@ except ImportError: print("缺少依赖,请先执行: pip install pymysql") sys.exit(1) -# 从 application.yml 读取的连接信息 +def required_env(name: str) -> str: + value = os.getenv(name) + if value is None or not value.strip(): + print(f"缺少必需环境变量: {name}") + sys.exit(2) + return value.strip() + + DB_CONFIG = { - "host": "119.29.78.52", - "port": 33306, - "user": "root", - "password": "!Fucker123..", - "database": "superbiz_agent", + "host": os.getenv("SUPERBIZ_MYSQL_HOST", "119.29.78.52"), + "port": int(os.getenv("SUPERBIZ_MYSQL_PORT", "33306")), + "user": os.getenv("SUPERBIZ_MYSQL_USERNAME", "root"), + "password": required_env("SUPERBIZ_MYSQL_PASSWORD"), + "database": os.getenv("SUPERBIZ_MYSQL_DATABASE", "superbiz_agent"), "charset": "utf8mb4", "cursorclass": pymysql.cursors.DictCursor, } diff --git a/src/main/java/com/superbiz/agent/harness/contract/AnalysisKind.java b/src/main/java/com/superbiz/agent/harness/contract/AnalysisKind.java new file mode 100644 index 0000000..01f14af --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/AnalysisKind.java @@ -0,0 +1,13 @@ +package com.superbiz.agent.harness.contract; + +public enum AnalysisKind { + NORMAL, + NEGATIVE_OBSERVATION; + + public boolean accepts(EvidenceStatus evidenceStatus) { + return switch (this) { + case NORMAL -> evidenceStatus == EvidenceStatus.EVIDENCE_FOUND; + case NEGATIVE_OBSERVATION -> evidenceStatus == EvidenceStatus.NO_EVIDENCE; + }; + } +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/ContractCollections.java b/src/main/java/com/superbiz/agent/harness/contract/ContractCollections.java new file mode 100644 index 0000000..8b31809 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/ContractCollections.java @@ -0,0 +1,13 @@ +package com.superbiz.agent.harness.contract; + +import java.util.List; + +final class ContractCollections { + + private ContractCollections() { + } + + static List immutable(List values) { + return values == null ? List.of() : List.copyOf(values); + } +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/DiagnosisDraft.java b/src/main/java/com/superbiz/agent/harness/contract/DiagnosisDraft.java new file mode 100644 index 0000000..78bdc3c --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/DiagnosisDraft.java @@ -0,0 +1,67 @@ +package com.superbiz.agent.harness.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record DiagnosisDraft( + @JsonProperty("conclusion") Conclusion conclusion, + @JsonProperty("analysis") List analysis, + @JsonProperty("action_plan") List actionPlan, + @JsonProperty("recommendations") List recommendations, + @JsonProperty("limitations") Limitations limitations) { + + public DiagnosisDraft { + analysis = ContractCollections.immutable(analysis); + actionPlan = ContractCollections.immutable(actionPlan); + recommendations = ContractCollections.immutable(recommendations); + } + + public record Conclusion( + @JsonProperty("text") String text, + @JsonProperty("based_on_analysis_ids") List basedOnAnalysisIds) { + + public Conclusion { + basedOnAnalysisIds = ContractCollections.immutable(basedOnAnalysisIds); + } + } + + public record AnalysisItem( + @JsonProperty("analysis_id") String analysisId, + @JsonProperty("kind") AnalysisKind kind, + @JsonProperty("text") String text, + @JsonProperty("tool_call_ids") List toolCallIds) { + + public AnalysisItem { + toolCallIds = ContractCollections.immutable(toolCallIds); + } + } + + public record ActionPlanItem( + @JsonProperty("action") String action, + @JsonProperty("based_on_analysis_ids") List basedOnAnalysisIds, + @JsonProperty("requires_human_confirmation") boolean requiresHumanConfirmation) { + + public ActionPlanItem { + basedOnAnalysisIds = ContractCollections.immutable(basedOnAnalysisIds); + } + } + + public record Recommendation( + @JsonProperty("text") String text, + @JsonProperty("based_on_analysis_ids") List basedOnAnalysisIds) { + + public Recommendation { + basedOnAnalysisIds = ContractCollections.immutable(basedOnAnalysisIds); + } + } + + public record Limitations( + @JsonProperty("scope") String scope, + @JsonProperty("missing_info") List missingInfo) { + + public Limitations { + missingInfo = ContractCollections.immutable(missingInfo); + } + } +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/EvidenceStatus.java b/src/main/java/com/superbiz/agent/harness/contract/EvidenceStatus.java new file mode 100644 index 0000000..d279d5e --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/EvidenceStatus.java @@ -0,0 +1,7 @@ +package com.superbiz.agent.harness.contract; + +public enum EvidenceStatus { + EVIDENCE_FOUND, + NO_EVIDENCE, + ERROR +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/FallbackType.java b/src/main/java/com/superbiz/agent/harness/contract/FallbackType.java new file mode 100644 index 0000000..d382d1c --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/FallbackType.java @@ -0,0 +1,7 @@ +package com.superbiz.agent.harness.contract; + +public enum FallbackType { + EVIDENCE_VALIDATION_FAILED, + SEMANTIC_UNSUPPORTED, + SEMANTIC_UNAVAILABLE +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/IntentType.java b/src/main/java/com/superbiz/agent/harness/contract/IntentType.java new file mode 100644 index 0000000..81d872c --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/IntentType.java @@ -0,0 +1,7 @@ +package com.superbiz.agent.harness.contract; + +public enum IntentType { + SYSTEM_CHAT, + KNOWLEDGE_QUERY, + DIAGNOSIS +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/InvocationStatus.java b/src/main/java/com/superbiz/agent/harness/contract/InvocationStatus.java new file mode 100644 index 0000000..43cff22 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/InvocationStatus.java @@ -0,0 +1,7 @@ +package com.superbiz.agent.harness.contract; + +public enum InvocationStatus { + PROJECTING, + READY, + ERROR +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/KnowledgeAnswerDraft.java b/src/main/java/com/superbiz/agent/harness/contract/KnowledgeAnswerDraft.java new file mode 100644 index 0000000..5931ab7 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/KnowledgeAnswerDraft.java @@ -0,0 +1,25 @@ +package com.superbiz.agent.harness.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record KnowledgeAnswerDraft( + @JsonProperty("answer_items") List answerItems, + @JsonProperty("limitations") List limitations) { + + public KnowledgeAnswerDraft { + answerItems = ContractCollections.immutable(answerItems); + limitations = ContractCollections.immutable(limitations); + } + + public record AnswerItem( + @JsonProperty("text") String text, + @JsonProperty("tool_call_id") String toolCallId, + @JsonProperty("document_ids") List documentIds) { + + public AnswerItem { + documentIds = ContractCollections.immutable(documentIds); + } + } +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/PreviousTurn.java b/src/main/java/com/superbiz/agent/harness/contract/PreviousTurn.java new file mode 100644 index 0000000..2228c26 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/PreviousTurn.java @@ -0,0 +1,27 @@ +package com.superbiz.agent.harness.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record PreviousTurn( + @JsonProperty("user_query") String userQuery, + @JsonProperty("published_conclusion") String publishedConclusion, + @JsonProperty("scope") String scope, + @JsonProperty("limitations") List limitations, + @JsonProperty("source_documents") List sourceDocuments) { + + public PreviousTurn { + limitations = ContractCollections.immutable(limitations); + sourceDocuments = ContractCollections.immutable(sourceDocuments); + } + + public static PreviousTurn from(PublishedResult result) { + return new PreviousTurn( + result.userQuery(), + result.publishedConclusion(), + result.scope(), + result.limitations(), + result.sourceDocuments()); + } +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/PublishedResult.java b/src/main/java/com/superbiz/agent/harness/contract/PublishedResult.java new file mode 100644 index 0000000..ea455f9 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/PublishedResult.java @@ -0,0 +1,18 @@ +package com.superbiz.agent.harness.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record PublishedResult( + @JsonProperty("user_query") String userQuery, + @JsonProperty("published_conclusion") String publishedConclusion, + @JsonProperty("scope") String scope, + @JsonProperty("limitations") List limitations, + @JsonProperty("source_documents") List sourceDocuments) { + + public PublishedResult { + limitations = ContractCollections.immutable(limitations); + sourceDocuments = ContractCollections.immutable(sourceDocuments); + } +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/ReleaseOutcome.java b/src/main/java/com/superbiz/agent/harness/contract/ReleaseOutcome.java new file mode 100644 index 0000000..73ec17a --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/ReleaseOutcome.java @@ -0,0 +1,8 @@ +package com.superbiz.agent.harness.contract; + +public enum ReleaseOutcome { + SUCCESS, + FALLBACK, + FAILED, + CANCELLED +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/SafeFallback.java b/src/main/java/com/superbiz/agent/harness/contract/SafeFallback.java new file mode 100644 index 0000000..bd07b2a --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/SafeFallback.java @@ -0,0 +1,26 @@ +package com.superbiz.agent.harness.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record SafeFallback( + @JsonProperty("type") FallbackType type, + @JsonProperty("conclusion") String conclusion, + @JsonProperty("message") String message, + @JsonProperty("verified_sources") List verifiedSources, + @JsonProperty("limitations") List limitations, + @JsonProperty("next_steps") List nextSteps) { + + public SafeFallback { + verifiedSources = ContractCollections.immutable(verifiedSources); + limitations = ContractCollections.immutable(limitations); + nextSteps = ContractCollections.immutable(nextSteps); + } + + public record VerifiedSource( + @JsonProperty("source_type") String sourceType, + @JsonProperty("source") String source, + @JsonProperty("scope") String scope) { + } +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/SemanticVerdict.java b/src/main/java/com/superbiz/agent/harness/contract/SemanticVerdict.java new file mode 100644 index 0000000..da8f0d0 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/SemanticVerdict.java @@ -0,0 +1,6 @@ +package com.superbiz.agent.harness.contract; + +public enum SemanticVerdict { + SUPPORTED, + UNSUPPORTED +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/SourceDocument.java b/src/main/java/com/superbiz/agent/harness/contract/SourceDocument.java new file mode 100644 index 0000000..a0bb768 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/SourceDocument.java @@ -0,0 +1,8 @@ +package com.superbiz.agent.harness.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record SourceDocument( + @JsonProperty("document_id") String documentId, + @JsonProperty("title") String title) { +} diff --git a/src/main/java/com/superbiz/agent/harness/contract/SseOutcome.java b/src/main/java/com/superbiz/agent/harness/contract/SseOutcome.java new file mode 100644 index 0000000..e61f888 --- /dev/null +++ b/src/main/java/com/superbiz/agent/harness/contract/SseOutcome.java @@ -0,0 +1,7 @@ +package com.superbiz.agent.harness.contract; + +public enum SseOutcome { + SUCCESS, + FALLBACK, + FAILED +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4a02b3a..54ccd64 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -22,7 +22,7 @@ milvus: password: "" database: db_4a578da0f27ce9d timeout: 10000 - token: d246a77f43a109685596e3c68ecfd359e1cd8b29d35c41d708160f02b97ae623d2ab392738df740d0c77b58ca1bbadfa7c412140 + token: ${MILVUS_TOKEN} secure: true vector-dim: 1024 # BGE-M3 = 1024,换模型时同步改 @@ -45,7 +45,7 @@ spring: datasource: url: jdbc:mysql://119.29.78.52:33306/superbiz_agent?useUnicode=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root - password: '!Fucker123..' + password: ${SUPERBIZ_MYSQL_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 5 @@ -83,7 +83,7 @@ spring: redis: host: 119.29.78.52 port: 33308 - password: '!Fucker123..' + password: ${SUPERBIZ_REDIS_PASSWORD} database: 0 timeout: 3000 lettuce: @@ -119,7 +119,7 @@ spring: # --- Chat: DeepSeek (原生) --- deepseek: - api-key: sk-1f44696abe644bd684f09cc43f12c557 + api-key: ${DEEPSEEK_API_KEY} base-url: https://api.deepseek.com chat: options: @@ -136,7 +136,7 @@ spring: # --- Embedding: SiliconFlow BGE-M3 --- siliconflow: - api-key: sk-rlxqcnlohjqwkzoffollthmzzfiohngdrabrmmqhcgtewnzx + api-key: ${SILICONFLOW_API_KEY} base-url: https://api.siliconflow.cn embedding: model: BAAI/bge-m3 diff --git a/src/test/java/com/superbiz/agent/harness/contract/HarnessContractTest.java b/src/test/java/com/superbiz/agent/harness/contract/HarnessContractTest.java new file mode 100644 index 0000000..9457dde --- /dev/null +++ b/src/test/java/com/superbiz/agent/harness/contract/HarnessContractTest.java @@ -0,0 +1,96 @@ +package com.superbiz.agent.harness.contract; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HarnessContractTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void diagnosisDraftUsesStableBoundedShape() throws Exception { + DiagnosisDraft draft = new DiagnosisDraft( + new DiagnosisDraft.Conclusion("Pool exhausted", List.of("a1")), + List.of(new DiagnosisDraft.AnalysisItem( + "a1", + AnalysisKind.NORMAL, + "active=50 max=50", + List.of("call-1"))), + List.of(new DiagnosisDraft.ActionPlanItem( + "Inspect long transactions", + List.of("a1"), + false)), + List.of(new DiagnosisDraft.Recommendation( + "Add pool wait alerts", + List.of("a1"))), + new DiagnosisDraft.Limitations("order-service, last 30 minutes", List.of("No slow SQL data"))); + + JsonNode json = objectMapper.valueToTree(draft); + + assertEquals("call-1", json.path("analysis").get(0).path("tool_call_ids").get(0).asText()); + assertEquals("NORMAL", json.path("analysis").get(0).path("kind").asText()); + assertTrue(json.has("action_plan")); + assertFalse(json.toString().contains("raw_response")); + assertFalse(json.toString().contains("thought")); + } + + @Test + void evidenceStatusRemainsSeparateFromInvocationStatus() { + assertTrue(AnalysisKind.NORMAL.accepts(EvidenceStatus.EVIDENCE_FOUND)); + assertFalse(AnalysisKind.NORMAL.accepts(EvidenceStatus.NO_EVIDENCE)); + assertTrue(AnalysisKind.NEGATIVE_OBSERVATION.accepts(EvidenceStatus.NO_EVIDENCE)); + assertFalse(AnalysisKind.NEGATIVE_OBSERVATION.accepts(EvidenceStatus.ERROR)); + assertEquals(InvocationStatus.READY, InvocationStatus.valueOf("READY")); + } + + @Test + void knowledgeAnswerBindsExactToolAndDocuments() throws Exception { + KnowledgeAnswerDraft draft = new KnowledgeAnswerDraft( + List.of(new KnowledgeAnswerDraft.AnswerItem( + "Check the timeout code first.", + "call-rag-1", + List.of("payment-timeout-guide"))), + List.of("Runtime state was not queried")); + + JsonNode json = objectMapper.valueToTree(draft); + + assertEquals("call-rag-1", json.path("answer_items").get(0).path("tool_call_id").asText()); + assertEquals("payment-timeout-guide", + json.path("answer_items").get(0).path("document_ids").get(0).asText()); + } + + @Test + void fallbackAndPreviousTurnExcludeInternalEvidence() throws Exception { + SafeFallback fallback = new SafeFallback( + FallbackType.SEMANTIC_UNSUPPORTED, + null, + "Current evidence is insufficient", + List.of(new SafeFallback.VerifiedSource("LOG", "APPLICATION", "last 30 minutes")), + List.of("Semantic validation did not pass"), + List.of("Collect the missing data and retry")); + PublishedResult result = new PublishedResult( + "Check payment timeout", + "Connection pool exhaustion", + "order-service, last 30 minutes", + List.of("No slow SQL data"), + List.of(new SourceDocument("payment-timeout-guide", "Payment timeout guide"))); + + JsonNode fallbackJson = objectMapper.valueToTree(fallback); + JsonNode previousJson = objectMapper.valueToTree(PreviousTurn.from(result)); + + assertNull(fallback.conclusion()); + assertEquals("SEMANTIC_UNSUPPORTED", fallbackJson.path("type").asText()); + assertEquals("payment-timeout-guide", + previousJson.path("source_documents").get(0).path("document_id").asText()); + assertFalse(previousJson.toString().contains("tool_call_id")); + assertFalse(previousJson.toString().contains("raw_response")); + } +} diff --git a/src/test/java/com/superbiz/agent/service/SimpleMilvusTest.java b/src/test/java/com/superbiz/agent/service/SimpleMilvusTest.java index 5c56091..f1c5b8c 100644 --- a/src/test/java/com/superbiz/agent/service/SimpleMilvusTest.java +++ b/src/test/java/com/superbiz/agent/service/SimpleMilvusTest.java @@ -4,6 +4,7 @@ import io.milvus.client.MilvusServiceClient; import io.milvus.param.ConnectParam; import io.milvus.param.R; import io.milvus.param.collection.HasCollectionParam; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; /** @@ -13,9 +14,12 @@ public class SimpleMilvusTest { @Test public void testConnection() { - String host = "in03-4a578da0f27ce9d.serverless.aws-eu-central-1.cloud.zilliz.com"; - int port = 443; - String token = "d246a77f43a109685596e3c68ecfd359e1cd8b29d35c41d708160f02b97ae623d2ab392738df740d0c77b58ca1bbadfa7c412140"; + String host = System.getenv().getOrDefault( + "MILVUS_HOST", + "in03-4a578da0f27ce9d.serverless.aws-eu-central-1.cloud.zilliz.com"); + int port = Integer.parseInt(System.getenv().getOrDefault("MILVUS_PORT", "443")); + String token = System.getenv("MILVUS_TOKEN"); + Assumptions.assumeTrue(token != null && !token.isBlank(), "MILVUS_TOKEN is required"); System.out.println("尝试连接 Milvus..."); System.out.println("Host: " + host); diff --git a/src/test/java/com/superbiz/agent/service/session/RedisSessionManagerTest.java b/src/test/java/com/superbiz/agent/service/session/RedisSessionManagerTest.java index 9bc2f1d..1225f53 100644 --- a/src/test/java/com/superbiz/agent/service/session/RedisSessionManagerTest.java +++ b/src/test/java/com/superbiz/agent/service/session/RedisSessionManagerTest.java @@ -4,6 +4,7 @@ import com.superbiz.agent.domain.model.SessionContext; import com.superbiz.agent.domain.model.ToolCall; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; @@ -20,10 +21,11 @@ import static org.junit.jupiter.api.Assertions.*; * RedisSessionManager 单元测试 */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@EnabledIfEnvironmentVariable(named = "SUPERBIZ_REDIS_PASSWORD", matches = ".+") @TestPropertySource(properties = { - "spring.redis.host=119.29.78.52", - "spring.redis.port=6379", - "spring.redis.password=!Fucker123.." + "spring.data.redis.host=${SUPERBIZ_REDIS_HOST:119.29.78.52}", + "spring.data.redis.port=${SUPERBIZ_REDIS_PORT:33308}", + "spring.data.redis.password=${SUPERBIZ_REDIS_PASSWORD}" }) class RedisSessionManagerTest {