superagent请求问题修复

This commit is contained in:
andy
2026-09-06 01:40:15 +08:00
parent ee9d2d7cf1
commit 7243319bbb
19 changed files with 388 additions and 74 deletions

View File

@@ -14,6 +14,11 @@ FIRE_SAFETY_SUPERAGENT_ENABLED=false
# Required when enabled, for example: https://superagent.example.com
FIRE_SAFETY_SUPERAGENT_BASE_URL=
FIRE_SAFETY_SUPERAGENT_OPEN_API_KEY=
# Default to the provider-compatible no-Trace stream. Its strict completion
# requires an AI stop, a non-empty message.final, and the top-level end event.
# Set true only when the external application's trace_policy.enabled is enabled.
# This controls trace visibility/validation; it does not disable Agent MCP calls.
FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=false
FIRE_SAFETY_SUPERAGENT_CONNECT_TIMEOUT=15s
FIRE_SAFETY_SUPERAGENT_RECOVERY_MAX_ATTEMPTS=5
FIRE_SAFETY_SUPERAGENT_RECOVERY_INITIAL_BACKOFF=250ms

View File

@@ -14,19 +14,19 @@
| --- | --- | --- |
| `cmd/server` | Go 服务进程入口 | 已建立 |
| `internal/app` | 应用装配、readiness 和 HTTP 生命周期 | 已建立;按开关装配原生/兼容 Chat、可选 `/chat` 测试页面、SuperAgent 和 MCP/PostGIS |
| `internal/config` | 环境配置入口 | 已包含 HTTP、SuperAgent、Chat 兼容 App ID/页面开关/legacy 短凭证开关、MCP 与 PostGIS 配置校验和凭证分离门禁 |
| `internal/config` | 环境配置入口 | 已包含 HTTP、SuperAgent(含默认关闭的 IncludeTrace、Chat 兼容 App ID/页面开关/legacy 短凭证开关、MCP 与 PostGIS 配置校验和凭证分离门禁 |
| `internal/handler` | HTTP/MCP 入站协议层 | `GET /health` 已启用;默认关闭的原生 `/api/chat`、可选 DashScope 风格 `completion`、可选 `/chat` 页面/资源和 `/mcp` 已实现 |
| `internal/service` | 业务用例编排 | 已实现单进程聊天会话/并发 Run 控制,以及地名候选、有界空间查询、可信数据库全范围/镇街白名单和结果语义 |
| `internal/domain` | 森林防火领域模型与规则 | 已包含点位、水源、候选设施、通道、队伍和风险区模型 |
| `internal/repository` | PostgreSQL/PostGIS 持久化适配 | 已实现 pgxpool、只读固定 SQL 与 schema/SRID readiness实库 SRID 元数据、严格 readiness 和 7 个工具真实查询已验证 |
| `cmd/postgis-srid-migrate` | 显式 SRID 元数据迁移 | 已执行;默认只读预检,写入需独立迁移凭证和明确 CRS 确认 |
| `internal/integration/superagent` | SuperAgent Open API 出站适配 | 已实现并通过模拟 Provider 测试,默认关闭 |
| `internal/integration/superagent` | SuperAgent Open API 出站适配 | 已实现并通过模拟 Provider 测试`FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` 默认 false支持无 Trace 严格完成判定 |
| `cmd/superagent-probe` | 无业务数据的显式连通性探针 | 已实现;需要项目专属测试配置 |
| `cmd/postgis-probe` | 不读取业务行的 PostGIS readiness 探针 | 已实现;需要只读数据库配置 |
| `Dockerfile` / `compose.yaml` | 测试环境容器构建与单实例进程托管 | 已建立;容器内监听 8080只发布宿主机回环端口 16587页面开关默认关闭目标机镜像已构建容器启动和 health 待重建后验证 |
| `pkg` | 可被外部 module 复用的稳定 Go API | 当前为空 |
| `docs/import` | 字段/表映射、通用模板和本地数据库样例 | 样例 SQL 含受限数据并被 Git 忽略,不会执行 |
| SuperAgent | 对话理解、工具选择和答案组织 | 对话客户端按仓库内 2026-07-12 协议基线实现;同一 SuperAgent 中 `th-hotel-simple-superagent` 已稳定启用/调用,证明其兼容档案不依赖版本配置;公网消防 `/mcp` 已收到一次请求并到达 Go旧版本门禁返回错误,消防兼容档案待部署验证,公网消防链路尚未调用数据库工具 |
| SuperAgent | 对话理解、工具选择和答案组织 | 对话客户端按仓库内 2026-07-12 协议基线实现;默认请求 `include_trace=false`,无 Trace 只隐藏工具/步骤轨迹,不禁止 Agent 调用 MCP同一 Key 的 `include_trace=true` 曾因应用策略关闭返回 403 `open_agent_trace_disabled`,改为 false 返回 HTTP 200同一 SuperAgent 中 `th-hotel-simple-superagent` 已稳定启用/调用,证明其兼容档案不依赖版本配置;公网消防 `/mcp` 已收到一次请求并到达 Go但消防兼容档案待部署验证公网消防链路尚未完成完整多工具对话 |
| PostgreSQL/PostGIS | 消防空间业务事实的预期权威来源 | 8 表共 4,055 条记录4,048 条非空几何已标记 EPSG:4326严格 readiness 与本地真实工具冒烟均通过35 条无效几何按当前策略排除并告警 |
## 3. 技术栈
@@ -37,9 +37,9 @@
- 本地工具链Go `1.26.6`
- HTTPGo 标准库 `net/http`
- 测试Go 标准库 `testing``httptest`
- 配置:环境变量;支持 HTTP、SuperAgent、Chat、MCP 与 PostGIS 配置,并默认关闭 Chat 及两个外部方向。
- 配置:环境变量;支持 HTTP、SuperAgent、Chat、MCP 与 PostGIS 配置,并默认关闭 Chat 及两个外部方向SuperAgent Trace 请求默认关闭,可通过 `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` 显式开启
- Chat API标准库 HTTP/SSE原生 `/api/chat` 使用静态联调 Bearer可选 `completion` 兼容入口使用同一信任方向的 `xtoken`;默认要求 Chat 凭证至少 32 个可打印 ASCII 字符,受控迁移时可通过默认关闭的 `FIRE_SAFETY_CHAT_ALLOW_LEGACY_SHORT_TOKEN` 兼容已交付短凭证;两者共享精确 Origin、严格 JSON、总超时、有界单进程会话和同会话并发冲突。兼容入口只在严格成功后发送正文。可选测试页面由 `FIRE_SAFETY_CHAT_PAGE_ENABLED` 控制,提供 `/chat`(开启时 308 到 `/chat/`)、`/chat/``/chat/app.css``/chat/app.js`;页面只在内存中使用用户手动输入的 `xtoken`,复用兼容 SSE 的 `session_id`,不嵌入或持久化 Token。
- SuperAgent标准库 HTTP/SSE 客户端,分离 Session 创建和消息发送,支持严格完成判定与既有 Run 断流恢复
- SuperAgent标准库 HTTP/SSE 客户端,分离 Session 创建和消息发送,支持按配置选择 Trace/无 Trace、严格完成判定与既有 Run 断流恢复。无 Trace 要求最终 AI 消息 `finish_reason=stop`、非空顶层 `message.final` 和顶层 `end`Trace 模式额外要求 `run.completed(status=success)`
- MCP标准库 HTTP/JSON-RPC服务端固定返回版本标识 `2025-06-18`,同步 JSON 响应,独立 Bearer 和 7 个只读工具;按已稳定接通的 `th-hotel-simple-superagent` 兼容档案处理initialize 中的 `protocolVersion` 和后续 `MCP-Protocol-Version` Header 都不作为版本拒绝门禁SuperAgent 无需也不能配置版本。固定返回该版本不表示支持任意其他版本,也不是追求最新协议。地名工具只搜索现有业务记录并要求用户确认候选。
- PostgreSQL`github.com/jackc/pgx/v5 v5.10.0` 原生连接池;连接默认只读并设置 statement timeout。
- PostGIS`ST_Covers``ST_DWithin``ST_Distance``ST_ClosestPoint`;只在实库确认 EPSG:4326 后启用。
@@ -48,11 +48,11 @@
### 计划但尚未接入或确认
- PostgreSQL/PostGIS 的适用索引和生产查询计划验证。
- SuperAgent 到 `/mcp` 的 proven-profile 兼容实现部署后重测,以及 TLS、网络白名单、Token 和完整工具调用链联调;版本 Header 不需要配置,也不是验收门禁。
- SuperAgent 到 `/mcp` 的 proven-profile 兼容实现部署后重测,以及 TLS、网络白名单、Token 和完整工具调用链联调;版本 Header 不需要配置,也不是验收门禁。无 Trace 模式已具备本地模拟验证,公网完整 MCP 对话仍待确认。
- 任意地址/山名的外部地理编码、别名词典和大数据量地名索引。
- 用户聊天的真实身份认证、动态授权、共享/持久会话、主动取消和限流策略;首版默认关闭的静态 Bearer + 内存会话 API 以及默认关闭的静态测试页面已实现。legacy 短凭证仅限受控测试/迁移窗口,轮换后须关闭兼容开关。
- 最终用户鉴权、动态角色/区域或租户隔离、持久审计与完整可观测性方案。
- 正式前端身份接入;当前 `/chat` 页面仅用于受控测试,现有第三方客户端仅通过受限 DashScope 风格协议适配。
- 正式前端身份接入;当前 `/chat` 页面仅用于受控测试,现有第三方客户端仅通过受限 DashScope 风格协议适配。Trace 是否开放由 SuperAgent 外部应用策略决定,不能仅凭客户端参数绕过。
## 4. 已知业务数据范围
@@ -75,13 +75,13 @@
- 用户侧应用:采集用户输入并展示结果;仓库提供默认关闭的 `/chat` 受控测试页面,正式用户端形态仍待确认。
- 本 Go 服务鉴权上下文、会话转发、MCP 工具、数据查询、权限、安全和审计。
- SuperAgent理解自然语言、决定是否调用工具、组织自然语言结果页面和第三方客户端都通过同一兼容 completion SSE 入口发起对话。
- SuperAgent理解自然语言、决定是否调用工具、组织自然语言结果页面和第三方客户端都通过同一兼容 completion SSE 入口发起对话。上游无 Trace 仅减少轨迹返回,不改变 Agent 选择或调用 MCP 的能力;是否调用成功需看 MCP 日志和结果。
- MCP 工具:提供固定的森林防火领域查询,不开放任意 SQL 或跨权限访问。
- PostgreSQL/PostGIS保存和计算可信空间事实资源是否可用仍取决于明确状态和数据时效。
## 6. 当前开发方向与非目标
当前阶段已有可运行、可测试、文档自解释的 Go 基线、SuperAgent Open API Adapter、默认关闭的原生用户对话 API、可选 DashScope 风格兼容入口、默认关闭的 `/chat` 测试页面,以及空间只读 MCP/PostGIS 实现。对话入口使用独立静态联调凭证和单进程内存会话;页面不嵌入或持久化 Token使用者手动输入 `xtoken` 并复用同页面内存中的 `session_id`。仓库已有多阶段 Docker/Compose 基线以及精确路径、无 Secret 的 Nginx HTTPS 反向代理示例,模板公开反代 `/chat``/chat/``/chat/app.css``/chat/app.js`、兼容 completion 和 `/mcp`,页面是否可用由 Go 开关控制。目标机镜像构建和 Nginx 语法检查已由现场截图证明通过,容器稳定运行、页面公网响应和 TLS 实际状态仍待验证。2026-09-05 22:35 的现场日志已证明公网 MCP 完成 `fire_safety_search_place_candidates` 一次成功调用(此前的 initialize/notifications/tools/list 也有日志),但其余消防工具和完整多工具链仍待验收。同一 SuperAgent 中 `th-hotel-simple-superagent` 已稳定启用/调用,为本项目提供了 proven-profile 兼容参照,但不能替代消防 endpoint 的完整验收。部署后通过 `direct_success``compatibility_success` 日志分类确认实际请求不记录客户端原始版本值。MCP 默认关闭,实库严格 readiness 和全部 7 个工具的本地真实查询已通过。下一阶段先在测试服务器验证页面开关、静态资源、兼容 Chat 首轮/多轮和公网 TLS再完成真实消防 Profile 的完整 MCP 工具链联调。
当前阶段已有可运行、可测试、文档自解释的 Go 基线、SuperAgent Open API Adapter、默认关闭的原生用户对话 API、可选 DashScope 风格兼容入口、默认关闭的 `/chat` 测试页面,以及空间只读 MCP/PostGIS 实现。对话入口使用独立静态联调凭证和单进程内存会话;页面不嵌入或持久化 Token使用者手动输入 `xtoken` 并复用同页面内存中的 `session_id`SuperAgent Open API 默认通过 `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=false` 使用无 Trace 流:严格成功要求最终 AI 消息 `finish_reason=stop`、非空顶层 `message.final` 和顶层 `end`;无 Trace 不代表 Agent 不能调用 MCP。若设置 true则仍要求 `run.completed(status=success)`,且外部应用策略必须允许 Trace。当前同一 Key 的 true 请求因 `open_agent_trace_disabled` 返回 403false 模式下真实探针和本地兼容 Chat SSE 均已严格成功。仓库已有多阶段 Docker/Compose 基线以及精确路径、无 Secret 的 Nginx HTTPS 反向代理示例,模板公开反代 `/chat``/chat/``/chat/app.css``/chat/app.js`、兼容 completion 和 `/mcp`,页面是否可用由 Go 开关控制。目标机镜像构建和 Nginx 语法检查已由现场截图证明通过,容器稳定运行、页面公网响应和 TLS 实际状态仍待验证。2026-09-05 22:35 的现场日志已证明公网 MCP 完成 `fire_safety_search_place_candidates` 一次成功调用(此前的 initialize/notifications/tools/list 也有日志),但其余消防工具和完整多工具链仍待验收。同一 SuperAgent 中 `th-hotel-simple-superagent` 已稳定启用/调用,为本项目提供了 proven-profile 兼容参照,但不能替代消防 endpoint 的完整验收。部署后通过 `direct_success``compatibility_success` 日志分类确认实际请求不记录客户端原始版本值。MCP 默认关闭,实库严格 readiness 和全部 7 个工具的本地真实查询已通过。下一阶段先在测试服务器重建包含无 Trace 配置的服务,验证页面开关、静态资源、兼容 Chat 首轮/多轮和公网 TLS再完成真实消防 Profile 的完整 MCP 工具链联调。
本阶段不实现:

View File

@@ -4,15 +4,15 @@
| --- | --- |
| 最近更新 | 2026-09-06 |
| 当前分支 | `main` |
| 当前阶段 | 对话、SuperAgent、空间 MCP、默认关闭的测试页面与测试环境容器部署基线已完成公网地点候选 MCP 已现场成功一次,页面公网验收和完整 MCP 多工具链仍待验证 |
| 当前重点 | 完成 `/chat` 测试页面的配置开关、静态资源与公网验收,同时重建目标机并验证完整公网 MCP 工具链路 |
| 当前阶段 | 对话、SuperAgent、空间 MCP、默认关闭的测试页面、无 Trace 兼容模式与测试环境容器部署基线已完成;公网地点候选 MCP 已现场成功一次,页面公网验收和完整 MCP 多工具链仍待验证 |
| 当前重点 | 使用 `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=false` 绕过当前 Trace 策略限制,重建目标机并验证兼容 Chat 与完整公网 MCP 工具链路 |
## 1. 当前 Checkpoint
- 名称:`fire-safety-ymd-public-chat-page-v1`
- 状态Ready for Review本地实现与验证完成;测试服务器部署待用户执行)
- 目标:提供默认关闭的 `/chat` 测试页面和 `/chat/` 规范化入口,按第三方兼容 `completion` SSE 请求,支持用户手动输入测试 `xtoken` 和同页面内存 `session_id` 复用;通过 Nginx 公网反代页面及其 CSS/JavaScript 资源,并保持 Token、真实用户认证和生产授权边界不变
- 非目标:替用户提交或推送 Git、直接修改远程服务器、创建数据库容器、迁移生产数据、签发证书、改变 DNS/安全组、实现真实用户认证、动态授权、会话持久化或生产审计。
- 名称:`fire-safety-ymd-superagent-no-trace-mode-v1`
- 状态Ready for Review无 Trace 自动化、真实 SuperAgent 探针和本地兼容 Chat 冒烟均已通过;测试服务器重建和公网完整 MCP 对话待用户执行)
- 目标:增加 `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` 配置,默认使用 `include_trace=false`,在不开放 SuperAgent Trace 策略时仍严格验证最终 AI 消息 `finish_reason=stop`、非空顶层 `message.final` 与顶层 `end`;开启 Trace 时继续要求 `run.completed(status=success)`,并保持既有 Chat/MCP 安全边界
- 非目标:替用户提交或推送 Git、直接修改远程服务器、创建数据库容器、迁移生产数据、签发证书、改变 DNS/安全组、实现真实用户认证、动态授权、会话持久化或生产审计;无 Trace 也不承诺已完成公网 MCP 多工具对话验证
当前进展:
@@ -23,6 +23,7 @@
- Nginx 示例增加 HTTP 到 HTTPS 跳转和 HTTP 429 JSON 限流响应,精确反代 `/chat``/chat/``/chat/app.css``/chat/app.js`、兼容 completion 和 `/mcp`,仍不比较、保存或注入 Chat、MCP、SuperAgent 或数据库 Secret。
- 运维手册记录 Git 前置条件、服务器目录、Secret 权限、Compose/Nginx 启停、页面开关、Chat/MCP 冒烟、SuperAgent 回调、更新和回滚。
- SuperAgent 无法在其配置中指定 MCP 协议版本;同一 SuperAgent 中 `th-hotel-simple-superagent` 已稳定启用/调用,现场实现不读取 `initialize.params.protocolVersion`,不读取或校验 `MCP-Protocol-Version` Header并固定返回 `2025-06-18`。消防 MCP 按该 proven profile 兼容:版本字段和 Header 不作为拒绝门禁,响应固定为 `2025-06-18`。这不表示支持任意其他版本,也不是追求最新协议;日志只使用 `direct_success`/`compatibility_success`,不记录原始版本值。
- SuperAgent Open API 现在由 `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` 控制消息流查询参数,默认 false。无 Trace 严格成功要求最终 AI 消息 `finish_reason=stop`、非空顶层 `message.final` 和顶层 `end`Trace=true 时仍要求 `run.completed(status=success)`。无 Trace 不返回公开工具/步骤轨迹,但不等于禁止 Agent 调 MCP。受控诊断中同一 Key 的 `include_trace=true` 因应用策略返回 HTTP 403 `open_agent_trace_disabled`;改用 false 后,真实探针返回严格成功,兼容 Chat 本地 SSE 也完成 `null -> stop`。公网完整 MCP 对话仍待验证。
已实现验收项:
@@ -40,9 +41,10 @@
3. 页面测试保持 `FIRE_SAFETY_CHAT_PAGE_ENABLED=false` 默认边界;需要开启时配置 `https://agent.nianxx.com` 精确 Origin重建容器并验证 `/chat` 308、`/chat/` 及 CSS/JavaScript 资源 200以及关闭后的直接 404。
4. reload 已通过语法检查的 Nginx 配置,只公开 HTTPS 页面/资源、兼容路径、`/mcp` 和可选 `/health`
5. 使用项目专属测试 Key 和已发布消防 Profile 做兼容 `completion` 首轮/多轮真实冒烟,核对最终回答、用量和会话复用;页面必须证明使用同一请求契约。
6. 使用 SuperAgent 无版本配置的实际握手重测:通过 `direct_success``compatibility_success` 日志确认请求已按 proven profile 处理,并验证 `initialize``notifications/initialized``tools/list` → 7 个工具的 `tools/call`;当前公网地点搜索的一次成功不替代完整链路,版本字段/Header 不需要配置,也不作为拒绝门禁
7. 为所有查询表补齐适用 GiST 索引并验证查询计划;当前小数据可做联调,但生产前必须完成索引与并发验证
8. 设计真实用户身份、动态角色/区域授权、共享会话、限流、Secret 轮换、指标和持久审计
6. 先以 `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=false` 重建并运行探针/Chat确认无 Trace 的 `finish_reason=stop` + 非空顶层 `message.final` + 顶层 `end` 严格成功;仅在外部应用策略允许时再测试 true 模式
7. 使用 SuperAgent 无版本配置的实际握手重测:通过 `direct_success``compatibility_success` 日志确认请求已按 proven profile 处理,并验证 `initialize``notifications/initialized``tools/list` → 7 个工具的 `tools/call`;当前公网地点搜索的一次成功不替代完整链路,版本字段/Header 不需要配置,也不作为拒绝门禁
8. 为所有查询表补齐适用 GiST 索引并验证查询计划;当前小数据可做联调,但生产前必须完成索引与并发验证
9. 设计真实用户身份、动态角色/区域授权、共享会话、限流、Secret 轮换、指标和持久审计。
## 3. 已确认事实
@@ -78,6 +80,7 @@
- 水源/设施 `syzt`、水源 `hc_datetime` 等字段的枚举、单位、时区和更新责任人尚未确认。
- SuperAgent MCP 的公网 URL 已有现场成功证据Bearer、`Content-Type`、JSON-RPC 通过,随后记录了 `initialize``notifications/initialized``tools/list`,并于 2026-09-05 22:35 成功调用 `fire_safety_search_place_candidates`。同一 SuperAgent 的 th-hotel 服务已稳定调用,为 proven profile 参照;消防服务其余 6 个工具、完整多工具链、TLS、网络白名单和 Token 轮换仍待联调。是否发送协议 Header 不构成兼容阻塞。
- `/api/chat` 静态 Bearer、兼容路径 `xtoken``/chat` 测试页面只适用于受控联调,浏览器用户可以看到手动输入的 Token页面默认关闭开启时要求 `FIRE_SAFETY_CHAT_ALLOWED_ORIGINS` 包含精确 `https://agent.nianxx.com`。真实用户身份、动态授权、生产速率限制和滥用防护尚未实现。Chat 短凭证仅可通过默认关闭的显式 legacy 开关在受控测试/迁移窗口使用MCP Token 仍要求至少 32 个可打印 ASCII 字符,三种凭证必须不同。
- SuperAgent Trace 策略当前不对本项目开放:同一 Key 请求 `include_trace=true` 返回 403 `open_agent_trace_disabled`;改为 `include_trace=false` 后真实探针已严格成功。无 Trace 只影响工具/步骤轨迹返回和成功判定,不禁止 Agent 调用 MCP实际消防 Profile 的无 Trace 多工具对话和工具轨迹替代证据仍待公网验收。
- 目标公网机器的 Docker/Compose 和 Nginx 版本、配置 include 层级、证书、DNS、安全组及 PostgreSQL 网络拓扑尚未完整验证;用户已开始远程部署,仓库资产与服务器现场配置仍需完成一致性核验。
- 目标服务器此前连续两次访问 `proxy.golang.org:443` 均在约 91 秒后超时,随后已通过可达的构建路径完成 Docker 镜像构建;该事实不代表所有外部 HTTPS 都可达。
- 目标机曾因已交付的 Chat 短凭证未通过默认配置门禁而反复重启;`801c0af` 已包含显式兼容方案。最新公网 `/mcp` 请求已完成地点候选工具调用,说明该启动阻塞已不再是当前首要问题;但独立 health、页面公网响应、兼容 Chat 多轮、MCP 其余工具和完整链路仍未形成通过证据。
@@ -111,6 +114,7 @@
- `GOCACHE=/private/tmp/fire-safety-ymd-go-cache go test -count=1 ./...`:通过;新增覆盖页面开关依赖、开启/关闭路由、HTML/静态资源、安全响应头和应用装配;原有 Chat、SuperAgent、MCP/PostGIS 覆盖继续通过。
- `GOCACHE=/private/tmp/fire-safety-ymd-go-cache go vet ./...`:通过。
- `GOCACHE=/private/tmp/fire-safety-ymd-go-cache go test -race -count=1 ./...`:通过。
- SuperAgent 无 Trace自动化覆盖 `include_trace=false` 请求参数、最终 AI 消息 `finish_reason=stop` + 非空顶层 `message.final` + 顶层 `end` 成功,以及 Trace=true 仍要求 `run.completed(status=success)`;真实同一 Key 诊断为 true=403 `open_agent_trace_disabled`false 模式下 `cmd/superagent-probe` 严格成功并返回最终回答,本地兼容 Chat SSE 也完成 `finish_reason=null -> stop`,公网完整 MCP 对话仍待验证。
- `node --check internal/handler/chatpage/app.js`:通过。
- 本地真实浏览器:首轮兼容 SSE、第二轮 `session_id` 复用、错误凭证、Nginx 风格 429、未完成 SSE 断流后清会话、恶意 HTML 纯文本显示均通过375px 视口无横向溢出4 个按钮高度均为 44px控制台无错误。
- `ruby -e 'require "yaml"; YAML.load_file("compose.yaml")'`:通过基础 YAML 解析;开发机没有 Docker CLI未执行 `docker compose config --quiet`

