@@ -500,3 +500,125 @@ def test_manually_emit_tool_call_with_empty_args(self, agent):
500500 assert EventType .TOOL_CALL_START in types
501501 assert EventType .TOOL_CALL_ARGS in types
502502 assert EventType .TOOL_CALL_END in types
503+
504+
505+ # ---------- AG-UI-style (unprefixed) event integration ----------
506+
507+ class TestAGUIStyleEventIntegration :
508+ """Verify that AG-UI-native (unprefixed) events flow correctly through CopilotKit.
509+
510+ CopilotKit's _dispatch_event only intercepts copilotkit_-prefixed CUSTOM events;
511+ unprefixed AG-UI events (manually_emit_message, manually_emit_tool_call, exit) are
512+ delegated to the AG-UI base class — never suppressed or double-converted by CopilotKit.
513+ """
514+
515+ def _make_agent (self ):
516+ mock_graph = MagicMock ()
517+ mock_graph .get_state = MagicMock ()
518+ agent = LangGraphAGUIAgent (name = "test" , graph = mock_graph )
519+ agent .active_run = {
520+ "id" : "run-1" , "thread_id" : "t1" ,
521+ "reasoning_process" : None , "node_name" : "agent" ,
522+ "has_function_streaming" : False , "model_made_tool_call" : False ,
523+ "state_reliable" : True , "streamed_messages" : [],
524+ "manually_emitted_state" : None ,
525+ "schema_keys" : {
526+ "input" : ["messages" , "tools" ],
527+ "output" : ["messages" , "tools" ],
528+ "config" : [],
529+ "context" : [],
530+ },
531+ }
532+ return agent
533+
534+ def test_agui_manually_emit_message_produces_text_events (self ):
535+ """on_custom_event/"manually_emit_message" through CopilotKit should produce
536+ TEXT_MESSAGE_START/CONTENT/END via the AG-UI base handler — not suppressed."""
537+ import asyncio
538+ agent = self ._make_agent ()
539+ lg_event = {
540+ "event" : "on_custom_event" ,
541+ "name" : "manually_emit_message" ,
542+ "data" : {"message_id" : "msg-1" , "message" : "Hello" , "role" : "assistant" },
543+ "metadata" : {},
544+ }
545+
546+ async def _run ():
547+ async for _ in agent ._handle_single_event (lg_event , {}):
548+ pass
549+
550+ with track_parent_dispatches (agent ) as dispatched :
551+ asyncio .run (_run ())
552+
553+ types = [e .type for e in dispatched ]
554+ assert EventType .TEXT_MESSAGE_START in types
555+ assert EventType .TEXT_MESSAGE_CONTENT in types
556+ assert EventType .TEXT_MESSAGE_END in types
557+
558+ def test_agui_manually_emit_tool_call_produces_tool_events (self ):
559+ """on_custom_event/"manually_emit_tool_call" through CopilotKit should produce
560+ TOOL_CALL_START/ARGS/END via the AG-UI base handler."""
561+ import asyncio
562+ agent = self ._make_agent ()
563+ lg_event = {
564+ "event" : "on_custom_event" ,
565+ "name" : "manually_emit_tool_call" ,
566+ "data" : {"id" : "tc-1" , "name" : "search" , "args" : {"q" : "weather" }},
567+ "metadata" : {},
568+ }
569+
570+ async def _run ():
571+ async for _ in agent ._handle_single_event (lg_event , {}):
572+ pass
573+
574+ with track_parent_dispatches (agent ) as dispatched :
575+ asyncio .run (_run ())
576+
577+ types = [e .type for e in dispatched ]
578+ assert EventType .TOOL_CALL_START in types
579+ assert EventType .TOOL_CALL_ARGS in types
580+ assert EventType .TOOL_CALL_END in types
581+
582+ def test_agui_exit_produces_custom_event (self ):
583+ """on_custom_event/"exit" through CopilotKit should emit a CUSTOM event —
584+ the exit signal is not suppressed by the CopilotKit layer."""
585+ import asyncio
586+ agent = self ._make_agent ()
587+ lg_event = {
588+ "event" : "on_custom_event" ,
589+ "name" : "exit" ,
590+ "data" : {},
591+ "metadata" : {},
592+ }
593+
594+ async def _run ():
595+ async for _ in agent ._handle_single_event (lg_event , {}):
596+ pass
597+
598+ with track_parent_dispatches (agent ) as dispatched :
599+ asyncio .run (_run ())
600+
601+ types = [e .type for e in dispatched ]
602+ assert EventType .CUSTOM in types
603+
604+ def test_agui_style_event_not_suppressed_by_dispatch (self ):
605+ """A CUSTOM event with an AG-UI-style unprefixed name passed to CopilotKit's
606+ _dispatch_event should be forwarded to the base class unchanged.
607+
608+ CopilotKit only converts copilotkit_-prefixed events (e.g. copilotkit_manually_emit_message
609+ → TEXT_MESSAGE_*). An unprefixed "manually_emit_message" CustomEvent must not trigger
610+ that conversion path — no TEXT_MESSAGE_START should be injected, and the original
611+ event object must be forwarded as-is."""
612+ agent = self ._make_agent ()
613+ original = CustomEvent (
614+ type = EventType .CUSTOM ,
615+ name = "manually_emit_message" ,
616+ value = {"message_id" : "msg-2" , "message" : "test" , "role" : "assistant" },
617+ )
618+ with track_parent_dispatches (agent ) as dispatched :
619+ agent ._dispatch_event (original )
620+
621+ types = [e .type for e in dispatched ]
622+ assert EventType .CUSTOM in types
623+ assert EventType .TEXT_MESSAGE_START not in types
624+ assert dispatched [0 ] is original
0 commit comments