Skip to content

Commit 27f75a2

Browse files
committed
refactor(agents): AgentConfig holds runtime-only settings; SDK log level on Configuration
Connection/auth (server_url, api_key, auth_key, auth_secret) and the dead llm_retry_count / secret_strict_mode fields are removed from AgentConfig — connection and auth belong to the conductor Configuration. log_level moves onto Configuration (settable param + CONDUCTOR_LOG_LEVEL/AGENTSPAN_LOG_LEVEL env) so it is SDK-wide; the runtime reads it from there. run.configure() now takes a Configuration for connection. AgentConfig keeps worker + liveness settings only. Unit tests updated.
1 parent 98bd421 commit 27f75a2

13 files changed

Lines changed: 219 additions & 344 deletions

src/conductor/ai/agents/run.py

Lines changed: 18 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232

3333
if TYPE_CHECKING:
3434
from conductor.ai.agents.runtime.config import AgentConfig
35-
from conductor.client.configuration.configuration import Configuration
3635

3736
from conductor.ai.agents.agent import Agent
3837
from conductor.ai.agents.result import (
@@ -48,23 +47,26 @@
4847
# ── Singleton runtime ────────────────────────────────────────────────────
4948

5049
_default_config: Optional[AgentConfig] = None
50+
_default_configuration = None # conductor Configuration (connection/auth/log level)
5151
_default_runtime = None
5252
_runtime_lock = threading.Lock()
5353

5454

55-
def configure(config: Optional[AgentConfig] = None, **kwargs):
55+
def configure(config: Optional[AgentConfig] = None, *, configuration=None, **kwargs):
5656
"""Pre-configure the default singleton runtime.
5757
5858
Must be called **before** the first :func:`run`, :func:`start`, or
5959
:func:`stream` call. The configuration persists across
6060
:func:`shutdown` / recreate cycles.
6161
6262
Args:
63-
config: An :class:`AgentConfig` instance. If provided, *kwargs*
64-
are ignored.
65-
**kwargs: Individual config fields to override on top of
66-
:meth:`AgentConfig.from_env` defaults (e.g.
67-
``server_url="https://prod:8080/api"``).
63+
config: An :class:`AgentConfig` (agent-runtime settings). If provided,
64+
*kwargs* are ignored.
65+
configuration: A conductor :class:`Configuration` carrying the server
66+
connection, auth, and log level. Defaults to the env-based
67+
``Configuration()``.
68+
**kwargs: Individual :class:`AgentConfig` field overrides on top of
69+
:meth:`AgentConfig.from_env` defaults (e.g. ``worker_thread_count=4``).
6870
6971
Raises:
7072
RuntimeError: If the singleton runtime already exists. Call
@@ -74,19 +76,22 @@ def configure(config: Optional[AgentConfig] = None, **kwargs):
7476
Example::
7577
7678
import conductor.ai.agents as ag
79+
from conductor.client.configuration.configuration import Configuration
7780
78-
ag.configure(server_url="https://prod:8080/api")
81+
ag.configure(configuration=Configuration(server_api_url="https://prod:8080/api"))
7982
result = ag.run(agent, "Hello!")
8083
"""
81-
global _default_config, _default_runtime
84+
global _default_config, _default_configuration, _default_runtime
8285
if _default_runtime is not None:
8386
raise RuntimeError(
8487
"configure() must be called before the first run/start/stream call. "
8588
"Call shutdown() first to reset the default runtime."
8689
)
90+
if configuration is not None:
91+
_default_configuration = configuration
8792
if config is not None:
8893
_default_config = config
89-
else:
94+
elif kwargs:
9095
from conductor.ai.agents.runtime.config import AgentConfig
9196

9297
base = AgentConfig.from_env()
@@ -97,27 +102,6 @@ def configure(config: Optional[AgentConfig] = None, **kwargs):
97102
_default_config = base
98103

99104

100-
def _configuration_from(cfg: AgentConfig) -> Configuration:
101-
"""Map an :class:`AgentConfig`'s connection fields to a conductor
102-
:class:`Configuration` (the sugar layer owns this mapping — the runtime
103-
itself only accepts a ``Configuration``)."""
104-
from conductor.client.configuration.configuration import Configuration
105-
from conductor.client.configuration.settings.authentication_settings import (
106-
AuthenticationSettings,
107-
)
108-
109-
effective_key = cfg.api_key or cfg.auth_key
110-
authentication_settings = (
111-
AuthenticationSettings(key_id=effective_key, key_secret=cfg.auth_secret or "")
112-
if effective_key
113-
else None
114-
)
115-
return Configuration(
116-
server_api_url=cfg.server_url,
117-
authentication_settings=authentication_settings,
118-
)
119-
120-
121105
def _get_default_runtime():
122106
"""Return (or create) the module-level default AgentRuntime singleton."""
123107
global _default_runtime
@@ -126,12 +110,9 @@ def _get_default_runtime():
126110
if _default_runtime is None:
127111
from conductor.ai.agents.runtime.runtime import AgentRuntime
128112

129-
if _default_config is not None:
130-
_default_runtime = AgentRuntime(
131-
_configuration_from(_default_config), settings=_default_config
132-
)
133-
else:
134-
_default_runtime = AgentRuntime()
113+
_default_runtime = AgentRuntime(
114+
_default_configuration, settings=_default_config
115+
)
135116
logger.info("Created default AgentRuntime singleton")
136117
return _default_runtime
137118

src/conductor/ai/agents/runtime/config.py

Lines changed: 29 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
99
Usage::
1010
11-
config = AgentConfig.from_env() # load from env
12-
config = AgentConfig(server_url="http://custom:8080/api") # explicit
11+
config = AgentConfig.from_env() # load from env
12+
config = AgentConfig(worker_thread_count=4) # explicit override
1313
"""
1414

1515
from __future__ import annotations
@@ -44,77 +44,59 @@ def _env_int(var: str, default: int = 0) -> int:
4444
return int(val)
4545

4646

47+
def _env_float(var: str, default: float = 0.0) -> float:
48+
"""Read a float environment variable."""
49+
val = os.environ.get(var)
50+
if val is None or val.strip() == "":
51+
return default
52+
return float(val)
53+
54+
4755
@dataclass
4856
class AgentConfig:
49-
"""Configuration for the agents runtime.
57+
"""Agent-runtime settings.
58+
59+
Server connection and auth (URL, credentials) and the SDK-wide log level
60+
live on the conductor :class:`Configuration`, not here — this holds only
61+
agent-runtime concerns (worker pool + liveness monitoring).
5062
5163
Attributes:
52-
server_url: Agentspan server API URL.
53-
api_key: Bearer token or static API key for the Authorization header.
54-
Preferred over auth_key/auth_secret for new deployments.
55-
auth_key: Auth key (kept for backward compatibility).
56-
auth_secret: Auth secret (kept for backward compatibility).
5764
worker_poll_interval_ms: Worker polling interval in milliseconds.
5865
worker_thread_count: Number of threads per worker.
5966
auto_start_workers: Whether to auto-start worker processes.
6067
daemon_workers: Whether worker processes are daemon (killed on exit).
6168
auto_register_integrations: Auto-create LLM integrations on startup.
62-
secret_strict_mode: When ``True``, disables env var fallback for
63-
credential resolution. Required credentials must come from the
64-
credential service.
65-
log_level: Logging level for the agentspan logger.
69+
streaming_enabled: Whether ``stream()`` uses server-sent events.
70+
liveness_enabled: Start a server-liveness monitor for stateful runs so
71+
``result()`` / ``join()`` don't block forever if the worker dies.
72+
liveness_stall_seconds: Idle window (no polls) before a run is
73+
considered stalled.
74+
liveness_check_interval_seconds: How often the monitor polls.
6675
"""
6776

68-
server_url: str = "http://localhost:8080/api"
69-
api_key: Optional[str] = None
70-
auth_key: Optional[str] = None
71-
auth_secret: Optional[str] = None
72-
llm_retry_count: int = 3
7377
worker_poll_interval_ms: int = 100
7478
worker_thread_count: int = 1
7579
auto_start_workers: bool = True
7680
daemon_workers: bool = True
7781
auto_register_integrations: bool = False
7882
streaming_enabled: bool = True
79-
secret_strict_mode: bool = False
80-
log_level: str = "INFO"
81-
82-
def __post_init__(self):
83-
"""Normalise server_url: auto-append /api if missing."""
84-
if self.server_url:
85-
stripped = self.server_url.rstrip("/")
86-
if not stripped.endswith("/api"):
87-
logger.info(
88-
"server_url %r does not end with '/api' — appending automatically.",
89-
self.server_url,
90-
)
91-
self.server_url = stripped + "/api"
92-
else:
93-
self.server_url = stripped
83+
liveness_enabled: bool = True
84+
liveness_stall_seconds: float = 30.0
85+
liveness_check_interval_seconds: float = 10.0
9486

9587
@classmethod
9688
def from_env(cls) -> AgentConfig:
9789
"""Create an ``AgentConfig`` by reading ``AGENTSPAN_*`` env vars."""
98-
log_level = _env("AGENTSPAN_LOG_LEVEL", "INFO")
99-
if isinstance(log_level, str) and log_level.strip() == "":
100-
log_level = "INFO"
10190
return cls(
102-
server_url=_env("AGENTSPAN_SERVER_URL", "http://localhost:8080/api"),
103-
api_key=_env("AGENTSPAN_API_KEY"),
104-
auth_key=_env("AGENTSPAN_AUTH_KEY"),
105-
auth_secret=_env("AGENTSPAN_AUTH_SECRET"),
106-
llm_retry_count=_env_int("AGENTSPAN_LLM_RETRY_COUNT", 3),
10791
worker_poll_interval_ms=_env_int("AGENTSPAN_WORKER_POLL_INTERVAL", 100),
10892
worker_thread_count=_env_int("AGENTSPAN_WORKER_THREADS", 1),
10993
auto_start_workers=_env_bool("AGENTSPAN_AUTO_START_WORKERS", True),
11094
daemon_workers=_env_bool("AGENTSPAN_DAEMON_WORKERS", True),
11195
auto_register_integrations=_env_bool("AGENTSPAN_INTEGRATIONS_AUTO_REGISTER", False),
11296
streaming_enabled=_env_bool("AGENTSPAN_STREAMING_ENABLED", True),
113-
secret_strict_mode=_env_bool("AGENTSPAN_SECRET_STRICT_MODE", False),
114-
log_level=log_level,
97+
liveness_enabled=_env_bool("AGENTSPAN_LIVENESS_ENABLED", True),
98+
liveness_stall_seconds=_env_float("AGENTSPAN_LIVENESS_STALL_SECONDS", 30.0),
99+
liveness_check_interval_seconds=_env_float(
100+
"AGENTSPAN_LIVENESS_CHECK_INTERVAL_SECONDS", 10.0
101+
),
115102
)
116-
117-
@property
118-
def api_secret(self) -> Optional[str]:
119-
"""Alias for :attr:`auth_secret` (industry-standard naming)."""
120-
return self.auth_secret

src/conductor/ai/agents/runtime/runtime.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -414,10 +414,8 @@ def __init__(
414414
self._integration_api_available: Optional[bool] = None
415415
self._sse_fallback_warned = False
416416

417-
# Apply user-configured log level to all conductor.ai loggers
418-
logging.getLogger("conductor.ai").setLevel(
419-
getattr(logging, self._config.log_level.upper(), logging.INFO)
420-
)
417+
# Apply the SDK-wide log level (from Configuration) to conductor.ai loggers
418+
logging.getLogger("conductor.ai").setLevel(self._conductor_config.log_level)
421419

422420
# Control-plane client for the agent API (start/deploy/compile/status/
423421
# execution/respond/stop/signal/SSE). Built from the same Conductor

src/conductor/client/configuration/configuration.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ def __init__(
3131
authentication_settings: AuthenticationSettings = None,
3232
server_api_url: Optional[str] = None,
3333
auth_token_ttl_min: int = 45,
34-
register_schema: Optional[bool] = None
34+
register_schema: Optional[bool] = None,
35+
log_level=None
3536
):
3637
if server_api_url is not None:
3738
self.host = server_api_url
@@ -59,8 +60,23 @@ def __init__(
5960
self.authentication_settings = None
6061

6162

62-
# Debug switch
63+
# Debug switch (sets __log_level to DEBUG/INFO)
6364
self.debug = debug
65+
# Explicit SDK-wide log level: a level name ("INFO", "DEBUG", …) or an
66+
# int. Overrides the debug-derived default; falls back to the
67+
# CONDUCTOR_LOG_LEVEL / AGENTSPAN_LOG_LEVEL env vars. Applies to every
68+
# SDK component (clients, workers, agent runtime).
69+
resolved_level = (
70+
log_level
71+
if log_level is not None
72+
else (os.getenv("CONDUCTOR_LOG_LEVEL") or os.getenv("AGENTSPAN_LOG_LEVEL"))
73+
)
74+
if resolved_level is not None and str(resolved_level).strip() != "":
75+
self.__log_level = (
76+
resolved_level
77+
if isinstance(resolved_level, int)
78+
else getattr(logging, str(resolved_level).upper(), self.__log_level)
79+
)
6480
# Log format
6581
self.logger_format = "%(asctime)s %(name)-12s %(levelname)-8s %(message)s"
6682

0 commit comments

Comments
 (0)