feat(orchestration): GLM 폴백 라우팅을 추가한다

Gemini 할당량 소진 시 등급별 GLM thinking 수준으로 전환해 Pi 실행 명령과 재시도 상태가 같은 실행 대상을 유지하도록 한다.
This commit is contained in:
toki 2026-08-02 15:39:36 +09:00
parent 128fe2e707
commit 6d6bb3a44d
7 changed files with 436 additions and 293 deletions

View file

@ -69,11 +69,11 @@ Treat Korean text inside code spans or fenced examples as exact runtime or file-
| PLAN route | Worker |
|---|---|
| `local-G01``local-G06` | Pi `iop/ornith:35b`, thinking high |
| `local-G07``local-G08` | KST `[07:00,23:00)` agy `Gemini 3.6 Flash (Medium)`; `[23:00,07:00)` Pi `iop/laguna-s:2.1` |
| `local-G07``local-G08` | KST day/night agy `Gemini 3.6 Flash (High)` → Pi `iop/glm-5.2`, thinking high |
| `local-G09``local-G10` | Claude `claude-opus-4-8`, effort xhigh |
| `cloud-G01``cloud-G02` | agy `Gemini 3.6 Flash (Low)` |
| `cloud-G03``cloud-G04` | agy `Gemini 3.6 Flash (Medium)` |
| `cloud-G05``cloud-G06` | agy `Gemini 3.6 Flash (High)` |
| `cloud-G01``cloud-G02` | Codex `gpt-5.3-codex-spark`agy `Gemini 3.6 Flash (Low)` → Pi `iop/glm-5.2`, thinking low |
| `cloud-G03``cloud-G04` | agy `Gemini 3.6 Flash (Medium)` → Pi `iop/glm-5.2`, thinking medium |
| `cloud-G05``cloud-G06` | agy `Gemini 3.6 Flash (High)` → Pi `iop/glm-5.2`, thinking high |
| `cloud-G07``cloud-G08` | Claude `claude-opus-4-8`, effort xhigh |
| `cloud-G09``cloud-G10` | Codex `gpt-5.6-sol`, reasoning xhigh |
| Every `CODE_REVIEW-*` | Codex `gpt-5.6-sol`, reasoning xhigh |
@ -229,7 +229,7 @@ When recovering a KST-night `local-G07``local-G08` Laguna locator or a termin
- Archive `WORK_LOG.md` as `work_log_N.log` only after the final task review process exits, the dispatcher appends `FINISH`, and a complete scan finds no active/running task in that group. Accept the log at either the active group path or the verified completed single-task archive; do not impose either location contract on common plan/code-review.
3. **Escalate and recover context.**
- Escalate `agy -> Claude -> Codex` or `Claude -> Codex` only on terminal provider error events or stderr evidence of context/output limits, provider quota/rate limits, unavailable models, or confirmed provider transport errors. For AGY, accept top-level `error`, `fatal`, `request.failed`, or `turn.failed` events; failed/rejected status with a top-level error/code; stderr; or strong `RESOURCE_EXHAUSTED`, HTTP 429, quota, or rate-limit evidence in `agy-cli.log`. For Claude, classify a `rate_limit_event` with `rate_limit_info.status=rejected`, an error `result` with `api_error_status=429` or `error=rate_limit`, or a `You've hit your session limit · resets ...` terminal diagnostic as `provider-quota`. Never escalate from an assistant message, source text, tool/test output, or a plain quota-configuration string in an AGY log.
- For every route that lists Gemini followed by Pi GLM, classify terminal provider errors or stderr evidence of context/output limits, provider quota/rate limits, unavailable models, or confirmed provider transport errors as a qualified failover to that next GLM candidate. For AGY, accept top-level `error`, `fatal`, `request.failed`, or `turn.failed` events; failed/rejected status with a top-level error/code; stderr; or strong `RESOURCE_EXHAUSTED`, HTTP 429, quota, or rate-limit evidence in `agy-cli.log`. For Claude, classify a `rate_limit_event` with `rate_limit_info.status=rejected`, an error `result` with `api_error_status=429` or `error=rate_limit`, or a `You've hit your session limit · resets ...` terminal diagnostic as `provider-quota`. Cloud-only escalation remains `Claude -> Codex`; never escalate from an assistant message, source text, tool/test output, or a plain quota-configuration string in an AGY log.
- Target Codex `gpt-5.6-terra` with reasoning `high` when escalating from Claude to Codex.
- If Codex returns the same error, retry in a fresh Codex session using the locator while preserving the previous Codex model/reasoning and sharing the same stage's 10-consecutive-failure limit. Continue dispatching other tasks during recovery.
- When current source reads a locator blocked 10 times as `generic-error` by older dispatcher source, collapse those 10 failures into one terminal error and clear only that task's blocker only if all 10 terminal-evidence records for the same task/plan/role/source/execution target reclassify to the same escalatable error. Include `stream.log` and the attempt's `agy-cli.log` for AGY. Do not adjust automatically when any history is missing or mixed, or when the locator dispatcher source hash equals the current source hash. Dry-run must display this escalation recovery and next model without writing state. Live execution must choose the higher target from the locator's actual failed target, not the initial PLAN route, inherit locator context, and restore the same escalation target and locator from persisted reclassification metadata after immediate restart.

View file

