Skip to content

Commit c9de7c5

Browse files
committed
feat(adk): make AgentEx client HTTP timeouts configurable by env var
The AgentEx client's timeout comes from the SDK's DEFAULT_TIMEOUT, Timeout(connect=5.0, read=300, write=300, pool=300). Application code cannot change it: the ADK constructs its own clients internally at 14 sites (messages, tasks, events, tracing), all through create_async_agentex_client(), and none accepts a timeout from the caller. No environment variable controlled it either. The connect timeout is the one that matters. An AgentEx backend accepts connections serially, so connect latency grows with the number of concurrent callers. Measured against a local backend: concurrency 1 84 ms concurrency 20 423 ms concurrency 100 500 ms concurrency 200 1,010 ms An agent running 100 concurrent activities, each making ADK calls, pushes past the 5s budget and fails with httpcore.ConnectTimeout. One run produced 197 such failures in a single activity. A 5s connect against a 300s read is also internally inconsistent. Adds AGENTEX_CLIENT_{CONNECT,READ,WRITE,POOL}_TIMEOUT_SECONDS, following the existing EnvVarKeys and EnvironmentVariables pattern. Defaults equal the current DEFAULT_TIMEOUT, so an unconfigured process is unchanged. An explicit timeout= argument still wins, and a malformed value falls back to the SDK default with a warning rather than preventing client creation.
1 parent 4b446e6 commit c9de7c5

3 files changed

Lines changed: 143 additions & 0 deletions

File tree

‎src/agentex/lib/adk/utils/_modules/client.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,35 @@ def auth_flow(self, request):
2626
yield request
2727

2828

29+
def _timeout_from_env() -> httpx.Timeout:
30+
"""Build the client timeout from environment variables.
31+
32+
Defaults match the SDK's DEFAULT_TIMEOUT, so an unconfigured process behaves
33+
exactly as before. The connect timeout is the one worth raising: an AgentEx
34+
backend accepts connections serially, so connect latency grows with the number
35+
of callers and the 5s default is reached when a few hundred are in flight.
36+
"""
37+
env_vars = EnvironmentVariables.refresh()
38+
return httpx.Timeout(
39+
connect=env_vars.AGENTEX_CLIENT_CONNECT_TIMEOUT_SECONDS,
40+
read=env_vars.AGENTEX_CLIENT_READ_TIMEOUT_SECONDS,
41+
write=env_vars.AGENTEX_CLIENT_WRITE_TIMEOUT_SECONDS,
42+
pool=env_vars.AGENTEX_CLIENT_POOL_TIMEOUT_SECONDS,
43+
)
44+
45+
2946
def create_async_agentex_client(**kwargs) -> AsyncAgentex:
47+
"""Create an AsyncAgentex client.
48+
49+
An explicit ``timeout=`` always wins; otherwise the timeout comes from the
50+
AGENTEX_CLIENT_*_TIMEOUT_SECONDS environment variables.
51+
"""
52+
if "timeout" not in kwargs:
53+
try:
54+
kwargs["timeout"] = _timeout_from_env()
55+
except Exception as exc:
56+
# Never let timeout configuration stop a client being created.
57+
logger.warning("Falling back to SDK default timeout: %r", exc)
3058
client = AsyncAgentex(**kwargs)
3159
client._client.auth = EnvAuth()
3260
return client