View File

@@ -44,6 +44,7 @@ func main() {
Enabled: cfg.SuperAgent.Enabled,
BaseURL: cfg.SuperAgent.BaseURL,
APIKey: cfg.SuperAgent.OpenAPIKey,
IncludeTrace: cfg.SuperAgent.IncludeTrace,
ConnectTimeout: cfg.SuperAgent.ConnectTimeout,
RecoveryMaxAttempts: cfg.SuperAgent.RecoveryMaxAttempts,
RecoveryInitialBackoff: cfg.SuperAgent.RecoveryInitialBackoff,

View File

@@ -51,6 +51,20 @@ Nginx 不再直接调用 DashScope。它只负责 TLS、精确公开路径、基
这样保留了当前应急辅助场景的失败语义:断流或 Provider 协议不完整时,客户端不会把半截模型文本误当成已完成方案。代价是首版没有逐字动画;如后续必须实时输出,需要独立安全决策、取消/失败 UX 和新 Spec。
### 4.1 SuperAgent Trace 模式
上游消息流的 `include_trace``FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` 控制,默认值为
`false`。两种模式都要求最终 AI 消息和顶层 `event: end`,但严格成功条件不同:
- 无 Trace最终 AI 消息的 `finish_reason=stop` + 非空顶层 `message.final` + 顶层 `end`
- Trace最终内容 + `run.completed(status=success)` + 顶层 `end`
无 Trace 只是不向调用方返回公开工具/步骤轨迹,不等于禁止 SuperAgent 执行中调用 MCPAgent
是否实际调用消防工具仍必须通过完整对话和 `/mcp` 日志验证。当前受控诊断中,应用策略关闭
Trace 时 `include_trace=true` 返回 HTTP 403、Provider code `open_agent_trace_disabled`
同一 Key 改为 `include_trace=false` 返回 HTTP 200。因此测试环境默认保持无 Trace只有在外部
应用策略显式启用 `trace_policy.enabled=true` 后才可切换为 Trace 模式。
测试页面使用浏览器 `fetch` 发起 POST以便同时设置 `xtoken` 并读取 SSE它不会把 Token
写进 HTML、Cookie、localStorage、sessionStorage、URL 或服务端配置。用户每次打开页面都要手动
输入测试 `xtoken`,页面只在本次页面生命周期内使用该值。页面可展示用户输入的问题和最终回答,
@@ -79,4 +93,5 @@ Nginx 不再直接调用 DashScope。它只负责 TLS、精确公开路径、基
- [`chat-api-v1.md`](chat-api-v1.md)
- [`../workflows/user-chat.md`](../workflows/user-chat.md)
- [`../project/operations/nginx-public-entry.md`](../project/operations/nginx-public-entry.md)
- [`../project/integrations/superagent-openapi.md`](../project/integrations/superagent-openapi.md)
- [`../specs/fire-safety-ymd-chat-page-v1.md`](../specs/fire-safety-ymd-chat-page-v1.md)

View File

@@ -13,7 +13,7 @@
| [`ai-nses-project-overlay.md`](ai-nses-project-overlay.md) | fire-safety-ymd 对通用标准的项目覆盖规则 | 低 |
| [`backend-development-guidelines.md`](backend-development-guidelines.md) | Go 后端工程规范 | 低 |
| [`security-access-control-boundary.md`](security-access-control-boundary.md) | AI、MCP、数据库、身份和敏感数据边界 | 中 |
| [`integrations/superagent-openapi.md`](integrations/superagent-openapi.md) | SuperAgent Open API、Session、SSE 恢复与探针说明 | 中 |
| [`integrations/superagent-openapi.md`](integrations/superagent-openapi.md) | SuperAgent Open API、Session、无 Trace/Trace SSE恢复与探针说明 | 中 |
| [`integrations/superagent-mcp-spatial.md`](integrations/superagent-mcp-spatial.md) | SuperAgent 空间只读 MCP、配置、工具和联调门禁 | 高 |
| [`operations/postgis-srid-4326.md`](operations/postgis-srid-4326.md) | 已确认 WGS84 数据的 SRID 元数据迁移、验证与权限边界 | 高 |
| [`operations/nginx-public-entry.md`](operations/nginx-public-entry.md) | `agent.nianxx.com` 到 Go 页面、对话/MCP 的 HTTPS 反向代理示例 | 高 |

View File

@@ -4,7 +4,7 @@
| --- | --- |
| 项目 | `fire-safety-ymd` |
| 协议基线 | TH Hotel 仓库保存的 2026-07-12 Open Agent API 资料 |
| 当前状态 | Go Adapter、默认关闭的原生用户对话 API 与可选兼容入口已完成模拟联调;上线前必须与当前 SuperAgent 环境重新联调 |
| 当前状态 | Go Adapter、默认关闭的原生用户对话 API 与可选兼容入口已完成模拟联调、真实无 Trace CLI 探针和本地兼容 Chat 冒烟均已通过,测试服务器重建及公网完整多工具链仍待验收 |
## 1. 项目调用边界
@@ -37,10 +37,23 @@ Open API 请求不直接提交 `profile_id`。平台通过 `df_open_*` 外部应
## 3. Open API 流程
1. 创建 Session`POST /api/open/agent-sessions`
2. 流式发送消息:`POST /api/open/agent-sessions/{session_id}/messages/stream?include_trace=true`
2. `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` 选择流式发送消息:
`POST /api/open/agent-sessions/{session_id}/messages/stream?include_trace=false`
(默认)或 `include_trace=true`
3. 初始流断开时查询 `GET {Content-Location}`
4. 使用 `GET {Content-Location}/events``Last-Event-ID` 恢复。
5. 只有最终内容、成功 `run.completed` 和顶层 `end` 同时存在时返回成功。
5. 两种模式都要求最终 AI 消息和顶层 `end`;无 Trace 模式还必须收到非空顶层
`message.final`,且最终 AI 消息必须带 `finish_reason=stop`Trace 模式还必须收到
`run.completed``status=success`
严格成功条件按模式区分:
- `include_trace=false``AI message.finish_reason=stop` + 非空顶层 `event: message.final` + 顶层 `event: end`
- `include_trace=true`:最终内容 + `run.completed(status=success)` + 顶层 `event: end`
- 任一模式收到顶层 `error``run.failed`、断流或协议不完整,都不返回部分答案。
无 Trace 只是不向调用方返回公开工具/步骤轨迹,不等于禁止 SuperAgent 在执行过程中调用
已配置的 MCP 工具;工具是否实际调用仍需通过完整对话和 MCP 日志验证。
如果未来需要主动取消,平台资料中的接口为:
@@ -58,9 +71,15 @@ POST /api/open/agent-sessions/{session_id}/runs/{run_id}/cancel
- 外部应用 Key 已绑定目标 Profile。
- 至少具备 `agent_sessions:create``agent_sessions:message``agent_sessions:read` scope。
- 后续需要取消 Run 时增加 `agent_sessions:cancel`
- 需要公开 Trace 时,应用策略的 `trace_policy.enabled=true`
- 默认无 Trace 时不要求开放 Trace 策略;如果设置 `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=true`
应用策略必须显式设置 `trace_policy.enabled=true`
- 工具输入、输出和步骤信息默认只暴露 summary不开放模型原始思考过程。
2026-09-06 的受控诊断使用同一个外部应用 Key`include_trace=true` 因应用策略关闭 Trace
返回 HTTP 403Provider code 为 `open_agent_trace_disabled`;改为 `include_trace=false`
后返回 HTTP 200。该结果说明默认无 Trace 是当前策略下的兼容运行模式;它不代表 Agent 不能
调用 MCP公网完整消防对话和多工具调用仍待验证。
## 5. 本地配置
复制 `.env.example` 中的占位配置到本地 Secret 管理方式,不提交真实值。
@@ -71,6 +90,9 @@ POST /api/open/agent-sessions/{session_id}/runs/{run_id}/cancel
FIRE_SAFETY_SUPERAGENT_ENABLED=true
FIRE_SAFETY_SUPERAGENT_BASE_URL=https://<superagent-domain>
FIRE_SAFETY_SUPERAGENT_OPEN_API_KEY=<secret>
# Default false: require AI finish_reason=stop, non-empty message.final, and end.
# Set true only if the external app policy enables trace_policy.enabled.
FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=false
FIRE_SAFETY_CHAT_ENABLED=true
FIRE_SAFETY_CHAT_AUTH_TOKEN=<another-secret-at-least-32-printable-ascii-characters>
@@ -91,6 +113,10 @@ go run ./cmd/superagent-probe
```
探针只发送代码内固定的无敏感信息消息,输出最终回答和必要的安全元数据。它不启动业务聊天、不查询消防数据库、不写业务状态。
探针遵循 `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE`:默认无 Trace 时按
`finish_reason=stop`、非空顶层 `message.final` 和顶层 `end` 判定成功;启用 Trace 时还要求
`run.completed(status=success)`
无 Trace 不会返回工具轨迹,但不等于禁止 Agent 调 MCP。
探针不会自动读取 TH Hotel 的 `DEERFLOW_*` 或其他项目变量。至少需要显式设置本项目的启用开关、Base URL 和 Open API Key未配置时命令会在任何网络请求前退出。
@@ -123,6 +149,10 @@ Open API 与 MCP 是两条独立连接:
- 用户聊天 HTTP API 和单进程并发 Run 控制已实现,但默认关闭,只有静态测试 Bearer没有最终用户认证或动态授权。
- 本地会话未持久化;重启、多实例切换和上游失败后不能恢复旧 `conversation_id`,也尚无主动取消。
- MCP/PostGIS 已完成本地实库冒烟,但尚未完成 SuperAgent 到公网 MCP 的联调。
- 真实 Profile、Key、scope、Trace 策略和网络连通性必须在目标环境验证
- MCP/PostGIS 已完成本地实库冒烟SuperAgent 到公网 MCP 已完成一次
`fire_safety_search_place_candidates` 调用,但其余 6 个工具和完整多工具链仍待验收
- 当前测试 Profile、Key、scope 和网络已通过真实无 Trace CLI 探针;本地兼容 Chat SSE 也已完成
`finish_reason=null -> stop`。生产凭证、轮换和目标服务器重建仍待验收;
`FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=true` 因当前应用策略关闭 Trace 而返回
`open_agent_trace_disabled`,只有策略显式开启后才能验证 Trace 模式。
- Provider 协议可能在 2026-07-12 资料后变化;出现差异时更新 Spec 和契约,不在 Adapter 中静默猜测。

View File

@@ -109,6 +109,8 @@ FIRE_SAFETY_BUILD_GOPROXY=https://proxy.golang.org,direct
FIRE_SAFETY_SUPERAGENT_ENABLED=true
FIRE_SAFETY_SUPERAGENT_BASE_URL=https://<superagent-api-host>
FIRE_SAFETY_SUPERAGENT_OPEN_API_KEY=<superagent-open-api-key>
# 当前外部应用策略关闭 Trace 时保持 false只有策略显式允许 Trace 才设置 true
FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=false
FIRE_SAFETY_CHAT_ENABLED=true
FIRE_SAFETY_CHAT_AUTH_TOKEN=<独立的高熵xtoken>
@@ -141,6 +143,10 @@ FIRE_SAFETY_POSTGIS_EXPECTED_SRID=4326
- FIRE_SAFETY_CHAT_PAGE_ENABLED 默认必须为 false。开启后 Go 提供 `/chat``/chat/` 以及同源的
`/chat/app.css``/chat/app.js``/chat` 会 308 到 `/chat/`,页面和资源的最终可用性仍由 Go
开关决定。页面只用于受控测试,不嵌入或持久化 Token。
- FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE 默认必须为 false。无 Trace 的严格成功条件是最终 AI 消息
`finish_reason=stop`、非空顶层 `event: message.final` 加顶层 `event: end`;设置为 true 时还要求
`run.completed(status=success)`,且外部应用策略必须开启 `trace_policy.enabled=true`。无 Trace
只是不返回公开工具/步骤轨迹,不等于禁止 SuperAgent 调用 MCP。
- 页面开启时 FIRE_SAFETY_CHAT_ALLOWED_ORIGINS 必须包含精确的 `https://agent.nianxx.com`,并且
Chat 兼容 App ID、Chat Token 和 SuperAgent 配置已经可用;该静态页面不是最终用户认证。
- 使用 FIRE_SAFETY_MCP_SCOPE_MODE=all 时FIRE_SAFETY_MCP_ALLOWED_TOWNS 必须保持为空。all 是服务账号级的固定查询表范围,不是最终用户级授权。
@@ -148,6 +154,10 @@ FIRE_SAFETY_POSTGIS_EXPECTED_SRID=4326
- FIRE_SAFETY_HTTP_ADDR 在容器内应为 :8080安全边界由 Compose 的 127.0.0.1:16587:8080 和宿主机 Nginx 提供;不要在 Compose 场景设为容器内的 127.0.0.1:8080。
- 不要用没有 --quiet 的 docker compose config 或 docker inspect 把完整环境渲染到终端、CI 日志或工单;检查 .env 权限仍为 600。
- 修改 `.env` 后必须重新创建容器(例如 `docker compose up -d --force-recreate --no-build`)才能加载新的 Chat 兼容开关;单独 `docker compose restart` 不会重新读取容器环境。旧短凭证仅用于受控测试/迁移,完成轮换后把开关改回 `false` 并再次 recreate。
- 修改 `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` 或其他 SuperAgent 代码/配置后,至少执行
`docker compose config --quiet``docker compose build --pull`
`docker compose up -d --force-recreate --remove-orphans`;仅 `docker compose restart` 不会
重新读取 `.env`,也不会把新代码构建进镜像。
## 4. 容器访问现有 PostgreSQL/PostGIS
@@ -381,7 +391,43 @@ curl -i https://agent.nianxx.com/chat/
版本导致故障,回到维护者指定的已验证 Git revision 或 root-only Nginx 备份,先 `nginx -t`
再 reload不要恢复旧的全路径 DashScope 代理、关闭 Go 鉴权或把 Secret 写入 Nginx。
## 9. SuperAgent MCP 回调配置与冒烟
## 9. SuperAgent Open API 无 Trace 模式
当前测试环境优先使用无 Trace 模式,避免外部应用策略未开放公开 Trace 导致消息流返回
`HTTP 403` / `open_agent_trace_disabled`。在服务器 `.env` 中确认:
~~~text
FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=false
~~~
无 Trace 模式仍严格要求上游最终 AI 消息的 `finish_reason=stop`、非空顶层
`event: message.final` 和顶层 `event: end`;最终回答只取 `message.final.text`。它只是不向调用方
返回公开工具/步骤轨迹,不等于禁止 SuperAgent 在执行中调用已配置的 MCP 工具。确认 MCP 是否
实际被调用,必须检查 `/mcp` 日志和返回结果。只有外部应用策略显式开启
`trace_policy.enabled=true` 时,才把该值改为 `true`Trace 模式会额外要求
`run.completed(status=success)`
修改 `.env` 后必须重新构建并重新创建服务,使配置在运行容器中生效:
~~~bash
cd /home/firee-safety-ymd
docker compose config --quiet
docker compose build --pull
docker compose up -d --force-recreate --remove-orphans
docker compose ps
docker compose logs --tail=100 api
curl --fail http://127.0.0.1:16587/health
~~~
不要在日志或工单中粘贴 Open API Key。若仍收到 403只记录 HTTP 状态、脱敏后的 Provider code、
时间和请求关联 ID交由 SuperAgent 平台核对外部应用策略;不要反复轮换无关的 Chat/MCP 凭证。
受控诊断已经确认:同一 Key 的 `include_trace=true` 因应用策略返回 403
`open_agent_trace_disabled`;同一 Key 的 `include_trace=false` 已由真实探针完成严格成功判定,
本地兼容 Chat SSE 也完成 `finish_reason=null -> stop`。公网完整消防对话、MCP 实际工具调用和
多轮会话仍需单独验收。
## 10. SuperAgent MCP 回调配置与冒烟
在 SuperAgent 的工具/MCP 配置中新增远程 MCP 服务:
@@ -454,7 +500,7 @@ curl --fail \
unset MCP_BEARER
~~~
## 10. 更新、重启与回滚
## 11. 更新、重启与回滚
### 发布新版本
@@ -515,7 +561,7 @@ cd /home/firee-safety-ymd
docker compose down
~~~
## 11. 完成判定与当前限制
## 12. 完成判定与当前限制
本手册完成不代表公网部署已完成。现场交付至少应记录:
@@ -525,6 +571,10 @@ docker compose down
- Chat 首轮/多轮 SSE、错误凭证、未知 app/path 的实际 HTTPS 响应。
- 页面浏览器验收证明用户手动输入 xtoken、Token 不被页面持久化、兼容 completion SSE 及同页面 `session_id` 复用;这只是静态测试页面,不是生产认证。
- 若为已交付旧客户端临时开启 Chat legacy 短凭证兼容,应记录受控迁移窗口,确认启动 warning 不含 Secret并在凭证轮换后恢复 `FIRE_SAFETY_CHAT_ALLOW_LEGACY_SHORT_TOKEN=false`
- SuperAgent Open API 使用 `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=false` 时,验证最终 AI 消息
`finish_reason=stop`、非空顶层 `message.final` 加顶层 `end`;若切换为 true验证外部应用
Trace 策略已开启且 `run.completed(status=success)` 仍为必要条件。无 Trace 不等同于 MCP
禁用;完整消防对话和 MCP 工具调用仍须单独记录。
- SuperAgent -> `/mcp` 的独立 Bearer、TLS/网络白名单、无版本配置 initialize 兼容响应、`notifications/initialized``tools/list` 和全部 7 个受控只读 `tools/call`。现场已有地点搜索一次成功证据,但其余工具未验收;版本字段/Header 不作为配置或拒绝门禁;公网请求到达但没有完成这条调用链,不算完成。
- 数据库未公开 5432运行账号保持只读PostGIS readiness warning 和 35 条无效面几何缺口已记录。

View File

@@ -12,13 +12,14 @@
fire-safety-ymd 需要由 Go 后端调用既有 SuperAgent 平台,并在后续让 SuperAgent 通过本项目 MCP 工具查询 PostgreSQL/PostGIS 消防数据。
TH Hotel 项目已经验证了“创建 Open Agent Session、流式发送消息、解析公开 Trace、严格判断完成状态和断流恢复”的调用形态。本 checkpoint 只迁移协议经验和安全边界,使用 Go 独立实现,不复制 Java、酒店业务、AgentBus、邮件、OSS 或任务结果写入逻辑。
TH Hotel 项目已经验证了“创建 Open Agent Session、流式发送消息、解析公开 Trace、严格判断完成状态和断流恢复”的调用形态。本 checkpoint 只迁移协议经验和安全边界,使用 Go 独立实现,不复制 Java、酒店业务、AgentBus、邮件、OSS 或任务结果写入逻辑。当前 SuperAgent 外部应用策略关闭了 Trace同一 Key 使用 `include_trace=true` 返回 HTTP 403、Provider code 为 `open_agent_trace_disabled`,使用 `include_trace=false` 返回 HTTP 200因此本项目默认采用无 Trace 模式,同时保留显式 Trace 开关。
## 2. 目标
- 建立独立、可测试的 Go SuperAgent Open API Adapter。
- 分离 `CreateSession``StreamMessage`,为后续一个本地对话复用一个 SuperAgent Session 做准备。
- 使用严格 SSE 成功条件,拒绝部分回答和不完整协议结果。
- 通过 `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` 选择无 Trace 或公开 Trace两种模式均不返回部分答案。
- 初始 SSE 断流后通过既有 Run 恢复,不重新发送原始消息。
- 提供默认关闭的 CLI 连通性探针,只发送固定无敏感信息消息。
- 所有自动化测试使用本地模拟 Provider不需要真实 Secret 或网络。
@@ -43,6 +44,7 @@ TH Hotel 项目已经验证了“创建 Open Agent Session、流式发送消息
- 目标与非目标已确认:是。
- 协议基线已确认:以 TH Hotel 仓库保存的 2026-07-12 SuperAgent Open API 文档和当前实现为输入,真实环境上线前重新验证。
- 权限和安全边界已确认Secret 仅由本项目环境变量注入;没有用户业务数据进入探针。
- Trace 策略已确认:默认无 Trace如需 `include_trace=true`,外部应用必须允许 `trace_policy.enabled=true`
- 外部依赖已确认:实现只使用 Go 标准库。
- 未确认问题已列出Profile、外部应用、scope、真实 Base URL 和 API Key 均由平台管理员后续提供。
@@ -65,10 +67,12 @@ POST /api/open/agent-sessions
### 6.2 流式发送消息
```text
POST /api/open/agent-sessions/{session_id}/messages/stream?include_trace=true
POST /api/open/agent-sessions/{session_id}/messages/stream?include_trace=false
```
请求包含 `message`、稳定 `idempotency_key` 和安全 `metadata`。Session ID 由上层明确传入Adapter 不在每条消息前隐式创建新 Session。
`include_trace``FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` 控制,默认值为 `false`;只有外部应用
策略明确开启 Trace 时才设置为 `true`。请求包含 `message`、稳定 `idempotency_key` 和安全
`metadata`。Session ID 由上层明确传入Adapter 不在每条消息前隐式创建新 Session。
### 6.3 鉴权与关联 Header
@@ -79,12 +83,18 @@ POST /api/open/agent-sessions/{session_id}/messages/stream?include_trace=true
## 7. SSE 成功与恢复规则
一次调用只有同时满足以下条件才成功:
一次调用只有满足对应模式的严格条件才成功:
1. 收到最终内容;优先使用 `message.final`,兼容累计 `message.delta`历史 `messages`/`values` AI 消息
2. 收到 `run.completed``status=success`
3. 收到顶层 `event: end`
4. 没有收到顶层 `error``run.failed`
- 无 Trace`include_trace=false`):收到历史 `messages`/`values` 中最终 AI 消息,且其
`response_metadata.finish_reason=stop`,同时收到非空顶层 `event: message.final` 和顶层
`event: end`;最终回答只采用 `message.final.text`
- Trace`include_trace=true`):收到最终内容(优先 `message.final`,兼容累计
`message.delta` 和历史 `messages`/`values` AI 消息)、`run.completed``status=success`
并收到顶层 `event: end`
- 两种模式都不能收到顶层 `error``run.failed`;断流、缺少必要条件或协议错误不得返回部分答案。
无 Trace 不向调用方返回公开工具/步骤轨迹,但只控制 Trace 暴露和完成判定,不禁止 SuperAgent
在执行中调用已配置的 MCP 工具。是否实际调用工具仍须使用完整对话和 MCP 服务日志验收。
解析器必须支持 `event:`、多行 `data:``id:`、心跳注释和空行分帧,并按 SSE event ID 去重。
@@ -104,6 +114,7 @@ POST /api/open/agent-sessions/{session_id}/messages/stream?include_trace=true
| `FIRE_SAFETY_SUPERAGENT_ENABLED` | `false` | 总开关,默认不调用真实 Provider |
| `FIRE_SAFETY_SUPERAGENT_BASE_URL` | 空 | SuperAgent Open API Base URL |
| `FIRE_SAFETY_SUPERAGENT_OPEN_API_KEY` | 空 | Secret启用时必填 |
| `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` | `false` | 是否请求公开 Trace`true` 要求外部应用策略开启 `trace_policy.enabled` |
| `FIRE_SAFETY_SUPERAGENT_CONNECT_TIMEOUT` | `15s` | 建连超时 |
| `FIRE_SAFETY_SUPERAGENT_RECOVERY_MAX_ATTEMPTS` | `5` | SSE 恢复最大次数;最大可配置为 20 |
| `FIRE_SAFETY_SUPERAGENT_RECOVERY_INITIAL_BACKOFF` | `250ms` | 首次恢复退避 |
@@ -128,16 +139,17 @@ POST /api/open/agent-sessions/{session_id}/messages/stream?include_trace=true
| --- | --- | --- | --- | --- |
| 环境配置与安全默认值 | Done | Passed | 本文第 8 节 | Implemented |
| CreateSession | Done | Passed | 本文第 6.1 节 | Implemented |
| StreamMessage 与严格成功条件 | Done | Passed | 本文第 6.2、7 节 | Implemented |
| StreamMessage 与按模式严格成功条件 | Done | Passed | 本文第 6.2、7 节 | Implemented |
| SSE 断流恢复 | Done | Passed | 本文第 7 节 | Implemented |
| CLI 探针 | Done | Compile passedlive test pending | 项目集成指南 | Implemented |
| CLI 探针 | Done | Automated passed2026-09-06 live no-Trace passed | 项目集成指南 | Verified |
## 11. 验收标准
- Given 未启用 SuperAgentWhen 执行真实调用Then 返回受控禁用错误且不发起网络请求。
- Given 配置完整When 创建 SessionThen请求具备鉴权、幂等、关联和 CSRF Header且能解析 Session ID。
- Given完整 SSEWhen 同时收到最终内容、成功完成和 `end`Then 返回最终回答与安全元数据
- Given SSE 缺少任一成功条件When 无法恢复Then 返回协议错误且不返回部分回答
- Given `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=false` 且收到 `finish_reason=stop`、非空顶层 `message.final``end`Then 返回 `message.final.text` 和安全元数据,不要求 `run.completed`
- Given `FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE=true` 且同时收到最终内容、成功完成和 `end`Then 返回最终回答与安全元数据
- Given 对应模式缺少任一必要成功条件When 无法恢复Then 返回协议错误且不返回部分回答。
- Given 初始 SSE 提前结束且存在 Run URLWhen 恢复Then只 GET Run/events、携带 `Last-Event-ID`,消息 POST 次数仍为 1。
- Given未设置真实环境变量When 运行全部测试Then 不访问外网且测试通过。
@@ -145,7 +157,8 @@ POST /api/open/agent-sessions/{session_id}/messages/stream?include_trace=true
- 配置默认值、合法值和错误值。
- Session 请求路径、Header、CSRF、请求体和响应解析。
- 当前 Trace SSE、历史 messages/values 兼容、事件 ID 去重和多行 data
- Trace 历史 messages/values 最终 AI 消息、`finish_reason=stop`、非空顶层 `message.final` 和顶层 `end`
- Trace SSE 的最终内容、`run.completed(status=success)`、历史 messages/values 兼容、事件 ID 去重和多行 data。
- 缺少 final/completed/end、顶层 error、run.failed 和非法 JSON。
- 提前 EOF 恢复、Last-Event-ID、同源 URL 和不重复 POST。
- HTTP 非 2xx、超大控制响应和 context 取消。
@@ -156,4 +169,5 @@ POST /api/open/agent-sessions/{session_id}/messages/stream?include_trace=true
- `gofmt``go test ./...``go vet ./...` 通过。
- 没有真实 Secret、用户数据、构建产物或无关用户变更。
- 项目索引、集成指南、安全边界和 `PROJECT_STATE.md` 已同步。
- 默认无 Trace 能在不开放 `trace_policy` 时完成严格成功判定Trace 模式仍保留 `run.completed` 成功门禁。
- 真实环境未配置时明确说明未做 live connectivity test。

