iop/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py
toki deea59d035 feat: task routing, orchestration, quota probe, and skill updates
- add finalize-task-routing skill with policy and test scripts
- add execution target selection scripts and tests for orchestrate-agent-task-loop
- add quota probe adapter and CLI status for node app
- update plan, finalize-task-routing, and orchestration skill files
- add agent-task archive for runtime target selector
- update stream-evidence-gate-core milestone
2026-07-25 19:33:17 +09:00

665 lines
23 KiB
Python

#!/usr/bin/env python3
"""Deterministic execution-target selector CLI over the pure route policy.
The selector consumes a static routing task file (``PLAN-*`` or
``CODE_REVIEW-*``), its ``task/plan/tag`` generation header and optional prior
decision / quota snapshot, and returns a stable JSON contract that the
dispatcher can persist. This module closes SDD S01~S03 (schema/invalid-input,
KST time route, worker/review grade matrix) at the selector surface. Failover
target switching, quota probe execution and dispatcher persisted state are owned
by later tasks and are intentionally not implemented here.
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import re
import sys
from datetime import datetime
from pathlib import Path
SCHEMA_VERSION = "1.0"
TIMEZONE_NAME = "Asia/Seoul"
DEFAULT_QUOTA_PROBE_COMMAND = "iop-node quota-probe"
_FILENAME_RE = re.compile(r"^(PLAN|CODE_REVIEW)-(local|cloud)-G(\d{2})\.md$")
_HEADER_RE = re.compile(r"<!--\s*task=(\S+)\s+plan=(\d+)\s+tag=(\S+)\s*-->")
_STAGE_BY_KIND = {"PLAN": "worker", "CODE_REVIEW": "review"}
_VALID_TRANSITIONS = {"initial", "resume", "failover"}
_VALID_EXECUTION_CLASSES = {"local_model", "cloud_model"}
_VALID_QUOTA_MODES = {"unbounded", "bounded"}
_VALID_QUOTA_STATUSES = {"not_applicable", "available", "exhausted", "unknown"}
_VALID_ELIGIBILITY = {"eligible", "ineligible"}
_VALID_REJECTION_REASONS = {"quota_exhausted"}
_VALID_TIME_WINDOWS = {
"kst-day-[07:00,23:00)",
"kst-night-[23:00,07:00)",
"not_applicable",
}
def _load_policy():
path = Path(__file__).resolve().parent / "execution_target_policy.py"
spec = importlib.util.spec_from_file_location("execution_target_policy", path)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
policy = _load_policy()
class SelectorInputError(Exception):
"""Input contract violation returned as stderr JSON with a non-zero exit."""
def __init__(self, code: str, message: str) -> None:
super().__init__(message)
self.code = code
def _parse_filename(task_file: Path) -> tuple[str, str, int]:
name = Path(task_file).name
match = _FILENAME_RE.match(name)
if match is None:
raise SelectorInputError(
"invalid_task_filename",
f"task file must match (PLAN|CODE_REVIEW)-(local|cloud)-GNN.md: {name!r}",
)
kind, lane, grade_str = match.group(1), match.group(2), match.group(3)
grade = int(grade_str)
if not 1 <= grade <= 10:
raise SelectorInputError(
"invalid_grade", f"grade must be G01..G10: G{grade_str}"
)
return kind, lane, grade
def _parse_header(task_file: Path) -> tuple[str, int, str]:
try:
with Path(task_file).open("rb") as handle:
head = handle.read(1024)
except OSError as exc:
raise SelectorInputError("task_file_unreadable", str(exc)) from exc
text = head.decode("utf-8", errors="replace")
match = _HEADER_RE.search(text)
if match is None:
raise SelectorInputError(
"malformed_header",
"first 1KiB must contain <!-- task=... plan=N tag=... -->",
)
return match.group(1), int(match.group(2)), match.group(3)
def _work_unit_id(header: tuple[str, int, str]) -> str:
task, plan, tag = header
return f"{task}::plan-{plan}::tag-{tag}"
def _validate_evaluated_at(evaluated_at: datetime) -> None:
if evaluated_at.tzinfo is None or evaluated_at.utcoffset() is None:
raise SelectorInputError(
"naive_evaluated_at", "evaluated_at must be timezone-aware"
)
def _validate_prior_selected(selected: object) -> None:
code = "malformed_prior_decision"
if not isinstance(selected, dict):
raise SelectorInputError(code, "prior_decision.selected must be an object")
for field in ("adapter", "target", "execution_class"):
candidate = selected.get(field)
if not isinstance(candidate, str) or not candidate:
raise SelectorInputError(
code,
f"prior_decision.selected.{field} must be a non-empty string",
)
if selected["execution_class"] not in _VALID_EXECUTION_CLASSES:
raise SelectorInputError(
code,
"prior_decision.selected.execution_class must be one of "
f"{sorted(_VALID_EXECUTION_CLASSES)}",
)
if not isinstance(selected.get("selfcheck_required"), bool):
raise SelectorInputError(
code,
"prior_decision.selected.selfcheck_required must be a boolean",
)
def _require_non_empty_string(
container: dict, field: str, prefix: str, code: str
) -> None:
value = container.get(field)
if not isinstance(value, str) or not value:
raise SelectorInputError(
code, f"{prefix}.{field} must be a non-empty string"
)
def _require_string_enum(
container: dict, field: str, allowed: set, prefix: str, code: str
) -> None:
value = container.get(field)
if not isinstance(value, str) or value not in allowed:
raise SelectorInputError(
code, f"{prefix}.{field} must be one of {sorted(allowed)}"
)
def _require_nullable_string(
container: dict, field: str, prefix: str, code: str
) -> None:
if field not in container:
raise SelectorInputError(code, f"{prefix}.{field} is required")
value = container[field]
if value is not None and not isinstance(value, str):
raise SelectorInputError(
code, f"{prefix}.{field} must be null or a string"
)
def _validate_prior_candidates(candidates: object) -> None:
"""Validate every reused candidate against the initial output schema.
Each entry must carry the full ``_initial`` candidate field set with the
correct types and enum values, and ``candidate_rank`` must be 1-based and
consecutive so a resume cannot re-emit a partial ranking.
"""
code = "malformed_prior_decision"
if not isinstance(candidates, list) or not candidates:
raise SelectorInputError(
code, "prior_decision.candidates must be a non-empty list"
)
for index, entry in enumerate(candidates):
prefix = f"prior_decision.candidates[{index}]"
if not isinstance(entry, dict):
raise SelectorInputError(code, f"{prefix} must be an object")
rank = entry.get("candidate_rank")
if (
isinstance(rank, bool)
or not isinstance(rank, int)
or rank != index + 1
):
raise SelectorInputError(
code,
f"{prefix}.candidate_rank must be {index + 1} "
"(1-based and consecutive)",
)
_require_non_empty_string(entry, "adapter", prefix, code)
_require_non_empty_string(entry, "target", prefix, code)
_require_string_enum(
entry, "execution_class", _VALID_EXECUTION_CLASSES, prefix, code
)
if not isinstance(entry.get("selfcheck_required"), bool):
raise SelectorInputError(
code, f"{prefix}.selfcheck_required must be a boolean"
)
_require_string_enum(entry, "quota_mode", _VALID_QUOTA_MODES, prefix, code)
_require_string_enum(
entry, "quota_status", _VALID_QUOTA_STATUSES, prefix, code
)
_require_string_enum(entry, "eligibility", _VALID_ELIGIBILITY, prefix, code)
if "rejection_reason" not in entry:
raise SelectorInputError(
code, f"{prefix}.rejection_reason is required"
)
rejection = entry["rejection_reason"]
if rejection is not None and (
not isinstance(rejection, str)
or rejection not in _VALID_REJECTION_REASONS
):
raise SelectorInputError(
code,
f"{prefix}.rejection_reason must be null or one of "
f"{sorted(_VALID_REJECTION_REASONS)}",
)
def _validate_prior_decision_evidence(decision: object) -> None:
"""Validate the reused decision evidence block against initial output."""
code = "malformed_prior_decision"
prefix = "prior_decision.decision"
if not isinstance(decision, dict):
raise SelectorInputError(code, f"{prefix} must be an object")
_require_non_empty_string(decision, "rule_id", prefix, code)
priority = decision.get("policy_priority")
if isinstance(priority, bool) or not isinstance(priority, int):
raise SelectorInputError(
code, f"{prefix}.policy_priority must be an integer"
)
reason_codes = decision.get("reason_codes")
if not isinstance(reason_codes, list) or not all(
isinstance(item, str) and item for item in reason_codes
):
raise SelectorInputError(
code, f"{prefix}.reason_codes must be a list of non-empty strings"
)
_require_non_empty_string(decision, "evaluated_at", prefix, code)
if decision.get("timezone") != TIMEZONE_NAME:
raise SelectorInputError(
code, f"{prefix}.timezone must be {TIMEZONE_NAME!r}"
)
_require_string_enum(
decision, "time_window", _VALID_TIME_WINDOWS, prefix, code
)
if not isinstance(decision.get("pinned"), bool):
raise SelectorInputError(code, f"{prefix}.pinned must be a boolean")
def _validate_prior_quota(quota: object) -> None:
"""Validate the reused quota block against the initial output schema."""
code = "malformed_prior_decision"
prefix = "prior_decision.quota"
if not isinstance(quota, dict):
raise SelectorInputError(code, f"{prefix} must be an object")
_require_nullable_string(quota, "snapshot_id", prefix, code)
_require_string_enum(quota, "mode", _VALID_QUOTA_MODES, prefix, code)
_require_string_enum(quota, "status", _VALID_QUOTA_STATUSES, prefix, code)
_require_non_empty_string(quota, "source", prefix, code)
_require_nullable_string(quota, "checked_at", prefix, code)
def _validate_prior_decision(value: object) -> dict:
"""Validate the nested prior-decision schema before it is reused on resume.
Only container/field/type/enum shape is enforced here; identity equality
against the current work unit stays in ``_resume``. Unknown extra keys are
tolerated so forward-compatible producers are not rejected.
"""
code = "malformed_prior_decision"
if not isinstance(value, dict):
raise SelectorInputError(code, "prior_decision must be an object")
if value.get("schema_version") != SCHEMA_VERSION:
raise SelectorInputError(
code, f"prior_decision.schema_version must be {SCHEMA_VERSION!r}"
)
required = (
"work_unit_id",
"stage",
"lane",
"grade",
"selected",
"candidates",
"decision",
"quota",
)
missing = [key for key in required if key not in value]
if missing:
raise SelectorInputError(
code, f"prior_decision missing keys: {missing}"
)
if not isinstance(value["work_unit_id"], str):
raise SelectorInputError(
code, "prior_decision.work_unit_id must be a string"
)
stage = value["stage"]
if not isinstance(stage, str) or stage not in policy.VALID_STAGES:
raise SelectorInputError(
code,
f"prior_decision.stage must be one of {sorted(policy.VALID_STAGES)}",
)
lane = value["lane"]
if not isinstance(lane, str) or lane not in policy.VALID_LANES:
raise SelectorInputError(
code,
f"prior_decision.lane must be one of {sorted(policy.VALID_LANES)}",
)
grade = value["grade"]
if isinstance(grade, bool) or not isinstance(grade, int) or not 1 <= grade <= 10:
raise SelectorInputError(
code, "prior_decision.grade must be an integer in G01..G10"
)
_validate_prior_selected(value["selected"])
_validate_prior_candidates(value["candidates"])
_validate_prior_decision_evidence(value["decision"])
_validate_prior_quota(value["quota"])
return value
def _validate_quota_snapshot(value: object | None) -> dict | None:
"""Validate the optional quota snapshot container before it is reflected.
Required-cap tri-state normalization and admission stay with
``02+01_quota_input``; here we only reject malformed containers and target
entries so ``_snapshot_status`` never dereferences a non-object.
"""
if value is None:
return None
code = "malformed_quota_snapshot"
if not isinstance(value, dict):
raise SelectorInputError(code, "quota_snapshot must be an object")
for field in ("snapshot_id", "checked_at"):
if field in value and value[field] is not None and not isinstance(
value[field], str
):
raise SelectorInputError(
code, f"quota_snapshot.{field} must be null or a string"
)
if "source" in value:
_require_non_empty_string(value, "source", "quota_snapshot", code)
targets = value.get("targets", [])
if not isinstance(targets, list):
raise SelectorInputError(code, "quota_snapshot.targets must be a list")
for index, entry in enumerate(targets):
if not isinstance(entry, dict):
raise SelectorInputError(
code, f"quota_snapshot.targets[{index}] must be an object"
)
for field in ("adapter", "target", "status"):
candidate = entry.get(field)
if not isinstance(candidate, str) or not candidate:
raise SelectorInputError(
code,
f"quota_snapshot.targets[{index}].{field} must be a "
"non-empty string",
)
return value
def _snapshot_status(target, quota_snapshot: dict | None) -> str:
"""Reflect an already-normalized snapshot status for a cloud target.
The required-cap tri-state derivation from the Go usage checker is owned by
``02+01_quota_input``. Here we only surface an injected, pre-normalized
status; an absent or unmatched snapshot is reported as ``unknown``.
"""
if quota_snapshot is None:
return "unknown"
for entry in quota_snapshot.get("targets", []):
if (
entry.get("adapter") == target.adapter
and entry.get("target") == target.target
):
status = entry.get("status", "unknown")
if status in {"available", "exhausted", "unknown"}:
return status
return "unknown"
return "unknown"
def _candidate_quota(target, quota_snapshot: dict | None) -> tuple[str, str]:
if target.execution_class == "local_model":
return "unbounded", "not_applicable"
return "bounded", _snapshot_status(target, quota_snapshot)
def _selected_quota(
selected, quota_snapshot: dict | None, quota_probe_command: str
) -> dict:
if selected.execution_class == "local_model":
return {
"snapshot_id": None,
"mode": "unbounded",
"status": "not_applicable",
"source": "local_unbounded",
"checked_at": None,
}
if quota_snapshot is None:
return {
"snapshot_id": None,
"mode": "bounded",
"status": "unknown",
"source": quota_probe_command,
"checked_at": None,
}
return {
"snapshot_id": quota_snapshot.get("snapshot_id"),
"mode": "bounded",
"status": _snapshot_status(selected, quota_snapshot),
"source": quota_snapshot.get("source", quota_probe_command),
"checked_at": quota_snapshot.get("checked_at"),
}
def _initial(
*,
work_unit_id: str,
stage: str,
lane: str,
grade: int,
evaluated_at: datetime,
quota_snapshot: dict | None,
quota_probe_command: str,
) -> dict:
decision = policy.select_policy(
stage=stage, lane=lane, grade=grade, evaluated_at=evaluated_at
)
candidates = []
selected = None
for rank, target in enumerate(decision.candidates, start=1):
mode, status = _candidate_quota(target, quota_snapshot)
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",
}
)
if eligible and selected is None:
selected = target
if selected is None:
raise SelectorInputError(
"no_eligible_target",
"all policy candidates are exhausted according to the quota snapshot",
)
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,
},
"candidates": candidates,
"decision": {
"rule_id": decision.rule_id,
"policy_priority": decision.policy_priority,
"reason_codes": list(decision.reason_codes),
"evaluated_at": evaluated_at.astimezone(policy.KST).isoformat(),
"timezone": TIMEZONE_NAME,
"time_window": decision.time_window,
"pinned": False,
},
"quota": _selected_quota(selected, quota_snapshot, quota_probe_command),
"transition": {
"previous_target": None,
"next_target": None,
"trigger": "initial",
"context_transfer": "none",
},
}
def _resume(
prior_decision: dict | None,
*,
work_unit_id: str,
stage: str,
lane: str,
grade: int,
) -> dict:
if prior_decision is None:
raise SelectorInputError(
"resume_requires_prior_decision",
"resume transition requires prior_decision",
)
prior_decision = _validate_prior_decision(prior_decision)
for key, expected in (
("work_unit_id", work_unit_id),
("stage", stage),
("lane", lane),
("grade", grade),
):
if prior_decision[key] != expected:
raise SelectorInputError(
"resume_work_unit_mismatch",
f"prior_decision {key}={prior_decision[key]!r} != {expected!r}",
)
selected = prior_decision["selected"]
decision = dict(prior_decision["decision"])
decision["pinned"] = True
target_ref = {"adapter": selected["adapter"], "target": selected["target"]}
return {
"schema_version": SCHEMA_VERSION,
"work_unit_id": work_unit_id,
"stage": stage,
"lane": lane,
"grade": grade,
"selected": selected,
"candidates": prior_decision["candidates"],
"decision": decision,
"quota": prior_decision["quota"],
"transition": {
"previous_target": target_ref,
"next_target": dict(target_ref),
"trigger": "resume",
"context_transfer": "none",
},
}
def select_execution_target(
task_file: Path,
*,
stage: str | None = None,
evaluated_at: datetime,
transition: str = "initial",
prior_decision: dict | None = None,
quota_snapshot: dict | None = None,
quota_probe_command: str = DEFAULT_QUOTA_PROBE_COMMAND,
) -> dict:
"""Return the stable JSON-serializable selector decision for one call."""
kind, lane, grade = _parse_filename(task_file)
prefix_stage = _STAGE_BY_KIND[kind]
if stage is not None and stage != prefix_stage:
raise SelectorInputError(
"stage_mismatch",
f"explicit stage {stage!r} conflicts with filename stage {prefix_stage!r}",
)
resolved_stage = stage or prefix_stage
header = _parse_header(task_file)
work_unit_id = _work_unit_id(header)
_validate_evaluated_at(evaluated_at)
quota_snapshot = _validate_quota_snapshot(quota_snapshot)
if not isinstance(quota_probe_command, str) or not quota_probe_command:
raise SelectorInputError(
"invalid_quota_probe_command",
"quota_probe_command must be a non-empty string",
)
if transition == "initial":
return _initial(
work_unit_id=work_unit_id,
stage=resolved_stage,
lane=lane,
grade=grade,
evaluated_at=evaluated_at,
quota_snapshot=quota_snapshot,
quota_probe_command=quota_probe_command,
)
if transition == "resume":
return _resume(
prior_decision,
work_unit_id=work_unit_id,
stage=resolved_stage,
lane=lane,
grade=grade,
)
if transition == "failover":
raise SelectorInputError(
"unsupported_transition",
"failover target switching is owned by the dispatcher continuation "
"epic; the selector does not switch targets",
)
raise SelectorInputError(
"invalid_transition", f"unknown transition: {transition!r}"
)
def to_json(payload: dict) -> str:
"""Serialize a decision to byte-stable JSON (sorted keys, fixed indent)."""
return json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)
def _load_json_arg(value: str | None):
if value is None:
return None
candidate = Path(value)
if candidate.exists():
text = candidate.read_text(encoding="utf-8")
else:
text = value
return json.loads(text)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Select the deterministic execution target for a task file.",
)
parser.add_argument("task_file", type=Path)
parser.add_argument("--stage", choices=["worker", "review"])
parser.add_argument("--evaluated-at")
parser.add_argument(
"--transition", default="initial", choices=sorted(_VALID_TRANSITIONS)
)
parser.add_argument("--prior-decision")
parser.add_argument("--quota-snapshot")
parser.add_argument(
"--quota-probe-command", default=DEFAULT_QUOTA_PROBE_COMMAND
)
args = parser.parse_args(argv)
try:
if args.evaluated_at is not None:
evaluated_at = datetime.fromisoformat(args.evaluated_at)
else:
evaluated_at = datetime.now(policy.KST)
payload = select_execution_target(
args.task_file,
stage=args.stage,
evaluated_at=evaluated_at,
transition=args.transition,
prior_decision=_load_json_arg(args.prior_decision),
quota_snapshot=_load_json_arg(args.quota_snapshot),
quota_probe_command=args.quota_probe_command,
)
except SelectorInputError as exc:
json.dump({"error": exc.code, "message": str(exc)}, sys.stderr)
sys.stderr.write("\n")
return 2
except (ValueError, OSError, json.JSONDecodeError) as exc:
json.dump({"error": "input_error", "message": str(exc)}, sys.stderr)
sys.stderr.write("\n")
return 2
sys.stdout.write(to_json(payload) + "\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())