‎src/agentex/lib/environment_variables.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ class EnvVarKeys(str, Enum):
2020
TEMPORAL_ADDRESS = "TEMPORAL_ADDRESS"
2121
REDIS_URL = "REDIS_URL"
2222
AGENTEX_BASE_URL = "AGENTEX_BASE_URL"
23+
# AgentEx client HTTP timeouts (seconds)
24+
AGENTEX_CLIENT_CONNECT_TIMEOUT_SECONDS = "AGENTEX_CLIENT_CONNECT_TIMEOUT_SECONDS"
25+
AGENTEX_CLIENT_READ_TIMEOUT_SECONDS = "AGENTEX_CLIENT_READ_TIMEOUT_SECONDS"
26+
AGENTEX_CLIENT_WRITE_TIMEOUT_SECONDS = "AGENTEX_CLIENT_WRITE_TIMEOUT_SECONDS"
27+
AGENTEX_CLIENT_POOL_TIMEOUT_SECONDS = "AGENTEX_CLIENT_POOL_TIMEOUT_SECONDS"
2328
# Agent Identifiers
2429
AGENT_NAME = "AGENT_NAME"
2530
AGENT_DESCRIPTION = "AGENT_DESCRIPTION"
@@ -61,6 +66,14 @@ class EnvironmentVariables(BaseModel):
6166
TEMPORAL_ADDRESS: str | None = "localhost:7233"
6267
REDIS_URL: str | None = None
6368
AGENTEX_BASE_URL: str | None = "http://localhost:5003"
69+
# HTTP timeouts for the AgentEx client, in seconds. Defaults match the
70+
# SDK's DEFAULT_TIMEOUT, so leaving these unset changes nothing.
71+
# Raise the connect timeout when many concurrent activities share one
72+
# backend: accepts queue, and 5s is reached at a few hundred in flight.
73+
AGENTEX_CLIENT_CONNECT_TIMEOUT_SECONDS: float = 5.0
74+
AGENTEX_CLIENT_READ_TIMEOUT_SECONDS: float = 300.0
75+
AGENTEX_CLIENT_WRITE_TIMEOUT_SECONDS: float = 300.0
76+
AGENTEX_CLIENT_POOL_TIMEOUT_SECONDS: float = 300.0
6477
# Agent Identifiers
6578
AGENT_NAME: str
6679
AGENT_DESCRIPTION: str | None = None
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Timeouts for the AgentEx client are configurable by environment variable.
2+
3+
The connect timeout is the one that matters in practice. An AgentEx backend
4+
accepts connections serially, so connect latency grows with the number of
5+
concurrent callers, and the 5s default is reached once a few hundred are in
6+
flight. Before this was configurable, the only way to change it was to pass
7+
``timeout=`` at every construction site, which application code cannot do for
8+
the client the ADK builds internally.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import httpx
14+
import pytest
15+
16+
import agentex.lib.environment_variables as env_module
17+
from agentex.lib.adk.utils._modules.client import (
18+
_timeout_from_env,
19+
create_async_agentex_client,
20+
)
21+
from agentex.lib.environment_variables import EnvironmentVariables
22+
23+
24+
@pytest.fixture(autouse=True)
25+
def _clear_env_cache():
26+
"""EnvironmentVariables.refresh() memoises into a module global."""
27+
env_module.refreshed_environment_variables = None
28+
yield
29+
env_module.refreshed_environment_variables = None
30+
31+
32+
def _set_env(monkeypatch, **overrides: str) -> None:
33+
# EnvironmentVariables has required fields; set them so construction succeeds.
34+
monkeypatch.setenv("AGENT_NAME", "test-agent")
35+
monkeypatch.setenv("ACP_URL", "http://localhost:8000")
36+
for key, value in overrides.items():
37+
monkeypatch.setenv(key, value)
38+
39+
40+
def test_defaults_match_the_sdk_default_timeout(monkeypatch):
41+
"""An unconfigured process must behave exactly as it did before."""
42+
_set_env(monkeypatch)
43+
timeout = _timeout_from_env()
44+
assert timeout.connect == 5.0
45+
assert timeout.read == 300.0
46+
assert timeout.write == 300.0
47+
assert timeout.pool == 300.0
48+
49+
50+
def test_connect_timeout_is_configurable(monkeypatch):
51+
_set_env(monkeypatch, AGENTEX_CLIENT_CONNECT_TIMEOUT_SECONDS="30")
52+
timeout = _timeout_from_env()
53+
assert timeout.connect == 30.0
54+
# the others are untouched
55+
assert timeout.read == 300.0
56+
57+
58+
def test_all_four_are_configurable(monkeypatch):
59+
_set_env(
60+
monkeypatch,
61+
AGENTEX_CLIENT_CONNECT_TIMEOUT_SECONDS="30",
62+
AGENTEX_CLIENT_READ_TIMEOUT_SECONDS="120",
63+
AGENTEX_CLIENT_WRITE_TIMEOUT_SECONDS="90",
64+
AGENTEX_CLIENT_POOL_TIMEOUT_SECONDS="60",
65+
)
66+
timeout = _timeout_from_env()
67+
assert (timeout.connect, timeout.read, timeout.write, timeout.pool) == (
68+
30.0,
69+
120.0,
70+
90.0,
71+
60.0,
72+
)
73+
74+
75+
def test_client_picks_up_the_env_timeout(monkeypatch):
76+
_set_env(monkeypatch, AGENTEX_CLIENT_CONNECT_TIMEOUT_SECONDS="30")
77+
client = create_async_agentex_client(api_key="test", base_url="http://localhost:5003")
78+
assert client.timeout.connect == 30.0
79+
80+
81+
def test_explicit_timeout_wins_over_the_environment(monkeypatch):
82+
_set_env(monkeypatch, AGENTEX_CLIENT_CONNECT_TIMEOUT_SECONDS="30")
83+
client = create_async_agentex_client(
84+
api_key="test",
85+
base_url="http://localhost:5003",
86+
timeout=httpx.Timeout(connect=7.0, read=8.0, write=9.0, pool=10.0),
87+
)
88+
assert client.timeout.connect == 7.0
89+
90+
91+
def test_env_auth_is_still_attached(monkeypatch):
92+
"""The factory's original job must survive the change."""
93+
_set_env(monkeypatch)
94+
client = create_async_agentex_client(api_key="test", base_url="http://localhost:5003")
95+
assert client._client.auth is not None
96+
97+
98+
def test_a_bad_value_does_not_prevent_client_creation(monkeypatch):
99+
"""Timeout configuration must never be the reason a client fails to build."""
100+
_set_env(monkeypatch, AGENTEX_CLIENT_CONNECT_TIMEOUT_SECONDS="not-a-number")
101+
client = create_async_agentex_client(api_key="test", base_url="http://localhost:5003")
102+
assert client is not None

0 commit comments

Comments
 (0)