feat: enhance daemon dispatching, device readiness fallback, and live streaming UI
- Admin Console & Task Queue: add cancellation, session startup progress endpoints, and support ingress metadata - Diagnostics & Runtime: enhance ADB probe to prefer unlocked devices and synchronize with readiness engine - MCP Server: dispatch tasks through Artemis Daemon scheduler with fallback to standalone runner, improve action overlays - Showcase UI: improve session auto-selection with pinning support, render startup progress, and refine stream styling - CLI & Makefile: expose server lifecycle commands (restart, stop, status) - Configuration: ignore runtime server state file in .gitignore
This commit is contained in:
1 parent
57655266cc
commit
8fdecf23d7
33 files changed
+1315
-293
No files matched your search
@@ -90,7 +90,14 @@ class ReadinessEngine:
|
||||
|
||||
def get_active_device_serial(self) -> str | None:
|
||||
"""Get currently selected active device serial."""
|
||||
return self._active_device_serial
|
||||
if self._report_cache and self._report_cache.active_device:
|
||||
if self._report_cache.active_device.is_locked is False:
|
||||
return self._report_cache.active_device.serial
|
||||
if self._active_device_serial:
|
||||
return self._active_device_serial
|
||||
if self._report_cache and self._report_cache.active_device:
|
||||
return self._report_cache.active_device.serial
|
||||
return None
|
||||
|
||||
async def run_probe(self, probe_id: str) -> ProbeResult | None:
|
||||
"""Execute a single specific probe by ID."""
|
||||
@@ -99,9 +106,11 @@ class ReadinessEngine:
|
||||
return None
|
||||
return await probe.probe()
|
||||
|
||||
async def run_device_submission_probe(self) -> ProbeResult:
|
||||
async def run_device_submission_probe(
|
||||
self, target_serial: str | None = None
|
||||
) -> ProbeResult:
|
||||
"""Run the bounded device gate used by task submission."""
|
||||
return await self._adb_probe.probe_submission_readiness()
|
||||
return await self._adb_probe.probe_submission_readiness(target_serial=target_serial)
|
||||
|
||||
def invalidate_cache(self) -> None:
|
||||
"""Invalidate the UI readiness snapshot after an explicit configuration change."""
|
||||
|
||||
@@ -358,7 +358,9 @@ class AdbDeviceProbe(BaseProbe):
|
||||
logger.debug(f"Failed submission-time ADB device check: {exc}")
|
||||
return None
|
||||
|
||||
async def probe_submission_readiness(self) -> ProbeResult:
|
||||
async def probe_submission_readiness(
|
||||
self, target_serial: str | None = None
|
||||
) -> ProbeResult:
|
||||
"""Run the minimal fail-safe device check required before enqueueing.
|
||||
|
||||
The full diagnostics probe enriches device metadata, scans packages,
|
||||
@@ -416,16 +418,42 @@ class AdbDeviceProbe(BaseProbe):
|
||||
},
|
||||
)
|
||||
|
||||
serial = (
|
||||
self._target_serial
|
||||
preferred = (
|
||||
target_serial
|
||||
if target_serial and target_serial in ready_serials
|
||||
else self._target_serial
|
||||
if self._target_serial in ready_serials
|
||||
else ready_serials[0]
|
||||
)
|
||||
|
||||
serial = preferred
|
||||
is_locked = await self._get_confirmed_device_lock_state(
|
||||
adb_path,
|
||||
serial,
|
||||
timeout_seconds=1.0,
|
||||
)
|
||||
|
||||
# If preferred device is locked or unknown, check remaining ready devices for an unlocked one
|
||||
if is_locked is not False and len(ready_serials) > 1:
|
||||
for candidate in ready_serials:
|
||||
if candidate == preferred:
|
||||
continue
|
||||
candidate_locked = await self._get_confirmed_device_lock_state(
|
||||
adb_path,
|
||||
candidate,
|
||||
timeout_seconds=1.0,
|
||||
)
|
||||
if candidate_locked is False:
|
||||
logger.info(
|
||||
f"Device {preferred} is not ready/unlocked ({is_locked}); "
|
||||
f"switching to unlocked ready device {candidate}"
|
||||
)
|
||||
serial = candidate
|
||||
is_locked = False
|
||||
break
|
||||
elif candidate_locked is None and is_locked is True:
|
||||
serial = candidate
|
||||
is_locked = None
|
||||
summary = (
|
||||
"Device Locked"
|
||||
if is_locked is True
|
||||
@@ -796,11 +824,21 @@ class AdbDeviceProbe(BaseProbe):
|
||||
)
|
||||
|
||||
# 4. Ready device available
|
||||
active_dev = ready_devices[0]
|
||||
preferred_dev = None
|
||||
if self._target_serial:
|
||||
matched = next((d for d in ready_devices if d.serial == self._target_serial), None)
|
||||
if matched:
|
||||
active_dev = matched
|
||||
preferred_dev = next((d for d in ready_devices if d.serial == self._target_serial), None)
|
||||
|
||||
if preferred_dev and preferred_dev.is_locked is False:
|
||||
active_dev = preferred_dev
|
||||
else:
|
||||
# Prefer a confirmed unlocked ready device
|
||||
unlocked_dev = next((d for d in ready_devices if d.is_locked is False), None)
|
||||
if unlocked_dev:
|
||||
active_dev = unlocked_dev
|
||||
else:
|
||||
# If no confirmed unlocked device, prefer undetermined lock state over confirmed locked
|
||||
unknown_dev = next((d for d in ready_devices if d.is_locked is None), None)
|
||||
active_dev = preferred_dev or unknown_dev or ready_devices[0]
|
||||
|
||||
display_name = active_dev.model or active_dev.serial
|
||||
if active_dev.screen_resolution:
|
||||
|
||||
@@ -77,7 +77,6 @@ async def execute_task(
|
||||
explorer_flash_mode: Override Explorer version mode for Flash execution profile.
|
||||
explorer_pro_mode: Override Explorer version mode for Pro execution profile.
|
||||
"""
|
||||
publish_startup_progress("configuration", "Loading the run configuration")
|
||||
effective_sid = (
|
||||
session_id
|
||||
or os.getenv("ARTEMIS_SESSION_ID")
|
||||
@@ -85,6 +84,9 @@ async def execute_task(
|
||||
)
|
||||
if effective_sid:
|
||||
os.environ["ARTEMIS_SESSION_ID"] = str(effective_sid)
|
||||
publish_startup_progress(
|
||||
"configuration", "Loading the run configuration", session_id=str(effective_sid) if effective_sid else None
|
||||
)
|
||||
|
||||
llm_config = initialize_llm_config()
|
||||
agent_profile = AgentProfile(name="default", llm_config=llm_config)
|
||||
@@ -142,13 +144,11 @@ async def execute_task(
|
||||
|
||||
agent: Agent | None = None
|
||||
try:
|
||||
publish_startup_progress("device_check", "Checking the Android device")
|
||||
agent = Agent(config=config.build(), session_id=effective_sid)
|
||||
await agent.init(
|
||||
retry_count=int(os.getenv("ARTEMIS_HEALTH_RETRIES", 5)),
|
||||
retry_wait_seconds=int(os.getenv("ARTEMIS_HEALTH_DELAY", 2)),
|
||||
)
|
||||
publish_startup_progress("device_ready", "Android device connected")
|
||||
|
||||
task = agent.new_task(goal)
|
||||
if locked_app_package:
|
||||
|
||||
@@ -23,12 +23,21 @@ from typing import Annotated
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from artemis.interfaces.cli.commands.server_lifecycle import (
|
||||
restart_command,
|
||||
status_command,
|
||||
stop_command,
|
||||
)
|
||||
from artemis.utils.logger import get_logger
|
||||
import typer
|
||||
|
||||
logger = get_logger(__name__)
|
||||
server_app = typer.Typer(help="Cloud proxy and server management commands.")
|
||||
|
||||
server_app.command("restart", help="Restart running Artemis Web UI & server.")(restart_command)
|
||||
server_app.command("stop", help="Stop running Artemis Web UI & server.")(stop_command)
|
||||
server_app.command("status", help="Display Artemis Web UI & server status.")(status_command)
|
||||
|
||||
cached_token = ""
|
||||
token_expire_time = 0.0
|
||||
|
||||
|
||||
@@ -158,6 +158,9 @@ def submit_task_to_daemon(
|
||||
enable_outputter: bool | None = None,
|
||||
locked_app_package: str | None = None,
|
||||
app_path: str | None = None,
|
||||
session_id: str | None = None,
|
||||
ingress: str = "client",
|
||||
conversation_id: str | None = None,
|
||||
base_url: str | None = None,
|
||||
timeout: float = 5.0,
|
||||
) -> dict[str, Any] | None:
|
||||
@@ -174,6 +177,9 @@ def submit_task_to_daemon(
|
||||
"enable_outputter": enable_outputter,
|
||||
"locked_app_package": locked_app_package,
|
||||
"app_path": app_path,
|
||||
"session_id": session_id,
|
||||
"ingress": ingress,
|
||||
"conversation_id": conversation_id,
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -223,6 +223,16 @@ class DevicePool:
|
||||
# If not detected by ADB, still return it so ADB can attempt connection
|
||||
return preferred_serial
|
||||
|
||||
# Prefer active ready device from readiness engine if available
|
||||
try:
|
||||
from artemis.core.diagnostics import readiness_engine
|
||||
|
||||
active_serial = readiness_engine.get_active_device_serial()
|
||||
if active_serial and any(d.serial == active_serial and d.state == "device" for d in all_devs):
|
||||
return active_serial
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Pick first idle device
|
||||
for dev in ready_devs:
|
||||
if not dev.is_busy:
|
||||
|
||||
@@ -203,6 +203,7 @@ class Agent:
|
||||
raise DeviceNotFoundError(error_msg)
|
||||
|
||||
# Initialize clients
|
||||
publish_startup_progress("device_check", "Checking the Android device", session_id=self._session_id)
|
||||
if os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
|
||||
self._init_clients(
|
||||
device_id=device_id,
|
||||
@@ -221,6 +222,7 @@ class Agent:
|
||||
device_id=device_id, platform=platform
|
||||
)
|
||||
logger.info(self._device_context.to_str())
|
||||
publish_startup_progress("device_ready", "Android device connected", session_id=self._session_id)
|
||||
|
||||
# Asynchronously pre-warm LLM connection pools in the background
|
||||
asyncio.create_task(self._prewarm_llm_connections(api_key))
|
||||
@@ -580,15 +582,15 @@ class Agent:
|
||||
if os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
|
||||
if self._ui_adb_client is not None:
|
||||
publish_startup_progress(
|
||||
"uiautomator", "Connecting to UI Automator", session_id=str(task.id)
|
||||
"uiautomator", "Connecting to UI Automator", session_id=str(sess_id)
|
||||
)
|
||||
await asyncio.to_thread(self._ui_adb_client.connect)
|
||||
publish_startup_progress(
|
||||
"uiautomator_ready", "UI Automator is ready", session_id=str(task.id)
|
||||
"uiautomator_ready", "UI Automator is ready", session_id=str(sess_id)
|
||||
)
|
||||
await self._ensure_device_unlocked()
|
||||
publish_startup_progress(
|
||||
"environment", "Preparing the device environment", session_id=str(task.id)
|
||||
"environment", "Preparing the device environment", session_id=str(sess_id)
|
||||
)
|
||||
await self._prepare_app_installation(task=task)
|
||||
await self._prepare_device_environment(context=context)
|
||||
@@ -620,7 +622,7 @@ class Agent:
|
||||
logger.error(f"[{task_name}] Failed to start screen recording: {e}")
|
||||
|
||||
publish_startup_progress(
|
||||
"environment_ready", "Device environment is ready", session_id=str(task.id)
|
||||
"environment_ready", "Device environment is ready", session_id=str(sess_id)
|
||||
)
|
||||
try:
|
||||
if request.profile and request.profile.lower() == "flash":
|
||||
|
||||
Reference in new issue
Block a user