feat(host-agent): make execution history authoritative
Tests / Test failed: 2, passed: 830

This commit is contained in:
2026-07-15 11:46:27 +08:00
parent ccde30e378
commit 77d4813bb2
46 changed files with 890 additions and 3132 deletions
+64 -31
View File
@@ -70,9 +70,13 @@ def _device_display_status(device: Any, *, busy_device_id: str | None) -> str:
return device.status
def _screenshot_data_uri(record: dict[str, Any]) -> str | None:
"""Return a ``data:`` URI for the step's screenshot, or ``None``."""
screenshot_path = record.get("screenshot_path")
def _screenshot_data_uri(
record: dict[str, Any],
*,
path_key: str = "screenshot_path",
) -> str | None:
"""Return a ``data:`` URI for one step screenshot, or ``None``."""
screenshot_path = record.get(path_key)
if not screenshot_path:
return None
path = Path(str(screenshot_path))
@@ -82,6 +86,49 @@ def _screenshot_data_uri(record: dict[str, Any]) -> str | None:
return f"data:image/png;base64,{encoded}"
def _ocr_results(record: dict[str, Any]) -> list[dict[str, Any]]:
raw_results = record.get("ocr_results")
if not isinstance(raw_results, list):
return []
return [result for result in raw_results if isinstance(result, dict)]
def _ui_tree_nodes(record: dict[str, Any]) -> list[dict[str, Any]]:
tool_call = record.get("tool_call")
if not isinstance(tool_call, dict):
return []
if tool_call.get("action") not in {"get_ui_tree", "ui_tree"}:
return []
step_result = record.get("result")
if not isinstance(step_result, dict):
return []
raw_nodes = step_result.get("result")
if not isinstance(raw_nodes, list):
return []
return [node for node in raw_nodes if isinstance(node, dict)]
def _timeline_step_context(record: dict[str, Any]) -> dict[str, Any]:
tool_call = record.get("tool_call")
result = record.get("result")
return {
"index": record.get("index", ""),
"timestamp": record.get("timestamp", ""),
"prompt": record.get("prompt") or "",
"tool_call": tool_call if isinstance(tool_call, dict) else {},
"result": result if isinstance(result, dict) else {},
"before_screenshot_src": _screenshot_data_uri(
record, path_key="before_screenshot_path"
),
"after_screenshot_src": _screenshot_data_uri(
record, path_key="after_screenshot_path"
)
or _screenshot_data_uri(record),
"ocr_results": _ocr_results(record),
"ui_tree_nodes": _ui_tree_nodes(record),
}
def _safe_submission_error(detail: str) -> str:
"""Return a safe, single-line error message for the operator.
@@ -525,7 +572,7 @@ def create_console_app(
if task_id:
notice = (
f"Task submitted. Cloud task ID: {task_id}. "
"Track it from the Cloud console for execution progress."
"It will appear below when this Host begins executing it."
)
elif request.query_params.get("outcome") == "unknown":
unknown = True
@@ -664,36 +711,22 @@ def create_console_app(
if timeline is not None:
timeline_records = await asyncio.to_thread(timeline.read, task_id)
task_rows = [
(key, task[key])
for key in (
"id",
"goal",
"device_id",
"status",
"created_at",
"updated_at",
(label, task[key])
for key, label in (
("source_task_id", "Cloud task ID"),
("source_attempt", "Cloud attempt"),
("id", "Execution ID"),
("goal", "Goal"),
("device_id", "Device"),
("status", "Status"),
("created_at", "Created"),
("updated_at", "Updated"),
("completed_at", "Completed"),
("failure_reason", "Failure reason"),
)
if task.get(key) is not None
]
timeline_steps = [
{
"index": record.get("index", ""),
"timestamp": record.get("timestamp", ""),
"prompt": record.get("prompt") or "",
"tool_call_text": (
json.dumps(record.get("tool_call"), ensure_ascii=False)
if record.get("tool_call")
else ""
),
"result_text": (
json.dumps(record.get("result"), ensure_ascii=False)
if record.get("result")
else ""
),
"screenshot_src": _screenshot_data_uri(record),
}
for record in timeline_records
]
timeline_steps = [_timeline_step_context(record) for record in timeline_records]
return _render(
"task_detail.html",
title=f"Task {task_id}",