feat(vwb): execute wait for state

This commit is contained in:
Dom
2026-05-29 17:22:35 +02:00
parent 7b1f30af1a
commit e66bc6d452
10 changed files with 491 additions and 9 deletions

View File

@@ -0,0 +1,187 @@
"""Runtime helpers for VWB wait_for_state actions."""
from __future__ import annotations
from dataclasses import dataclass, asdict
from time import monotonic, sleep
from typing import Any, Callable, Dict, List, Mapping, Optional
StateProvider = Callable[[], Mapping[str, Any]]
@dataclass
class StateMatch:
matched: bool
matched_signals: List[str]
failed_signals: List[str]
observed_state: Dict[str, Any]
expected_state: Dict[str, Any]
evidence_required: str = "window_or_process"
unsupported_evidence: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
@dataclass
class WaitForStateResult:
matched: bool
timed_out: bool
elapsed_ms: int
polls: int
match: Dict[str, Any]
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
def _as_string_list(value: Any) -> List[str]:
if value is None:
return []
if isinstance(value, str):
return [value] if value.strip() else []
if isinstance(value, (list, tuple, set)):
return [str(item).strip() for item in value if str(item).strip()]
return [str(value).strip()] if str(value).strip() else []
def _normalize_text(value: Any) -> str:
return str(value or "").strip().casefold()
def _normalize_process(value: Any) -> str:
text = _normalize_text(value)
return text[:-4] if text.endswith(".exe") else text
def get_active_window_state() -> Dict[str, Any]:
"""Return the current foreground window title and process/app name."""
try:
from agent_v0.agent_v1.window_info_crossplatform import get_active_window_info
info = get_active_window_info() or {}
except Exception as exc:
return {
"window_title": "",
"process_active": "",
"error": str(exc),
}
title = info.get("title") or info.get("window_title") or ""
process = (
info.get("process_active")
or info.get("process_name")
or info.get("app_name")
or info.get("application")
or ""
)
return {
"window_title": title,
"process_active": process,
"raw": dict(info),
}
def match_expected_state(
expected_state: Mapping[str, Any],
observed_state: Mapping[str, Any],
evidence_required: str = "window_or_process",
) -> StateMatch:
expected = dict(expected_state or {})
observed = {
"window_title": observed_state.get("window_title")
or observed_state.get("title")
or "",
"process_active": observed_state.get("process_active")
or observed_state.get("process_name")
or observed_state.get("app_name")
or "",
}
if "raw" in observed_state:
observed["raw"] = observed_state["raw"]
if "error" in observed_state:
observed["error"] = observed_state["error"]
matched_signals: List[str] = []
failed_signals: List[str] = []
title = _normalize_text(observed["window_title"])
exact_titles = _as_string_list(expected.get("window_title_in"))
if exact_titles:
if any(title == _normalize_text(candidate) for candidate in exact_titles):
matched_signals.append("window_title_in")
else:
failed_signals.append("window_title_in")
contains_titles = _as_string_list(expected.get("window_title_contains"))
if contains_titles:
if any(_normalize_text(candidate) in title for candidate in contains_titles):
matched_signals.append("window_title_contains")
else:
failed_signals.append("window_title_contains")
process_expected = _as_string_list(expected.get("process_active"))
if process_expected:
process = _normalize_process(observed["process_active"])
if any(process == _normalize_process(candidate) for candidate in process_expected):
matched_signals.append("process_active")
else:
failed_signals.append("process_active")
unsupported = None
if evidence_required != "window_or_process":
unsupported = evidence_required
return StateMatch(
matched=bool(matched_signals) and not failed_signals and unsupported is None,
matched_signals=matched_signals,
failed_signals=failed_signals,
observed_state=observed,
expected_state=expected,
evidence_required=evidence_required,
unsupported_evidence=unsupported,
)
def wait_for_expected_state(
expected_state: Mapping[str, Any],
timeout_ms: int = 5000,
poll_interval_ms: int = 250,
evidence_required: str = "window_or_process",
state_provider: Optional[StateProvider] = None,
) -> WaitForStateResult:
provider = state_provider or get_active_window_state
timeout_s = max(0, int(timeout_ms)) / 1000.0
poll_s = max(0.05, int(poll_interval_ms) / 1000.0)
started = monotonic()
deadline = started + timeout_s
polls = 0
last_match = match_expected_state(expected_state, {}, evidence_required)
while True:
polls += 1
observed = provider() or {}
last_match = match_expected_state(expected_state, observed, evidence_required)
elapsed_ms = int((monotonic() - started) * 1000)
if last_match.matched:
return WaitForStateResult(
matched=True,
timed_out=False,
elapsed_ms=elapsed_ms,
polls=polls,
match=last_match.to_dict(),
)
remaining = deadline - monotonic()
if remaining <= 0:
return WaitForStateResult(
matched=False,
timed_out=True,
elapsed_ms=elapsed_ms,
polls=polls,
match=last_match.to_dict(),
)
sleep(min(poll_s, remaining))