77import os
88import uuid
99from collections .abc import AsyncIterator
10+ from typing import Any
1011
1112from fastapi import HTTPException
1213from sqlalchemy import select
@@ -318,6 +319,8 @@ async def create_agent_run_view(
318319 "uid" : str (current_uid ),
319320 "request_id" : request_id ,
320321 "attachment_file_ids" : (meta or {}).get ("attachment_file_ids" ) or [],
322+ "source" : (meta or {}).get ("source" ),
323+ "evaluation" : (meta or {}).get ("evaluation" ) or None ,
321324 "created_at" : utc_now_naive ().isoformat (),
322325 }
323326 try :
@@ -344,6 +347,10 @@ async def create_agent_run_view(
344347 "attachments" : [],
345348 "model_spec" : resolved_model_spec ,
346349 }
350+ if (meta or {}).get ("source" ):
351+ input_metadata ["source" ] = (meta or {}).get ("source" )
352+ if (meta or {}).get ("evaluation" ):
353+ input_metadata ["evaluation" ] = (meta or {}).get ("evaluation" )
347354 if run_type == "resume" :
348355 input_metadata ["source" ] = "ask_user_question_resume"
349356
@@ -383,6 +390,77 @@ async def get_agent_run_view(*, run_id: str, current_uid: str, db: AsyncSession)
383390 return {"run" : run .to_dict ()}
384391
385392
393+ def _select_output_message (messages : list [Message ], * , output_message_id : int | None ) -> Message | None :
394+ """优先选用运行记录的输出消息,否则回退到最后一条 assistant 消息。"""
395+ if output_message_id :
396+ for message in messages :
397+ if message .id == output_message_id and message .role == "assistant" :
398+ return message
399+
400+ for message in reversed (messages ):
401+ if message .role == "assistant" :
402+ return message
403+ return None
404+
405+
406+ async def get_agent_run_result (* , run_id : str , current_uid : str , db : AsyncSession ) -> dict :
407+ """加载某个 run 的最终结果(状态/输出/Langfuse trace/错误),供 chat/eval/cron 等统一复用。"""
408+ run = await AgentRunRepository (db ).get_run_for_user (run_id , str (current_uid ))
409+ if not run :
410+ return {
411+ "status" : "failed" ,
412+ "agent_run_id" : run_id ,
413+ "output" : "" ,
414+ "error" : {"type" : "run_not_found" , "message" : "运行任务不存在" },
415+ }
416+
417+ messages : list [Message ] = []
418+ if run .conversation_id :
419+ result = await db .execute (
420+ select (Message )
421+ .where (Message .conversation_id == run .conversation_id )
422+ .order_by (Message .created_at .asc (), Message .id .asc ())
423+ )
424+ messages = list (result .scalars ().unique ().all ())
425+
426+ output_message = _select_output_message (messages , output_message_id = run .output_message_id )
427+ output_metadata = (
428+ output_message .extra_metadata if output_message and isinstance (output_message .extra_metadata , dict ) else {}
429+ )
430+
431+ payload : dict [str , Any ] = {
432+ "status" : run .status ,
433+ "output" : output_message .content if output_message else "" ,
434+ "agent_slug" : run .agent_id ,
435+ "thread_id" : run .thread_id ,
436+ "conversation_id" : run .conversation_id ,
437+ "agent_run_id" : run .id ,
438+ "request_id" : run .request_id ,
439+ "final_message_id" : output_message .id if output_message else None ,
440+ "langfuse_trace_id" : output_metadata .get ("langfuse_trace_id" ),
441+ }
442+ if run .error_type or run .error_message :
443+ payload ["error" ] = {"type" : run .error_type , "message" : run .error_message }
444+ return payload
445+
446+
447+ async def load_agent_run_result (* , run_id : str , current_uid : str ) -> dict :
448+ """自开独立会话读取 run 结果,用于流结束/后台调用等请求会话已不可用的场景。"""
449+ async with pg_manager .get_async_session_context () as db :
450+ return await get_agent_run_result (run_id = run_id , current_uid = current_uid , db = db )
451+
452+
453+ async def await_agent_run_result (* , run_id : str , current_uid : str ) -> dict :
454+ """阻塞至 run 终结并返回最终结果,供 cron 等 in-process 调用。
455+
456+ 复用有限事件流 ``stream_agent_run_events``:它在 run 终结或超时后自然结束,
457+ 因此排空即等待,无需额外轮询。等待上限继承事件流内部的 ``SSE_MAX_CONNECTION_MINUTES``。
458+ """
459+ async for _ in stream_agent_run_events (run_id = run_id , after_seq = "0-0" , current_uid = current_uid , verbose = False ):
460+ pass
461+ return await load_agent_run_result (run_id = run_id , current_uid = current_uid )
462+
463+
386464async def cancel_agent_run_view (* , run_id : str , current_uid : str , db : AsyncSession ) -> dict :
387465 repo = AgentRunRepository (db )
388466 run = await repo .get_run_for_user (run_id , str (current_uid ))
0 commit comments