Files
GUI-Agent/SystemTest-Agent/app/agent_core/session.py
T

849 lines
39 KiB
Python

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