-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathqa_phase_routing.py
More file actions
168 lines (143 loc) · 6.6 KB
/
qa_phase_routing.py
File metadata and controls
168 lines (143 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""Runtime-modes routing for QA agent phases.
QA agents (``qa_reviewer``, ``qa_fixer``) are kept on the Claude Agent
SDK execution path while the runtime layer learns to provide MCP
execution, mutating subagents, and a Claude-equivalent native tool loop
for direct API providers (see Phase 1 of
``docs/roadmap/non-claude-provider-autonomy.md``).
This module resolves the provider, runtime mode, and AutonomyPolicy for
a QA phase *before* the session starts so that
``AGENT_PROVIDER_<TYPE>`` and ``AGENT_RUNTIME_MODE_<TYPE>`` env
overrides actually reach a decision. A non-Claude provider or a
non-``full_autonomous`` runtime fails fast with a clear capability error
and a persisted ``runtime_fallback_<phase>_*.json`` diagnostic artifact.
"""
from __future__ import annotations
import os
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from agents.runtime import (
get_runtime_mode,
resolve_runtime_mode_with_fallback,
save_runtime_fallback_artifact,
)
from agents.runtime.direct_api_autonomy import resolve_direct_api_autonomous_gate
from core.autonomy_policy import autonomy_policy_for
from core.providers.config import ProviderConfig
from debug import debug_error
# Opt-in: route a *promoted* direct-API provider's QA agents through the
# provider-neutral runtime layer instead of the Claude SDK. Default off, so
# the Claude QA path is unchanged unless an operator both sets this AND the
# provider passes the direct-API promotion gate.
QA_DIRECT_RUNTIME_ENV = "AUTO_CODE_QA_DIRECT_RUNTIME"
_TRUTHY = {"1", "true", "yes", "on"}
# QA agents whose runtime path exists and may run on a promoted direct-API
# provider. The reviewer is read-only; the fixer mutates source but the
# runtime layer confines it (generic_edit mutation snapshots + transaction
# rollback + sandbox), so both are gated identically on opt-in + promotion.
PORTABLE_QA_AGENTS = frozenset({"qa_reviewer", "qa_fixer"})
class QaRuntimeUnsupportedError(RuntimeError):
"""Raised when a QA agent cannot run with the requested provider/runtime."""
@dataclass(frozen=True)
class QaRuntimeDecision:
"""How a QA phase should execute.
``use_runtime_layer`` is ``False`` for the default Claude Agent SDK
path and ``True`` when a promoted direct-API provider may run a portable
QA agent through ``create_runtime_session`` / ``run_runtime_session``.
"""
agent_type: str
provider_name: str
runtime_mode: str
use_runtime_layer: bool
def _qa_direct_runtime_opt_in(env: Mapping[str, str]) -> bool:
"""Return whether the operator opted into the direct-API QA reviewer path."""
return env.get(QA_DIRECT_RUNTIME_ENV, "").strip().lower() in _TRUTHY
def resolve_qa_runtime(
*,
agent_type: str,
spec_dir: Path,
qa_iteration: int,
project_dir: Path | None = None,
env: Mapping[str, str] | None = None,
) -> QaRuntimeDecision:
"""Resolve how a QA phase should execute, before its session starts.
Always persists a ``runtime_fallback`` artifact and returns a
:class:`QaRuntimeDecision`:
* Claude + ``full_autonomous`` -> ``use_runtime_layer=False`` (the
existing Claude SDK path).
* A promoted direct-API provider running ``qa_reviewer`` with the
``AUTO_CODE_QA_DIRECT_RUNTIME`` opt-in -> ``use_runtime_layer=True``.
Raises :class:`QaRuntimeUnsupportedError` for every other non-Claude /
non-``full_autonomous`` request (qa_fixer, un-promoted providers, or
providers without the opt-in), so the default safety contract holds.
"""
env_map = os.environ if env is None else env
provider_config = ProviderConfig.from_env(agent_type=agent_type)
provider_name = provider_config.provider
requested_runtime_mode = get_runtime_mode(agent_type)
runtime_decision = resolve_runtime_mode_with_fallback(
provider_name=provider_name,
requested_mode=requested_runtime_mode,
phase="coding",
)
selected_mode = runtime_decision.selected_mode
artifact_path = save_runtime_fallback_artifact(
spec_dir=spec_dir,
decision=runtime_decision,
phase=agent_type,
session_num=qa_iteration,
)
# Default Claude SDK path — unchanged.
if provider_name == "claude" and selected_mode == "full_autonomous":
return QaRuntimeDecision(
agent_type=agent_type,
provider_name=provider_name,
runtime_mode=selected_mode,
use_runtime_layer=False,
)
# Opt-in direct-API path for a portable QA agent, gated on the provider
# having passed the direct-API promotion gate (evidence-backed trust).
# The fixer mutates source but the runtime layer confines it.
if (
agent_type in PORTABLE_QA_AGENTS
and provider_name != "claude"
and _qa_direct_runtime_opt_in(env_map)
):
gate = resolve_direct_api_autonomous_gate(
provider_name=provider_name,
project_dir=project_dir or spec_dir,
phase="coding",
env=env_map,
)
if gate.allowed:
return QaRuntimeDecision(
agent_type=agent_type,
provider_name=provider_name,
runtime_mode="generic_edit",
use_runtime_layer=True,
)
message = (
f"QA phase '{agent_type}' opted into the direct-API runtime "
f"({QA_DIRECT_RUNTIME_ENV}) for provider={provider_name}, but the "
f"direct-API promotion gate is not satisfied (status={gate.status}, "
f"reason={gate.reason}, missing={gate.missing_requirements}). "
f"Runtime decision saved to {artifact_path}."
)
debug_error("qa_runtime_routing", message)
raise QaRuntimeUnsupportedError(message)
policy = autonomy_policy_for(provider_name)
message = (
f"QA phase '{agent_type}' cannot run with provider={provider_name} "
f"runtime={selected_mode}. QA agents require the Claude Agent SDK "
"full_autonomous surface (multi-turn tool loop, Electron MCP, "
f"recovery hooks). Set AGENT_PROVIDER_{agent_type.upper()}=claude and "
f"AGENT_RUNTIME_MODE_{agent_type.upper()}=full_autonomous to run this "
"phase. Direct API providers are being enabled by Phase 1 of "
"docs/roadmap/non-claude-provider-autonomy.md; the read-only "
f"qa_reviewer can opt in via {QA_DIRECT_RUNTIME_ENV}=true once its "
"provider passes the promotion gate "
f"(autonomy policy snapshot: {policy.to_dict()}; "
f"runtime decision saved to {artifact_path})."
)
debug_error("qa_runtime_routing", message)
raise QaRuntimeUnsupportedError(message)