feat(agent): complete closed-loop runtime and pricing fallback
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# Agent Runtime Closed Loop E2E Design
|
||||
|
||||
## 背景
|
||||
|
||||
当前 `test_agent_sse_flow.py` 不能稳定证明真实闭环:
|
||||
- `session_id` 由随机 UUID 生成,导致 `POST /api/v1/agent/runs` 经常 404。
|
||||
- 测试脚本存在不可达重复代码,诊断信息不完整。
|
||||
- 未覆盖首聊自动建会话语义,和真实聊天入口不匹配。
|
||||
|
||||
目标是验证真实环境下业务闭环是否可用:
|
||||
1. 用户请求 `agent` 路由
|
||||
2. 请求进入异步任务
|
||||
3. runtime 读取 `system_agents` 和 `llm` 配置并构建执行流程
|
||||
4. 真实 LLM 请求发出并返回
|
||||
5. `sessions`/`messages` 正确落库
|
||||
6. 成本和 token 统计正确
|
||||
7. 事件按 AG-UI 规范发布并可由 `stream_events` 订阅
|
||||
|
||||
## 设计原则
|
||||
|
||||
- 真实优先:不使用 mock,不替换 queue/redis/db/llm。
|
||||
- 双轨验证:
|
||||
- 诊断脚本用于本地排障(快速观察全链路状态)。
|
||||
- pytest E2E 用例用于可重复回归。
|
||||
- 明确前置条件:必须先使用 `infra/scripts/app.sh start` 启动 tmux 服务。
|
||||
- 本地真实 LLM 基线:DashScope Qwen。
|
||||
|
||||
## API 契约调整
|
||||
|
||||
### `POST /api/v1/agent/runs`
|
||||
|
||||
- 现状:`session_id` 必填且必须存在。
|
||||
- 新契约:`session_id` 可选。
|
||||
- 有值:复用现有会话,校验 owner。
|
||||
- 无值:在服务层先创建会话,再入队 run。
|
||||
- 响应扩展:返回 `created` 标识是否为首聊自动建会话。
|
||||
|
||||
该契约与聊天产品行为一致:用户首条消息即可开始,不需要前置调用创建会话接口。
|
||||
|
||||
## 数据关系与删除语义
|
||||
|
||||
- `messages.session_id -> sessions.id` 为外键,且硬删除级联(`ondelete=CASCADE`)。
|
||||
- 软删除需要补齐级联:
|
||||
- 软删 `sessions` 时,同事务更新对应 `messages.deleted_at`。
|
||||
- E2E 增加验证,确保软删后默认查询不可见。
|
||||
|
||||
## 测试架构
|
||||
|
||||
### A. 诊断脚本(根目录)
|
||||
|
||||
重构 `test_agent_sse_flow.py`:
|
||||
- 增加环境健康检查(web/redis/db)。
|
||||
- 支持两种模式:
|
||||
- `--new-session`:不传 `session_id`,验证首聊自动创建。
|
||||
- `--reuse-session <id>`:验证复聊路径。
|
||||
- 输出结构化阶段日志:HTTP、task_id、SSE 事件、数据库断言、失败根因。
|
||||
|
||||
### B. pytest E2E(`backend/tests/e2e`)
|
||||
|
||||
新增 `test_agent_closed_loop_live.py`:
|
||||
- 标记为 `live`,默认不在 CI 执行。
|
||||
- 用真实 JWT、真实 HTTP 请求、真实 SSE 订阅。
|
||||
- 断言最小闭环标准:
|
||||
- run 返回 202
|
||||
- SSE 至少收到 `RUN_STARTED` 与终态(`RUN_FINISHED` 或 `RUN_ERROR`)
|
||||
- `sessions` 状态和计数更新
|
||||
- `messages` 有新增记录
|
||||
- token/cost 字段非负且会话聚合一致
|
||||
|
||||
## 验收标准
|
||||
|
||||
- `uv run python test_agent_sse_flow.py --new-session` 通过。
|
||||
- `uv run pytest backend/tests/e2e/test_agent_closed_loop_live.py -v -m live` 通过。
|
||||
- 首聊场景不需要外部先建 `session_id`。
|
||||
- 软删除会话后,消息软删除行为与约束一致。
|
||||
|
||||
## 风险与回退
|
||||
|
||||
- 真实 LLM 网络抖动会造成不稳定:通过重试和超时策略降低误报。
|
||||
- 生产契约变更风险:保持字段向后兼容(原 `session_id` 仍可传)。
|
||||
- 如果新契约引入问题,可临时退回“必传 session_id”路径并保留测试脚本诊断能力。
|
||||
@@ -0,0 +1,230 @@
|
||||
# Agent Runtime Closed Loop E2E Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 让 agent 闭环在真实本地环境中可验证:`runs` 支持首聊自动建会话,并通过真实异步任务、真实 LLM、真实落库与真实 SSE 证明端到端可用。
|
||||
|
||||
**Architecture:** 在 `v1/agent` 服务层引入“可选 session_id + 自动建会话”语义;保持已有 owner 鉴权路径。重构诊断脚本并新增 live E2E 用例,统一验证 run 入队、事件流、数据库状态、成本统计与删除语义。通过最小侵入改造现有 run/resume 流程,确保兼容已存在调用。
|
||||
|
||||
**Tech Stack:** FastAPI, SQLAlchemy async, Celery, Redis Stream, LiteLLM, PyJWT, pytest, httpx
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 扩展 API 契约(session_id 可选)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/agent/schemas.py`
|
||||
- Modify: `backend/src/v1/agent/router.py`
|
||||
- Test: `backend/tests/integration/v1/agent/test_routes.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
在 `test_routes.py` 新增用例:请求体不传 `session_id` 仍返回 202,且响应含 `session_id`。
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/v1/agent/test_routes.py -k "runs and session" -v`
|
||||
Expected: FAIL,提示 `session_id` 缺失导致 422 或 mock 接口签名不匹配。
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
- `RunRequest.session_id` 改为可选。
|
||||
- `enqueue_run` 调用 service 时传可选值。
|
||||
- `TaskAcceptedResponse` 增加 `created: bool` 字段。
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/v1/agent/test_routes.py -v`
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/agent/schemas.py backend/src/v1/agent/router.py backend/tests/integration/v1/agent/test_routes.py
|
||||
git commit -m "feat: allow agent runs without pre-created session"
|
||||
```
|
||||
|
||||
### Task 2: 服务层支持自动建会话并保持鉴权
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/agent/service.py`
|
||||
- Modify: `backend/src/v1/agent/repository.py`
|
||||
- Modify: `backend/src/v1/agent/dependencies.py`
|
||||
- Test: `backend/tests/unit/v1/agent/test_service.py` (new)
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
新增单测覆盖:
|
||||
- `session_id is None` 时调用 `create_session_for_user` 并返回 `created=True`
|
||||
- `session_id 有值` 时复用并校验 owner
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest backend/tests/unit/v1/agent/test_service.py -v`
|
||||
Expected: FAIL,当前 service 无自动建会话能力。
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
- repository 增加 `create_session_for_user(user_id)`。
|
||||
- service `enqueue_run` 处理两条路径:
|
||||
- 无 `session_id`:先创建 session。
|
||||
- 有 `session_id`:校验 owner。
|
||||
- 返回 `TaskAccepted(task_id, session_id, created)`。
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest backend/tests/unit/v1/agent/test_service.py -v`
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/agent/service.py backend/src/v1/agent/repository.py backend/src/v1/agent/dependencies.py backend/tests/unit/v1/agent/test_service.py
|
||||
git commit -m "feat: auto-create chat session on first agent run"
|
||||
```
|
||||
|
||||
### Task 3: 对齐 runtime 闭环数据断言(messages/sessions/cost)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/core/agent/application/run_service.py`
|
||||
- Modify: `backend/src/core/agent/application/resume_service.py`
|
||||
- Modify: `backend/src/core/agent/infrastructure/persistence/message_repository.py`
|
||||
- Modify: `backend/src/core/agent/infrastructure/persistence/session_repository.py`
|
||||
- Test: `backend/tests/integration/core/agent/test_queue_run_resume.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
在集成测试增加断言:
|
||||
- `sessions.total_tokens`、`sessions.total_cost` 有更新
|
||||
- `messages` 的 token/cost 字段与 session 聚合一致
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/core/agent/test_queue_run_resume.py -v`
|
||||
Expected: FAIL,当前默认 token/cost 为 0,未做聚合更新。
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
- run/resume 流程接入 usage/cost 结果(来自 litellm 返回或 fallback 规则)。
|
||||
- message 写入时填充 input/output tokens 与 cost。
|
||||
- session 更新时累加 total_tokens/total_cost。
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/core/agent/test_queue_run_resume.py -v`
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/core/agent/application/run_service.py backend/src/core/agent/application/resume_service.py backend/src/core/agent/infrastructure/persistence/message_repository.py backend/src/core/agent/infrastructure/persistence/session_repository.py backend/tests/integration/core/agent/test_queue_run_resume.py
|
||||
git commit -m "feat: persist runtime token and cost aggregates"
|
||||
```
|
||||
|
||||
### Task 4: 补齐软删除级联(session -> messages)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/core/agent/infrastructure/persistence/session_repository.py`
|
||||
- Modify: `backend/src/v1/agent/service.py`
|
||||
- Test: `backend/tests/integration/core/agent/test_queue_run_resume.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
新增用例:软删 session 后,同会话 messages 的 `deleted_at` 同步写入。
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/core/agent/test_queue_run_resume.py -k soft_delete -v`
|
||||
Expected: FAIL,当前无软删级联。
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
- repository 增加 `soft_delete_session_with_messages(session_id)`。
|
||||
- service 调用时使用同事务批量更新 messages。
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/core/agent/test_queue_run_resume.py -k soft_delete -v`
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/core/agent/infrastructure/persistence/session_repository.py backend/src/v1/agent/service.py backend/tests/integration/core/agent/test_queue_run_resume.py
|
||||
git commit -m "fix: cascade soft delete from sessions to messages"
|
||||
```
|
||||
|
||||
### Task 5: 重构诊断脚本并新增 live E2E
|
||||
|
||||
**Files:**
|
||||
- Modify: `test_agent_sse_flow.py`
|
||||
- Create: `backend/tests/e2e/test_agent_closed_loop_live.py`
|
||||
- Modify: `docs/bugs/2026-03-05-agent-runtime-bugs.md`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
新增 live E2E 用例(`@pytest.mark.live`):
|
||||
- 首聊不传 `session_id` 返回 202
|
||||
- 订阅 SSE 收到关键事件
|
||||
- DB 断言 session/messages/tokens/cost
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest backend/tests/e2e/test_agent_closed_loop_live.py -m live -v`
|
||||
Expected: FAIL,当前契约或脚本未对齐。
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
- 清理脚本重复/不可达逻辑。
|
||||
- 增加健康检查、阶段化日志、超时和错误根因输出。
|
||||
- E2E 用例复用脚本中的 helper(JWT、SSE 解析、DB 断言)。
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run:
|
||||
- `uv run python test_agent_sse_flow.py --new-session`
|
||||
- `uv run pytest backend/tests/e2e/test_agent_closed_loop_live.py -m live -v`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add test_agent_sse_flow.py backend/tests/e2e/test_agent_closed_loop_live.py docs/bugs/2026-03-05-agent-runtime-bugs.md
|
||||
git commit -m "test: add live closed-loop agent e2e verification"
|
||||
```
|
||||
|
||||
### Task 6: 全量验证与文档同步
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/runtime/runtime-runbook.md`
|
||||
- Modify: `docs/runtime/runtime-route.md`
|
||||
|
||||
**Step 1: Run targeted checks**
|
||||
|
||||
Run:
|
||||
- `uv run pytest backend/tests/unit/v1/agent/test_service.py -v`
|
||||
- `uv run pytest backend/tests/integration/v1/agent/test_routes.py -v`
|
||||
- `uv run pytest backend/tests/integration/core/agent/test_queue_run_resume.py -v`
|
||||
- `uv run pytest backend/tests/e2e/test_agent_closed_loop_live.py -m live -v`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 2: Run quality gates**
|
||||
|
||||
Run:
|
||||
- `uv run ruff check backend/src backend/tests`
|
||||
- `uv run basedpyright`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 3: Update docs**
|
||||
|
||||
记录本地启动流程、真实 LLM 前置配置、live E2E 执行方式和故障排查。
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/runtime/runtime-runbook.md docs/runtime/runtime-route.md
|
||||
git commit -m "docs: document live agent closed-loop e2e workflow"
|
||||
```
|
||||
Reference in New Issue
Block a user