From b46202f6d57914663d4633c4714e75a993b78780 Mon Sep 17 00:00:00 2001 From: Oleg Miagkov Date: Mon, 6 Jul 2026 11:03:50 +0400 Subject: [PATCH] feat(autonomy): runtime factory logs the autonomy level per session (P3.T3) create_runtime_session() now accepts an optional pre-resolved ResolvedAutonomySettings and logs provider/mode/agent/autonomy at INFO for every session it builds; when no settings are injected the level is resolved from the environment, so the log line appears on every call path (coder, planner, QA) without touching the callers. Tests: 4 new (env-resolved level, default level, injected settings skip re-resolution, claude runtime path); test_agent_runtime.py suite green (245 total). Co-Authored-By: Claude Fable 5 Signed-off-by: Oleg Miagkov --- .../agents/runtime/adapters/__init__.py | 23 ++++++ tests/test_runtime_factory_autonomy_log.py | 82 +++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 tests/test_runtime_factory_autonomy_log.py diff --git a/apps/backend/agents/runtime/adapters/__init__.py b/apps/backend/agents/runtime/adapters/__init__.py index 9ab8d21ac..39b44b391 100644 --- a/apps/backend/agents/runtime/adapters/__init__.py +++ b/apps/backend/agents/runtime/adapters/__init__.py @@ -1,9 +1,12 @@ """Runtime adapter factory.""" +import logging from collections.abc import Awaitable, Callable from pathlib import Path from typing import Any +from core.autonomy_level import ResolvedAutonomySettings, resolve_autonomy_settings + from ..cli_profiles import CLI_RUNNER_PROFILES from ..direct_api_autonomy import DIRECT_API_AUTONOMOUS_PROVIDERS from .claude import ClaudeAgentRuntimeSession @@ -14,6 +17,8 @@ from .generic_edit import GenericEditRuntimeSession from .patch_proposal import PatchProposalRuntimeSession +logger = logging.getLogger(__name__) + CLI_RUNTIME_PROVIDER_NAMES = { profile.runner_id for profile in CLI_RUNNER_PROFILES @@ -33,6 +38,7 @@ def create_runtime_session( allow_direct_api_autonomous: bool = False, write_scope_guard: tuple[str, ...] | list[str] | None = None, changeset_export: bool = False, + autonomy_settings: ResolvedAutonomySettings | None = None, ) -> Any: """Create a runtime adapter for a provider session. @@ -43,11 +49,28 @@ def create_runtime_session( instead of committing them to the shared workspace. Together they build mutating subagent child sessions whose write contract is enforced, not advisory. + + ``autonomy_settings`` lets callers inject their already-resolved autonomy + level; when omitted it is resolved from the environment, so the level is + logged for every session regardless of the call path (P3·T3). """ provider_name = provider_name.lower() runtime_mode = runtime_mode.lower().replace("-", "_") + autonomy = ( + autonomy_settings + if autonomy_settings is not None + else resolve_autonomy_settings() + ) + logger.info( + "[runtime-factory] provider=%s mode=%s agent=%s autonomy=%s", + provider_name, + runtime_mode, + agent_type or "-", + autonomy.level.value, + ) + if runtime_mode == "patch_proposal": if project_dir is None: raise ValueError("project_dir is required for patch proposal runtime") diff --git a/tests/test_runtime_factory_autonomy_log.py b/tests/test_runtime_factory_autonomy_log.py new file mode 100644 index 000000000..b50c06f5d --- /dev/null +++ b/tests/test_runtime_factory_autonomy_log.py @@ -0,0 +1,82 @@ +"""Tests for the runtime factory autonomy logging (P3.T3). + +create_runtime_session() must surface the resolved autonomy level in the +session log for every call path, and accept an injected +ResolvedAutonomySettings without re-resolving from the environment. +""" + +import logging +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend")) + +import agents.runtime.adapters as adapters_mod # noqa: E402 +from agents.runtime.adapters import create_runtime_session # noqa: E402 +from core.autonomy_level import ( # noqa: E402 + AUTONOMY_LEVEL_ENV, + resolve_autonomy_settings, +) + +FACTORY_LOGGER = "agents.runtime.adapters" + + +def make_session(**kwargs): + """Create the simplest runtime session (analysis_only needs no extras).""" + return create_runtime_session( + provider_name="openai", + agent_session=object(), + runtime_mode="analysis_only", + agent_type="coder", + **kwargs, + ) + + +def test_logs_level_resolved_from_env(monkeypatch, caplog): + monkeypatch.setenv(AUTONOMY_LEVEL_ENV, "safe") + with caplog.at_level(logging.INFO, logger=FACTORY_LOGGER): + session = make_session() + assert session is not None + assert "autonomy=safe" in caplog.text + assert "provider=openai" in caplog.text + assert "mode=analysis_only" in caplog.text + assert "agent=coder" in caplog.text + + +def test_default_level_logged_when_env_unset(monkeypatch, caplog): + monkeypatch.delenv(AUTONOMY_LEVEL_ENV, raising=False) + with caplog.at_level(logging.INFO, logger=FACTORY_LOGGER): + make_session() + assert "autonomy=claude" in caplog.text # default level + + +def test_injected_settings_skip_env_resolution(monkeypatch, caplog): + settings = resolve_autonomy_settings(env={AUTONOMY_LEVEL_ENV: "bold"}) + + def boom(): + raise AssertionError("factory must not re-resolve injected settings") + + monkeypatch.setattr(adapters_mod, "resolve_autonomy_settings", boom) + with caplog.at_level(logging.INFO, logger=FACTORY_LOGGER): + session = make_session(autonomy_settings=settings) + assert session is not None + assert "autonomy=bold" in caplog.text + + +def test_level_logged_for_claude_runtime(monkeypatch, caplog): + monkeypatch.setenv(AUTONOMY_LEVEL_ENV, "off") + + async def runner(*args, **kwargs): + return () + + with caplog.at_level(logging.INFO, logger=FACTORY_LOGGER): + session = create_runtime_session( + provider_name="claude", + agent_session=object(), + claude_session_runner=runner, + runtime_mode="full_autonomous", + agent_type="planner", + ) + assert session is not None + assert "autonomy=off" in caplog.text + assert "agent=planner" in caplog.text