88from collections .abc import AsyncIterator , Awaitable
99from contextlib import asynccontextmanager
1010from dataclasses import dataclass , field
11- from typing import Any
11+ from typing import Any , cast
1212
1313import pytest
1414import uvicorn
@@ -126,7 +126,6 @@ def __init__(self) -> None:
126126
127127 async def enqueue_event (self , event : Any ) -> None :
128128 self .events .append (event )
129- await super ().enqueue_event (event )
130129
131130
132131class _FakeRequestContext :
@@ -147,13 +146,20 @@ def get_user_input(self) -> str:
147146
148147
149148class _HostedAgent :
150- name = "HostedAssistant"
151- description = "A hosted test assistant."
149+ id = "hosted-agent"
150+ name : str | None = "HostedAssistant"
151+ description : str | None = "A hosted test assistant."
152152
153153 async def run (self , messages : Any = None , * , stream : bool = False , ** _kwargs : Any ) -> AgentResponse [Any ]:
154154 text = messages .text if isinstance (messages , AFMessage ) else str (messages )
155155 return AgentResponse (messages = [AFMessage (role = "assistant" , contents = [Content .from_text (text = f"host: { text } " )])])
156156
157+ def create_session (self , * , session_id : str | None = None ) -> Any :
158+ return {"session_id" : session_id }
159+
160+ def get_session (self , service_session_id : str , * , session_id : str | None = None ) -> Any :
161+ return {"service_session_id" : service_session_id , "session_id" : session_id }
162+
157163
158164@asynccontextmanager
159165async def _serve_app (app : ASGIApp , * , port : int ) -> AsyncIterator [str ]:
@@ -182,6 +188,18 @@ def _status_states(events: list[Any]) -> list[int]:
182188 return states
183189
184190
191+ def _status_texts (events : list [Any ]) -> list [str ]:
192+ texts : list [str ] = []
193+ for event in events :
194+ status = getattr (event , "status" , None )
195+ message = getattr (status , "message" , None )
196+ for part in cast ("list[Any]" , getattr (message , "parts" , None ) or []):
197+ text = getattr (part , "text" , None )
198+ if isinstance (text , str ):
199+ texts .append (text )
200+ return texts
201+
202+
185203# --------------------------------------------------------------------------- #
186204# A2AChannel tests #
187205# --------------------------------------------------------------------------- #
@@ -195,7 +213,7 @@ def test_default_name_and_root_path() -> None:
195213
196214def test_build_agent_card_defaults_from_target () -> None :
197215 channel = A2AChannel (url = "https://example.com/" )
198- card = channel ._build_agent_card (_FakeContext ()) # type: ignore[arg-type]
216+ card = channel ._build_agent_card (cast ( Any , _FakeContext ()))
199217 assert card .name == "Assistant"
200218 assert card .description == "A helpful assistant."
201219 assert card .capabilities .streaming is True
@@ -208,21 +226,21 @@ def test_build_agent_card_accepts_supported_interfaces() -> None:
208226 AgentInterface (url = "https://example.com/grpc" , protocol_binding = "GRPC" ),
209227 ]
210228 channel = A2AChannel (supported_interfaces = interfaces )
211- card = channel ._build_agent_card (_FakeContext ()) # type: ignore[arg-type]
229+ card = channel ._build_agent_card (cast ( Any , _FakeContext ()))
212230 assert card .supported_interfaces == interfaces
213231
214232
215233def test_build_agent_card_override_wins () -> None :
216234 custom = AgentCard (name = "Custom" , description = "custom card" , version = "9.9.9" )
217235 channel = A2AChannel (agent_card = custom )
218- card = channel ._build_agent_card (_FakeContext ()) # type: ignore[arg-type]
236+ card = channel ._build_agent_card (cast ( Any , _FakeContext ()))
219237 assert card .name == "Custom"
220238 assert card .version == "9.9.9"
221239
222240
223241def test_contribute_returns_card_and_jsonrpc_routes () -> None :
224242 channel = A2AChannel (url = "https://example.com/" )
225- contribution = channel .contribute (_FakeContext ()) # type: ignore[arg-type]
243+ contribution = channel .contribute (cast ( Any , _FakeContext ()))
226244 assert isinstance (contribution , ChannelContribution )
227245 paths = {getattr (r , "path" , None ) for r in contribution .routes }
228246 assert "/.well-known/agent-card.json" in paths
@@ -236,11 +254,11 @@ def test_contribute_returns_card_and_jsonrpc_routes() -> None:
236254
237255async def test_execute_routes_through_host_and_completes () -> None :
238256 ctx = _FakeContext (reply = "hi back" )
239- executor = HostAgentExecutor (ctx , channel_name = "a2a" , streaming = False ) # type: ignore[arg-type]
257+ executor = HostAgentExecutor (cast ( Any , ctx ) , channel_name = "a2a" , streaming = False )
240258 queue = _RecordingEventQueue ()
241259 request_context = _FakeRequestContext (context_id = "conv-1" , text = "hello" )
242260
243- await executor .execute (request_context , queue ) # type: ignore[arg-type]
261+ await executor .execute (cast ( Any , request_context ) , queue )
244262
245263 # Routed through the host with the context id mapped onto the session.
246264 assert len (ctx .requests ) == 1
@@ -257,31 +275,34 @@ async def test_execute_routes_through_host_and_completes() -> None:
257275
258276async def test_execute_streaming_emits_artifacts () -> None :
259277 ctx = _FakeContext (chunks = ["foo" , "bar" ])
260- executor = HostAgentExecutor (ctx , channel_name = "a2a" , streaming = True ) # type: ignore[arg-type]
278+ executor = HostAgentExecutor (cast ( Any , ctx ) , channel_name = "a2a" , streaming = True )
261279 queue = _RecordingEventQueue ()
262280 request_context = _FakeRequestContext (context_id = "conv-2" , text = "hello" )
263281
264- await executor .execute (request_context , queue ) # type: ignore[arg-type]
282+ await executor .execute (cast ( Any , request_context ) , queue )
265283
266284 artifact_events = [e for e in queue .events if getattr (e , "artifact" , None )]
267285 assert artifact_events , "expected at least one artifact update event"
286+ artifact_ids = {getattr (getattr (e , "artifact" , None ), "artifact_id" , None ) for e in artifact_events }
287+ assert len (artifact_ids ) == 1
288+ assert None not in artifact_ids
268289 assert ctx .requests [0 ].stream is True
269290 assert TaskState .TASK_STATE_COMPLETED in _status_states (queue .events )
270291
271292
272293async def test_execute_requires_context_id () -> None :
273294 ctx = _FakeContext ()
274- executor = HostAgentExecutor (ctx , channel_name = "a2a" ) # type: ignore[arg-type]
295+ executor = HostAgentExecutor (cast ( Any , ctx ) , channel_name = "a2a" )
275296 queue = _RecordingEventQueue ()
276297 request_context = _FakeRequestContext (context_id = "x" , text = "hello" )
277- request_context .context_id = None # type: ignore[assignment]
298+ cast ( Any , request_context ) .context_id = None
278299
279300 with pytest .raises (ValueError , match = "Context ID" ):
280- await executor .execute (request_context , queue ) # type: ignore[arg-type]
301+ await executor .execute (cast ( Any , request_context ) , queue )
281302
282303
283304async def test_a2a_agent_can_call_hosted_channel (unused_tcp_port : int ) -> None :
284- host = AgentFrameworkHost (target = _HostedAgent (), channels = [A2AChannel (streaming = False )])
305+ host = AgentFrameworkHost (target = cast ( Any , _HostedAgent () ), channels = [A2AChannel (streaming = False )])
285306
286307 async with (
287308 _serve_app (host .app , port = unused_tcp_port ) as base_url ,
@@ -295,6 +316,36 @@ async def test_a2a_agent_can_call_hosted_channel(unused_tcp_port: int) -> None:
295316 assert response .messages [0 ].text == "host: hello"
296317
297318
319+ async def test_execute_projects_workflow_outputs () -> None :
320+ class _WorkflowResult :
321+ value = None
322+
323+ def get_outputs (self ) -> list [AFMessage ]:
324+ return [AFMessage (role = "assistant" , contents = [Content .from_text ("workflow output" )])]
325+
326+ class _WorkflowContext (_FakeContext ):
327+ async def run (
328+ self ,
329+ request : ChannelRequest ,
330+ * ,
331+ run_hook : Any | None = None ,
332+ protocol_request : Any | None = None ,
333+ response_hook : Any | None = None ,
334+ channel_name : str | None = None ,
335+ ) -> HostedRunResult [Any ]:
336+ self .requests .append (request )
337+ return HostedRunResult (_WorkflowResult ())
338+
339+ ctx = _WorkflowContext ()
340+ executor = HostAgentExecutor (cast (Any , ctx ), channel_name = "a2a" , streaming = False )
341+ queue = _RecordingEventQueue ()
342+ request_context = _FakeRequestContext (context_id = "conv-workflow" , text = "hello" )
343+
344+ await executor .execute (cast (Any , request_context ), queue )
345+
346+ assert "workflow output" in _status_texts (queue .events )
347+
348+
298349def test_contents_to_parts_conversion () -> None :
299350 from agent_framework_hosting_a2a ._executor import _contents_to_parts
300351
0 commit comments