View File

@@ -42,6 +42,13 @@ Network 应显示同源 POST 到精确 app ID 的 completion 路径Request He
发送第二个问题时,应在第二次请求中看到上一轮返回的 `session_id` 被放入 `input.session_id`。不要把初始 `finish_reason=null`
断流内容当作最终答案。
页面客户端看到的成功契约不随上游 Trace 开关变化,始终以兼容流最后的
`output.finish_reason=stop` 为准。服务端到 SuperAgent 的严格判定由
`FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` 控制:默认 `false` 时要求上游最终 AI 消息
`finish_reason=stop`、非空顶层 `message.final` 和顶层 `event: end`;设置为 `true` 时还要求
`run.completed(status=success)`。无 Trace 只是不返回工具/步骤轨迹,不代表 SuperAgent 不会
调用 MCP要确认 MCP 是否实际执行,必须同时检查服务日志和工具结果。
## 3. 后续对话
后续请求同时发送 `message` 和前一轮保存的 `conversation_id`。成功流中的 `conversation` 事件返回 `reused=true`,说明复用了原 SuperAgent Session 上下文。
@@ -56,6 +63,12 @@ Network 应显示同源 POST 到精确 app ID 的 completion 路径Request He
- 上游超时、协议错误、Run 失败或客户端中途断开:当前实现会使会话映射失效,以免继续复用可能仍有活动 Run 的 Provider Session。
- 网络断开且没有看到 `done`:结果未知;首版不自动重放原消息,避免重复 Run。
如果上游返回 `open_agent_trace_disabled` 或 HTTP 403先检查是否将
`FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE` 设置为 `true`。当前应用策略关闭 Trace 时,使用
`include_trace=true` 会被拒绝;同一 Key 使用默认的 `include_trace=false` 可以返回 HTTP 200。
只有在外部应用策略明确启用 `trace_policy.enabled=true` 后,才应切换为 `true`。修改环境变量后
必须重建或重新创建运行容器,不能只依赖 `docker compose restart` 重新读取配置。
## 5. curl 联调
先把 `.env` 显式加载到当前 shell再启动服务Go 程序不会自动读取 `.env`

