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 diff --git a/src/agentex/lib/cli/handlers/run_handlers.py b/src/agentex/lib/cli/handlers/run_handlers.py index adf44a197..c9db31c1c 100644 --- a/src/agentex/lib/cli/handlers/run_handlers.py +++ b/src/agentex/lib/cli/handlers/run_handlers.py @@ -365,13 +365,22 @@ 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). 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/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." + ), + ) diff --git a/tests/lib/cli/test_run_handlers.py b/tests/lib/cli/test_run_handlers.py new file mode 100644 index 000000000..3f2252cc8 --- /dev/null +++ b/tests/lib/cli/test_run_handlers.py @@ -0,0 +1,71 @@ +"""Tests for run_handlers.create_agent_environment — REDIS_URL gating (MAY-1086).""" + +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. +""" + + +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, 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 = _write_manifest(tmp_path, redis_enabled=None) + 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, 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 = _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, 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 = _write_manifest(tmp_path, redis_enabled=False) + + env = create_agent_environment(manifest) + + assert "REDIS_URL" not in env