22Module for A2A Agent.
33"""
44
5+ import asyncio
56import logging
67import os
78import sys
9+ import threading
810import traceback
911
1012import uvicorn
11- from crewai_tools import MCPServerAdapter
1213from crewai_tools .adapters .tool_collection import ToolCollection
1314
1415
3233
3334from git_issue_agent .config import settings , Settings
3435from git_issue_agent .event import Event
35- from git_issue_agent .main import GitIssueAgent
36+ from git_issue_agent .main import GitIssueAgent , TaskCancelled
37+ from git_issue_agent .mcp_connect import mcp_tools_session
3638
3739logger = logging .getLogger (__name__ )
3840logging .basicConfig (level = settings .LOG_LEVEL , stream = sys .stdout , format = "%(levelname)s: %(message)s" )
@@ -115,11 +117,26 @@ class GithubExecutor(AgentExecutor):
115117 A class to handle research execution for A2A Agent.
116118 """
117119
118- async def _run_agent (self , messages : dict , settings : Settings , event_emitter : Event , toolkit : ToolCollection ):
120+ def __init__ (self ):
121+ # Per-request cooperative-cancel flags, keyed by task id. CrewAI runs its
122+ # crew synchronously in a worker thread (kickoff_async -> asyncio.to_thread),
123+ # so cancelling the awaiting coroutine cannot stop the work; instead the
124+ # crew checks this Event between steps and stops itself.
125+ self ._cancel_events : dict [str , threading .Event ] = {}
126+
127+ async def _run_agent (
128+ self ,
129+ messages : dict ,
130+ settings : Settings ,
131+ event_emitter : Event ,
132+ toolkit : ToolCollection ,
133+ cancel_event : threading .Event ,
134+ ):
119135 git_issue_agent = GitIssueAgent (
120136 config = settings ,
121137 eventer = event_emitter ,
122138 mcp_toolkit = toolkit ,
139+ cancel_event = cancel_event ,
123140 )
124141 result = await git_issue_agent .execute (messages )
125142 await event_emitter .emit_event (result , True )
@@ -163,6 +180,11 @@ async def execute(self, context: RequestContext, event_queue: EventQueue):
163180 }
164181 )
165182
183+ # Register a cooperative-cancel flag for this task so cancel()/disconnect
184+ # can stop the (thread-bound) crew run.
185+ cancel_event = threading .Event ()
186+ self ._cancel_events [task .id ] = cancel_event
187+
166188 # Hook up MCP tools
167189 try :
168190 if settings .MCP_URL :
@@ -173,7 +195,13 @@ async def execute(self, context: RequestContext, event_queue: EventQueue):
173195 "transport" : "streamable-http" ,
174196 "headers" : headers ,
175197 }
176- with MCPServerAdapter (server_params , connect_timeout = settings .MCP_TIMEOUT ) as mcp_tools :
198+ # mcp_tools_session fails fast if the connection errors out, while
199+ # still allowing up to MCP_TIMEOUT for a slow (e.g. OAuth) connect.
200+ async with mcp_tools_session (
201+ server_params ,
202+ connect_timeout = settings .MCP_TIMEOUT ,
203+ poll_interval = settings .MCP_POLL_INTERVAL ,
204+ ) as mcp_tools :
177205 # Keep only search and list issue-related tools.
178206 issue_tools = [
179207 tool
@@ -187,21 +215,47 @@ async def execute(self, context: RequestContext, event_queue: EventQueue):
187215 "No issue-related tools found from the GitHub MCP server. "
188216 "Ensure your PAT scopes allow issue access and the server is reachable."
189217 )
190- await self ._run_agent (messages , settings , event_emitter , issue_tools )
218+ # Tool output is bounded inside GitIssueAgent (wrap_tool_output).
219+ await self ._run_agent (messages , settings , event_emitter , issue_tools , cancel_event )
191220 else :
192- await self ._run_agent (messages , settings , event_emitter , None )
193-
221+ await self ._run_agent (messages , settings , event_emitter , None , cancel_event )
222+
223+ except (asyncio .CancelledError , TaskCancelled ):
224+ # A2A raises CancelledError in this task on cancel/disconnect; the crew
225+ # may also surface TaskCancelled after observing the flag. Signal the
226+ # worker thread to stop and report the task as cancelled.
227+ cancel_event .set ()
228+ logging .info ("Task %s cancelled; stopping crew run" , task .id )
229+ try :
230+ await task_updater .cancel ()
231+ except Exception : # noqa: BLE001 - best-effort status on an already-torn-down queue
232+ pass
233+ raise
194234 except Exception as e :
195235 traceback .print_exc ()
196236 await event_emitter .emit_event (
197237 f"I'm sorry I was unable to fulfill your request. I encountered the following exception: { str (e )} " , True
198238 )
239+ finally :
240+ self ._cancel_events .pop (task .id , None )
199241
200242 async def cancel (self , context : RequestContext , event_queue : EventQueue ) -> None :
201243 """
202- Not implemented
244+ Signal the running crew for this task to stop, and report cancellation.
245+
246+ The crew runs in a worker thread, so we flip its cooperative-cancel flag;
247+ it stops at the next step boundary rather than mid-LLM-call.
203248 """
204- raise Exception ("cancel not supported" )
249+ task = context .current_task
250+ task_id = task .id if task else None
251+ cancel_event = self ._cancel_events .get (task_id ) if task_id else None
252+ if cancel_event is not None :
253+ cancel_event .set ()
254+ logging .info ("Cancellation requested for task %s" , task_id )
255+
256+ if task_id :
257+ task_updater = TaskUpdater (event_queue , task_id , task .context_id )
258+ await task_updater .cancel ()
205259
206260
207261def run ():
0 commit comments