View File

@@ -60,6 +60,7 @@ func New(ctx context.Context, cfg config.Config) (*Application, error) {
Enabled: cfg.SuperAgent.Enabled,
BaseURL: cfg.SuperAgent.BaseURL,
APIKey: cfg.SuperAgent.OpenAPIKey,
IncludeTrace: cfg.SuperAgent.IncludeTrace,
ConnectTimeout: cfg.SuperAgent.ConnectTimeout,
RecoveryMaxAttempts: cfg.SuperAgent.RecoveryMaxAttempts,
RecoveryInitialBackoff: cfg.SuperAgent.RecoveryInitialBackoff,

View File

@@ -115,6 +115,7 @@ func TestNewWiresChatAPIToSuperAgent(t *testing.T) {
Enabled: true,
BaseURL: provider.URL,
OpenAPIKey: "provider-open-api-key",
IncludeTrace: true,
ConnectTimeout: time.Second,
RecoveryMaxAttempts: 1,
RecoveryInitialBackoff: time.Millisecond,

View File

@@ -18,6 +18,7 @@ const (
SuperAgentEnabledEnv = "FIRE_SAFETY_SUPERAGENT_ENABLED"
SuperAgentBaseURLEnv = "FIRE_SAFETY_SUPERAGENT_BASE_URL"
SuperAgentOpenAPIKeyEnv = "FIRE_SAFETY_SUPERAGENT_OPEN_API_KEY"
SuperAgentIncludeTraceEnv = "FIRE_SAFETY_SUPERAGENT_INCLUDE_TRACE"
SuperAgentConnectTimeoutEnv = "FIRE_SAFETY_SUPERAGENT_CONNECT_TIMEOUT"
SuperAgentRecoveryMaxAttemptsEnv = "FIRE_SAFETY_SUPERAGENT_RECOVERY_MAX_ATTEMPTS"
SuperAgentRecoveryInitialBackoffEnv = "FIRE_SAFETY_SUPERAGENT_RECOVERY_INITIAL_BACKOFF"
@@ -107,6 +108,7 @@ type SuperAgentConfig struct {
Enabled bool
BaseURL string
OpenAPIKey string
IncludeTrace bool
ConnectTimeout time.Duration
RecoveryMaxAttempts int
RecoveryInitialBackoff time.Duration
@@ -163,6 +165,10 @@ func Load() (Config, error) {
if err != nil {
return Config{}, err
}
includeTrace, err := parseBool(SuperAgentIncludeTraceEnv, false)
if err != nil {
return Config{}, err
}
connectTimeout, err := parseDuration(SuperAgentConnectTimeoutEnv, defaultSuperAgentConnectTimeout)
if err != nil {
return Config{}, err
@@ -188,6 +194,7 @@ func Load() (Config, error) {
Enabled: enabled,
BaseURL: strings.TrimRight(strings.TrimSpace(os.Getenv(SuperAgentBaseURLEnv)), "/"),
OpenAPIKey: strings.TrimSpace(os.Getenv(SuperAgentOpenAPIKeyEnv)),
IncludeTrace: includeTrace,
ConnectTimeout: connectTimeout,
RecoveryMaxAttempts: recoveryMaxAttempts,
RecoveryInitialBackoff: recoveryInitialBackoff,

View File

@@ -20,6 +20,9 @@ func TestLoadDefaults(t *testing.T) {
if cfg.SuperAgent.Enabled {
t.Fatal("SuperAgent.Enabled = true, want false")
}
if cfg.SuperAgent.IncludeTrace {
t.Fatal("SuperAgent.IncludeTrace = true, want false")
}
if cfg.SuperAgent.ConnectTimeout != 15*time.Second {
t.Fatalf("ConnectTimeout = %v, want 15s", cfg.SuperAgent.ConnectTimeout)
}
@@ -169,6 +172,7 @@ func TestLoadConfiguredValues(t *testing.T) {
t.Setenv(SuperAgentEnabledEnv, "true")
t.Setenv(SuperAgentBaseURLEnv, " https://superagent.example.test/root/ ")
t.Setenv(SuperAgentOpenAPIKeyEnv, " test-open-api-key ")
t.Setenv(SuperAgentIncludeTraceEnv, "true")
t.Setenv(SuperAgentConnectTimeoutEnv, "3s")
t.Setenv(SuperAgentRecoveryMaxAttemptsEnv, "2")
t.Setenv(SuperAgentRecoveryInitialBackoffEnv, "20ms")
@@ -189,6 +193,9 @@ func TestLoadConfiguredValues(t *testing.T) {
if cfg.SuperAgent.OpenAPIKey != "test-open-api-key" {
t.Fatal("OpenAPIKey was not loaded")
}
if !cfg.SuperAgent.IncludeTrace {
t.Fatal("IncludeTrace = false, want true")
}
if cfg.SuperAgent.ConnectTimeout != 3*time.Second || cfg.SuperAgent.RecoveryMaxAttempts != 2 || cfg.SuperAgent.RecoveryInitialBackoff != 20*time.Millisecond {
t.Fatalf("unexpected recovery config: %#v", cfg.SuperAgent)
}
@@ -204,6 +211,7 @@ func TestLoadRejectsInvalidValues(t *testing.T) {
value string
}{
{name: "boolean", key: SuperAgentEnabledEnv, value: "sometimes"},
{name: "include trace boolean", key: SuperAgentIncludeTraceEnv, value: "sometimes"},
{name: "connect timeout syntax", key: SuperAgentConnectTimeoutEnv, value: "soon"},
{name: "connect timeout non-positive", key: SuperAgentConnectTimeoutEnv, value: "0s"},
{name: "recovery attempts syntax", key: SuperAgentRecoveryMaxAttemptsEnv, value: "many"},
@@ -574,6 +582,7 @@ func clearEnvironment(t *testing.T) {
SuperAgentEnabledEnv,
SuperAgentBaseURLEnv,
SuperAgentOpenAPIKeyEnv,
SuperAgentIncludeTraceEnv,
SuperAgentConnectTimeoutEnv,
SuperAgentRecoveryMaxAttemptsEnv,
SuperAgentRecoveryInitialBackoffEnv,

View File

@@ -13,6 +13,7 @@ import (
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
@@ -34,6 +35,7 @@ type HTTPClient struct {
enabled bool
baseURL *url.URL
apiKey string
includeTrace bool
httpClient *http.Client
recoveryMaxAttempts int
recoveryInitialBackoff time.Duration
@@ -88,9 +90,10 @@ func NewHTTPClient(cfg Config) (*HTTPClient, error) {
}).DialContext
return &HTTPClient{
enabled: true,
baseURL: baseURL,
apiKey: cfg.APIKey,
enabled: true,
baseURL: baseURL,
apiKey: cfg.APIKey,
includeTrace: cfg.IncludeTrace,
httpClient: &http.Client{
Transport: transport,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
@@ -198,7 +201,9 @@ func (c *HTTPClient) StreamMessage(
}
streamURL := c.endpoint("/api/open/agent-sessions/" + request.SessionID + "/messages/stream")
streamURL.RawQuery = "include_trace=true"
streamQuery := streamURL.Query()
streamQuery.Set("include_trace", strconv.FormatBool(c.includeTrace))
streamURL.RawQuery = streamQuery.Encode()
httpRequest, err := c.newRequest(
ctx,
http.MethodPost,
@@ -229,7 +234,7 @@ func (c *HTTPClient) StreamMessage(
return Result{}, err
}
state := newStreamState(request.SessionID)
state := newStreamState(request.SessionID, c.includeTrace)
if contentLocation := strings.TrimSpace(response.Header.Get("Content-Location")); contentLocation != "" {
runURL, locationErr := c.resolveRunURL(httpRequest.URL, request.SessionID, contentLocation)
if locationErr != nil {

View File

@@ -105,6 +105,39 @@ func TestHTTPClientCreatesSessionAndStreamsMessage(t *testing.T) {
}
}
func TestHTTPClientStreamsWithoutTraceWhenDisabled(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if got := request.URL.Query().Get("include_trace"); got != "false" {
t.Errorf("include_trace = %q, want false", got)
}
writer.Header().Set("Content-Type", "text/event-stream")
fmt.Fprint(writer, "event: messages\ndata: [{\"type\":\"AIMessageChunk\",\"content\":\"\",\"response_metadata\":{\"finish_reason\":\"stop\"}}]\n\n")
fmt.Fprint(writer, "event: message.final\ndata: {\"run_id\":\"run-no-trace\",\"text\":\"no-trace answer\"}\n\n")
fmt.Fprint(writer, "event: end\n\n")
}))
defer server.Close()
client, err := NewHTTPClient(Config{
Enabled: true,
BaseURL: server.URL,
APIKey: testAPIKey,
IncludeTrace: false,
ConnectTimeout: time.Second,
RecoveryInitialBackoff: time.Millisecond,
MaxMessageBytes: 1024,
})
if err != nil {
t.Fatalf("NewHTTPClient() error = %v", err)
}
result, err := client.StreamMessage(context.Background(), validMessageRequest(), nil)
if err != nil {
t.Fatalf("StreamMessage() error = %v", err)
}
if result.Answer != "no-trace answer" {
t.Fatalf("Answer = %q, want no-trace answer", result.Answer)
}
}
func TestHTTPClientRecoversWithoutRepostingMessage(t *testing.T) {
var messagePosts atomic.Int32
var runQueries atomic.Int32
@@ -405,6 +438,7 @@ func newTestHTTPClient(t *testing.T, baseURL string, recoveryAttempts int, backo
Enabled: true,
BaseURL: baseURL,
APIKey: testAPIKey,
IncludeTrace: true,
ConnectTimeout: time.Second,
RecoveryMaxAttempts: recoveryAttempts,
RecoveryInitialBackoff: backoff,

View File

@@ -28,13 +28,16 @@ var (
)
type streamState struct {
sessionID string
sessionID string
requireRunCompleted bool
endSeen bool
runCompleted bool
runID string
runURL string
lastEventID string
endSeen bool
runCompleted bool
finalAIStopSeen bool
topLevelFinalSeen bool
runID string
runURL string
lastEventID string
profileID string
profileVersionID string
@@ -56,11 +59,12 @@ type streamState struct {
streamBytes int64
}
func newStreamState(sessionID string) *streamState {
func newStreamState(sessionID string, requireRunCompleted bool) *streamState {
return &streamState{
sessionID: sessionID,
processedIDs: make(map[string]struct{}),
eventTypeSet: make(map[string]struct{}),
sessionID: sessionID,
requireRunCompleted: requireRunCompleted,
processedIDs: make(map[string]struct{}),
eventTypeSet: make(map[string]struct{}),
}
}
@@ -191,6 +195,21 @@ func (s *streamState) consumeFrame(frame sseFrame, traceHandler TraceHandler) er
return err
}
return s.consumeMessages(payload)
case "message.final":
payload, err := decodeJSONObject(data)
if err != nil {
return err
}
text := stringValue(payload, "text")
if strings.TrimSpace(text) == "" {
return nil
}
if len(text) > maxAnswerBytes {
return fmt.Errorf("%w: final answer exceeds size limit", ErrProtocol)
}
s.runID = valueOrExisting(safeValue(stringValue(payload, "run_id")), s.runID)
s.finalContent = text
s.topLevelFinalSeen = true
case "values":
payload, err := decodeJSONObject(data)
if err != nil {
@@ -277,16 +296,10 @@ func (s *streamState) consumeMessages(value any) error {
if nested, exists := current["messages"]; exists {
return s.consumeMessages(nested)
}
if stringValue(current, "type") != "ai" {
messageType := stringValue(current, "type")
if messageType != "ai" && messageType != "AIMessageChunk" {
return nil
}
content := contentText(current["content"])
if strings.TrimSpace(content) == "" {
return nil
}
if len(content) > maxAnswerBytes {
return fmt.Errorf("%w: final answer exceeds size limit", ErrProtocol)
}
responseMetadata, _ := current["response_metadata"].(map[string]any)
usageMetadata, _ := current["usage_metadata"].(map[string]any)
modelName := valueOrExisting(safeTrace(stringValue(responseMetadata, "model_name")), s.modelName)
@@ -295,10 +308,22 @@ func (s *streamState) consumeMessages(value any) error {
Output: int64Value(usageMetadata, "output_tokens"),
Total: int64Value(usageMetadata, "total_tokens"),
}
if stringValue(responseMetadata, "finish_reason") == "stop" {
s.finalContent = content
stopSeen := stringValue(responseMetadata, "finish_reason") == "stop"
if stopSeen {
s.finalAIStopSeen = true
s.modelName = modelName
s.usage = usage
}
content := contentText(current["content"])
if strings.TrimSpace(content) == "" {
return nil
}
if len(content) > maxAnswerBytes {
return fmt.Errorf("%w: final answer exceeds size limit", ErrProtocol)
}
if stopSeen {
s.finalContent = content
return nil
}
s.fallbackContent = content
@@ -315,14 +340,20 @@ func (s *streamState) result() (Result, error) {
if !s.endSeen {
return Result{}, fmt.Errorf("%w: missing end event", ErrProtocol)
}
if !s.runCompleted {
if s.requireRunCompleted && !s.runCompleted {
return Result{}, fmt.Errorf("%w: missing successful run.completed event", ErrProtocol)
}
if !s.requireRunCompleted && !s.finalAIStopSeen {
return Result{}, fmt.Errorf("%w: missing AI stop finish reason", ErrProtocol)
}
if !s.requireRunCompleted && !s.topLevelFinalSeen {
return Result{}, fmt.Errorf("%w: missing message.final event", ErrProtocol)
}
answer := s.finalContent
if strings.TrimSpace(answer) == "" {
if s.requireRunCompleted && strings.TrimSpace(answer) == "" {
answer = s.deltaContent.String()
}
if strings.TrimSpace(answer) == "" {
if s.requireRunCompleted && strings.TrimSpace(answer) == "" {
answer = s.fallbackContent
s.modelName = s.fallbackModel
s.usage = s.fallbackUsage

View File

@@ -7,7 +7,7 @@ import (
)
func TestStreamStateConsumesCurrentTraceProtocol(t *testing.T) {
state := newStreamState("session-1")
state := newStreamState("session-1", true)
var traces []TraceEvent
stream := strings.Join([]string{
"event: metadata\nid: 1\ndata: {\"run_id\":\"run-1\",\"resolved_profile_id\":\"profile-1\",\"resolved_profile_version_id\":\"version-1\"}\n",
@@ -39,7 +39,7 @@ func TestStreamStateConsumesCurrentTraceProtocol(t *testing.T) {
}
func TestStreamStateConsumesLegacyValuesAndStructuredContent(t *testing.T) {
state := newStreamState("session-legacy")
state := newStreamState("session-legacy", true)
stream := "event: values\n" +
"data: {\"messages\":[{\"type\":\"human\",\"content\":\"ignored\"},{\"type\":\"ai\",\"content\":[{\"text\":\"first\"},{\"content\":\"second\"}],\"response_metadata\":{\"finish_reason\":\"stop\",\"model_name\":\"model-1\"},\"usage_metadata\":{\"input_tokens\":2,\"output_tokens\":3,\"total_tokens\":5}}]}\n\n" +
"event: trace\ndata: {\"event\":\"run.completed\",\"status\":\"success\"}\n\n" +
@@ -60,8 +60,101 @@ func TestStreamStateConsumesLegacyValuesAndStructuredContent(t *testing.T) {
}
}
func TestStreamStateConsumesNoTraceFinalMessage(t *testing.T) {
state := newStreamState("session-no-trace", false)
stream := "event: messages\n" +
"data: [{\"type\":\"AIMessageChunk\",\"content\":\"\",\"response_metadata\":{\"finish_reason\":\"stop\",\"model_name\":\"model-1\"},\"usage_metadata\":{\"input_tokens\":2,\"output_tokens\":3,\"total_tokens\":5}}]\n\n" +
"event: message.final\n" +
"data: {\"run_id\":\"run-no-trace\",\"text\":\"No trace answer\"}\n\n" +
"event: end\n\n"
if err := state.consume(strings.NewReader(stream), nil); err != nil {
t.Fatalf("consume() error = %v", err)
}
result, err := state.result()
if err != nil {
t.Fatalf("result() error = %v", err)
}
if result.Answer != "No trace answer" || result.ModelName != "model-1" || result.RunID != "run-no-trace" {
t.Fatalf("unexpected result: %#v", result)
}
if result.Usage != (TokenUsage{Input: 2, Output: 3, Total: 5}) {
t.Fatalf("Usage = %#v", result.Usage)
}
}
func TestStreamStateNoTraceRequiresStopAndEnd(t *testing.T) {
tests := []struct {
name string
stream string
wantErr error
result bool
}{
{
name: "missing stop",
stream: "event: messages\n" +
"data: [{\"type\":\"AIMessageChunk\",\"content\":\"partial\",\"response_metadata\":{\"finish_reason\":null}}]\n\n" +
"event: message.final\ndata: {\"run_id\":\"run-1\",\"text\":\"complete\"}\n\n" +
"event: end\n\n",
wantErr: ErrProtocol,
result: true,
},
{
name: "missing message final",
stream: "event: messages\n" +
"data: [{\"type\":\"AIMessageChunk\",\"content\":\"\",\"response_metadata\":{\"finish_reason\":\"stop\"}}]\n\n" +
"event: end\n\n",
wantErr: ErrProtocol,
result: true,
},
{
name: "missing end",
stream: "event: messages\n" +
"data: [{\"type\":\"AIMessageChunk\",\"content\":\"\",\"response_metadata\":{\"finish_reason\":\"stop\"}}]\n\n" +
"event: message.final\ndata: {\"run_id\":\"run-1\",\"text\":\"complete\"}\n\n",
wantErr: ErrStreamIncomplete,
},
{
name: "top-level error",
stream: "event: error\ndata: {\"code\":\"provider_error\",\"message\":\"do not expose me\"}\n\n",
wantErr: ErrRunFailed,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := newStreamState("session-no-trace", false)
err := state.consume(strings.NewReader(tt.stream), nil)
if tt.result && err == nil {
_, err = state.result()
}
if !errors.Is(err, tt.wantErr) {
t.Fatalf("error = %v, want errors.Is(..., %v)", err, tt.wantErr)
}
if err != nil && strings.Contains(strings.ToLower(err.Error()), "do not expose") {
t.Fatalf("error leaked provider message: %v", err)
}
})
}
}
func TestStreamStateTraceStillRequiresRunCompleted(t *testing.T) {
state := newStreamState("session-trace", true)
stream := "event: values\n" +
"data: {\"messages\":[{\"type\":\"ai\",\"content\":\"complete\",\"response_metadata\":{\"finish_reason\":\"stop\"}}]}\n\n" +
"event: end\n\n"
if err := state.consume(strings.NewReader(stream), nil); err != nil {
t.Fatalf("consume() error = %v", err)
}
_, err := state.result()
if !errors.Is(err, ErrProtocol) || !strings.Contains(err.Error(), "run.completed") {
t.Fatalf("result() error = %v, want missing run.completed protocol error", err)
}
}
func TestStreamStateSupportsMultilineDataAndDeduplicatesIDs(t *testing.T) {
state := newStreamState("session-1")
state := newStreamState("session-1", true)
stream := ": heartbeat\n\n" +
"event: trace\nid: delta-1\ndata: {\"event\":\"message.delta\",\ndata: \"text\":\"A\"}\n\n" +
"event: trace\nid: delta-1\ndata: {\"event\":\"message.delta\",\"text\":\"duplicate\"}\n\n" +
@@ -128,7 +221,7 @@ func TestStreamStateRejectsIncompleteOrFailedStreams(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := newStreamState("session-1")
state := newStreamState("session-1", true)
err := state.consume(strings.NewReader(tt.stream), nil)
if tt.result && err == nil {
_, err = state.result()
@@ -144,7 +237,7 @@ func TestStreamStateRejectsIncompleteOrFailedStreams(t *testing.T) {
}
func TestTraceProjectionRedactsAndBoundsText(t *testing.T) {
state := newStreamState("session-1")
state := newStreamState("session-1", true)
secretText := "token=highly-secret " + strings.Repeat("x", maxTraceText+100)
stream := "event: trace\ndata: {\"event\":\"message.final\",\"text\":" + quotedJSON(secretText) + "}\n\n" +
"event: trace\ndata: {\"event\":\"run.completed\",\"status\":\"success\"}\n\n" +

View File

@@ -35,6 +35,7 @@ type Config struct {
Enabled bool
BaseURL string
APIKey string
IncludeTrace bool
ConnectTimeout time.Duration
RecoveryMaxAttempts int
RecoveryInitialBackoff time.Duration