feat: add multi-device concurrency, server lifecycle CLI, and unified execution stream

- Implement DevicePool and DeviceExecutionLock concurrency modes (per-device FIFO vs global serialization)
- Extend ArtemisClient SDK and CLI with explicit device targeting and concurrency strategy configuration
- Add server lifecycle management (artemis restart, stop, status) with cross-platform PID discovery
- Update MCP server tools to support multi-device dispatch, trace tagging, and per-device execution
- Enhance Showcase UI with step-by-step visual replay, playback speed controls, and device serial badges
- Add comprehensive unit test coverage and edge-case verification scripts
This commit is contained in:
somew1nd committed 2026-08-26 15:53:14 -07:00
1 parent a36a66ce6d
commit c2c90a6378
84 files changed
+5612 -645

No files matched your search

+55 -1
View File
@@ -39,6 +39,8 @@ logger = get_logger(__name__)
async def execute_task(
goal: str,
device_serial: str | None = None,
session_id: str | None = None,
locked_app_package: str | None = None,
test_name: str | None = None,
traces_output_path_str: str | None = None,
@@ -76,6 +78,14 @@ async def execute_task(
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")
or os.getenv("ARTEMIS_CLOUD_SESSION_ID")
)
if effective_sid:
os.environ["ARTEMIS_SESSION_ID"] = str(effective_sid)
llm_config = initialize_llm_config()
agent_profile = AgentProfile(name="default", llm_config=llm_config)
config = Builders.AgentConfig.with_default_profile(profile=agent_profile)
@@ -115,13 +125,25 @@ async def execute_task(
if settings.ADB_HOST:
config.with_adb_server(host=settings.ADB_HOST, port=settings.ADB_PORT)
target_serial = device_serial or settings.ADB_DEVICE_SERIAL or os.environ.get("ADB_DEVICE_SERIAL")
if not target_serial:
try:
from artemis.runtime import device_pool
target_serial = device_pool.select_device()
except Exception:
target_serial = None
if target_serial:
from artemis.context import DevicePlatform
config.for_device(DevicePlatform.ANDROID, target_serial)
if graph_config_callbacks:
config.with_graph_config_callbacks(graph_config_callbacks)
agent: Agent | None = None
try:
publish_startup_progress("device_check", "Checking the Android device")
agent = Agent(config=config.build())
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)),
@@ -273,6 +295,28 @@ def run_command(
help="Explorer version mode when running under Pro profile (Operator/Validator).",
),
] = None,
device_serial: Annotated[
str | None,
typer.Option(
"--device-serial",
"-s",
help="Target specific Android device by serial number (e.g. emulator-5554).",
),
] = None,
session_id: Annotated[
str | None,
typer.Option(
"--session-id",
help="Canonical session UUID for trace and stream telemetry.",
),
] = None,
standalone: Annotated[
bool,
typer.Option(
"--standalone",
help="Run in standalone embedded mode without auto-spawning the Artemis Daemon.",
),
] = False,
) -> None:
"""Run an autonomous UI automation task on the connected Android device."""
if with_video_recording_tools:
@@ -280,6 +324,14 @@ def run_command(
console = Console()
# Daemon auto-spawn: By default, ensure Daemon is running in background unless standalone mode requested
if not standalone and os.environ.get("ARTEMIS_STANDALONE") != "1":
try:
from artemis.runtime import ensure_daemon_running
ensure_daemon_running(timeout=1.5)
except Exception as exc:
logger.debug(f"Daemon probe/auto-spawn skipped: {exc}")
adb_client = None
try:
if which("adb"):
@@ -306,6 +358,8 @@ def run_command(
asyncio.run(
execute_task(
goal=goal,
device_serial=device_serial,
session_id=session_id,
locked_app_package=locked_app_package,
test_name=test_name,
traces_output_path_str=traces_path,