"""The closed action vocabulary. A tiny model is only dependable when its output space is small and every mistake is cheap to catch. The harness therefore accepts exactly six verbs, and validates each field *before* anything touches the machine. """ from __future__ import annotations from dataclasses import dataclass from typing import Any, Final, Mapping ACTION_KINDS: Final[tuple[str, ...]] = ("click", "type", "key", "scroll", "wait", "done") class ActionError(ValueError): """Raised when a brain proposes something outside the vocabulary.""" @dataclass(frozen=True) class Action: """One step of computer use: exactly one verb, validated on construction. label : click target *by element label* — the preferred form, because it survives window moves that would break raw pixel coordinates x, y : pixel fallback, used only when no label matches text : literal text to type keys : key chord, e.g. ``ctrl+s`` amount : scroll ticks; negative scrolls up seconds : how long to wait reason : the brain's stated rationale — kept for traces, never executed """ kind: str label: str | None = None x: int | None = None y: int | None = None text: str | None = None keys: str | None = None amount: int = 0 seconds: float = 0.0 reason: str = "" def __post_init__(self) -> None: if self.kind not in ACTION_KINDS: raise ActionError(f"unknown action {self.kind!r}; expected one of {ACTION_KINDS}") if self.kind == "click": if not self.label and not (self.x is not None and self.y is not None): raise ActionError("click needs a label, or x and y") if (self.x is not None and self.x < 0) or (self.y is not None and self.y < 0): raise ActionError("click coordinates must be non-negative") if self.kind == "type" and not self.text: raise ActionError("type needs non-empty text") if self.kind == "key" and not self.keys: raise ActionError("key needs a chord, e.g. 'ctrl+s'") if self.kind == "scroll" and self.amount == 0: raise ActionError("scroll needs a non-zero amount") if self.kind == "wait" and self.seconds <= 0: raise ActionError("wait needs a positive number of seconds") def describe(self) -> str: """One-line, human-readable form used in traces and dry-run logs.""" parts: list[str] = [self.kind] if self.label: parts.append(f"label={self.label!r}") if self.x is not None and self.y is not None: parts.append(f"at=({self.x},{self.y})") if self.text is not None: parts.append(f"text={self.text!r}") if self.keys: parts.append(f"keys={self.keys!r}") if self.kind == "scroll": parts.append(f"amount={self.amount}") if self.kind == "wait": parts.append(f"seconds={self.seconds}") if self.reason: parts.append(f"reason={self.reason!r}") return " ".join(parts) def to_dict(self) -> dict[str, Any]: return { "kind": self.kind, "label": self.label, "x": self.x, "y": self.y, "text": self.text, "keys": self.keys, "amount": self.amount, "seconds": self.seconds, "reason": self.reason, } ACTION_MENU: Final[str] = """\ Reply with exactly one JSON object and nothing else. Allowed verbs: {"kind": "click", "label": "", "reason": "..."} {"kind": "click", "x": 640, "y": 400, "reason": "..."} // pixels, only if no label fits {"kind": "type", "text": "...", "reason": "..."} {"kind": "key", "keys": "ctrl+s", "reason": "..."} {"kind": "scroll", "amount": -3, "reason": "..."} {"kind": "wait", "seconds": 1.5, "reason": "..."} {"kind": "done", "reason": "why the goal is met"} Rules: one action per reply; prefer a label over guessing pixels; reply "done" as soon as the goal is met; never invent new verbs.""" _FIELDS: Final[tuple[str, ...]] = ( "label", "x", "y", "text", "keys", "amount", "seconds", "reason", ) def parse_action(payload: Mapping[str, Any]) -> Action: """Turn a brain's raw mapping into a validated :class:`Action`. Unknown keys are ignored, so a chatty model cannot crash the loop. An unknown verb or a missing field raises :class:`ActionError`, which the loop records as a failed step *without executing anything*. """ if not isinstance(payload, Mapping): raise ActionError(f"expected a mapping, got {type(payload).__name__}") kind = payload.get("kind") if kind not in ACTION_KINDS: raise ActionError(f"unknown action {kind!r}; expected one of {ACTION_KINDS}") kwargs = {k: payload[k] for k in _FIELDS if k in payload} return Action(kind=str(kind), **kwargs)