Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/backend/agents/runtime/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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.

Expand All @@ -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")
Expand Down
82 changes: 82 additions & 0 deletions tests/test_runtime_factory_autonomy_log.py
Original file line number Diff line number Diff line change
@@ -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):

Check warning on line 69 in tests/test_runtime_factory_autonomy_log.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use asynchronous features in this function or remove the `async` keyword.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ82Ps8JYF6faK45XGoh&open=AZ82Ps8JYF6faK45XGoh&pullRequest=404
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
Loading