** Please make sure you read the contribution guide and file the issues in the right place. **
Describe the bug
BaseCodeExecutor.execute_code() has a synchronous contract, and the LLM flow calls it directly on the running event loop:
src/google/adk/flows/llm_flows/_code_execution.py — _run_pre_processor (call site around L246)
src/google/adk/flows/llm_flows/_code_execution.py — _run_post_processor (call site around L356)
Both enclosing functions are async, so any executor whose execute_code performs blocking I/O freezes the entire event loop for the duration of the execution.
This is especially severe with AgentEngineSandboxCodeExecutor: a single sandboxes.execute_code call is a blocking HTTP request that regularly takes 2–3+ minutes for document-generation workloads. While it is in flight, the whole ADK FastAPI server (get_fast_api_app / api_server) is unresponsive:
/health-style endpoints and every other request stall (we measured 113–127 s of complete unresponsiveness that aligned exactly with in-flight execute_code calls),
- on Kubernetes this fails liveness probes and the pod gets killed mid-task,
- SSE streams of other sessions co-hosted on the same process stall as well,
- in-process timers (e.g. per-step LLM timeouts of other agents) fire spuriously once the loop unfreezes.
To Reproduce
- Configure an
LlmAgent with any BaseCodeExecutor whose execute_code blocks (e.g. AgentEngineSandboxCodeExecutor with a long-running snippet, or a toy executor with time.sleep(120)).
- Serve it with the ADK FastAPI app and trigger a code-execution turn via
/run_sse.
- While the execution is in flight, request
/health (or anything else) — the server does not respond until execute_code returns.
Expected behavior
Long-running code execution should not block the event loop / other requests served by the same process.
Proposed fix
Offload the call at both sites, keeping the executor contract unchanged:
code_execution_result = await asyncio.to_thread(
code_executor.execute_code,
invocation_context,
CodeExecutionInput(...),
)
BuiltInCodeExecutor is not affected (it short-circuits earlier), and executor implementations need no changes. An alternative (larger) fix would be an async contract such as execute_code_async with a default to_thread wrapper around the sync method.
I'm happy to send a PR for the asyncio.to_thread variant if that direction is acceptable.
Desktop
- OS: Linux (GKE) / macOS
- Python version: 3.12 / 3.13
- ADK version: 2.3.0 (call sites verified unchanged on current
main)
Model Information
** Please make sure you read the contribution guide and file the issues in the right place. **
Describe the bug
BaseCodeExecutor.execute_code()has a synchronous contract, and the LLM flow calls it directly on the running event loop:src/google/adk/flows/llm_flows/_code_execution.py—_run_pre_processor(call site around L246)src/google/adk/flows/llm_flows/_code_execution.py—_run_post_processor(call site around L356)Both enclosing functions are
async, so any executor whoseexecute_codeperforms blocking I/O freezes the entire event loop for the duration of the execution.This is especially severe with
AgentEngineSandboxCodeExecutor: a singlesandboxes.execute_codecall is a blocking HTTP request that regularly takes 2–3+ minutes for document-generation workloads. While it is in flight, the whole ADK FastAPI server (get_fast_api_app/ api_server) is unresponsive:/health-style endpoints and every other request stall (we measured 113–127 s of complete unresponsiveness that aligned exactly with in-flightexecute_codecalls),To Reproduce
LlmAgentwith anyBaseCodeExecutorwhoseexecute_codeblocks (e.g.AgentEngineSandboxCodeExecutorwith a long-running snippet, or a toy executor withtime.sleep(120))./run_sse./health(or anything else) — the server does not respond untilexecute_codereturns.Expected behavior
Long-running code execution should not block the event loop / other requests served by the same process.
Proposed fix
Offload the call at both sites, keeping the executor contract unchanged:
BuiltInCodeExecutoris not affected (it short-circuits earlier), and executor implementations need no changes. An alternative (larger) fix would be an async contract such asexecute_code_asyncwith a defaultto_threadwrapper around the sync method.I'm happy to send a PR for the
asyncio.to_threadvariant if that direction is acceptable.Desktop
main)Model Information