from __future__ import annotations import hashlib import json import time from dataclasses import dataclass from pathlib import Path from typing import Any, Callable from urllib.parse import urljoin, urlparse from .memory import Blackboard from .model_client import ModelClient from .prompts import APP_AGENT_SYSTEM_PROMPT, HOST_AGENT_SYSTEM_PROMPT, SYSTEM_TEST_ANALYST_PROMPT from .tools import AgentToolRegistry, ToolResult, normalize_url SubmitTask = Callable[[dict[str, Any]], dict[str, Any]] @dataclass class AgentSessionResult: payload: dict[str, Any] class AgentSession: """ Minimal UFO-inspired execution loop for one claimed task. The implementation keeps the same conceptual boundaries as UFO: HostAgent decides the subtask, AppAgent performs one safe step, Blackboard persists requests/observations/trajectories for later rounds. """ def __init__( self, *, task: dict[str, Any], model_config_state: dict[str, Any], memory_path: Path, request_timeout_seconds: float, submit_task: SubmitTask | None = None, ) -> None: self.task = task self.model_config_state = model_config_state self.model_config = model_config_state.get("value") if isinstance(model_config_state.get("value"), dict) else {} self.request_timeout_seconds = request_timeout_seconds self.submit_task = submit_task self.blackboard = Blackboard(memory_path) self.tools = AgentToolRegistry() self.model = ModelClient(self.model_config, timeout_seconds=self._model_timeout_seconds()) self.logs: list[str] = [] self.screenshot_dir = memory_path.parent / "screenshots" def _model_timeout_seconds(self) -> float: try: timeout = float(self.model_config.get("request_timeout_seconds") or self.request_timeout_seconds) except (TypeError, ValueError): timeout = self.request_timeout_seconds return max(1.0, min(timeout, 300.0)) @property def payload(self) -> dict[str, Any]: value = self.task.get("payload") return value if isinstance(value, dict) else {} @property def task_id(self) -> str: return str(self.task.get("external_id") or self.task.get("id") or "") def target_url(self) -> str: return normalize_url( self.payload.get("target_url") or self.payload.get("url") or self.payload.get("site_url") or self.task.get("target_url") or self.model_config.get("target_system_url") ) def run(self) -> dict[str, Any]: started = time.monotonic() self.blackboard.append( "requests", { "task": self._task_brief(), "model_config_version": self.model_config_state.get("version"), }, task_id=self.task_id, ) host_decision = self._host_decision() if self.execution_mode() == "gui": return self._run_gui_task(started, host_decision) target_url = self.target_url() if not target_url: return self._finish_without_target(started, host_decision) open_browser_result: ToolResult | None = None if self.payload.get("open_in_browser", True): open_browser_result = self.tools.web.open_in_browser( target_url, wait_seconds=float(self.payload.get("browser_wait_seconds") or 1.5), ) self.blackboard.append( "actions", { "tool": "open_in_browser", "ok": open_browser_result.ok, "error": open_browser_result.error, "result": open_browser_result.result, }, task_id=self.task_id, ) if open_browser_result.ok: self.logs.append(f"Opened target URL in browser: {target_url}") else: self.logs.append(f"Browser open failed: {open_browser_result.error}") observation_result = self.tools.web.observe_page( target_url, timeout_seconds=self.request_timeout_seconds, ) observation = observation_result.result desktop_screenshot_result: ToolResult | None = None if self.payload.get("capture_screenshot", True): desktop_screenshot_result = self.tools.gui.capture_desktop( self.screenshot_dir, prefix=f"task_{self.task.get('id') or self.task_id}_web", ) if desktop_screenshot_result.ok: self.blackboard.append( "screenshots", { "image_path": desktop_screenshot_result.result.get("image_path"), "scope": "web_desktop", "target_url": target_url, }, task_id=self.task_id, ) elif desktop_screenshot_result.error: self.logs.append(f"Desktop screenshot failed: {desktop_screenshot_result.error}") self.blackboard.append( "observations", { "tool": "observe_web_page", "ok": observation_result.ok, "error": observation_result.error, "browser_open": open_browser_result.result if open_browser_result else None, "screenshot": desktop_screenshot_result.result if desktop_screenshot_result and desktop_screenshot_result.ok else None, "summary": self._observation_summary(observation), }, task_id=self.task_id, ) app_decision = self._app_decision(host_decision, observation_result) discovered_children: list[dict[str, Any]] = [] model_generated_children: list[dict[str, Any]] = [] rule_discovered_children: list[dict[str, Any]] = [] submitted_children: list[dict[str, Any]] = [] model_analysis: dict[str, Any] = { "model_used": False, "model_available": self.model.available, } discovery_source = "none" if self._allow_child_discovery(observation_result): model_analysis, model_generated_children = self._model_discover_child_tasks(observation_result) if model_generated_children: discovered_children = model_generated_children discovery_source = "llm" else: rule_discovered_children = self.tools.web.discover_child_tasks( self.task, observation, max_depth=int(self.payload.get("discovery_max_depth") or 2), ) discovered_children = rule_discovered_children discovery_source = "rule" if model_analysis.get("model_available") and model_analysis.get("model_used"): self.logs.append("Model returned no usable functional tasks; used rule-based fallback.") submitted_children = self._submit_child_tasks(discovered_children) success, message, needs_user_input = self._status_from_observation(observation_result, app_decision) if model_analysis.get("needs_user_input"): needs_user_input = True success = False message = str(model_analysis.get("user_input_reason") or "Model determined that user input is required.") duration = round(time.monotonic() - started, 3) result = { "success": success, "message": message, "task_type": self.payload.get("task_type") or "manual", "agent_framework": "ufo-inspired-lite", "p0_mode": True, "needs_user_input": needs_user_input, "target_url": target_url, "final_url": observation.get("final_url"), "http_status": observation.get("http_status"), "content_type": observation.get("content_type"), "page_title": observation.get("page_title"), "duration_seconds": duration, "action_duration_seconds": round( observation_result.duration_seconds + (open_browser_result.duration_seconds if open_browser_result else 0) + (desktop_screenshot_result.duration_seconds if desktop_screenshot_result else 0), 3, ), "model_duration_seconds": round( self._model_duration(host_decision, app_decision) + float(model_analysis.get("model_duration_seconds") or 0), 3, ), "hierarchy": self.payload.get("hierarchy") or self._task_hierarchy(), "host_decision": host_decision, "app_decision": app_decision, "model_available": self.model.available, "model_analysis": model_analysis, "model_errors": self._model_errors(host_decision, app_decision, model_analysis), "observation": self._observation_summary(observation), "browser_open": open_browser_result.result if open_browser_result else None, "screenshots": [ desktop_screenshot_result.result ] if desktop_screenshot_result and desktop_screenshot_result.ok else [], "available_tools": self.tools.descriptions(), "discovered_child_tasks": self._child_task_briefs(discovered_children), "model_generated_child_tasks": self._child_task_briefs(model_generated_children), "rule_discovered_child_tasks": self._child_task_briefs(rule_discovered_children), "discovery_source": discovery_source, "submitted_child_tasks": submitted_children, "memory": self.blackboard.summary(), "logs": [*self.logs, *(observation_result.result.get("logs") or [])], } self.blackboard.append( "trajectories", { "host_decision": host_decision, "app_decision": app_decision, "result": { "success": success, "message": message, "submitted_child_count": len(submitted_children), "duration_seconds": duration, }, }, task_id=self.task_id, ) result["memory"] = self.blackboard.summary() return result def execution_mode(self) -> str: explicit = str(self.payload.get("execution_mode") or self.payload.get("agent_mode") or "").strip().lower() if explicit in {"web", "gui"}: return explicit task_type = str(self.payload.get("task_type") or "").lower() if task_type.startswith("gui") or self.payload.get("window_id") or self.payload.get("window_title"): return "gui" return "web" def _run_gui_task(self, started: float, host_decision: dict[str, Any]) -> dict[str, Any]: windows_result = self.tools.gui.list_windows() screenshot_result = self.tools.gui.capture_desktop(self.screenshot_dir, prefix=f"task_{self.task.get('id') or self.task_id}_desktop") selected_window = self._select_window(windows_result.result.get("windows") or []) inspect_result: ToolResult | None = None window_screenshot_result: ToolResult | None = None if selected_window: inspect_result = self.tools.gui.inspect_window(str(selected_window["id"])) window_screenshot_result = self.tools.gui.capture_window( str(selected_window["id"]), self.screenshot_dir, prefix=f"task_{self.task.get('id') or self.task_id}_window", ) observation = self._gui_observation_summary( windows_result, screenshot_result, selected_window, inspect_result, window_screenshot_result, ) self.blackboard.append( "observations", { "tool": "gui_observe", "ok": windows_result.ok, "error": windows_result.error, "summary": observation, }, task_id=self.task_id, ) if screenshot_result.ok: self.blackboard.append( "screenshots", {"image_path": screenshot_result.result.get("image_path"), "scope": "desktop"}, task_id=self.task_id, ) if window_screenshot_result and window_screenshot_result.ok: self.blackboard.append( "screenshots", {"image_path": window_screenshot_result.result.get("image_path"), "scope": "window", "window": selected_window}, task_id=self.task_id, ) app_decision = { "observation": json.dumps(observation, ensure_ascii=False), "thought": "GUI P0 阶段只枚举窗口、控件并保存证据截图,不执行点击或输入。", "action": { "function": "list_windows" if not selected_window else "inspect_window_controls", "arguments": { "window_id": selected_window.get("id") if selected_window else None, "window_title": self.payload.get("window_title"), }, "status": "FINISH" if windows_result.ok and (not self.payload.get("window_title") or selected_window) else "FAIL", }, "plan": [], "comment": "GUI 观察完成。" if windows_result.ok else windows_result.error, "save_screenshot": { "save": bool(screenshot_result.ok or (window_screenshot_result and window_screenshot_result.ok)), "reason": "GUI P0 观察需要截图作为证据。" if screenshot_result.ok else screenshot_result.error, }, "result": "GUI 窗口和控件信息已记录。" if windows_result.ok else windows_result.error, "model_used": False, } success = windows_result.ok and (not self.payload.get("window_title") or bool(selected_window)) message = "Agent GUI P0 observation completed." if success else "Requested GUI window was not found or could not be inspected." duration = round(time.monotonic() - started, 3) result = { "success": success, "message": message, "task_type": self.payload.get("task_type") or "gui_p0", "agent_framework": "ufo-inspired-lite", "p0_mode": True, "execution_mode": "gui", "needs_user_input": False, "duration_seconds": duration, "action_duration_seconds": round( windows_result.duration_seconds + screenshot_result.duration_seconds + (inspect_result.duration_seconds if inspect_result else 0) + (window_screenshot_result.duration_seconds if window_screenshot_result else 0), 3, ), "model_duration_seconds": self._model_duration(host_decision, app_decision), "hierarchy": self.payload.get("hierarchy") or self._task_hierarchy(), "host_decision": host_decision, "app_decision": app_decision, "observation": observation, "available_tools": self.tools.descriptions(), "memory": self.blackboard.summary(), "logs": [ *self.logs, *(windows_result.error and [windows_result.error] or []), *(screenshot_result.error and [screenshot_result.error] or []), *(inspect_result and inspect_result.error and [inspect_result.error] or []), ], } self.blackboard.append( "trajectories", { "host_decision": host_decision, "app_decision": app_decision, "result": { "success": success, "message": message, "duration_seconds": duration, }, }, task_id=self.task_id, ) result["memory"] = self.blackboard.summary() return result def _host_decision(self) -> dict[str, Any]: fallback = { "observation": "收到一个主控端分配的测试任务。", "thought": "P0 阶段先把任务限定到单个 Web/AppAgent,执行无副作用观察,并按页面结构生成后续子任务。", "current_subtask": self.payload.get("objective") or self.payload.get("instruction") or self.task.get("title") or "", "message": ["只执行无副作用观察动作;遇到登录、验证码、权限或破坏性入口时暂停并回传。"], "status": "ASSIGN", "plan": ["观察目标页面", "记录页面元信息", "识别可继续拆分的链接、表单和入口"], "function": "assign_app_agent", "arguments": {"agent": "web_app_agent", "target_url": self.target_url()}, "questions": [], "result": "已分配给 Web AppAgent 执行 P0 观察。", "model_used": False, } if not self.model.available: return fallback context = { "task": self._task_brief(), "blackboard": self.blackboard.snapshot(limit=5), "tools": self.tools.descriptions(), } response = self.model.chat_json( [ {"role": "system", "content": HOST_AGENT_SYSTEM_PROMPT}, {"role": "user", "content": json.dumps(context, ensure_ascii=False)}, ] ) if response.ok and isinstance(response.parsed, dict): parsed = {**fallback, **response.parsed, "model_used": True, "model_duration_seconds": response.duration_seconds} return parsed self.logs.append(f"HostAgent model fallback: {response.error or 'invalid JSON response'}") return {**fallback, "model_error": response.error, "model_duration_seconds": response.duration_seconds} def _app_decision(self, host_decision: dict[str, Any], observation_result: ToolResult) -> dict[str, Any]: observation = self._observation_summary(observation_result.result) fallback_status = "FINISH" if observation_result.ok else "FAIL" if observation.get("requires_auth"): fallback_status = "PENDING" fallback = { "observation": json.dumps(observation, ensure_ascii=False), "thought": "P0 阶段采用 observe_web_page 工具完成可达性和页面结构观察,不执行提交、保存、删除等有副作用动作。", "action": { "function": "observe_web_page", "arguments": {"target_url": self.target_url()}, "status": fallback_status, }, "plan": [], "comment": "页面观察完成。" if observation_result.ok else observation_result.error, "save_screenshot": {"save": False, "reason": "P0 Web 观察暂不保存截图。"}, "result": "页面可达性和结构信息已记录。" if observation_result.ok else observation_result.error, "model_used": False, } if not self.model.available: return fallback context = { "host_decision": host_decision, "task": self._task_brief(), "observation": observation, "available_tools": self.tools.descriptions(), "recent_trajectories": self.blackboard.recent("trajectories", 5), } response = self.model.chat_json( [ {"role": "system", "content": APP_AGENT_SYSTEM_PROMPT}, {"role": "user", "content": json.dumps(context, ensure_ascii=False)}, ] ) if response.ok and isinstance(response.parsed, dict): parsed = {**fallback, **response.parsed, "model_used": True, "model_duration_seconds": response.duration_seconds} return parsed self.logs.append(f"AppAgent model fallback: {response.error or 'invalid JSON response'}") return {**fallback, "model_error": response.error, "model_duration_seconds": response.duration_seconds} def _model_discover_child_tasks(self, observation_result: ToolResult) -> tuple[dict[str, Any], list[dict[str, Any]]]: base_analysis: dict[str, Any] = { "model_available": self.model.available, "model_called": False, "model_call_succeeded": False, "model_used": False, "model_duration_seconds": 0, "summary": "", "coverage_notes": [], "needs_user_input": False, "user_input_reason": "", "functional_task_count": 0, } if not observation_result.ok: return {**base_analysis, "error": observation_result.error or "Page observation failed."}, [] if not self.model.available: self.logs.append("Model config is incomplete; used rule-based discovery fallback.") return {**base_analysis, "error": "Model config is incomplete."}, [] observation = observation_result.result context = { "task": self._task_brief(), "hierarchy": self.payload.get("hierarchy") or self._task_hierarchy(), "page": self._observation_for_model(observation), "recent_memory": self.blackboard.snapshot(limit=4), "constraints": { "p0_no_side_effects": True, "max_functional_tasks": 8, "discovery_max_depth": int(self.payload.get("discovery_max_depth") or 2), }, } response = self.model.chat_json( [ {"role": "system", "content": SYSTEM_TEST_ANALYST_PROMPT}, {"role": "user", "content": json.dumps(context, ensure_ascii=False)}, ] ) if not response.ok or not isinstance(response.parsed, dict): self.logs.append(f"SystemTestAnalyst model fallback: {response.error or 'invalid JSON response'}") return { **base_analysis, "model_called": True, "model_call_succeeded": False, "model_used": False, "model_duration_seconds": response.duration_seconds, "error": response.error or "Model did not return a valid JSON object.", }, [] parsed = response.parsed children = self._children_from_model_analysis(parsed, observation) analysis = { **base_analysis, "model_called": True, "model_call_succeeded": True, "model_used": True, "model_duration_seconds": response.duration_seconds, "summary": str(parsed.get("summary") or ""), "coverage_notes": parsed.get("coverage_notes") if isinstance(parsed.get("coverage_notes"), list) else [], "needs_user_input": bool(parsed.get("needs_user_input")), "user_input_reason": str(parsed.get("user_input_reason") or ""), "functional_task_count": len(children), } self.logs.append( "Model analyzed page and generated " f"{len(children)} functional task(s): {analysis.get('summary') or 'no summary'}" ) return analysis, children def _observation_for_model(self, observation: dict[str, Any]) -> dict[str, Any]: def clean_items(items: Any, limit: int) -> list[dict[str, Any]]: if not isinstance(items, list): return [] cleaned: list[dict[str, Any]] = [] for item in items[:limit]: if not isinstance(item, dict): continue cleaned.append( { key: value for key, value in item.items() if key in {"href", "text", "action", "method", "id", "name", "class", "type", "fields", "placeholder", "aria_label", "in_form"} } ) return cleaned return { "url": observation.get("url"), "final_url": observation.get("final_url"), "http_status": observation.get("http_status"), "content_type": observation.get("content_type"), "page_title": observation.get("page_title"), "requires_auth": observation.get("requires_auth"), "has_login_form": observation.get("has_login_form"), "links": clean_items(observation.get("links"), 20), "forms": clean_items(observation.get("forms"), 12), "buttons": clean_items(observation.get("buttons"), 20), } def _children_from_model_analysis(self, analysis: dict[str, Any], observation: dict[str, Any]) -> list[dict[str, Any]]: raw_items = analysis.get("functional_tasks") if not isinstance(raw_items, list): return [] max_children = 8 children: list[dict[str, Any]] = [] seen: set[tuple[str, str]] = set() for index, item in enumerate(raw_items[:max_children], start=1): if not isinstance(item, dict): continue child = self._child_from_model_item(item, observation, index) key = ( str(child.get("title") or "").strip().lower(), str((child.get("payload") or {}).get("target_url") or "").strip().lower(), ) if key in seen: continue seen.add(key) children.append(child) return children def _child_from_model_item(self, item: dict[str, Any], observation: dict[str, Any], index: int) -> dict[str, Any]: parent_external_id = str(self.task.get("external_id") or self.task.get("id") or "") root_external_id = str(self.task.get("root_external_id") or self.payload.get("root_external_id") or parent_external_id) parent_depth = int(self.task.get("depth") or self.payload.get("depth") or 0) depth = parent_depth + 1 discovery_max_depth = int(self.payload.get("discovery_max_depth") or 2) base_url = observation.get("final_url") or observation.get("url") or self.target_url() raw_target_url = str(item.get("target_url") or "").strip() target_url = urljoin(base_url, raw_target_url) if raw_target_url else base_url title = str(item.get("title") or f"模型生成测试任务 {index}").strip() objective = str(item.get("objective") or title).strip() module = str(item.get("module") or title).strip() level = str(item.get("level") or "feature").strip().lower() if level not in {"module", "feature", "case"}: level = "feature" risk = str(item.get("risk_level") or "low").strip().lower() if risk not in {"low", "medium", "high"}: risk = "low" try: priority = int(item.get("priority") or 30 + depth * 10 + index) except (TypeError, ValueError): priority = 30 + depth * 10 + index preconditions = item.get("preconditions") if isinstance(item.get("preconditions"), list) else [] external_id = self._stable_child_id(root_external_id, parent_external_id, title, target_url, objective) payload = { "task_type": "smart_discovery_p0", "instruction": objective, "objective": objective, "target_url": target_url, "source": "llm_analysis", "llm_reason": item.get("reason") or "", "level": level, "depth": depth, "risk_level": risk, "allow_child_discovery": level in {"module", "feature"} and depth < discovery_max_depth, "discovery_max_depth": discovery_max_depth, "open_in_browser": True, "capture_screenshot": True, "preconditions": preconditions, "hierarchy": { "id": external_id, "parent_id": parent_external_id, "root_id": root_external_id, "depth": depth, "level": level, }, } return { "external_id": external_id, "title": title, "module": module, "parent_external_id": parent_external_id, "root_external_id": root_external_id, "depth": depth, "task_level": level, "risk_level": risk, "priority": priority, "failure_policy": "pause" if risk in {"medium", "high"} or preconditions else "skip", "max_retries": 2, "timeout_seconds": 90, "tags": ["llm-generated", "system-test", "p0"], "payload": payload, } def _stable_child_id(self, *parts: Any) -> str: raw = "|".join(str(part or "") for part in parts) return f"llm-{hashlib.sha1(raw.encode('utf-8')).hexdigest()[:12]}" def _allow_child_discovery(self, observation_result: ToolResult) -> bool: if not observation_result.ok: return False allow = self.payload.get("allow_child_discovery") if allow is not None: return bool(allow) if self.payload.get("task_type") != "smart_discovery_p0": return False level = str(self.payload.get("level") or self.task.get("task_level") or "") depth = int(self.payload.get("depth") or self.task.get("depth") or 0) return level in {"site", "module", ""} and depth < int(self.payload.get("discovery_max_depth") or 2) def _submit_child_tasks(self, children: list[dict[str, Any]]) -> list[dict[str, Any]]: if not children or self.submit_task is None: return [] submitted: list[dict[str, Any]] = [] plan_name = self.task.get("plan_name") or self.payload.get("plan_name") or "智能探索 P0" for child in children: payload = {**child, "plan_name": plan_name} try: created = self.submit_task(payload) except Exception as exc: self.logs.append(f"Submit child task failed: {child.get('external_id')} error={exc}") continue submitted.append( { "external_id": created.get("external_id") or child.get("external_id"), "title": created.get("title") or child.get("title"), "depth": created.get("depth") if created.get("depth") is not None else child.get("depth"), "task_level": created.get("task_level") or child.get("task_level"), } ) return submitted def _status_from_observation(self, observation_result: ToolResult, app_decision: dict[str, Any]) -> tuple[bool, str, bool]: action = app_decision.get("action") if isinstance(app_decision.get("action"), dict) else {} status = str(action.get("status") or "").upper() if status == "PENDING": return False, "Task needs user input before it can continue.", True if status in {"FAIL", "CONFIRM"}: return False, app_decision.get("result") or app_decision.get("comment") or "Agent action did not complete.", status == "CONFIRM" if not observation_result.ok: return False, observation_result.error or "Target observation failed.", False if observation_result.result.get("http_status") in {401, 403}: return False, "Target page requires authentication or permission.", True return True, "Agent P0 observation completed.", False def _finish_without_target(self, started: float, host_decision: dict[str, Any]) -> dict[str, Any]: task_type = self.payload.get("task_type") or "manual" success = task_type != "smart_discovery_p0" duration = round(time.monotonic() - started, 3) result = { "success": success, "message": "No executable target URL was provided." if not success else "Task accepted; no executable URL was provided for P0 mode.", "task_type": task_type, "agent_framework": "ufo-inspired-lite", "p0_mode": True, "needs_user_input": not success, "duration_seconds": duration, "hierarchy": self.payload.get("hierarchy") or self._task_hierarchy(), "host_decision": host_decision, "app_decision": { "action": {"function": "no_action", "arguments": {}, "status": "FAIL" if not success else "FINISH"}, "result": "Missing target URL.", }, "available_tools": self.tools.descriptions(), "memory": self.blackboard.summary(), "logs": ["AgentSession requires target_url for Web P0 execution."], } self.blackboard.append("trajectories", {"host_decision": host_decision, "result": result}, task_id=self.task_id) return result def _select_window(self, windows: list[dict[str, Any]]) -> dict[str, Any] | None: window_id = self.payload.get("window_id") if window_id: for window in windows: if str(window.get("id")) == str(window_id) or str(window.get("hex_id")).lower() == str(window_id).lower(): return window return None title = str(self.payload.get("window_title") or self.payload.get("application") or "").strip().lower() if not title: return None for window in windows: if title in str(window.get("title") or "").lower(): return window for window in windows: if title in str(window.get("class_name") or "").lower(): return window return None def _gui_observation_summary( self, windows_result: ToolResult, screenshot_result: ToolResult, selected_window: dict[str, Any] | None, inspect_result: ToolResult | None, window_screenshot_result: ToolResult | None, ) -> dict[str, Any]: windows = windows_result.result.get("windows") if isinstance(windows_result.result, dict) else [] controls = [] if inspect_result and isinstance(inspect_result.result, dict): controls = inspect_result.result.get("controls") or [] return { "backend": (windows_result.result or {}).get("backend") or self.tools.gui.backend_status(), "window_count": len(windows or []), "windows": [ { "id": window.get("id"), "title": window.get("title"), "class_name": window.get("class_name"), "process_id": window.get("process_id"), "rect": window.get("rect"), "minimized": window.get("minimized"), } for window in (windows or [])[:20] ], "selected_window": selected_window, "control_count": len(controls), "controls": [ { "id": control.get("id"), "text": control.get("text"), "class_name": control.get("class_name"), "rect": control.get("rect"), "visible": control.get("visible"), "enabled": control.get("enabled"), } for control in controls[:80] ], "desktop_screenshot": screenshot_result.result if screenshot_result.ok else None, "window_screenshot": window_screenshot_result.result if window_screenshot_result and window_screenshot_result.ok else None, "errors": [ message for message in [ windows_result.error, screenshot_result.error, inspect_result.error if inspect_result else "", window_screenshot_result.error if window_screenshot_result else "", ] if message ], } def _task_brief(self) -> dict[str, Any]: return { "id": self.task.get("id"), "external_id": self.task.get("external_id"), "title": self.task.get("title"), "plan_name": self.task.get("plan_name"), "module": self.task.get("module"), "status": self.task.get("status"), "depth": self.task.get("depth"), "task_level": self.task.get("task_level"), "risk_level": self.task.get("risk_level"), "payload": self.payload, } def _task_hierarchy(self) -> dict[str, Any]: return { "id": self.task.get("external_id") or self.task.get("id"), "parent_id": self.task.get("parent_external_id"), "root_id": self.task.get("root_external_id") or self.task.get("external_id") or self.task.get("id"), "depth": self.task.get("depth") or 0, "level": self.task.get("task_level") or self.payload.get("level") or "", } def _observation_summary(self, observation: dict[str, Any]) -> dict[str, Any]: return { "url": observation.get("url"), "final_url": observation.get("final_url"), "http_status": observation.get("http_status"), "content_type": observation.get("content_type"), "page_title": observation.get("page_title"), "requires_auth": observation.get("requires_auth"), "auth_challenge": observation.get("auth_challenge"), "has_login_form": observation.get("has_login_form"), "link_count": len(observation.get("links") or []), "form_count": len(observation.get("forms") or []), "button_count": len(observation.get("buttons") or []), } def _child_task_briefs(self, children: list[dict[str, Any]]) -> list[dict[str, Any]]: return [ { "external_id": child.get("external_id"), "title": child.get("title"), "parent_external_id": child.get("parent_external_id"), "root_external_id": child.get("root_external_id"), "depth": child.get("depth"), "task_level": child.get("task_level"), "risk_level": child.get("risk_level"), "target_url": (child.get("payload") or {}).get("target_url"), "source": (child.get("payload") or {}).get("source"), "preconditions": (child.get("payload") or {}).get("preconditions") or [], "llm_reason": (child.get("payload") or {}).get("llm_reason") or "", } for child in children ] def _model_duration(self, host_decision: dict[str, Any], app_decision: dict[str, Any]) -> float: return round( float(host_decision.get("model_duration_seconds") or 0) + float(app_decision.get("model_duration_seconds") or 0), 3, ) def _model_errors(self, *parts: dict[str, Any]) -> list[str]: errors: list[str] = [] for part in parts: error = str(part.get("model_error") or part.get("error") or "").strip() if error and error not in errors: errors.append(error) return errors