@@ -499,6 +499,7 @@ def _convert_stream_values_event(
499499def _convert_astream_events_event (
500500 event_dict : Dict [str , Any ],
501501 tool_call_id_map : Optional [Dict [int , str ]] = None ,
502+ tool_call_started_set : Optional [set ] = None ,
502503) -> Iterator [Union [AgentResult , str ]]:
503504 """转换 astream_events 格式的单个事件
504505
@@ -507,6 +508,8 @@ def _convert_astream_events_event(
507508 tool_call_id_map: 可选的 index -> tool_call_id 映射字典。
508509 在流式工具调用中,第一个 chunk 有 id,后续只有 index。
509510 此映射用于确保所有 chunk 使用一致的 tool_call_id。
511+ tool_call_started_set: 可选的已发送 TOOL_CALL_START 的 tool_call_id 集合。
512+ 用于确保每个工具调用只发送一次 TOOL_CALL_START。
510513
511514 Yields:
512515 str (文本内容) 或 AgentResult (事件)
@@ -527,6 +530,7 @@ def _convert_astream_events_event(
527530 for tc in _extract_tool_call_chunks (chunk ):
528531 tc_index = tc .get ("index" )
529532 tc_raw_id = tc .get ("id" )
533+ tc_name = tc .get ("name" , "" )
530534 tc_args = tc .get ("args" , "" )
531535
532536 # 解析 tool_call_id:
@@ -552,8 +556,28 @@ def _convert_astream_events_event(
552556 else :
553557 tc_id = ""
554558
559+ if not tc_id :
560+ continue
561+
562+ # AG-UI 协议要求:先发送 TOOL_CALL_START,再发送 TOOL_CALL_ARGS
563+ # 第一次遇到某个工具调用时(有 id 和 name),先发送 TOOL_CALL_START
564+ if tc_raw_id and tc_name :
565+ if (
566+ tool_call_started_set is None
567+ or tc_id not in tool_call_started_set
568+ ):
569+ yield AgentResult (
570+ event = EventType .TOOL_CALL_START ,
571+ data = {
572+ "tool_call_id" : tc_id ,
573+ "tool_call_name" : tc_name ,
574+ },
575+ )
576+ if tool_call_started_set is not None :
577+ tool_call_started_set .add (tc_id )
578+
555579 # 只有有 args 时才生成 TOOL_CALL_ARGS 事件
556- if tc_args and tc_id :
580+ if tc_args :
557581 if isinstance (tc_args , (dict , list )):
558582 tc_args = _safe_json_dumps (tc_args )
559583 yield AgentResult (
@@ -598,23 +622,41 @@ def _convert_astream_events_event(
598622 tool_input = _filter_tool_input (tool_input_raw )
599623
600624 if tool_call_id :
601- yield AgentResult (
602- event = EventType .TOOL_CALL_START ,
603- data = {
604- "tool_call_id" : tool_call_id ,
605- "tool_call_name" : tool_name ,
606- },
625+ # 检查是否已在 on_chat_model_stream 中发送过 TOOL_CALL_START
626+ already_started = (
627+ tool_call_started_set is not None
628+ and tool_call_id in tool_call_started_set
607629 )
608- if tool_input :
609- args_str = (
610- _safe_json_dumps (tool_input )
611- if isinstance (tool_input , dict )
612- else str (tool_input )
613- )
630+
631+ if not already_started :
632+ # 非流式场景或未收到流式事件,需要发送 TOOL_CALL_START
614633 yield AgentResult (
615- event = EventType .TOOL_CALL_ARGS ,
616- data = {"tool_call_id" : tool_call_id , "delta" : args_str },
634+ event = EventType .TOOL_CALL_START ,
635+ data = {
636+ "tool_call_id" : tool_call_id ,
637+ "tool_call_name" : tool_name ,
638+ },
617639 )
640+ if tool_call_started_set is not None :
641+ tool_call_started_set .add (tool_call_id )
642+
643+ # 非流式场景下,在 START 后发送完整参数
644+ if tool_input :
645+ args_str = (
646+ _safe_json_dumps (tool_input )
647+ if isinstance (tool_input , dict )
648+ else str (tool_input )
649+ )
650+ yield AgentResult (
651+ event = EventType .TOOL_CALL_ARGS ,
652+ data = {"tool_call_id" : tool_call_id , "delta" : args_str },
653+ )
654+
655+ # AG-UI 协议:TOOL_CALL_END 表示参数传输完成,在工具执行前发送
656+ yield AgentResult (
657+ event = EventType .TOOL_CALL_END ,
658+ data = {"tool_call_id" : tool_call_id },
659+ )
618660
619661 # 4. 工具结束
620662 elif event_type == "on_tool_end" :
@@ -625,17 +667,15 @@ def _convert_astream_events_event(
625667 tool_call_id = _extract_tool_call_id (tool_input_raw ) or run_id
626668
627669 if tool_call_id :
670+ # AG-UI 协议:TOOL_CALL_RESULT 在工具执行完成后发送
671+ # 注意:TOOL_CALL_END 已在 on_tool_start 中发送(表示参数传输完成)
628672 yield AgentResult (
629673 event = EventType .TOOL_CALL_RESULT ,
630674 data = {
631675 "tool_call_id" : tool_call_id ,
632676 "result" : _format_tool_output (output ),
633677 },
634678 )
635- yield AgentResult (
636- event = EventType .TOOL_CALL_END ,
637- data = {"tool_call_id" : tool_call_id },
638- )
639679
640680 # 5. LLM 结束
641681 elif event_type == "on_chat_model_end" :
@@ -652,6 +692,7 @@ def to_agui_events(
652692 event : Union [Dict [str , Any ], Any ],
653693 messages_key : str = "messages" ,
654694 tool_call_id_map : Optional [Dict [int , str ]] = None ,
695+ tool_call_started_set : Optional [set ] = None ,
655696) -> Iterator [Union [AgentResult , str ]]:
656697 """将 LangGraph/LangChain 流式事件转换为 AG-UI 协议事件
657698
@@ -667,12 +708,15 @@ def to_agui_events(
667708 messages_key: state 中消息列表的 key,默认 "messages"
668709 tool_call_id_map: 可选的 index -> tool_call_id 映射字典,用于流式工具调用
669710 的 ID 一致性。如果提供,函数会自动更新此映射。
711+ tool_call_started_set: 可选的已发送 TOOL_CALL_START 的 tool_call_id 集合。
712+ 用于确保每个工具调用只发送一次 TOOL_CALL_START,
713+ 并在正确的时机发送 TOOL_CALL_END。
670714
671715 Yields:
672716 str (文本内容) 或 AgentResult (AG-UI 事件)
673717
674718 Example:
675- >>> # 使用 astream_events(推荐使用 AguiEventConverter 类)
719+ >>> # 使用 astream_events(推荐使用 AgentRunConverter 类)
676720 >>> async for event in agent.astream_events(input, version="v2"):
677721 ... for item in to_agui_events(event):
678722 ... yield item
@@ -692,7 +736,9 @@ def to_agui_events(
692736 # 根据事件格式选择对应的转换器
693737 if _is_astream_events_format (event_dict ):
694738 # astream_events 格式:{"event": "on_xxx", "data": {...}}
695- yield from _convert_astream_events_event (event_dict , tool_call_id_map )
739+ yield from _convert_astream_events_event (
740+ event_dict , tool_call_id_map , tool_call_started_set
741+ )
696742
697743 elif _is_stream_updates_format (event_dict ):
698744 # stream/astream(stream_mode="updates") 格式:{node_name: state_update}
@@ -707,11 +753,17 @@ class AgentRunConverter:
707753 """AgentRun 事件转换器
708754
709755 将 LangGraph/LangChain 流式事件转换为 AG-UI 协议事件。
710- 此类维护必要的状态以确保流式工具调用的 tool_call_id 一致性。
756+ 此类维护必要的状态以确保:
757+ 1. 流式工具调用的 tool_call_id 一致性
758+ 2. AG-UI 协议要求的事件顺序(TOOL_CALL_START → TOOL_CALL_ARGS → TOOL_CALL_END)
711759
712- 在流式工具调用中,第一个 chunk 包含 id,后续 chunk 只有 index。
760+ 在流式工具调用中,第一个 chunk 包含 id 和 name ,后续 chunk 只有 index 和 args 。
713761 此类维护 index -> id 的映射,确保所有相关事件使用相同的 tool_call_id。
714762
763+ 同时,此类跟踪已发送 TOOL_CALL_START 的工具调用,确保:
764+ - 在流式场景中,TOOL_CALL_START 在第一个参数 chunk 前发送
765+ - 避免在 on_tool_start 中重复发送 TOOL_CALL_START
766+
715767 Example:
716768 >>> from agentrun.integration.langchain import AgentRunConverter
717769 >>>
@@ -724,6 +776,7 @@ class AgentRunConverter:
724776
725777 def __init__ (self ):
726778 self ._tool_call_id_map : Dict [int , str ] = {}
779+ self ._tool_call_started_set : set = set ()
727780
728781 def convert (
729782 self ,
@@ -739,15 +792,21 @@ def convert(
739792 Yields:
740793 str (文本内容) 或 AgentResult (AG-UI 事件)
741794 """
742- yield from to_agui_events (event , messages_key , self ._tool_call_id_map )
795+ yield from to_agui_events (
796+ event ,
797+ messages_key ,
798+ self ._tool_call_id_map ,
799+ self ._tool_call_started_set ,
800+ )
743801
744802 def reset (self ):
745- """重置状态,清空 tool_call_id 映射
803+ """重置状态,清空 tool_call_id 映射和已发送状态
746804
747805 在处理新的请求时,建议创建新的 AgentRunConverter 实例,
748806 而不是复用旧实例并调用 reset。
749807 """
750808 self ._tool_call_id_map .clear ()
809+ self ._tool_call_started_set .clear ()
751810
752811
753812# 保留向后兼容的别名
0 commit comments