Skip to content

Commit 6ab765b

Browse files
fix(code_executors): handle genai.ClientError 404 in sandbox recovery
sandboxes.get() raises google.genai.errors.ClientError (HTTP 404) when a cached sandbox has been externally deleted, but the recovery path only catches google.api_core.exceptions.NotFound. The two have no shared base class, so the 404 slips past the except clause and the session becomes unrecoverable instead of falling through to the sandbox recreate logic. Added a second except for genai_errors.ClientError that treats e.code == 404 the same as NotFound, sets create_new_sandbox = True and continues normally. Non-404 errors are still re-raised. Also added two unit tests: one covering the 404 recovery path, and one verifying that non-404 ClientErrors are not silently swallowed. Fixes #5480
1 parent 22fae7e commit 6ab765b

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

src/google/adk/code_executors/agent_engine_sandbox_code_executor.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def execute_code(
131131
sandbox_name = self.sandbox_resource_name
132132
if self.sandbox_resource_name is None:
133133
from google.api_core import exceptions
134+
from google.genai import errors as genai_errors
134135
from vertexai import types
135136

136137
# use sandbox name stored in session if available.
@@ -148,6 +149,11 @@ def execute_code(
148149
create_new_sandbox = True
149150
except exceptions.NotFound:
150151
create_new_sandbox = True
152+
except genai_errors.ClientError as e:
153+
if e.code == 404:
154+
create_new_sandbox = True
155+
else:
156+
raise
151157

152158
if create_new_sandbox:
153159
# Create a new sandbox and assign it to sandbox_name.

tests/unittests/code_executors/test_agent_engine_sandbox_code_executor.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,95 @@ def test_execute_code_with_auto_create_agent_engine(
325325
== created_sandbox_name
326326
)
327327

328+
@patch("vertexai.Client")
329+
def test_execute_code_recreates_sandbox_on_genai_404(
330+
self,
331+
mock_vertexai_client,
332+
mock_invocation_context,
333+
):
334+
"""Tests sandbox recreation when sandboxes.get() raises genai ClientError 404."""
335+
from google.genai.errors import ClientError
336+
337+
mock_api_client = MagicMock()
338+
mock_vertexai_client.return_value = mock_api_client
339+
340+
existing_sandbox_name = "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/old"
341+
mock_invocation_context.session.state = {
342+
"sandbox_name": existing_sandbox_name
343+
}
344+
345+
# SDK raises genai ClientError(404), not api_core.NotFound
346+
mock_api_client.agent_engines.sandboxes.get.side_effect = ClientError(
347+
404,
348+
{"error": {"code": 404, "message": "Not Found", "status": "NOT_FOUND"}},
349+
)
350+
351+
operation_mock = MagicMock()
352+
created_sandbox_name = "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789"
353+
operation_mock.response.name = created_sandbox_name
354+
mock_api_client.agent_engines.sandboxes.create.return_value = operation_mock
355+
356+
mock_response = MagicMock()
357+
mock_json_output = MagicMock()
358+
mock_json_output.mime_type = "application/json"
359+
mock_json_output.data = json.dumps(
360+
{"msg_out": "recreated", "msg_err": ""}
361+
).encode("utf-8")
362+
mock_json_output.metadata = None
363+
mock_response.outputs = [mock_json_output]
364+
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
365+
mock_response
366+
)
367+
368+
executor = AgentEngineSandboxCodeExecutor(
369+
agent_engine_resource_name=(
370+
"projects/123/locations/us-central1/reasoningEngines/456"
371+
)
372+
)
373+
result = executor.execute_code(
374+
mock_invocation_context, CodeExecutionInput(code='print("hi")')
375+
)
376+
377+
mock_api_client.agent_engines.sandboxes.create.assert_called_once()
378+
assert (
379+
mock_invocation_context.session.state["sandbox_name"]
380+
== created_sandbox_name
381+
)
382+
assert result.stdout == "recreated"
383+
384+
@patch("vertexai.Client")
385+
def test_execute_code_reraises_non_404_genai_client_error(
386+
self,
387+
mock_vertexai_client,
388+
mock_invocation_context,
389+
):
390+
"""Tests that non-404 genai ClientErrors are re-raised."""
391+
from google.genai.errors import ClientError
392+
393+
mock_api_client = MagicMock()
394+
mock_vertexai_client.return_value = mock_api_client
395+
396+
mock_invocation_context.session.state = {
397+
"sandbox_name": "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/old"
398+
}
399+
400+
mock_api_client.agent_engines.sandboxes.get.side_effect = ClientError(
401+
500,
402+
{"error": {"code": 500, "message": "Internal error", "status": "INTERNAL"}},
403+
)
404+
405+
executor = AgentEngineSandboxCodeExecutor(
406+
agent_engine_resource_name=(
407+
"projects/123/locations/us-central1/reasoningEngines/456"
408+
)
409+
)
410+
with pytest.raises(ClientError):
411+
executor.execute_code(
412+
mock_invocation_context, CodeExecutionInput(code='print("hi")')
413+
)
414+
415+
mock_api_client.agent_engines.sandboxes.create.assert_not_called()
416+
328417
@patch("vertexai.Client")
329418
@patch.dict(
330419
os.environ,

0 commit comments

Comments
 (0)