@ -501,6 +501,7 @@ class AgentSpec:
display: str
local_pi: bool = False
reasoning_effort: str | None = None
thinking_level: str | None = None
def effective_reasoning_effort(spec: AgentSpec) -> str | None:
@ -509,6 +510,17 @@ def effective_reasoning_effort(spec: AgentSpec) -> str | None:
return None
def effective_pi_thinking_level(spec: AgentSpec) -> str | None:
if spec.cli == "pi":
return spec.thinking_level or "high"
return None
def pi_display(model: str, thinking_level: str | None) -> str:
suffix = f" {thinking_level}" if thinking_level is not None else ""
return f"pi/iop/{model}{suffix}"
def agent_spec_from_record(record: dict[str, Any]) -> AgentSpec | None:
cli = str(record.get("cli") or "")
model = str(record.get("model") or "")
@ -517,12 +529,15 @@ def agent_spec_from_record(record: dict[str, Any]) -> AgentSpec | None:
reasoning_effort = record.get("reasoning_effort")
if reasoning_effort is not None:
reasoning_effort = str(reasoning_effort)
thinking_level = record.get("thinking_level")
if thinking_level is not None:
thinking_level = str(thinking_level)
local_pi = cli == "pi"
if cli in {"codex", "claude"}:
effort = reasoning_effort or "xhigh"
display = f"{cli}/{model} {effort}"
elif cli == "pi":
display = f"pi/iop/{model}"
display = pi_display(model, thinking_level)
else:
display = f"{cli}/{model}"
return AgentSpec(
@ -531,6 +546,7 @@ def agent_spec_from_record(record: dict[str, Any]) -> AgentSpec | None:
display,
local_pi=local_pi,
reasoning_effort=reasoning_effort,
thinking_level=thinking_level,
)
@ -1730,6 +1746,7 @@ def agent_spec_from_decision(decision: dict[str, Any]) -> AgentSpec:
if not isinstance(selected, dict):
raise ExecutionDecisionError("selector selected가 object가 아니다")
adapter, target = selected.get("adapter"), selected.get("target")
thinking_level = selected.get("thinking_level")
local_pi = selected.get("selfcheck_required")
execution_class = selected.get("execution_class")
if (not isinstance(adapter, str) or not isinstance(target, str) or not target
@ -1751,7 +1768,7 @@ def agent_spec_from_decision(decision: dict[str, Any]) -> AgentSpec:
lane=decision["lane"],
grade=decision["grade"],
)
canonical = selector.policy.canonical_target(adapter, target)
canonical = selector.policy.canonical_target(adapter, target, thinking_level)
except Exception as exc:
raise ExecutionDecisionError(f"selector policy validation 실패: {exc}") from exc
if canonical is None or (
@ -1759,8 +1776,11 @@ def agent_spec_from_decision(decision: dict[str, Any]) -> AgentSpec:
or canonical.selfcheck_required != local_pi
):
raise ExecutionDecisionError("selector selected가 canonical policy target이 아니다")
initial_keys = {(item.adapter, item.target) for item in policy_targets}
if (adapter, target) not in initial_keys:
initial_keys = {
(item.adapter, item.target, item.thinking_level)
for item in policy_targets
}
if (adapter, target, thinking_level) not in initial_keys:
promotion_path = decision.get("promotion_path")
if not isinstance(promotion_path, list) or len(promotion_path) < 2:
raise ExecutionDecisionError("selector promotion path가 없다")
@ -1771,14 +1791,20 @@ def agent_spec_from_decision(decision: dict[str, Any]) -> AgentSpec:
f"selector promotion path[{index}]가 object가 아니다"
)
resolved = selector.policy.canonical_target(
entry.get("adapter"), entry.get("target")
entry.get("adapter"),
entry.get("target"),
entry.get("thinking_level"),
)
if resolved is None:
raise ExecutionDecisionError(
f"selector promotion path[{index}] target이 canonical이 아니다"
)
resolved_path.append(resolved)
if (resolved_path[0].adapter, resolved_path[0].target) not in initial_keys:
if (
resolved_path[0].adapter,
resolved_path[0].target,
resolved_path[0].thinking_level,
) not in initial_keys:
raise ExecutionDecisionError("selector promotion path 시작 target이 잘못됐다")
if any(
selector.policy.promotion_target(previous) != current
@ -1790,7 +1816,14 @@ def agent_spec_from_decision(decision: dict[str, Any]) -> AgentSpec:
if adapter == "pi":
if not target.startswith("iop/") or not local_pi:
raise ExecutionDecisionError("Pi selector target/schema가 유효하지 않다")
return AgentSpec("pi", target.removeprefix("iop/"), f"pi/{target}", local_pi=True)
model = target.removeprefix("iop/")
return AgentSpec(
"pi",
model,
pi_display(model, canonical.thinking_level),
local_pi=True,
thinking_level=canonical.thinking_level,
)
if adapter not in {"agy", "claude", "codex"} or local_pi:
raise ExecutionDecisionError(f"selector adapter/schema가 유효하지 않다: {adapter!r}")
reasoning_effort = "high" if canonical == selector.policy.CODEX_TERRA_HIGH else None
@ -1825,6 +1858,7 @@ def _spec_from_completing_decision(decision: dict[str, Any]) -> AgentSpec:
target = selected.get("target")
execution_class = selected.get("execution_class")
selfcheck_required = selected.get("selfcheck_required")
thinking_level = selected.get("thinking_level")
if not all(isinstance(value, str) and value for value in (adapter, target, execution_class)):
raise ExecutionDecisionError(
"completing decision selected의 adapter/target/execution_class는 빈 문자열이 아닌 string이어야 한다"
@ -1851,8 +1885,18 @@ def _spec_from_completing_decision(decision: dict[str, Any]) -> AgentSpec:
"Pi completing decision selfcheck_required가 False이다"
)
model = target.removeprefix("iop/")
display = f"pi/{target}"
return AgentSpec(adapter, model, display, local_pi=True)
if thinking_level is not None and thinking_level not in {"low", "medium", "high"}:
raise ExecutionDecisionError(
f"Pi completing decision thinking_level이 유효하지 않다: {thinking_level!r}"
)
display = pi_display(model, thinking_level)
return AgentSpec(
adapter,
model,
display,
local_pi=True,
thinking_level=thinking_level,
)
if adapter not in {"agy", "claude", "codex"}:
raise ExecutionDecisionError(
f"completing decision adapter가 유효하지 않다: {adapter!r}"
@ -2818,6 +2862,10 @@ def legacy_promotion_recovery(
agent_spec_from_record(record) or failed_spec
)
!= effective_reasoning_effort(failed_spec)
or effective_pi_thinking_level(
agent_spec_from_record(record) or failed_spec
)
!= effective_pi_thinking_level(failed_spec)
or not isinstance(record.get("attempt"), int)
):
continue
@ -3743,7 +3791,7 @@ def build_command(
if spec.cli == "pi":
command = [
"pi", "-p", "--mode", "json", "--approve", "--provider", "iop", "--model", spec.model,
"--thinking", "high",
"--thinking", str(effective_pi_thinking_level(spec)),
]
if pi_resume_session is not None:
command.extend(
@ -3849,6 +3897,7 @@ async def invoke(
"cli": spec.cli,
"model": spec.model,
"reasoning_effort": effective_reasoning_effort(spec),
"thinking_level": effective_pi_thinking_level(spec),
"agent_process_marker": process_marker,
"plan_path": str(task.plan) if task.plan else None,
"review_path": str(task.review) if task.review else None,

View file

@ -13,6 +13,7 @@ KST = ZoneInfo("Asia/Seoul")
VALID_STAGES = {"worker", "review"}
VALID_LANES = {"local", "cloud"}
VALID_PI_THINKING_LEVELS = frozenset({"low", "medium", "high"})
@dataclass(frozen=True)
@ -21,6 +22,7 @@ class RouteTarget:
target: str
execution_class: str
selfcheck_required: bool
thinking_level: str | None = None
@dataclass(frozen=True)
@ -43,6 +45,15 @@ AGY_GEMINI_HIGH = RouteTarget(
"agy", "Gemini 3.6 Flash (High)", "cloud_model", False
)
PI_LAGUNA = RouteTarget("pi", "iop/laguna-s:2.1", "local_model", True)
PI_GLM_LOW = RouteTarget(
"pi", "iop/glm-5.2", "local_model", True, thinking_level="low"
)
PI_GLM_MEDIUM = RouteTarget(
"pi", "iop/glm-5.2", "local_model", True, thinking_level="medium"
)
PI_GLM_HIGH = RouteTarget(
"pi", "iop/glm-5.2", "local_model", True, thinking_level="high"
)
CLAUDE_OPUS = RouteTarget("claude", "claude-opus-4-8", "cloud_model", False)
CLAUDE_HAIKU_XHIGH = RouteTarget(
"claude", "claude-haiku-4-5", "cloud_model", False
@ -60,6 +71,9 @@ CANONICAL_TARGETS = (
AGY_GEMINI_MEDIUM,
AGY_GEMINI_HIGH,
PI_LAGUNA,
PI_GLM_LOW,
PI_GLM_MEDIUM,
PI_GLM_HIGH,
CLAUDE_OPUS,
CLAUDE_HAIKU_XHIGH,
CODEX_SPARK_XHIGH,
@ -68,13 +82,19 @@ CANONICAL_TARGETS = (
)
def canonical_target(adapter: str, target: str) -> RouteTarget | None:
"""Resolve one policy-owned adapter + target identity."""
def canonical_target(
adapter: str, target: str, thinking_level: str | None = None,
) -> RouteTarget | None:
"""Resolve one policy-owned adapter + target + thinking identity."""
return next(
(
candidate
for candidate in CANONICAL_TARGETS
if candidate.adapter == adapter and candidate.target == target
if (
candidate.adapter == adapter
and candidate.target == target
and candidate.thinking_level == thinking_level
)
),
None,
)
@ -82,12 +102,6 @@ def canonical_target(adapter: str, target: str) -> RouteTarget | None:
def promotion_target(current: RouteTarget) -> RouteTarget | None:
"""Return the next target in the policy-owned cloud promotion chain."""
if current.adapter == "agy" and current in {
AGY_GEMINI_LOW,
AGY_GEMINI_MEDIUM,
AGY_GEMINI_HIGH,
}:
return CLAUDE_OPUS
if current == CLAUDE_OPUS:
return CODEX_TERRA_HIGH
return None
@ -166,12 +180,12 @@ def select_policy(
time_window = _kst_time_window(evaluated_at)
if time_window == "kst-day-[07:00,23:00)":
rule_id = "worker-local-g07-g08-kst-day"
reason_code = "kst_day_gemini_medium"
candidates = (AGY_GEMINI_MEDIUM, PI_LAGUNA)
reason_code = "kst_day_gemini_high"
candidates = (AGY_GEMINI_HIGH, PI_GLM_HIGH)
else:
rule_id = "worker-local-g07-g08-kst-night"
reason_code = "kst_night_laguna"
candidates = (PI_LAGUNA, AGY_GEMINI_MEDIUM)
reason_code = "kst_night_gemini_high"
candidates = (AGY_GEMINI_HIGH, PI_GLM_HIGH)
return PolicyDecision(
rule_id=rule_id,
policy_priority=20,
@ -191,16 +205,16 @@ def select_policy(
candidates = (
CODEX_SPARK_XHIGH,
AGY_GEMINI_LOW,
CLAUDE_HAIKU_XHIGH,
PI_GLM_LOW,
)
rule_id = "worker-cloud-g01-g02"
reason_code = "cloud_spark_priority_grade"
elif grade <= 4:
candidates = (AGY_GEMINI_MEDIUM,)
candidates = (AGY_GEMINI_MEDIUM, PI_GLM_MEDIUM)
rule_id = "worker-cloud-g03-g04"
reason_code = "cloud_gemini_medium_grade"
elif grade <= 6:
candidates = (AGY_GEMINI_HIGH,)
candidates = (AGY_GEMINI_HIGH, PI_GLM_HIGH)
rule_id = "worker-cloud-g05-g06"
reason_code = "cloud_gemini_high_grade"
elif grade <= 8:

View file

@ -181,6 +181,16 @@ def _validate_prior_selected(selected: object) -> None:
code,
"prior_decision.selected.selfcheck_required must be a boolean",
)
thinking_level = selected.get("thinking_level")
if thinking_level is not None and (
not isinstance(thinking_level, str)
or thinking_level not in policy.VALID_PI_THINKING_LEVELS
):
raise SelectorInputError(
code,
"prior_decision.selected.thinking_level must be null or one of "
f"{sorted(policy.VALID_PI_THINKING_LEVELS)}",
)
def _require_non_empty_string(
@ -252,6 +262,16 @@ def _validate_prior_candidates(candidates: object) -> None:
raise SelectorInputError(
code, f"{prefix}.selfcheck_required must be a boolean"
)
thinking_level = entry.get("thinking_level")
if thinking_level is not None and (
not isinstance(thinking_level, str)
or thinking_level not in policy.VALID_PI_THINKING_LEVELS
):
raise SelectorInputError(
code,
f"{prefix}.thinking_level must be null or one of "
f"{sorted(policy.VALID_PI_THINKING_LEVELS)}",
)
_require_string_enum(entry, "quota_mode", _VALID_QUOTA_MODES, prefix, code)
_require_string_enum(
entry, "quota_status", _VALID_QUOTA_STATUSES, prefix, code
@ -823,19 +843,20 @@ def _initial(
target, quota_snapshot, evaluated_at, quota_probe_command
)
eligible = status != "exhausted"
candidates.append(
{
"candidate_rank": rank,
"adapter": target.adapter,
"target": target.target,
"execution_class": target.execution_class,
"selfcheck_required": target.selfcheck_required,
"quota_mode": mode,
"quota_status": status,
"eligibility": "eligible" if eligible else "ineligible",
"rejection_reason": None if eligible else "quota_exhausted",
}
)
candidate = {
"candidate_rank": rank,
"adapter": target.adapter,
"target": target.target,
"execution_class": target.execution_class,
"selfcheck_required": target.selfcheck_required,
"quota_mode": mode,
"quota_status": status,
"eligibility": "eligible" if eligible else "ineligible",
"rejection_reason": None if eligible else "quota_exhausted",
}
if target.thinking_level is not None:
candidate["thinking_level"] = target.thinking_level
candidates.append(candidate)
if eligible and selected is None:
selected = target
selected_probed_snapshot = probed_snapshot
@ -844,18 +865,21 @@ def _initial(
"no_eligible_target",
"all policy candidates are exhausted according to the quota snapshot",
)
selected_fields = {
"adapter": selected.adapter,
"target": selected.target,
"execution_class": selected.execution_class,
"selfcheck_required": selected.selfcheck_required,
}
if selected.thinking_level is not None:
selected_fields["thinking_level"] = selected.thinking_level
return {
"schema_version": SCHEMA_VERSION,
"work_unit_id": work_unit_id,
"stage": stage,
"lane": lane,
"grade": grade,
"selected": {
"adapter": selected.adapter,
"target": selected.target,
"execution_class": selected.execution_class,
"selfcheck_required": selected.selfcheck_required,
},
"selected": selected_fields,
"candidates": candidates,
"decision": {
"rule_id": decision.rule_id,
@ -888,8 +912,14 @@ def _validate_selected_and_used_history(
if not isinstance(selected, dict):
raise SelectorInputError(code, "prior_decision.selected must be an object")
sel_key = (selected.get("adapter"), selected.get("target"))
canon_keys_list = [(c.adapter, c.target) for c in canonical_targets]
sel_key = (
selected.get("adapter"),
selected.get("target"),
selected.get("thinking_level"),
)
canon_keys_list = [
(c.adapter, c.target, c.thinking_level) for c in canonical_targets
]
canon_keys_set = set(canon_keys_list)
matching_cand = policy.canonical_target(*sel_key)
@ -921,14 +951,20 @@ def _validate_selected_and_used_history(
code, f"promotion_path[{index}] must be an object"
)
target = policy.canonical_target(
entry.get("adapter"), entry.get("target")
entry.get("adapter"),
entry.get("target"),
entry.get("thinking_level"),
)
if target is None:
raise SelectorInputError(
code, f"promotion_path[{index}] is not policy-owned"
)
path_targets.append(target)
if (path_targets[0].adapter, path_targets[0].target) not in canon_keys_set:
if (
path_targets[0].adapter,
path_targets[0].target,
path_targets[0].thinking_level,
) not in canon_keys_set:
raise SelectorInputError(
code, "promotion_path must begin at the initial policy target"
)
@ -958,7 +994,11 @@ def _validate_selected_and_used_history(
raise SelectorInputError(
code, f"prior_decision.used_candidates[{idx}] must be an object"
)
u_key = (entry.get("adapter"), entry.get("target"))
u_key = (
entry.get("adapter"),
entry.get("target"),
entry.get("thinking_level"),
)
if u_key not in canon_keys_set:
raise SelectorInputError(
code,
@ -985,7 +1025,11 @@ def _validate_selected_and_used_history(
else:
prior_cands = prior_decision.get("candidates", [])
eligible_cands = [
(c.get("adapter"), c.get("target"))
(
c.get("adapter"),
c.get("target"),
c.get("thinking_level"),
)
for c in prior_cands
if isinstance(c, dict) and c.get("eligibility") == "eligible"
]
@ -1073,6 +1117,7 @@ def _validate_prior_candidate_identity(
or p_cand.get("target") != c_target.target
or p_cand.get("execution_class") != c_target.execution_class
or p_cand.get("selfcheck_required") != c_target.selfcheck_required
or p_cand.get("thinking_level") != c_target.thinking_level
):
raise SelectorInputError(
code,
@ -1135,7 +1180,10 @@ def _resume(
def _target_ref(candidate: dict) -> dict:
return {"adapter": candidate["adapter"], "target": candidate["target"]}
ref = {"adapter": candidate["adapter"], "target": candidate["target"]}
if candidate.get("thinking_level") is not None:
ref["thinking_level"] = candidate["thinking_level"]
return ref
def _validate_used_candidates(value: object) -> list[dict]:
@ -1150,7 +1198,21 @@ def _validate_used_candidates(value: object) -> list[dict]:
adapter, target = entry.get("adapter"), entry.get("target")
if not isinstance(adapter, str) or not adapter or not isinstance(target, str) or not target:
raise SelectorInputError("malformed_prior_decision", f"used_candidates[{index}] needs adapter and target")
refs.append({"adapter": adapter, "target": target})
thinking_level = entry.get("thinking_level")
if thinking_level is not None and (
not isinstance(thinking_level, str)
or thinking_level not in policy.VALID_PI_THINKING_LEVELS
):
raise SelectorInputError(
"malformed_prior_decision",
"used_candidates["
f"{index}].thinking_level must be null or one of "
f"{sorted(policy.VALID_PI_THINKING_LEVELS)}",
)
ref = {"adapter": adapter, "target": target}
if thinking_level is not None:
ref["thinking_level"] = thinking_level
refs.append(ref)
return refs
@ -1215,7 +1277,17 @@ def _failover(
selected_probed_snapshot = current_snapshot
if selected_candidate is None:
raise SelectorInputError("no_failover_candidate", "no unused eligible candidate remains for this work unit")
selected = {field: selected_candidate[field] for field in ("adapter", "target", "execution_class", "selfcheck_required")}
selected = {
field: selected_candidate[field]
for field in (
"adapter",
"target",
"execution_class",
"selfcheck_required",
)
}
if selected_candidate.get("thinking_level") is not None:
selected["thinking_level"] = selected_candidate["thinking_level"]
next_target = _target_ref(selected)
used.append(next_target)
decision = dict(prior["decision"])
@ -1284,7 +1356,9 @@ def _promotion(
"multi-candidate policy routes use failover instead of promotion",
)
current = policy.canonical_target(
prior["selected"]["adapter"], prior["selected"]["target"]
prior["selected"]["adapter"],
prior["selected"]["target"],
prior["selected"].get("thinking_level"),
)
promoted = policy.promotion_target(current) if current is not None else None
if promoted is None:
@ -1292,8 +1366,20 @@ def _promotion(
"no_promotion_target",
"no unused canonical promotion target remains for this work unit",
)
previous_target = {"adapter": current.adapter, "target": current.target}
next_target = {"adapter": promoted.adapter, "target": promoted.target}
previous_target = _target_ref(
{
"adapter": current.adapter,
"target": current.target,
"thinking_level": current.thinking_level,
}
)
next_target = _target_ref(
{
"adapter": promoted.adapter,
"target": promoted.target,
"thinking_level": promoted.thinking_level,
}
)
promotion_path = list(prior.get("promotion_path", [previous_target]))
if not promotion_path or promotion_path[-1] != previous_target:
raise SelectorInputError(
@ -1314,6 +1400,11 @@ def _promotion(
"target": promoted.target,
"execution_class": promoted.execution_class,
"selfcheck_required": promoted.selfcheck_required,
**(
{"thinking_level": promoted.thinking_level}
if promoted.thinking_level is not None
else {}
),
},
"candidates": prior["candidates"],
"decision": decision,

View file

@ -120,6 +120,26 @@ class CommandConstructionTest(unittest.TestCase):
command[-2:], ["--log-file", str(workspace / "attempt" / "agy-cli.log")]
)
def test_pi_glm_preserves_policy_thinking_level(self):
with tempfile.TemporaryDirectory() as temporary:
workspace = Path(temporary)
command = dispatch.build_command(
dispatch.AgentSpec(
"pi",
"glm-5.2",
"pi/iop/glm-5.2 medium",
local_pi=True,
thinking_level="medium",
),
"Implement the active plan.",
workspace,
"test-session",
workspace / "attempt",
)
thinking_index = command.index("--thinking")
self.assertEqual(command[thinking_index + 1], "medium")
class TaskStageTest(unittest.TestCase):
def make_task(self, root: Path, review_text: str = ""):
@ -467,11 +487,11 @@ class TaskStageTest(unittest.TestCase):
day_dec = dispatch.select_execution_decision(task, stage="worker", evaluated_at=daytime)
self.assertEqual(day_dec["selected"]["adapter"], "agy")
self.assertEqual(day_dec["selected"]["target"], "Gemini 3.6 Flash (Medium)")
self.assertEqual(day_dec["selected"]["target"], "Gemini 3.6 Flash (High)")
night_dec = dispatch.select_execution_decision(task, stage="worker", evaluated_at=nighttime)
self.assertEqual(night_dec["selected"]["adapter"], "pi")
self.assertEqual(night_dec["selected"]["target"], "iop/laguna-s:2.1")
self.assertEqual(night_dec["selected"]["adapter"], "agy")
self.assertEqual(night_dec["selected"]["target"], "Gemini 3.6 Flash (High)")
@ -8276,7 +8296,7 @@ class DynamicFailoverBudgetTest(unittest.TestCase):
self.assertEqual(worker_budget2.count(), 10)
self.assertEqual(review_budget2.count(), 0)
raw_entry = state2.get("stage_failure_budgets", {}).get(worker_budget2.key, {})
self.assertEqual(raw_entry.get("last_target"), {"adapter": "pi", "target": "iop/laguna-s:2.1"})
self.assertEqual(raw_entry.get("last_target"), {"adapter": "pi", "target": "iop/glm-5.2"})
self.assertEqual(raw_entry.get("last_transition"), "provider-quota")
self.assertEqual(state2["execution_decisions"], decisions1)
self.assertEqual([h["transition"] for h in state2["route_transition_history"]], ["initial", "provider-quota"])
@ -8382,7 +8402,7 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
locator.write_text(json.dumps(record), encoding="utf-8")
return locator
async def test_cloud_g01_g02_quota_failover_runs_spark_gemini_haiku(self):
async def test_cloud_g01_g02_quota_failover_runs_spark_gemini_glm_low(self):
daytime = datetime(
2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))
)
@ -8431,10 +8451,12 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
"Gemini 3.6 Flash (Low)",
"agy/Gemini 3.6 Flash (Low)",
),
"claude": dispatch.AgentSpec(
"claude",
"claude-haiku-4-5",
"claude/claude-haiku-4-5 xhigh",
"pi": dispatch.AgentSpec(
"pi",
"glm-5.2",
"pi/iop/glm-5.2 low",
local_pi=True,
thinking_level="low",
),
}
locators = {
@ -8446,7 +8468,7 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
async def mock_invoke(*args, **kwargs):
spec = args[4]
invoked_specs.append(spec)
if spec.cli == "claude":
if spec.cli == "pi":
return 0, None, locators[spec.cli]
return 1, "provider-quota", locators[spec.cli]
@ -8467,13 +8489,13 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
)
self.assertTrue(success)
self.assertEqual(final_locator, locators["claude"])
self.assertEqual(final_locator, locators["pi"])
self.assertEqual(
[(spec.cli, spec.model) for spec in invoked_specs],
[
("codex", "gpt-5.3-codex-spark"),
("agy", "Gemini 3.6 Flash (Low)"),
("claude", "claude-haiku-4-5"),
("pi", "glm-5.2"),
],
)
decision = store.task_state(task)["execution_decisions"]["worker"]
@ -8482,7 +8504,7 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
[
{"adapter": "codex", "target": "gpt-5.3-codex-spark"},
{"adapter": "agy", "target": "Gemini 3.6 Flash (Low)"},
{"adapter": "claude", "target": "claude-haiku-4-5"},
{"adapter": "pi", "target": "iop/glm-5.2", "thinking_level": "low"},
],
)
finally:
@ -8693,7 +8715,7 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
finally:
store.close()
async def test_cloud_agy_promotion_chain_commits_each_transition(self):
async def test_cloud_agy_quota_failover_commits_glm_high(self):
daytime = datetime(
2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))
)
@ -8708,20 +8730,16 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
"Gemini 3.6 Flash (High)",
"agy/Gemini 3.6 Flash (High)",
)
claude_spec = dispatch.AgentSpec(
"claude",
"claude-opus-4-8",
"claude/claude-opus-4-8 xhigh",
)
terra_spec = dispatch.AgentSpec(
"codex",
"gpt-5.6-terra",
"codex/gpt-5.6-terra high",
reasoning_effort="high",
glm_spec = dispatch.AgentSpec(
"pi",
"glm-5.2",
"pi/iop/glm-5.2 high",
local_pi=True,
thinking_level="high",
)
locators = {
spec.cli: self.make_attempt_locator(workspace, task, spec)
for spec in (agy_spec, claude_spec, terra_spec)
for spec in (agy_spec, glm_spec)
}
invoked_specs = []
invoked_prompts = []
@ -8739,9 +8757,7 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
store, task, decision
)
transition_budget_counts.append(budget.count())
if spec.cli == "claude":
return (1, "context-limit", locators["claude"])
return (0, None, locators["codex"])
return (0, None, locators["pi"])
with (
mock.patch.object(dispatch, "invoke", new=mock_invoke),
@ -8757,33 +8773,31 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
)
self.assertTrue(success)
self.assertEqual(final_locator, locators["codex"])
self.assertEqual(
invoked_specs, [agy_spec, claude_spec, terra_spec]
)
self.assertEqual(transition_budget_counts, [1, 2])
self.assertEqual(final_locator, locators["pi"])
self.assertEqual(invoked_specs, [agy_spec, glm_spec])
self.assertEqual(transition_budget_counts, [1])
self.assertIn(
str(locators["agy"].resolve()), invoked_prompts[1]
)
self.assertIn(
str(locators["claude"].resolve()), invoked_prompts[2]
)
state = store.task_state(task)
decision = state["execution_decisions"]["worker"]
self.assertEqual(
decision["promotion_path"],
decision["used_candidates"],
[
{
"adapter": "agy",
"target": "Gemini 3.6 Flash (High)",
},
{"adapter": "claude", "target": "claude-opus-4-8"},
{"adapter": "codex", "target": "gpt-5.6-terra"},
{
"adapter": "pi",
"target": "iop/glm-5.2",
"thinking_level": "high",
},
],
)
self.assertEqual(
[entry["transition"] for entry in state["route_transition_history"]],
["initial", "provider-quota", "context-limit"],
["initial", "provider-quota"],
)
self.assertEqual(
dispatch.StageFailureBudget.from_decision(
@ -8794,7 +8808,7 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
finally:
store.close()
async def test_night_laguna_failure_continues_on_available_gemini(self):
async def test_night_gemini_quota_fails_over_to_glm_high(self):
nighttime = datetime(2026, 7, 26, 1, 0, 0, tzinfo=timezone(timedelta(hours=9)))
with tempfile.TemporaryDirectory() as temporary:
workspace = Path(temporary)
@ -8802,15 +8816,19 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
task = self.make_task(workspace)
store = dispatch.StateStore(workspace)
try:
laguna_spec = dispatch.AgentSpec("pi", "laguna-s:2.1", "pi/iop/laguna-s:2.1", local_pi=True)
locator = self.make_attempt_locator(workspace, task, laguna_spec)
gemini_spec = dispatch.AgentSpec("agy", "Gemini 3.6 Flash (High)", "agy/Gemini 3.6 Flash (High)")
glm_spec = dispatch.AgentSpec(
"pi", "glm-5.2", "pi/iop/glm-5.2 high",
local_pi=True, thinking_level="high",
)
locator = self.make_attempt_locator(workspace, task, gemini_spec)
invoked_specs = []
async def mock_invoke(*args, **kwargs):
spec = args[4]
invoked_specs.append(spec)
if spec.cli == "pi":
return (1, "provider-stream-disconnect", locator)
if spec.cli == "agy":
return (1, "provider-quota", locator)
return (0, None, locator)
with (
@ -8818,20 +8836,21 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
mock.patch.object(dispatch.asyncio, "sleep", new=mock.AsyncMock()),
):
dispatch.persisted_execution_decision(store, task, stage="worker", evaluated_at=nighttime)
success, final_loc = await dispatch.run_escalating(workspace, store, task, "worker", laguna_spec)
success, final_loc = await dispatch.run_escalating(workspace, store, task, "worker", gemini_spec)
self.assertTrue(success)
self.assertEqual(len(invoked_specs), 2)
self.assertEqual(invoked_specs[0].cli, "pi")
self.assertEqual(invoked_specs[1].cli, "agy")
self.assertEqual(invoked_specs, [gemini_spec, glm_spec])
state = store.task_state(task)
decisions = state["execution_decisions"]["worker"]
self.assertEqual(decisions["selected"]["adapter"], "agy")
self.assertEqual(decisions["transition"]["trigger"], "provider-stream-disconnect")
self.assertEqual(decisions["selected"]["adapter"], "pi")
self.assertEqual(decisions["selected"]["target"], "iop/glm-5.2")
self.assertEqual(decisions["selected"]["thinking_level"], "high")
self.assertEqual(decisions["transition"]["trigger"], "provider-quota")
finally:
store.close()
async def test_night_gemini_quota_exhaustion_blocks_without_bounce(self):
async def test_night_gemini_quota_initially_exhausted_selects_glm_high(self):
nighttime = datetime(2026, 7, 26, 1, 0, 0, tzinfo=timezone(timedelta(hours=9)))
with tempfile.TemporaryDirectory() as temporary:
workspace = Path(temporary)
@ -8839,18 +8858,21 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
task = self.make_task(workspace)
store = dispatch.StateStore(workspace)
try:
laguna_spec = dispatch.AgentSpec("pi", "laguna-s:2.1", "pi/iop/laguna-s:2.1", local_pi=True)
locator = self.make_attempt_locator(workspace, task, laguna_spec)
glm_spec = dispatch.AgentSpec(
"pi", "glm-5.2", "pi/iop/glm-5.2 high",
local_pi=True, thinking_level="high",
)
locator = self.make_attempt_locator(workspace, task, glm_spec)
invoked_specs = []
async def mock_invoke(*args, **kwargs):
spec = args[4]
invoked_specs.append(spec)
return (1, "provider-quota", locator)
return (0, None, locator)
quota_snap = {
"targets": [
{"adapter": "agy", "target": "Gemini 3.6 Flash (Medium)", "status": "exhausted"}
{"adapter": "agy", "target": "Gemini 3.6 Flash (High)", "status": "exhausted"}
]
}
store.update_task(task, quota_snapshot=quota_snap)
@ -8859,12 +8881,14 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
mock.patch.object(dispatch, "invoke", new=mock_invoke),
mock.patch.object(dispatch.asyncio, "sleep", new=mock.AsyncMock()),
):
dispatch.persisted_execution_decision(store, task, stage="worker", evaluated_at=nighttime)
success, final_loc = await dispatch.run_escalating(workspace, store, task, "worker", laguna_spec)
_, selected = dispatch.persisted_execution_decision(store, task, stage="worker", evaluated_at=nighttime)
success, final_loc = await dispatch.run_escalating(workspace, store, task, "worker", selected)
self.assertFalse(success)
self.assertTrue(success)
self.assertEqual(invoked_specs, [glm_spec])
state = store.task_state(task)
self.assertIn("no_failover_candidate", state.get("blocked", ""))
self.assertEqual(state["execution_decisions"]["worker"]["selected"]["thinking_level"], "high")
self.assertIsNone(state.get("blocked"))
finally:
store.close()
@ -8883,16 +8907,19 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
"source": "iop-node quota-probe",
"checked_at": "2026-07-25T03:00:00+09:00",
"targets": [
{"adapter": "agy", "target": "Gemini 3.6 Flash (Medium)", "status": "exhausted"}
{"adapter": "agy", "target": "Gemini 3.6 Flash (High)", "status": "exhausted"}
],
},
)
laguna_spec = dispatch.AgentSpec("pi", "iop/laguna-s:2.1", "pi/iop/laguna-s:2.1", local_pi=True)
glm_spec = dispatch.AgentSpec(
"pi", "glm-5.2", "pi/iop/glm-5.2 high",
local_pi=True, thinking_level="high",
)
decision, spec = dispatch.persisted_execution_decision(store, task, stage="worker", evaluated_at=daytime)
self.assertEqual(spec.cli, "pi")
self.assertEqual(spec, glm_spec)
locator = self.make_attempt_locator(workspace, task, laguna_spec)
locator = self.make_attempt_locator(workspace, task, glm_spec)
store.update_task(
task,
@ -8901,7 +8928,7 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
"source": "iop-node quota-probe",
"checked_at": "2026-07-25T04:00:00+09:00",
"targets": [
{"adapter": "agy", "target": "Gemini 3.6 Flash (Medium)", "status": "available"}
{"adapter": "agy", "target": "Gemini 3.6 Flash (High)", "status": "available"}
],
},
)
@ -8916,7 +8943,7 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
mock.patch.object(dispatch, "invoke", new=mock_invoke),
mock.patch.object(dispatch.asyncio, "sleep", new=mock.AsyncMock()),
):
success, final_loc = await dispatch.run_escalating(workspace, store, task, "worker", laguna_spec)
success, final_loc = await dispatch.run_escalating(workspace, store, task, "worker", glm_spec)
self.assertFalse(success)
self.assertEqual(len(invoked_specs), 1)
@ -9043,13 +9070,8 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
finally:
store.close()
async def test_promotion_chain_exhaustion_stays_on_last_target(self):
"""Cloud promotion chain: AGY→Claude→Terra exhausted.
When the last canonical target (Terra) fails with a promotable failure
and no promotion target remains, the worker must stay on Terra for
same-target recovery rather than falling through to legacy promoted_spec().
"""
async def test_cloud_g05_g06_gemini_quota_fails_over_to_glm_high(self):
"""Cloud G05G06 sends qualified Gemini failures to Pi GLM High."""
daytime = datetime(
2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9))
)
@ -9062,16 +9084,12 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
agy_spec = dispatch.AgentSpec(
"agy", "Gemini 3.6 Flash (High)", "agy/Gemini 3.6 Flash (High)"
)
claude_spec = dispatch.AgentSpec(
"claude", "claude-opus-4-8", "claude/claude-opus-4-8 xhigh"
)
terra_spec = dispatch.AgentSpec(
"codex", "gpt-5.6-terra", "codex/gpt-5.6-terra high",
reasoning_effort="high",
glm_spec = dispatch.AgentSpec(
"pi", "glm-5.2", "pi/iop/glm-5.2 high",
local_pi=True, thinking_level="high",
)
loc_agy = self.make_attempt_locator(workspace, task, agy_spec)
loc_claude = self.make_attempt_locator(workspace, task, claude_spec)
loc_terra = self.make_attempt_locator(workspace, task, terra_spec)
loc_glm = self.make_attempt_locator(workspace, task, glm_spec)
invoked_specs = []
async def mock_invoke(*args, **kwargs):
@ -9079,13 +9097,7 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
invoked_specs.append(spec)
if spec.cli == "agy":
return (1, "provider-quota", loc_agy)
if spec.cli == "claude":
return (1, "context-limit", loc_claude)
# Terra fails once, then succeeds — chain exhaustion keeps it on Terra
terra_count = sum(1 for s in invoked_specs if s.cli == "codex")
if terra_count == 1:
return (1, "provider-quota", loc_terra)
return (0, None, loc_terra)
return (0, None, loc_glm)
with (
mock.patch.object(dispatch, "invoke", new=mock_invoke),
@ -9098,34 +9110,27 @@ class DispatcherCanonicalFailoverIntegrationTest(unittest.IsolatedAsyncioTestCas
workspace, store, task, "worker", agy_spec
)
# Chain: AGY → Claude → Terra, then Terra retries on same target (no legacy fallthrough)
self.assertTrue(success)
self.assertEqual(
[s.cli for s in invoked_specs],
["agy", "claude", "codex", "codex"],
["agy", "pi"],
)
# The third and fourth invocations are both Terra (same-target recovery)
self.assertEqual(invoked_specs[2].cli, "codex")
self.assertEqual(invoked_specs[2].model, "gpt-5.6-terra")
self.assertEqual(invoked_specs[3].cli, "codex")
self.assertEqual(invoked_specs[3].model, "gpt-5.6-terra")
self.assertEqual(invoked_specs[1], glm_spec)
state = store.task_state(task)
decision = state["execution_decisions"]["worker"]
# Promotion path should include all three transitions
self.assertEqual(
len(decision["promotion_path"]), 3,
decision["used_candidates"],
[
{"adapter": "agy", "target": "Gemini 3.6 Flash (High)"},
{"adapter": "pi", "target": "iop/glm-5.2", "thinking_level": "high"},
],
)
# History should show the promotions but no legacy recovery
transitions = [h["transition"] for h in state["route_transition_history"]]
self.assertIn("provider-quota", transitions)
self.assertIn("context-limit", transitions)
# No legacy promoted_spec() fallthrough: selected stays on Terra
self.assertEqual(
decision["selected"]["adapter"], "codex"
)
self.assertEqual(
decision["selected"]["target"], "gpt-5.6-terra"
)
self.assertEqual(decision["selected"]["adapter"], "pi")
self.assertEqual(decision["selected"]["target"], "iop/glm-5.2")
self.assertEqual(decision["selected"]["thinking_level"], "high")
finally:
store.close()
@ -9261,14 +9266,14 @@ class SelectorDispatcherIntegrationTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(call_args[3], "worker")
spec_worker = call_args[4]
self.assertEqual(spec_worker.cli, "agy")
self.assertEqual(spec_worker.model, "Gemini 3.6 Flash (Medium)")
self.assertEqual(spec_worker.model, "Gemini 3.6 Flash (High)")
state_after_worker = store.task_state(task)
self.assertTrue(state_after_worker.get("worker_done"))
self.assertIn("worker", state_after_worker.get("execution_decisions", {}))
self.assertEqual(
state_after_worker["execution_decisions"]["worker"]["selected"]["target"],
"Gemini 3.6 Flash (Medium)",
"Gemini 3.6 Flash (High)",
)
await dispatch.run_review(workspace, store, task)
@ -9371,7 +9376,7 @@ class SelectorDispatcherIntegrationTest(unittest.IsolatedAsyncioTestCase):
task1_banners = [b for b in banners if b[1] == task1.name]
task2_banners = [b for b in banners if b[1] == task2.name]
self.assertTrue(any("model=agy/" in line for b in task1_banners for line in b[2]))
self.assertTrue(any("model=pi/" in line for b in task2_banners for line in b[2]))
self.assertTrue(any("model=agy/" in line for b in task2_banners for line in b[2]))
state1_after = store.task_state(task1)
state2_after = store.task_state(task2)
@ -9397,21 +9402,21 @@ class SelectorDispatcherIntegrationTest(unittest.IsolatedAsyncioTestCase):
task = self.make_task(workspace, lane="local", grade=8)
store = dispatch.StateStore(workspace)
try:
# 1. Initial decision daytime (KST 14:00) -> agy Gemini Medium
# 1. Initial decision daytime (KST 14:00) -> agy Gemini High
dec1, spec1 = dispatch.persisted_execution_decision(
store, task, stage="worker", evaluated_at=daytime
)
self.assertEqual(spec1.cli, "agy")
self.assertEqual(spec1.model, "Gemini 3.6 Flash (Medium)")
self.assertEqual(spec1.model, "Gemini 3.6 Flash (High)")
# 2. Resuming at nighttime (KST 23:00) keeps pinned Gemini Medium
# 2. Resuming at nighttime (KST 23:00) keeps pinned Gemini High
dec2, spec2 = dispatch.persisted_execution_decision(
store, task, stage="worker", evaluated_at=nighttime
)
self.assertEqual(spec2.cli, "agy")
self.assertEqual(spec2.model, "Gemini 3.6 Flash (Medium)")
self.assertEqual(spec2.model, "Gemini 3.6 Flash (High)")
# 3. Body edit (header intact) keeps pinned Gemini Medium
# 3. Body edit (header intact) keeps pinned Gemini High
plan_file = task.plan
header = f"<!-- task=selector_dispatch_integration/01_unit plan=0 tag=API -->\n"
plan_file.write_text(header + "\n# Modified Body Content\n", encoding="utf-8")
@ -9420,14 +9425,14 @@ class SelectorDispatcherIntegrationTest(unittest.IsolatedAsyncioTestCase):
)
self.assertEqual(spec3.cli, "agy")
# 4. New generation header (plan=1) re-evaluates initial decision at nighttime -> pi Laguna
# 4. New generation header (plan=1) re-evaluates initial decision at nighttime -> Gemini High
plan_file.write_text("<!-- task=selector_dispatch_integration/01_unit plan=1 tag=API -->\n\n# New Plan\n", encoding="utf-8")
task_new = dispatch.scan_tasks(workspace, None)[0]
dec4, spec4 = dispatch.persisted_execution_decision(
store, task_new, stage="worker", evaluated_at=nighttime
)
self.assertEqual(spec4.cli, "pi")
self.assertEqual(spec4.model, "laguna-s:2.1")
self.assertEqual(spec4.cli, "agy")
self.assertEqual(spec4.model, "Gemini 3.6 Flash (High)")
finally:
store.close()
@ -9907,17 +9912,20 @@ class SelectorDispatcherIntegrationTest(unittest.IsolatedAsyncioTestCase):
daytime = datetime(2026, 7, 26, 14, 0, 0, tzinfo=timezone(timedelta(hours=9)))
nighttime = datetime(2026, 7, 26, 1, 0, 0, tzinfo=timezone(timedelta(hours=9)))
# Case 1: Day local G08 completion on Laguna requires selfcheck with pinned Laguna
# Case 1: Day local G08 Gemini quota failover completes on pinned GLM High.
with tempfile.TemporaryDirectory() as temporary:
workspace = Path(temporary)
(workspace / ".git").mkdir()
task = self.make_task(workspace, lane="local", grade=8)
store = dispatch.StateStore(workspace)
try:
gemini_spec = dispatch.AgentSpec("agy", "Gemini 3.6 Flash (Medium)", "agy/Gemini 3.6 Flash (Medium)")
laguna_spec = dispatch.AgentSpec("pi", "laguna-s:2.1", "pi/iop/laguna-s:2.1", local_pi=True)
gemini_spec = dispatch.AgentSpec("agy", "Gemini 3.6 Flash (High)", "agy/Gemini 3.6 Flash (High)")
glm_spec = dispatch.AgentSpec(
"pi", "glm-5.2", "pi/iop/glm-5.2 high",
local_pi=True, thinking_level="high",
)
loc_gemini = self.make_attempt_locator(workspace, task, gemini_spec)
loc_laguna = self.make_attempt_locator(workspace, task, laguna_spec)
loc_glm = self.make_attempt_locator(workspace, task, glm_spec)
invoked_specs = []
async def mock_invoke(*args, **kwargs):
@ -9925,7 +9933,7 @@ class SelectorDispatcherIntegrationTest(unittest.IsolatedAsyncioTestCase):
invoked_specs.append(spec)
if spec.cli == "agy":
return (1, "provider-quota", loc_gemini)
return (0, None, loc_laguna)
return (0, None, loc_glm)
with (
mock.patch.object(dispatch, "invoke", new=mock_invoke),
@ -9950,7 +9958,7 @@ class SelectorDispatcherIntegrationTest(unittest.IsolatedAsyncioTestCase):
async def mock_invoke_selfcheck(*args, **kwargs):
spec = args[4]
selfcheck_specs.append(spec)
return (0, None, loc_laguna)
return (0, None, loc_glm)
with (
mock.patch.object(dispatch, "invoke", new=mock_invoke_selfcheck),
@ -9968,24 +9976,20 @@ class SelectorDispatcherIntegrationTest(unittest.IsolatedAsyncioTestCase):
finally:
store.close()
# Case 2: Night local G08 completion on Gemini skips selfcheck
# Case 2: Night local G08 remains on Gemini High and skips selfcheck.
with tempfile.TemporaryDirectory() as temporary:
workspace = Path(temporary)
(workspace / ".git").mkdir()
task = self.make_task(workspace, lane="local", grade=8)
store = dispatch.StateStore(workspace)
try:
gemini_spec = dispatch.AgentSpec("agy", "Gemini 3.6 Flash (Medium)", "agy/Gemini 3.6 Flash (Medium)")
laguna_spec = dispatch.AgentSpec("pi", "laguna-s:2.1", "pi/iop/laguna-s:2.1", local_pi=True)
gemini_spec = dispatch.AgentSpec("agy", "Gemini 3.6 Flash (High)", "agy/Gemini 3.6 Flash (High)")
loc_gemini = self.make_attempt_locator(workspace, task, gemini_spec)
loc_laguna = self.make_attempt_locator(workspace, task, laguna_spec)
invoked_specs = []
async def mock_invoke(*args, **kwargs):
spec = args[4]
invoked_specs.append(spec)
if spec.cli == "pi":
return (1, "provider-stream-disconnect", loc_laguna)
return (0, None, loc_gemini)
with (
@ -9995,12 +9999,12 @@ class SelectorDispatcherIntegrationTest(unittest.IsolatedAsyncioTestCase):
dispatch.persisted_execution_decision(store, task, stage="worker", evaluated_at=nighttime)
await dispatch.run_worker(workspace, store, task)
self.assertEqual([s.cli for s in invoked_specs], ["pi", "agy"])
self.assertEqual([s.cli for s in invoked_specs], ["agy"])
state = store.task_state(task)
self.assertEqual(state["execution_class"], "cloud_model")
self.assertTrue(state["selfcheck_done"])
self.assertEqual(dispatch.task_stage(task, state), "review")
self.assertEqual([h["transition"] for h in state["route_transition_history"]], ["initial", "resume", "provider-stream-disconnect"])
self.assertEqual([h["transition"] for h in state["route_transition_history"]], ["initial", "resume"])
finally:
store.close()
@ -10213,7 +10217,7 @@ class ThroughputQuotaBatchTest(unittest.TestCase):
finally:
store.close()
def test_night_local_and_official_review_zero_probe_count(self):
def test_night_local_gemini_high_and_official_review_probe_count(self):
with tempfile.TemporaryDirectory() as temporary:
workspace = Path(temporary)
(workspace / ".git").mkdir()
@ -10236,9 +10240,11 @@ class ThroughputQuotaBatchTest(unittest.TestCase):
ready = [(t_night, "worker"), (t_review, "review")]
batch_snap = dispatch.build_admission_batch_snapshot(store, ready, now)
# Night local-G08 candidate is local_model first, official review stage is not worker -> 0 probes needed!
self.assertIsNone(batch_snap)
self.assertEqual(len(probe_calls), 0)
# Night local-G08 now starts on Gemini High; review remains excluded.
self.assertIsNotNone(batch_snap)
self.assertEqual(len(probe_calls), 1)
self.assertEqual(probe_calls[0]["adapter"], "agy")
self.assertEqual(probe_calls[0]["target"], "Gemini 3.6 Flash (High)")
finally:
store.close()
@ -10649,7 +10655,7 @@ class ThroughputQuotaBatchTest(unittest.TestCase):
finally:
store.close()
def test_retry_blocked_scopes_to_blocked_worker_and_refreshes_pinned_alternate(self):
def test_retry_blocked_scopes_to_blocked_worker_and_selects_glm_fallback(self):
async def _async_run():
nighttime = datetime(2026, 7, 26, 23, 0, 0, tzinfo=timezone(timedelta(hours=9)))
with tempfile.TemporaryDirectory() as temporary:
@ -10689,8 +10695,8 @@ class ThroughputQuotaBatchTest(unittest.TestCase):
store, t_blocked, stage="worker", evaluated_at=nighttime, quota_snapshot=normal_snap
)
run.assert_not_called()
self.assertEqual(d_blocked["selected"]["adapter"], "pi")
self.assertEqual(d_blocked["selected"]["target"], "iop/laguna-s:2.1")
self.assertEqual(d_blocked["selected"]["adapter"], "agy")
self.assertEqual(d_blocked["selected"]["target"], "Gemini 3.6 Flash (High)")
loc_path = workspace / "attempt-loc.json"
loc_path.write_text("{}", encoding="utf-8")
@ -10756,6 +10762,7 @@ class ThroughputQuotaBatchTest(unittest.TestCase):
with mock.patch.object(selector, "probe_candidate_quota", side_effect=mock_probe), \
mock.patch.object(dispatch, "run_review", side_effect=fake_run_review), \
mock.patch.object(dispatch, "ensure_review_shared_state"), \
mock.patch.object(dispatch, "implementation_review_errors", return_value=[]), \
mock.patch.object(dispatch, "invoke", side_effect=fake_invoke), \
mock.patch.object(dispatch, "datetime") as datetime_mock, \
mock.patch("subprocess.run", side_effect=AssertionError) as run_sub:
@ -10763,16 +10770,15 @@ class ThroughputQuotaBatchTest(unittest.TestCase):
res = await dispatch.dispatch_with_store(args, workspace, store)
run_sub.assert_not_called()
self.assertEqual(len(probe_calls), 1)
self.assertEqual(probe_calls[0]["adapter"], "agy")
self.assertEqual(probe_calls[0]["target"], "Gemini 3.6 Flash (Medium)")
self.assertEqual(len(probe_calls), 0)
st_blocked_after = store.task_state(t_blocked)
self.assertIsNone(st_blocked_after.get("blocked"))
self.assertFalse(st_blocked_after.get("retry_quota_refresh_pending"))
dec_after = st_blocked_after["execution_decisions"]["worker"]
self.assertEqual(dec_after["selected"]["adapter"], "agy")
self.assertEqual(dec_after["selected"]["target"], "Gemini 3.6 Flash (Medium)")
self.assertEqual(dec_after["selected"]["adapter"], "pi")
self.assertEqual(dec_after["selected"]["target"], "iop/glm-5.2")
self.assertEqual(dec_after["selected"]["thinking_level"], "high")
self.assertEqual(dec_after["transition"]["trigger"], "provider-quota")
self.assertEqual(dec_after["work_unit_id"], d_blocked["work_unit_id"])
@ -10782,9 +10788,10 @@ class ThroughputQuotaBatchTest(unittest.TestCase):
self.assertIn("agy", used_adapters)
self.assertTrue(len(st_blocked_after.get("route_transition_history", [])) >= 2)
blocked_invocations = [call for call in invoke_calls if call[0] == t_blocked.name]
self.assertEqual(len(blocked_invocations), 1)
self.assertEqual(len(blocked_invocations), 2)
self.assertEqual(blocked_invocations[0][1], "worker")
self.assertEqual(blocked_invocations[0][4], loc_path)
self.assertEqual(blocked_invocations[1][1], "selfcheck")
st_normal_after = store.task_state(t_normal)
self.assertFalse(st_normal_after.get("retry_quota_refresh_pending"))

View file

@ -25,10 +25,10 @@ def at_utc(hour: int, minute: int = 0, second: int = 0) -> datetime:
class ExecutionTargetPolicyTests(unittest.TestCase):
def test_local_g07_route_uses_kst_boundaries(self):
cases = [
(at_utc(21, 59, 59), "pi", "iop/laguna-s:2.1", "kst-night-[23:00,07:00)"),
(at_utc(22, 0, 0), "agy", "Gemini 3.6 Flash (Medium)", "kst-day-[07:00,23:00)"),
(at_utc(13, 59, 59), "agy", "Gemini 3.6 Flash (Medium)", "kst-day-[07:00,23:00)"),
(at_utc(14, 0, 0), "pi", "iop/laguna-s:2.1", "kst-night-[23:00,07:00)"),
(at_utc(21, 59, 59), "agy", "Gemini 3.6 Flash (High)", "kst-night-[23:00,07:00)"),
(at_utc(22, 0, 0), "agy", "Gemini 3.6 Flash (High)", "kst-day-[07:00,23:00)"),
(at_utc(13, 59, 59), "agy", "Gemini 3.6 Flash (High)", "kst-day-[07:00,23:00)"),
(at_utc(14, 0, 0), "agy", "Gemini 3.6 Flash (High)", "kst-night-[23:00,07:00)"),
]
for evaluated_at, adapter, target, time_window in cases:
with self.subTest(evaluated_at=evaluated_at):
@ -49,9 +49,9 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
stage="worker", lane="local", grade=8, evaluated_at=night_time
)
self.assertEqual(decision.rule_id, "worker-local-g07-g08-kst-night")
self.assertEqual(decision.candidates, (policy.PI_LAGUNA, policy.AGY_GEMINI_MEDIUM))
self.assertEqual(decision.candidates, (policy.AGY_GEMINI_HIGH, policy.PI_GLM_HIGH))
self.assertEqual(decision.time_window, "kst-night-[23:00,07:00)")
self.assertEqual(decision.candidates[0].target, "iop/laguna-s:2.1")
self.assertEqual(decision.candidates[0].target, "Gemini 3.6 Flash (High)")
def test_worker_grade_matrix_has_no_gaps(self):
daytime = at_utc(3)
@ -61,8 +61,8 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
grade: ("pi", "iop/ornith:35b", True)
for grade in range(1, 7)
},
7: ("agy", "Gemini 3.6 Flash (Medium)", False),
8: ("agy", "Gemini 3.6 Flash (Medium)", False),
7: ("agy", "Gemini 3.6 Flash (High)", False),
8: ("agy", "Gemini 3.6 Flash (High)", False),
9: ("claude", "claude-opus-4-8", False),
10: ("claude", "claude-opus-4-8", False),
},
@ -103,7 +103,7 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
route,
)
def test_cloud_g01_g02_uses_ordered_spark_gemini_haiku_candidates(self):
def test_cloud_g01_g02_uses_ordered_spark_gemini_glm_candidates(self):
for grade in (1, 2):
with self.subTest(grade=grade):
decision = policy.select_policy(
@ -117,7 +117,7 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
(
policy.CODEX_SPARK_XHIGH,
policy.AGY_GEMINI_LOW,
policy.CLAUDE_HAIKU_XHIGH,
policy.PI_GLM_LOW,
),
)
self.assertEqual(
@ -138,7 +138,7 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
self.assertEqual(decision.rule_id, "official-review-codex")
self.assertEqual(decision.candidates, (policy.CODEX_SOL_XHIGH,))
def test_local_g07_g08_candidate_order_uses_kst_boundaries(self):
def test_local_g07_g08_candidate_order_uses_gemini_high_then_glm_high(self):
daytime = policy.select_policy(
stage="worker",
lane="local",
@ -151,14 +151,8 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
grade=8,
evaluated_at=at_utc(15),
)
self.assertEqual(
[candidate.adapter for candidate in daytime.candidates],
["agy", "pi"],
)
self.assertEqual(
[candidate.adapter for candidate in nighttime.candidates],
["pi", "agy"],
)
self.assertEqual(daytime.candidates, (policy.AGY_GEMINI_HIGH, policy.PI_GLM_HIGH))
self.assertEqual(nighttime.candidates, (policy.AGY_GEMINI_HIGH, policy.PI_GLM_HIGH))
def test_invalid_inputs_are_rejected(self):
cases = [
@ -184,9 +178,6 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
def test_cloud_promotion_matrix(self):
cases = [
(policy.AGY_GEMINI_LOW, policy.CLAUDE_OPUS),
(policy.AGY_GEMINI_MEDIUM, policy.CLAUDE_OPUS),
(policy.AGY_GEMINI_HIGH, policy.CLAUDE_OPUS),
(policy.CLAUDE_OPUS, policy.CODEX_TERRA_HIGH),
(policy.CLAUDE_HAIKU_XHIGH, None),
(policy.CODEX_SPARK_XHIGH, None),
@ -194,6 +185,9 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
(policy.CODEX_TERRA_HIGH, None),
(policy.PI_ORNITH, None),
(policy.PI_LAGUNA, None),
(policy.PI_GLM_LOW, None),
(policy.PI_GLM_MEDIUM, None),
(policy.PI_GLM_HIGH, None),
]
for current, expected in cases:
with self.subTest(current=current):
@ -202,7 +196,9 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
for target in policy.CANONICAL_TARGETS:
with self.subTest(identity=target.target):
self.assertEqual(
policy.canonical_target(target.adapter, target.target),
policy.canonical_target(
target.adapter, target.target, target.thinking_level
),
target,
)
self.assertIsNone(policy.canonical_target("codex", "unknown"))
@ -211,6 +207,9 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
cases = [
(policy.PI_ORNITH, None),
(policy.PI_LAGUNA, None),
(policy.PI_GLM_LOW, None),
(policy.PI_GLM_MEDIUM, None),
(policy.PI_GLM_HIGH, None),
(
policy.AGY_GEMINI_LOW,
policy.QuotaProbeSpec("agy", "Gemini 3.6 Flash (Low)", ("overall", "model:Gemini 3.6 Flash (Low)")),

View file

@ -356,12 +356,12 @@ class SelectorContractTests(unittest.TestCase):
task_file, evaluated_at=kst(12)
)
self.assertEqual(daytime["selected"]["adapter"], "agy")
# A new night route changes target, but resume preserves the pin.
# Day/night retain the same Gemini High primary, and resume remains pinned.
night_initial = selector.select_execution_target(
task_file, evaluated_at=kst(2)
)
self.assertEqual(night_initial["selected"]["adapter"], "pi")
self.assertEqual(night_initial["selected"]["target"], "iop/laguna-s:2.1")
self.assertEqual(night_initial["selected"]["adapter"], "agy")
self.assertEqual(night_initial["selected"]["target"], "Gemini 3.6 Flash (High)")
resumed = selector.select_execution_target(
task_file,
evaluated_at=kst(2),
@ -373,7 +373,7 @@ class SelectorContractTests(unittest.TestCase):
self.assertEqual(resumed["transition"]["trigger"], "resume")
self.assertEqual(
resumed["transition"]["previous_target"],
{"adapter": "agy", "target": "Gemini 3.6 Flash (Medium)"},
{"adapter": "agy", "target": "Gemini 3.6 Flash (High)"},
)
def test_resume_requires_matching_prior_decision(self):
@ -432,12 +432,12 @@ class SelectorContractTests(unittest.TestCase):
class SelectorRouteMatrixTests(unittest.TestCase):
def test_local_g07_g08_use_kst_boundaries(self):
def test_local_g07_g08_use_gemini_high_on_both_kst_windows(self):
cases = [
(kst(6, 59, 59), "pi", "iop/laguna-s:2.1"),
(kst(7, 0, 0), "agy", "Gemini 3.6 Flash (Medium)"),
(kst(22, 59, 59), "agy", "Gemini 3.6 Flash (Medium)"),
(kst(23, 0, 0), "pi", "iop/laguna-s:2.1"),
(kst(6, 59, 59), "agy", "Gemini 3.6 Flash (High)"),
(kst(7, 0, 0), "agy", "Gemini 3.6 Flash (High)"),
(kst(22, 59, 59), "agy", "Gemini 3.6 Flash (High)"),
(kst(23, 0, 0), "agy", "Gemini 3.6 Flash (High)"),
]
with TemporaryDirectory() as tmp:
for grade in (7, 8):
@ -455,8 +455,8 @@ class SelectorRouteMatrixTests(unittest.TestCase):
"local": {
**{g: ("pi", "iop/ornith:35b", "local_model", True)
for g in range(1, 7)},
7: ("agy", "Gemini 3.6 Flash (Medium)", "cloud_model", False),
8: ("agy", "Gemini 3.6 Flash (Medium)", "cloud_model", False),
7: ("agy", "Gemini 3.6 Flash (High)", "cloud_model", False),
8: ("agy", "Gemini 3.6 Flash (High)", "cloud_model", False),
9: ("claude", "claude-opus-4-8", "cloud_model", False),
10: ("claude", "claude-opus-4-8", "cloud_model", False),
},
@ -530,13 +530,13 @@ class SelectorRouteMatrixTests(unittest.TestCase):
[c["adapter"] for c in daytime], ["agy", "pi"]
)
self.assertEqual(
[c["adapter"] for c in nighttime], ["pi", "agy"]
[c["adapter"] for c in nighttime], ["agy", "pi"]
)
single = write_task_file(Path(tmp), "PLAN", "cloud", 5)
candidates = selector.select_execution_target(
single, evaluated_at=kst(12)
)["candidates"]
self.assertEqual([c["candidate_rank"] for c in candidates], [1])
self.assertEqual([c["candidate_rank"] for c in candidates], [1, 2])
class SelectorQuotaRepresentationTests(unittest.TestCase):
@ -641,7 +641,7 @@ class SelectorQuotaRepresentationTests(unittest.TestCase):
result["quota"]["targets"], snapshot["targets"]
)
def test_exhausted_gemini_falls_back_to_laguna(self):
def test_exhausted_gemini_falls_back_to_glm_high(self):
snapshot = {
"snapshot_id": "gemini-exhausted",
"source": "iop-node quota-probe",
@ -649,7 +649,7 @@ class SelectorQuotaRepresentationTests(unittest.TestCase):
"targets": [
{
"adapter": "agy",
"target": "Gemini 3.6 Flash (Medium)",
"target": "Gemini 3.6 Flash (High)",
"status": "exhausted",
}
],
@ -660,7 +660,8 @@ class SelectorQuotaRepresentationTests(unittest.TestCase):
task_file, evaluated_at=kst(12), quota_snapshot=snapshot
)
self.assertEqual(result["selected"]["adapter"], "pi")
self.assertEqual(result["selected"]["target"], "iop/laguna-s:2.1")
self.assertEqual(result["selected"]["target"], "iop/glm-5.2")
self.assertEqual(result["selected"]["thinking_level"], "high")
def test_all_candidates_exhausted_returns_no_eligible_target(self):
snapshot = {
@ -1148,7 +1149,7 @@ class SelectorIdentityAndQuotaRoundtripTests(unittest.TestCase):
class SelectorFailoverContractTests(unittest.TestCase):
def test_cloud_g01_g02_quota_failover_follows_spark_gemini_haiku_order(self):
def test_cloud_g01_g02_quota_failover_follows_spark_gemini_glm_low_order(self):
with TemporaryDirectory() as tmp:
task_file = write_task_file(Path(tmp), "PLAN", "cloud", 1)
initial = selector.select_execution_target(
@ -1164,7 +1165,7 @@ class SelectorFailoverContractTests(unittest.TestCase):
failure_class="provider-quota",
quota_probe_command="missing-probe",
)
haiku = selector.select_execution_target(
glm = selector.select_execution_target(
task_file,
evaluated_at=kst(12),
transition="failover",
@ -1181,7 +1182,7 @@ class SelectorFailoverContractTests(unittest.TestCase):
[
("codex", "gpt-5.3-codex-spark"),
("agy", "Gemini 3.6 Flash (Low)"),
("claude", "claude-haiku-4-5"),
("pi", "iop/glm-5.2"),
],
)
self.assertEqual(
@ -1189,15 +1190,16 @@ class SelectorFailoverContractTests(unittest.TestCase):
("agy", "Gemini 3.6 Flash (Low)"),
)
self.assertEqual(
(haiku["selected"]["adapter"], haiku["selected"]["target"]),
("claude", "claude-haiku-4-5"),
(glm["selected"]["adapter"], glm["selected"]["target"]),
("pi", "iop/glm-5.2"),
)
self.assertEqual(glm["selected"]["thinking_level"], "low")
self.assertEqual(
haiku["used_candidates"],
glm["used_candidates"],
[
{"adapter": "codex", "target": "gpt-5.3-codex-spark"},
{"adapter": "agy", "target": "Gemini 3.6 Flash (Low)"},
{"adapter": "claude", "target": "claude-haiku-4-5"},
{"adapter": "pi", "target": "iop/glm-5.2", "thinking_level": "low"},
],
)
with self.assertRaises(selector.SelectorInputError) as exhausted:
@ -1205,7 +1207,7 @@ class SelectorFailoverContractTests(unittest.TestCase):
task_file,
evaluated_at=kst(12),
transition="failover",
prior_decision=haiku,
prior_decision=glm,
failure_class="provider-quota",
quota_probe_command="missing-probe",
)
@ -1266,7 +1268,7 @@ class SelectorFailoverContractTests(unittest.TestCase):
"targets": [
{
"adapter": "agy",
"target": "Gemini 3.6 Flash (Medium)",
"target": "Gemini 3.6 Flash (High)",
"status": "exhausted",
}
],
@ -1278,7 +1280,7 @@ class SelectorFailoverContractTests(unittest.TestCase):
"targets": [
{
"adapter": "agy",
"target": "Gemini 3.6 Flash (Medium)",
"target": "Gemini 3.6 Flash (High)",
"status": "available",
}
],
@ -1289,7 +1291,8 @@ class SelectorFailoverContractTests(unittest.TestCase):
task_file, evaluated_at=kst(12), quota_snapshot=gemini_exhausted_snapshot
)
self.assertEqual(prior["selected"]["adapter"], "pi")
self.assertEqual(prior["selected"]["target"], "iop/laguna-s:2.1")
self.assertEqual(prior["selected"]["target"], "iop/glm-5.2")
self.assertEqual(prior["selected"]["thinking_level"], "high")
with self.assertRaises(selector.SelectorInputError) as ctx:
selector.select_execution_target(
@ -1425,8 +1428,8 @@ class SelectorFailoverContractTests(unittest.TestCase):
self.assertEqual(
night_failover["used_candidates"],
[
{"adapter": "agy", "target": "Gemini 3.6 Flash (Medium)"},
{"adapter": "pi", "target": "iop/laguna-s:2.1"},
{"adapter": "agy", "target": "Gemini 3.6 Flash (High)"},
{"adapter": "pi", "target": "iop/glm-5.2", "thinking_level": "high"},
],
)
@ -1444,9 +1447,9 @@ class SelectorFailoverContractTests(unittest.TestCase):
def test_runtime_probed_cloud_alternate_round_trips_selected_snapshot(self):
snapshot = go_quota_snapshot(
"agy",
"Gemini 3.6 Flash (Medium)",
"Gemini 3.6 Flash (High)",
"available",
snapshot_id="night-gemini-available",
snapshot_id="gemini-high-available",
)
completed = mock.Mock(returncode=0, stdout=json.dumps(snapshot))
with TemporaryDirectory() as tmp, mock.patch(
@ -1454,9 +1457,9 @@ class SelectorFailoverContractTests(unittest.TestCase):
) as run_mock:
task_file = write_task_file(Path(tmp), "PLAN", "local", 8)
prior = selector.select_execution_target(
task_file, evaluated_at=kst(1)
task_file, evaluated_at=kst(1), quota_snapshot=snapshot
)
self.assertEqual(prior["selected"]["adapter"], "pi")
self.assertEqual(prior["selected"]["adapter"], "agy")
result = selector.select_execution_target(
task_file,
evaluated_at=kst(1),
@ -1465,29 +1468,21 @@ class SelectorFailoverContractTests(unittest.TestCase):
failure_class="provider-stream-disconnect",
)
self.assertEqual(result["selected"]["adapter"], "agy")
selected_candidate = next(
candidate
for candidate in result["candidates"]
if candidate["adapter"] == "agy"
)
self.assertEqual(selected_candidate["quota_status"], "available")
self.assertEqual(result["quota"]["status"], "available")
self.assertEqual(
result["quota"]["snapshot_id"], snapshot["snapshot_id"]
)
self.assertEqual(result["quota"]["targets"], snapshot["targets"])
self.assertEqual(run_mock.call_count, 2)
self.assertEqual(result["selected"]["adapter"], "pi")
self.assertEqual(result["selected"]["target"], "iop/glm-5.2")
self.assertEqual(result["selected"]["thinking_level"], "high")
self.assertEqual(result["quota"]["status"], "not_applicable")
self.assertEqual(run_mock.call_count, 1)
def test_policy_owned_cloud_promotion_chain_and_no_bounce(self):
with TemporaryDirectory() as tmp:
task_file = write_task_file(Path(tmp), "PLAN", "cloud", 5)
task_file = write_task_file(Path(tmp), "PLAN", "cloud", 7)
initial = selector.select_execution_target(
task_file,
evaluated_at=kst(12),
quota_probe_command="missing-probe",
)
claude = selector.select_execution_target(
terra = selector.select_execution_target(
task_file,
evaluated_at=kst(12),
transition="promotion",
@ -1497,23 +1492,15 @@ class SelectorFailoverContractTests(unittest.TestCase):
resumed = selector.select_execution_target(
task_file,
evaluated_at=kst(23),
transition="resume",
prior_decision=claude,
)
terra = selector.select_execution_target(
task_file,
evaluated_at=kst(23),
transition="promotion",
prior_decision=resumed,
failure_class="context-limit",
transition="resume", prior_decision=terra,
)
self.assertEqual(
(claude["selected"]["adapter"], claude["selected"]["target"]),
(initial["selected"]["adapter"], initial["selected"]["target"]),
("claude", "claude-opus-4-8"),
)
self.assertEqual(claude["transition"]["kind"], "promotion")
self.assertEqual(claude["transition"]["trigger"], "provider-quota")
self.assertEqual(terra["transition"]["kind"], "promotion")
self.assertEqual(terra["transition"]["trigger"], "provider-quota")
self.assertEqual(
(terra["selected"]["adapter"], terra["selected"]["target"]),
("codex", "gpt-5.6-terra"),
@ -1521,10 +1508,6 @@ class SelectorFailoverContractTests(unittest.TestCase):
self.assertEqual(
terra["promotion_path"],
[
{
"adapter": "agy",
"target": "Gemini 3.6 Flash (High)",
},
{"adapter": "claude", "target": "claude-opus-4-8"},
{"adapter": "codex", "target": "gpt-5.6-terra"},
],
@ -1534,7 +1517,7 @@ class SelectorFailoverContractTests(unittest.TestCase):
task_file,
evaluated_at=kst(23),
transition="promotion",
prior_decision=terra,
prior_decision=resumed,
failure_class="provider-quota",
)
self.assertEqual(exhausted.exception.code, "no_promotion_target")