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
@@ -12,48 +12,6 @@ from core.auth.models import CurrentUser
from v1.agent import router as agent_router
@pytest.mark.asyncio
async def test_allow_run_request_fails_closed_when_redis_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _raise_redis_error():
raise RuntimeError("redis unavailable")
monkeypatch.setattr(agent_router, "get_or_init_redis_client", _raise_redis_error)
allowed = await agent_router._allow_run_request(user_id="user-1")
assert allowed is False
@pytest.mark.asyncio
async def test_acquire_sse_slot_fails_closed_when_redis_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _raise_redis_error():
raise RuntimeError("redis unavailable")
monkeypatch.setattr(agent_router, "get_or_init_redis_client", _raise_redis_error)
allowed = await agent_router._acquire_sse_slot(user_id="user-1")
assert allowed is False
@pytest.mark.asyncio
async def test_allow_transcribe_request_fails_closed_when_redis_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _raise_redis_error():
raise RuntimeError("redis unavailable")
monkeypatch.setattr(agent_router, "get_or_init_redis_client", _raise_redis_error)
allowed = await agent_router._allow_transcribe_request(user_id="user-1")
assert allowed is False
def _resume_input_with_tool_message() -> RunAgentInput:
return RunAgentInput.model_validate(
{
@@ -82,13 +40,7 @@ async def test_enqueue_resume_rejects_without_tool_contract() -> None:
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-resume-invalid",
"state": {},
"messages": [
{
"id": "u1",
"role": "user",
"content": "continue",
}
],
"messages": [{"id": "u1", "role": "user", "content": "continue"}],
"tools": [],
"context": [],
"forwardedProps": {},
@@ -109,10 +61,6 @@ async def test_enqueue_resume_rejects_without_tool_contract() -> None:
)
assert exc_info.value.status_code == 422
assert (
exc_info.value.detail
== "RunAgentInput.messages requires a tool message with toolCallId for resume"
)
@pytest.mark.asyncio
@@ -141,7 +89,6 @@ async def test_enqueue_resume_rejects_when_rate_limited(
)
assert exc_info.value.status_code == 429
assert exc_info.value.detail == "Too many run requests"
@pytest.mark.asyncio
@@ -173,96 +120,4 @@ async def test_enqueue_resume_accepts_valid_tool_contract(
)
assert result.task_id == "task-resume-1"
assert result.thread_id == "00000000-0000-0000-0000-000000000001"
assert result.run_id == "run-resume-1"
@pytest.mark.asyncio
async def test_stream_events_retries_on_redis_timeout(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _acquire(*, user_id: str) -> bool:
del user_id
return True
async def _release(*, user_id: str) -> None:
del user_id
monkeypatch.setattr(agent_router, "_acquire_sse_slot", _acquire)
monkeypatch.setattr(agent_router, "_release_sse_slot", _release)
class _Request:
async def is_disconnected(self) -> bool:
return False
class _Service:
def __init__(self) -> None:
self.calls = 0
async def stream_events(self, **kwargs): # noqa: ANN003
del kwargs
self.calls += 1
if self.calls == 1:
raise RuntimeError("Timeout reading from localhost:6379")
if self.calls == 2:
return [{"id": "1-0", "event": {"type": "RUN_FINISHED"}}]
return []
response = await agent_router.stream_events(
request=cast(Any, _Request()),
thread_id="00000000-0000-0000-0000-000000000001",
service=cast(Any, _Service()),
current_user=CurrentUser(id=uuid4(), email="user@example.com"),
last_event_id=None,
idle_limit=2,
)
chunks: list[str] = []
async for chunk in response.body_iterator:
chunks.append(str(chunk))
if any("RUN_FINISHED" in item for item in chunks):
break
merged = "".join(chunks)
assert "event: RUN_FINISHED" in merged
@pytest.mark.asyncio
async def test_get_attachment_preview_rejects_negative_index() -> None:
class _Service:
async def get_attachment_preview(self, **kwargs): # noqa: ANN003
del kwargs
raise AssertionError("get_attachment_preview should not be called")
with pytest.raises(HTTPException) as exc_info:
await agent_router.get_attachment_preview(
thread_id="00000000-0000-0000-0000-000000000001",
message_id="00000000-0000-0000-0000-000000000010",
attachment_index=-1,
service=cast(Any, _Service()),
current_user=CurrentUser(id=uuid4(), email="user@example.com"),
)
assert exc_info.value.status_code == 422
@pytest.mark.asyncio
async def test_get_attachment_preview_returns_streaming_response() -> None:
class _Service:
async def get_attachment_preview(self, **kwargs): # noqa: ANN003
del kwargs
return b"png-bytes", "image/png"
response = await agent_router.get_attachment_preview(
thread_id="00000000-0000-0000-0000-000000000001",
message_id="00000000-0000-0000-0000-000000000010",
attachment_index=0,
service=cast(Any, _Service()),
current_user=CurrentUser(id=uuid4(), email="user@example.com"),
)
chunks: list[bytes] = []
async for chunk in response.body_iterator:
chunks.append(cast(bytes, chunk))
assert response.media_type == "image/png"
assert b"".join(chunks) == b"png-bytes"