From 7817ced90b80430a69b6f51a6841aa921a33a093 Mon Sep 17 00:00:00 2001 From: Stas Moreinis Date: Fri, 22 May 2026 10:14:57 -0700 Subject: [PATCH 1/4] chore(deps): relax redis pin to support 6.x/7.x (#363) --- pyproject.toml | 2 +- requirements-dev.lock | 21 +++++++++++++++++++-- requirements.lock | 21 +++++++++++++++++++-- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 733c455b3..5a73f2dfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "jsonref>=1.1.0,<2", "temporalio>=1.26.0,<2", "aiohttp>=3.10.10,<4", - "redis>=5.2.0,<6", + "redis>=5.2.0,<8", "litellm>=1.83.7,<2", "kubernetes>=25.0.0,<36.0.0", "jinja2>=3.1.3,<4", diff --git a/requirements-dev.lock b/requirements-dev.lock index 62167cd44..b2263af59 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -112,10 +112,13 @@ frozenlist==1.8.0 # via aiosignal fsspec==2026.3.0 # via huggingface-hub +genai-prices==0.0.61 + # via pydantic-ai-slim google-auth==2.49.1 # via kubernetes griffelib==2.0.2 # via openai-agents + # via pydantic-ai-slim h11==0.16.0 # via httpcore # via uvicorn @@ -126,12 +129,15 @@ httpcore==1.0.9 httpx==0.28.1 # via agentex-sdk # via anthropic + # via genai-prices # via httpx-aiohttp # via huggingface-hub # via langsmith # via litellm # via mcp # via openai + # via pydantic-ai-slim + # via pydantic-graph # via respx # via scale-gp # via scale-gp-beta @@ -196,6 +202,8 @@ langsmith==0.7.22 # via langchain-core litellm==1.83.7 # via agentex-sdk +logfire-api==4.33.0 + # via pydantic-graph markdown-it-py==3.0.0 # via rich markupsafe==3.0.3 @@ -236,6 +244,7 @@ opentelemetry-api==1.40.0 # via ddtrace # via opentelemetry-sdk # via opentelemetry-semantic-conventions + # via pydantic-ai-slim opentelemetry-sdk==1.40.0 # via agentex-sdk opentelemetry-semantic-conventions==0.61b0 @@ -287,18 +296,25 @@ pydantic==2.12.5 # via agentex-sdk # via anthropic # via fastapi + # via genai-prices # via langchain-core # via langsmith # via litellm # via mcp # via openai # via openai-agents + # via pydantic-ai-slim + # via pydantic-graph # via pydantic-settings # via python-on-whales # via scale-gp # via scale-gp-beta +pydantic-ai-slim==1.101.0 + # via agentex-sdk pydantic-core==2.41.5 # via pydantic +pydantic-graph==1.101.0 + # via pydantic-ai-slim pydantic-settings==2.13.1 # via mcp pygments==2.19.2 @@ -308,7 +324,6 @@ pygments==2.19.2 # via rich pyjwt==2.12.1 # via mcp - # via redis pyright==1.1.399 pytest==8.4.2 # via agentex-sdk @@ -339,7 +354,7 @@ pyzmq==27.1.0 # via jupyter-client questionary==2.1.1 # via agentex-sdk -redis==5.3.1 +redis==7.4.0 # via agentex-sdk referencing==0.37.0 # via jsonschema @@ -459,6 +474,8 @@ typing-inspection==0.4.2 # via fastapi # via mcp # via pydantic + # via pydantic-ai-slim + # via pydantic-graph # via pydantic-settings tzdata==2025.3 # via agentex-sdk diff --git a/requirements.lock b/requirements.lock index 414afb203..986bff99b 100644 --- a/requirements.lock +++ b/requirements.lock @@ -99,10 +99,13 @@ frozenlist==1.8.0 # via aiosignal fsspec==2026.3.0 # via huggingface-hub +genai-prices==0.0.61 + # via pydantic-ai-slim google-auth==2.49.1 # via kubernetes griffelib==2.0.2 # via openai-agents + # via pydantic-ai-slim h11==0.16.0 # via httpcore # via uvicorn @@ -113,12 +116,15 @@ httpcore==1.0.9 httpx==0.28.1 # via agentex-sdk # via anthropic + # via genai-prices # via httpx-aiohttp # via huggingface-hub # via langsmith # via litellm # via mcp # via openai + # via pydantic-ai-slim + # via pydantic-graph # via scale-gp # via scale-gp-beta httpx-aiohttp==0.1.12 @@ -180,6 +186,8 @@ langsmith==0.7.22 # via langchain-core litellm==1.83.7 # via agentex-sdk +logfire-api==4.33.0 + # via pydantic-graph markdown-it-py==4.0.0 # via rich markupsafe==3.0.3 @@ -214,6 +222,7 @@ opentelemetry-api==1.40.0 # via ddtrace # via opentelemetry-sdk # via opentelemetry-semantic-conventions + # via pydantic-ai-slim opentelemetry-sdk==1.40.0 # via agentex-sdk opentelemetry-semantic-conventions==0.61b0 @@ -260,18 +269,25 @@ pydantic==2.12.5 # via agentex-sdk # via anthropic # via fastapi + # via genai-prices # via langchain-core # via langsmith # via litellm # via mcp # via openai # via openai-agents + # via pydantic-ai-slim + # via pydantic-graph # via pydantic-settings # via python-on-whales # via scale-gp # via scale-gp-beta +pydantic-ai-slim==1.101.0 + # via agentex-sdk pydantic-core==2.41.5 # via pydantic +pydantic-graph==1.101.0 + # via pydantic-ai-slim pydantic-settings==2.13.1 # via mcp pygments==2.20.0 @@ -281,7 +297,6 @@ pygments==2.20.0 # via rich pyjwt==2.12.1 # via mcp - # via redis pytest==9.0.2 # via agentex-sdk # via pytest-asyncio @@ -308,7 +323,7 @@ pyzmq==27.1.0 # via jupyter-client questionary==2.1.1 # via agentex-sdk -redis==5.3.1 +redis==7.4.0 # via agentex-sdk referencing==0.37.0 # via jsonschema @@ -424,6 +439,8 @@ typing-inspection==0.4.2 # via fastapi # via mcp # via pydantic + # via pydantic-ai-slim + # via pydantic-graph # via pydantic-settings tzdata==2025.3 # via agentex-sdk From f10b378777413e97e83848b94e28cd3d359c0a68 Mon Sep 17 00:00:00 2001 From: Bill Zhang Date: Mon, 25 May 2026 02:41:18 -0700 Subject: [PATCH 2/4] fix(cli): add local_development.redis_enabled manifest opt-out (MAY-1086) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agentex agents run unconditionally sets REDIS_URL=redis://localhost:6379 in the agent process env. Combined with the module-level RedisStreamRepository instantiated by `from agentex.lib import adk`, this causes silent request hangs for agents that don't use adk.messages/adk.streaming when no local Redis is reachable (e.g. Temporal-direct async agents in restricted dev pods). The lazy redis.asyncio client only fails on first publish, so there is no startup error to point at the misconfiguration. Add an explicit `local_development.redis_enabled: bool = true` flag. Default true preserves existing behavior; users with no local Redis can set false to skip the REDIS_URL default. acp_type is not a reliable discriminator here — the codebase has acp_type=async tutorials that legitimately call adk.messages.create. Co-Authored-By: Claude Opus 4.7 --- src/agentex/lib/cli/handlers/run_handlers.py | 8 +++++++- src/agentex/lib/sdk/config/local_development_config.py | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/agentex/lib/cli/handlers/run_handlers.py b/src/agentex/lib/cli/handlers/run_handlers.py index adf44a197..4dd892271 100644 --- a/src/agentex/lib/cli/handlers/run_handlers.py +++ b/src/agentex/lib/cli/handlers/run_handlers.py @@ -365,13 +365,19 @@ def create_agent_environment(manifest: AgentManifest) -> dict[str, str]: env_vars = { "ENVIRONMENT": "development", "TEMPORAL_ADDRESS": "localhost:7233", - "REDIS_URL": "redis://localhost:6379", "AGENT_NAME": manifest.agent.name, "ACP_TYPE": manifest.agent.acp_type, "ACP_URL": f"http://{manifest.local_development.agent.host_address}", # type: ignore[union-attr] "ACP_PORT": str(manifest.local_development.agent.port), # type: ignore[union-attr] } + # Gate the default REDIS_URL on an explicit manifest flag. Agents that don't use + # adk.messages/adk.streaming can set local_development.redis_enabled: false to avoid + # the localhost:6379 default, which otherwise causes silent request hangs when no + # local Redis is reachable (MAY-1086). + if manifest.local_development is None or manifest.local_development.redis_enabled: + env_vars["REDIS_URL"] = "redis://localhost:6379" + if manifest.agent.agent_input_type: env_vars["AGENT_INPUT_TYPE"] = manifest.agent.agent_input_type diff --git a/src/agentex/lib/sdk/config/local_development_config.py b/src/agentex/lib/sdk/config/local_development_config.py index 061500ab7..2bb7c70ae 100644 --- a/src/agentex/lib/sdk/config/local_development_config.py +++ b/src/agentex/lib/sdk/config/local_development_config.py @@ -56,3 +56,12 @@ class LocalDevelopmentConfig(BaseModel): paths: LocalPathsConfig | None = Field( default=None, description="File paths for local development" ) + redis_enabled: bool = Field( + default=True, + description=( + "Whether the local CLI should set REDIS_URL=redis://localhost:6379 for the " + "agent process. Set to false for agents that don't use adk.messages/adk.streaming " + "when no local Redis is available, to avoid silent request hangs from the lazy " + "Redis client." + ), + ) From f41703c90c8879efde6d1a2cf639886115c681ec Mon Sep 17 00:00:00 2001 From: Bill Zhang Date: Mon, 25 May 2026 02:53:26 -0700 Subject: [PATCH 3/4] fix(cli): plug parent-env REDIS_URL leak and add coverage (MAY-1086) Address staff-engineer review on #365: - Pop REDIS_URL from the inherited env when redis_enabled is false. The previous implementation only avoided seeding a default, so a stale REDIS_URL exported by the parent shell still leaked into the agent process and reproduced the same silent hang the opt-out was meant to prevent. - Drop the unreachable `local_development is None` short-circuit; the dict literal above already dereferences manifest.local_development with a # type: ignore[union-attr], so the None branch can never run. - Add tests/lib/cli/test_run_handlers.py covering the three branches: default seeds REDIS_URL, opt-out with clean parent env clears it, opt-out with a parent-shell REDIS_URL still clears it. Co-Authored-By: Claude Opus 4.7 --- src/agentex/lib/cli/handlers/run_handlers.py | 7 ++- tests/lib/cli/test_run_handlers.py | 53 ++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 tests/lib/cli/test_run_handlers.py diff --git a/src/agentex/lib/cli/handlers/run_handlers.py b/src/agentex/lib/cli/handlers/run_handlers.py index 4dd892271..c9db31c1c 100644 --- a/src/agentex/lib/cli/handlers/run_handlers.py +++ b/src/agentex/lib/cli/handlers/run_handlers.py @@ -374,9 +374,12 @@ def create_agent_environment(manifest: AgentManifest) -> dict[str, str]: # Gate the default REDIS_URL on an explicit manifest flag. Agents that don't use # adk.messages/adk.streaming can set local_development.redis_enabled: false to avoid # the localhost:6379 default, which otherwise causes silent request hangs when no - # local Redis is reachable (MAY-1086). - if manifest.local_development is None or manifest.local_development.redis_enabled: + # local Redis is reachable (MAY-1086). When opting out, also drop any REDIS_URL the + # parent shell may have exported, so the opt-out actually guarantees a clean env. + if manifest.local_development.redis_enabled: # type: ignore[union-attr] env_vars["REDIS_URL"] = "redis://localhost:6379" + else: + env.pop("REDIS_URL", None) if manifest.agent.agent_input_type: env_vars["AGENT_INPUT_TYPE"] = manifest.agent.agent_input_type diff --git a/tests/lib/cli/test_run_handlers.py b/tests/lib/cli/test_run_handlers.py new file mode 100644 index 000000000..fbaffe9fd --- /dev/null +++ b/tests/lib/cli/test_run_handlers.py @@ -0,0 +1,53 @@ +"""Tests for run_handlers.create_agent_environment — REDIS_URL gating (MAY-1086).""" + +from __future__ import annotations + +import pytest + +from agentex.lib.cli.handlers.run_handlers import create_agent_environment +from agentex.lib.sdk.config.agent_manifest import AgentManifest + + +@pytest.fixture +def manifest_path() -> str: + """A real tutorial manifest with acp_type=async and default redis_enabled.""" + return "examples/tutorials/10_async/00_base/110_pydantic_ai/manifest.yaml" + + +class TestCreateAgentEnvironmentRedisGating: + def test_default_seeds_redis_url(self, manifest_path: str, monkeypatch: pytest.MonkeyPatch): + """With redis_enabled unset (default true), CLI seeds the localhost REDIS_URL.""" + monkeypatch.delenv("REDIS_URL", raising=False) + manifest = AgentManifest.from_yaml(file_path=manifest_path) + assert manifest.local_development is not None + assert manifest.local_development.redis_enabled is True + + env = create_agent_environment(manifest) + + assert env.get("REDIS_URL") == "redis://localhost:6379" + + def test_opt_out_clears_redis_url_when_parent_env_clean( + self, manifest_path: str, monkeypatch: pytest.MonkeyPatch + ): + """With redis_enabled=false and no parent REDIS_URL, REDIS_URL is absent.""" + monkeypatch.delenv("REDIS_URL", raising=False) + manifest = AgentManifest.from_yaml(file_path=manifest_path) + assert manifest.local_development is not None + manifest.local_development.redis_enabled = False + + env = create_agent_environment(manifest) + + assert "REDIS_URL" not in env + + def test_opt_out_clears_redis_url_when_parent_env_has_one( + self, manifest_path: str, monkeypatch: pytest.MonkeyPatch + ): + """With redis_enabled=false, a stale parent-shell REDIS_URL must not leak through.""" + monkeypatch.setenv("REDIS_URL", "redis://leftover.from.parent.shell:6379") + manifest = AgentManifest.from_yaml(file_path=manifest_path) + assert manifest.local_development is not None + manifest.local_development.redis_enabled = False + + env = create_agent_environment(manifest) + + assert "REDIS_URL" not in env From 760328a60feaecdba9b315dbaa9924bed903a2b8 Mon Sep 17 00:00:00 2001 From: Bill Zhang Date: Mon, 25 May 2026 02:59:18 -0700 Subject: [PATCH 4/4] test(cli): decouple run_handlers tests from tutorial path Use a tmp_path YAML fixture rendered from a minimal inline template instead of reading examples/tutorials/.../manifest.yaml, so the test survives tutorial renames or removals while still exercising the real AgentManifest.from_yaml loader. Co-Authored-By: Claude Opus 4.7 --- tests/lib/cli/test_run_handlers.py | 46 +++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/tests/lib/cli/test_run_handlers.py b/tests/lib/cli/test_run_handlers.py index fbaffe9fd..3f2252cc8 100644 --- a/tests/lib/cli/test_run_handlers.py +++ b/tests/lib/cli/test_run_handlers.py @@ -2,23 +2,45 @@ from __future__ import annotations +from pathlib import Path + import pytest from agentex.lib.cli.handlers.run_handlers import create_agent_environment from agentex.lib.sdk.config.agent_manifest import AgentManifest +_MANIFEST_TEMPLATE = """\ +build: + context: + root: . + dockerfile: Dockerfile + +local_development: + agent: + port: 8000 + host_address: host.docker.internal +{redis_line} + +agent: + acp_type: async + name: test-agent + description: Fixture manifest for run_handlers tests. +""" -@pytest.fixture -def manifest_path() -> str: - """A real tutorial manifest with acp_type=async and default redis_enabled.""" - return "examples/tutorials/10_async/00_base/110_pydantic_ai/manifest.yaml" + +def _write_manifest(tmp_path: Path, redis_enabled: bool | None) -> AgentManifest: + """Write a minimal manifest with the requested redis_enabled value (or omit for default).""" + redis_line = "" if redis_enabled is None else f" redis_enabled: {str(redis_enabled).lower()}" + manifest_path = tmp_path / "manifest.yaml" + manifest_path.write_text(_MANIFEST_TEMPLATE.format(redis_line=redis_line)) + return AgentManifest.from_yaml(file_path=str(manifest_path)) class TestCreateAgentEnvironmentRedisGating: - def test_default_seeds_redis_url(self, manifest_path: str, monkeypatch: pytest.MonkeyPatch): + def test_default_seeds_redis_url(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): """With redis_enabled unset (default true), CLI seeds the localhost REDIS_URL.""" monkeypatch.delenv("REDIS_URL", raising=False) - manifest = AgentManifest.from_yaml(file_path=manifest_path) + manifest = _write_manifest(tmp_path, redis_enabled=None) assert manifest.local_development is not None assert manifest.local_development.redis_enabled is True @@ -27,26 +49,22 @@ def test_default_seeds_redis_url(self, manifest_path: str, monkeypatch: pytest.M assert env.get("REDIS_URL") == "redis://localhost:6379" def test_opt_out_clears_redis_url_when_parent_env_clean( - self, manifest_path: str, monkeypatch: pytest.MonkeyPatch + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ): """With redis_enabled=false and no parent REDIS_URL, REDIS_URL is absent.""" monkeypatch.delenv("REDIS_URL", raising=False) - manifest = AgentManifest.from_yaml(file_path=manifest_path) - assert manifest.local_development is not None - manifest.local_development.redis_enabled = False + manifest = _write_manifest(tmp_path, redis_enabled=False) env = create_agent_environment(manifest) assert "REDIS_URL" not in env def test_opt_out_clears_redis_url_when_parent_env_has_one( - self, manifest_path: str, monkeypatch: pytest.MonkeyPatch + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ): """With redis_enabled=false, a stale parent-shell REDIS_URL must not leak through.""" monkeypatch.setenv("REDIS_URL", "redis://leftover.from.parent.shell:6379") - manifest = AgentManifest.from_yaml(file_path=manifest_path) - assert manifest.local_development is not None - manifest.local_development.redis_enabled = False + manifest = _write_manifest(tmp_path, redis_enabled=False) env = create_agent_environment(manifest)