Skip to content

Commit b46202f

Browse files
OBennerclaude
andcommitted
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 <noreply@anthropic.com> Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>
1 parent a6f6538 commit b46202f

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

apps/backend/agents/runtime/adapters/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""Runtime adapter factory."""
22

3+
import logging
34
from collections.abc import Awaitable, Callable
45
from pathlib import Path
56
from typing import Any
67

8+
from core.autonomy_level import ResolvedAutonomySettings, resolve_autonomy_settings
9+
710
from ..cli_profiles import CLI_RUNNER_PROFILES
811
from ..direct_api_autonomy import DIRECT_API_AUTONOMOUS_PROVIDERS
912
from .claude import ClaudeAgentRuntimeSession
@@ -14,6 +17,8 @@
1417
from .generic_edit import GenericEditRuntimeSession
1518
from .patch_proposal import PatchProposalRuntimeSession
1619

20+
logger = logging.getLogger(__name__)
21+
1722
CLI_RUNTIME_PROVIDER_NAMES = {
1823
profile.runner_id
1924
for profile in CLI_RUNNER_PROFILES
@@ -33,6 +38,7 @@ def create_runtime_session(
3338
allow_direct_api_autonomous: bool = False,
3439
write_scope_guard: tuple[str, ...] | list[str] | None = None,
3540
changeset_export: bool = False,
41+
autonomy_settings: ResolvedAutonomySettings | None = None,
3642
) -> Any:
3743
"""Create a runtime adapter for a provider session.
3844
@@ -43,11 +49,28 @@ def create_runtime_session(
4349
instead of committing them to the shared workspace. Together they build
4450
mutating subagent child sessions whose write contract is enforced, not
4551
advisory.
52+
53+
``autonomy_settings`` lets callers inject their already-resolved autonomy
54+
level; when omitted it is resolved from the environment, so the level is
55+
logged for every session regardless of the call path (P3·T3).
4656
"""
4757

4858
provider_name = provider_name.lower()
4959
runtime_mode = runtime_mode.lower().replace("-", "_")
5060

61+
autonomy = (
62+
autonomy_settings
63+
if autonomy_settings is not None
64+
else resolve_autonomy_settings()
65+
)
66+
logger.info(
67+
"[runtime-factory] provider=%s mode=%s agent=%s autonomy=%s",
68+
provider_name,
69+
runtime_mode,
70+
agent_type or "-",
71+
autonomy.level.value,
72+
)
73+
5174
if runtime_mode == "patch_proposal":
5275
if project_dir is None:
5376
raise ValueError("project_dir is required for patch proposal runtime")
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Tests for the runtime factory autonomy logging (P3.T3).
2+
3+
create_runtime_session() must surface the resolved autonomy level in the
4+
session log for every call path, and accept an injected
5+
ResolvedAutonomySettings without re-resolving from the environment.
6+
"""
7+
8+
import logging
9+
import sys
10+
from pathlib import Path
11+
12+
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
13+
14+
import agents.runtime.adapters as adapters_mod # noqa: E402
15+
from agents.runtime.adapters import create_runtime_session # noqa: E402
16+
from core.autonomy_level import ( # noqa: E402
17+
AUTONOMY_LEVEL_ENV,
18+
resolve_autonomy_settings,
19+
)
20+
21+
FACTORY_LOGGER = "agents.runtime.adapters"
22+
23+
24+
def make_session(**kwargs):
25+
"""Create the simplest runtime session (analysis_only needs no extras)."""
26+
return create_runtime_session(
27+
provider_name="openai",
28+
agent_session=object(),
29+
runtime_mode="analysis_only",
30+
agent_type="coder",
31+
**kwargs,
32+
)
33+
34+
35+
def test_logs_level_resolved_from_env(monkeypatch, caplog):
36+
monkeypatch.setenv(AUTONOMY_LEVEL_ENV, "safe")
37+
with caplog.at_level(logging.INFO, logger=FACTORY_LOGGER):
38+
session = make_session()
39+
assert session is not None
40+
assert "autonomy=safe" in caplog.text
41+
assert "provider=openai" in caplog.text
42+
assert "mode=analysis_only" in caplog.text
43+
assert "agent=coder" in caplog.text
44+
45+
46+
def test_default_level_logged_when_env_unset(monkeypatch, caplog):
47+
monkeypatch.delenv(AUTONOMY_LEVEL_ENV, raising=False)
48+
with caplog.at_level(logging.INFO, logger=FACTORY_LOGGER):
49+
make_session()
50+
assert "autonomy=claude" in caplog.text # default level
51+
52+
53+
def test_injected_settings_skip_env_resolution(monkeypatch, caplog):
54+
settings = resolve_autonomy_settings(env={AUTONOMY_LEVEL_ENV: "bold"})
55+
56+
def boom():
57+
raise AssertionError("factory must not re-resolve injected settings")
58+
59+
monkeypatch.setattr(adapters_mod, "resolve_autonomy_settings", boom)
60+
with caplog.at_level(logging.INFO, logger=FACTORY_LOGGER):
61+
session = make_session(autonomy_settings=settings)
62+
assert session is not None
63+
assert "autonomy=bold" in caplog.text
64+
65+
66+
def test_level_logged_for_claude_runtime(monkeypatch, caplog):
67+
monkeypatch.setenv(AUTONOMY_LEVEL_ENV, "off")
68+
69+
async def runner(*args, **kwargs):
70+
return ()
71+
72+
with caplog.at_level(logging.INFO, logger=FACTORY_LOGGER):
73+
session = create_runtime_session(
74+
provider_name="claude",
75+
agent_session=object(),
76+
claude_session_runner=runner,
77+
runtime_mode="full_autonomous",
78+
agent_type="planner",
79+
)
80+
assert session is not None
81+
assert "autonomy=off" in caplog.text
82+
assert "agent=planner" in caplog.text

0 commit comments

Comments
 (0)