diff --git a/backend/package/yuxi/services/agent_run_service.py b/backend/package/yuxi/services/agent_run_service.py index 169588db..9ed14bf7 100644 --- a/backend/package/yuxi/services/agent_run_service.py +++ b/backend/package/yuxi/services/agent_run_service.py @@ -17,6 +17,7 @@ from yuxi.repositories.agent_repository import AgentRepository from yuxi.repositories.agent_run_repository import TERMINAL_RUN_STATUSES, AgentRunRepository from yuxi.repositories.conversation_repository import ConversationRepository from yuxi.services.run_queue_service import ( + build_run_event_envelope, get_arq_pool, get_last_run_stream_seq, list_run_stream_events, @@ -273,14 +274,13 @@ async def stream_agent_run_events( if terminal_seq in {"", "0-0"}: terminal_seq = None yield _format_sse( - { - "schema_version": 1, - "run_id": run_id, - "thread_id": run.thread_id, - "event": "end", - "payload": {"status": run.status}, - "created_at": utc_now_naive().isoformat(), - }, + build_run_event_envelope( + run_id=run_id, + thread_id=run.thread_id, + event_type="end", + payload={"status": run.status}, + created_at=utc_now_naive().isoformat(), + ), event="end", event_id=terminal_seq, ) diff --git a/backend/package/yuxi/services/run_queue_service.py b/backend/package/yuxi/services/run_queue_service.py index e6c13d35..df93fe32 100644 --- a/backend/package/yuxi/services/run_queue_service.py +++ b/backend/package/yuxi/services/run_queue_service.py @@ -55,6 +55,32 @@ def normalize_after_seq(after_seq: str | None) -> str: return "0-0" +def build_run_event_envelope( + *, + run_id: str, + event_type: str, + payload: dict | None = None, + thread_id: str | None = None, + created_at: str | None = None, +) -> dict: + return { + "schema_version": 1, + "run_id": run_id, + "thread_id": thread_id, + "event": event_type, + "payload": payload or {}, + "created_at": created_at or datetime.now(tz=UTC).isoformat(), + } + + +def _payload_thread_id(payload: dict | None) -> str | None: + chunk = payload.get("chunk") if isinstance(payload, dict) else None + if not isinstance(chunk, dict): + return None + thread_id = chunk.get("thread_id") + return thread_id.strip() if isinstance(thread_id, str) and thread_id.strip() else None + + async def get_redis_client(): global _redis_client if _redis_client is not None: @@ -164,14 +190,14 @@ async def append_run_stream_event(run_id: str, event_type: str, payload: dict, * key = _event_stream_key(run_id) now = datetime.now(tz=UTC) now_ms = int(now.timestamp() * 1000) - envelope = { - "schema_version": 1, - "run_id": run_id, - "thread_id": thread_id, - "event": event_type, - "payload": payload or {}, - "created_at": now.isoformat(), - } + event_thread_id = thread_id or _payload_thread_id(payload) + envelope = build_run_event_envelope( + run_id=run_id, + event_type=event_type, + payload=payload or {}, + thread_id=event_thread_id, + created_at=now.isoformat(), + ) fields = { "event_type": event_type, "payload": json.dumps(envelope, ensure_ascii=False), diff --git a/backend/test/unit/services/test_run_queue_service.py b/backend/test/unit/services/test_run_queue_service.py index ecccc417..1366fdee 100644 --- a/backend/test/unit/services/test_run_queue_service.py +++ b/backend/test/unit/services/test_run_queue_service.py @@ -97,7 +97,11 @@ async def test_run_stream_event_roundtrip(monkeypatch: pytest.MonkeyPatch): run_id = "run-1" seq1 = await run_queue_service.append_run_stream_event(run_id, "loading", {"items": [1]}) - seq2 = await run_queue_service.append_run_stream_event(run_id, "finished", {"chunk": {"status": "finished"}}) + seq2 = await run_queue_service.append_run_stream_event( + run_id, + "finished", + {"chunk": {"status": "finished", "thread_id": "child-thread"}}, + ) assert seq1 < seq2 @@ -106,6 +110,7 @@ async def test_run_stream_event_roundtrip(monkeypatch: pytest.MonkeyPatch): assert events[0]["payload"]["schema_version"] == 1 assert events[0]["payload"]["run_id"] == run_id assert events[0]["payload"]["payload"] == {"items": [1]} + assert events[1]["payload"]["thread_id"] == "child-thread" next_events = await run_queue_service.list_run_stream_events(run_id, after_seq=seq1, limit=100) assert len(next_events) == 1 diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index ea615c0e..4230f5fc 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -67,7 +67,7 @@ - 收敛用户身份命名:原业务登录标识统一改为 `uid`,Agent/LangGraph runtime、conversation、agent_run、sandbox 路径和前端用户态均使用字符串 `uid`;`user_id` 仅保留给外部响应中的数值 `users.id` 或真实外键场景。 - 工作区知识库分类显示:知识库侧边栏按创建者分组为“我的知识库”和“共享知识库”,自己创建的知识库显示在“我的知识库”下,非自己创建的显示在“共享知识库”下;`knowledge_bases` 表新增 `created_by` 字段记录创建者 uid。 - 聊天附件新增 MinIO tmp 临时上传、可选 PDF/图片解析、确认后加入线程附件的流程;前端改为弹窗内上传、解析与确认。 -- 标准化 Agent run/SSE 执行链路:run 创建时持久化输入消息并提交后入队,worker 统一写入 Redis Stream envelope,SSE 输出 `event/data/id`、心跳注释、`Last-Event-ID` 回放和终止 `end` 事件;前端强制使用 run API 并支持 ask_user_question 中断后以 resume run 恢复。 +- 标准化 Agent run/SSE 执行链路:run 创建时持久化输入消息并提交后入队,worker 统一写入 Redis Stream envelope,SSE 输出 `event/data/id`、心跳注释、`Last-Event-ID` 回放和终止 `end` 事件;前端强制使用 run API 并支持 ask_user_question 中断后以 resume run 恢复;事件 envelope 构造收敛到统一 helper,前端优先使用 envelope 一级 `thread_id` 路由。 - 收敛后端模块边界:文档解析从 `plugins.parser` 移动到 `knowledge.parser`,内容审查从 `plugins.guard` 移动到 `services.guard`。 - 收敛文件服务边界:文件预览判断抽为独立服务,Viewer 文件系统的 workspace 分支复用用户 workspace 服务,线程运行时上下文解析从泛化 `filesystem_service` 拆出为 agent runtime helper。 - 升级 DeepAgents 到 0.6.7 并适配新版文件系统协议:SubAgentMiddleware 改为显式 subagent spec,Skills prompt 补齐新版占位符;sandbox/skills backend 复用新版 `ReadResult`、`GlobResult`、`GrepResult` 等协议类型,文件权限在 backend 层明确区分 skills、uploads、outputs 与 workspace,保留最小 `CustomCompositeBackend` 以避免非 route glob 误扫其他 route;Agent 上下文压缩改为复用 DeepAgents SummarizationMiddleware,历史摘要与大工具结果统一 offload 到 outputs。 diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index 92cce254..6482d4d7 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -45,10 +45,7 @@