refactor: 简化 AgentScope 运行时模块与事件处理

- 移除冗余的 user_token 参数传递
- 重构 tool.result 事件使用 ToolAgentOutput 模型
- 重构 text.end 事件使用 WorkerAgentOutput 模型
- 简化 store 模块的 tool result 处理逻辑
- 更新 router/service 适配新事件结构
- 清理废弃的测试文件与设计文档
- 新增 AgentRuns 多模态存储设计文档
This commit is contained in:
qzl
2026-03-13 17:27:18 +08:00
parent 3273d63b23
commit 1c02503d1d
29 changed files with 1259 additions and 2725 deletions
@@ -5,7 +5,6 @@ from types import SimpleNamespace
from uuid import uuid4
from ag_ui.core import RunAgentInput
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app import app
@@ -24,9 +23,8 @@ class _FakeAgentService:
*,
run_input: RunAgentInput,
current_user: CurrentUser,
user_token: str | None = None,
):
del current_user, user_token
del current_user
return SimpleNamespace(
task_id="task-run-1",
thread_id=run_input.thread_id,
@@ -40,9 +38,8 @@ class _FakeAgentService:
thread_id: str,
run_input: RunAgentInput,
current_user: CurrentUser,
user_token: str | None = None,
):
del thread_id, current_user, user_token
del thread_id, current_user
return SimpleNamespace(
task_id="task-resume-1",
thread_id=run_input.thread_id,
@@ -73,31 +70,6 @@ class _FakeAgentService:
}
]
async def get_history_snapshot(
self,
*,
thread_id: str,
before: str | None,
current_user: CurrentUser,
) -> dict[str, object]:
del current_user
return {
"type": "STATE_SNAPSHOT",
"threadId": thread_id,
"snapshot": {
"scope": "history_day",
"day": before or "2026-03-07",
"hasMore": False,
"messages": [
{
"id": "msg-h1",
"role": "assistant",
"content": "history-message",
}
],
},
}
async def get_user_history_snapshot(
self,
*,
@@ -134,6 +106,20 @@ class _FakeAgentService:
"url": "https://signed.example/upload.png",
}
async def create_attachment_signed_url(
self,
*,
bucket: str,
path: str,
current_user: CurrentUser,
) -> dict[str, str]:
del current_user
return {
"bucket": bucket,
"path": path,
"url": "https://signed.example/temp-url.png",
}
class _FailingStreamAgentService(_FakeAgentService):
async def stream_events(
@@ -151,7 +137,6 @@ def test_run_requires_auth_and_returns_202_task_id() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
client = TestClient(app)
original_allow_run = agent_router._allow_run_request
original_verify_token = agent_router._verified_access_token_for_user
async def _allow_run(*, user_id: str) -> bool:
del user_id
@@ -159,13 +144,6 @@ def test_run_requires_auth_and_returns_202_task_id() -> None:
agent_router._allow_run_request = _allow_run # type: ignore[assignment]
def _verify_token(**kwargs: object) -> str:
if kwargs.get("authorization"):
return "token-ok"
raise HTTPException(status_code=401, detail="Unauthorized")
agent_router._verified_access_token_for_user = _verify_token # type: ignore[assignment]
try:
unauthorized = client.post(
"/api/v1/agent/runs",
@@ -186,7 +164,6 @@ def test_run_requires_auth_and_returns_202_task_id() -> None:
)
authorized = client.post(
"/api/v1/agent/runs",
headers={"Authorization": "Bearer token-ok"},
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-1",
@@ -202,23 +179,8 @@ def test_run_requires_auth_and_returns_202_task_id() -> None:
assert authorized.json()["threadId"] == "00000000-0000-0000-0000-000000000001"
assert authorized.json()["runId"] == "run-1"
assert authorized.json()["created"] is False
missing_header = client.post(
"/api/v1/agent/runs",
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-2",
"state": {},
"messages": [{"id": "u2", "role": "user", "content": "hello"}],
"tools": [],
"context": [],
"forwardedProps": {},
},
)
assert missing_header.status_code == 401
finally:
agent_router._allow_run_request = original_allow_run # type: ignore[assignment]
agent_router._verified_access_token_for_user = original_verify_token # type: ignore[assignment]
app.dependency_overrides = {}
@@ -313,7 +275,8 @@ def test_history_returns_state_snapshot() -> None:
try:
unauthorized = client.get(
"/api/v1/agent/runs/00000000-0000-0000-0000-000000000001/history"
"/api/v1/agent/history",
params={"threadId": "00000000-0000-0000-0000-000000000001"},
)
assert unauthorized.status_code == 401
@@ -321,8 +284,11 @@ def test_history_returns_state_snapshot() -> None:
id=uuid4(), email="user@example.com"
)
authorized = client.get(
"/api/v1/agent/runs/00000000-0000-0000-0000-000000000001/history",
params={"before": "2026-03-07"},
"/api/v1/agent/history",
params={
"threadId": "00000000-0000-0000-0000-000000000001",
"before": "2026-03-07",
},
)
assert authorized.status_code == 200
payload = authorized.json()
@@ -415,19 +381,10 @@ def test_resume_accepts_tool_message_without_user_message() -> None:
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
original_verify_token = agent_router._verified_access_token_for_user
def _verify_token(**kwargs: object) -> str:
if kwargs.get("authorization"):
return "token-ok"
raise HTTPException(status_code=401, detail="Unauthorized")
agent_router._verified_access_token_for_user = _verify_token # type: ignore[assignment]
try:
response = client.post(
"/api/v1/agent/runs/00000000-0000-0000-0000-000000000001/resume",
headers={"Authorization": "Bearer token-ok"},
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-resume-1",
@@ -447,29 +404,7 @@ def test_resume_accepts_tool_message_without_user_message() -> None:
)
assert response.status_code == 202
assert response.json()["taskId"] == "task-resume-1"
missing_header = client.post(
"/api/v1/agent/runs/00000000-0000-0000-0000-000000000001/resume",
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-resume-2",
"state": {},
"messages": [
{
"id": "tool-2",
"role": "tool",
"toolCallId": "call-2",
"content": '{"toolName":"navigate_to_route","toolArgs":{"target":"/calendar/dayweek"},"nonce":"n2","result":{"ok":true}}',
}
],
"tools": [],
"context": [],
"forwardedProps": {},
},
)
assert missing_header.status_code == 401
finally:
agent_router._verified_access_token_for_user = original_verify_token # type: ignore[assignment]
app.dependency_overrides = {}
@@ -498,6 +433,30 @@ def test_upload_attachment_returns_reference() -> None:
app.dependency_overrides = {}
def test_create_attachment_signed_url_returns_url() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
try:
response = client.get(
"/api/v1/agent/attachments/signed-url",
params={
"bucket": "bucket-test",
"path": "agent-inputs/user/thread/upload.png",
},
)
assert response.status_code == 200
body = response.json()
assert body["bucket"] == "bucket-test"
assert body["path"] == "agent-inputs/user/thread/upload.png"
assert body["url"].startswith("https://signed.example/")
finally:
app.dependency_overrides = {}
def test_asr_transcribe_returns_sync_transcript(monkeypatch) -> None:
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
@@ -168,8 +168,9 @@ async def test_agent_runs_events_history_live_with_image_input() -> None:
assert "RUN_FINISHED" in event_names or "RUN_ERROR" in event_names
history_resp = await client.get(
f"{BASE_URL}/api/v1/agent/runs/{thread_id}/history",
f"{BASE_URL}/api/v1/agent/history",
headers=headers,
params={"threadId": thread_id},
)
assert history_resp.status_code == 200
history = history_resp.json()
@@ -183,10 +184,11 @@ async def test_agent_runs_events_history_live_with_image_input() -> None:
if isinstance(item, dict) and item.get("role") == "user"
]
assert user_messages
attachments = user_messages[0].get("attachments")
assert isinstance(attachments, list)
assert attachments and isinstance(attachments[0], dict)
assert isinstance(attachments[0].get("path"), str)
metadata = user_messages[0].get("metadata")
assert isinstance(metadata, dict)
user_attachment = metadata.get("user_message_attachments")
assert isinstance(user_attachment, dict)
assert isinstance(user_attachment.get("path"), str)
async with AsyncSessionLocal() as session:
session_row = await session.get(AgentChatSession, UUID(thread_id))
@@ -212,7 +214,6 @@ async def test_agent_runs_events_history_live_with_image_input() -> None:
]
assert user_rows
metadata = user_rows[0].metadata_json or {}
attachments = metadata.get("attachments")
assert isinstance(attachments, list)
assert attachments and isinstance(attachments[0], dict)
assert isinstance(attachments[0].get("path"), str)
user_attachment = metadata.get("user_message_attachments")
assert isinstance(user_attachment, dict)
assert isinstance(user_attachment.get("path"), str)