Files
Artemis-Eapil/mcp_server/tools/task_runner.py
T
somew1nd c2c90a6378 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
2026-08-26 15:53:14 -07:00

272 lines
12 KiB
Python

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""MCP Tool: mobile_run_task."""
import os
import subprocess
from typing import Any
import uuid
from mcp_server.base import mcp
from mcp_server.utils import env_utils, trace_store
from artemis.runtime import DeviceExecutionLock
@mcp.tool()
def mobile_run_task(
task_desc: str,
conversation_id: str,
model: str = "Flash",
locked_app_package: str | None = None,
app_path: str | None = None,
expected_output_desc: str | None = None,
device_serial: str | None = None,
) -> dict[str, Any]:
"""Starts a specialized mobile UI automation subagent to autonomously plan and execute tasks on a connected Android device.
With this tool, you can delegate complex mobile workflows and UI
interactions to a background agent.
This tool supports two execution models: **Flash** (for rapid,
straightforward UI scripts) and **Pro** (for complex, exploratory tasks
requiring deep reasoning).
### Device Selection & Multi-Device Support (CRITICAL)
ARTEMIS supports multi-device execution and provides two device selection modes:
1. **Direct Device Specification**: Specify `device_serial` explicitly
(e.g., `device_serial="63191FDKX00062"` or `device_serial="emulator-5554"`).
Tasks targeting different devices run concurrently via per-device locking.
2. **Automatic Device Selection**: If `device_serial` is omitted or `None`,
ARTEMIS will automatically select the connected device or allocate an idle device
from the device pool.
**Device Selection Policy & Diagnostics**:
- **Prioritize User Choice**: When multiple devices or emulators are connected (or when
user intent is not explicitly specified), **ALWAYS PRIORITIZE ASKING THE USER** to
choose or confirm the target device before launching tasks.
- **Device Diagnosis with `adb devices`**: Always use `adb devices` (or `adb devices -l`)
to inspect connected hardware, verify authorization states (`device` vs `unauthorized`),
and retrieve active device serials when diagnosing or before delegating tasks.
### Model Selection Guide (CRITICAL FOR ROUTING)
You MUST choose the appropriate `model` based on the task complexity:
- **Flash**: Best for simple, deterministic tasks that can be completed
within **30 UI steps**. You should prioritize using this model to complete
the task.
*Limitations*: Flash CANNOT analyze video recordings, query system logs,
execute arbitrary ADB shell commands, maintain a structured task plan, or
write custom execution notes/reports. Do not use Flash if the task
requires exploration, complex error recovery, continuous monitoring/polling
(due to step count limits and lack of state persistence), or outputting
large amounts of detailed information (as Flash cannot access the
notes/scratchpad system to retain extensive details across steps; use
**Pro** instead).
- **Pro**: Required for complex, non-linear, or exploratory tasks (e.g.,
browsing deep lists, troubleshooting app crashes, tasks requiring multi-step
planning), continuous monitoring or polling tasks (e.g., periodically checking
UI state, polling until specific conditions or events occur, watching progress,
or waiting for background changes), OR when you need the subagent to extract,
remember, and output large amounts of detailed information. Pro is slower but
fully featured.
*Capabilities*: Supports full video analysis, system log querying, ADB
shell access, robust monitoring/polling execution loops with multi-agent
planning, and generating custom text outputs/reports via the expected
output agent and structured notes (`notes_dir`).
Regardless of the model, precise timing for waiting actions cannot be
guaranteed due to latency. If you need to implement a wait, please note that
the total waiting time comprises both the time required for the Artemis large
model to generate output and the actual waiting duration. (For Flash, the
interval between output steps is typically 3-7 seconds, whereas Pro requires
25-35 seconds.)
For long-term repetitive tasks, run the workflow once with this tool to
discover the path, then author a dedicated automation script instead of
repeatedly delegating.
This tool is non-blocking and will immediately return a dictionary
containing:
- `trace_id`: The unique session identifier of the task for status tracking
and inspection.
- `device_serial`: The assigned target device serial (or "auto-select").
- `notes_dir`: (Pro Model Only) The directory where the subagent's execution
files/notes are located.
- `stdout_log`: The path to the execution framework log.
- `stderr_log`: The path to the error log containing critical failures and
Python tracebacks (essential for debugging).
Upon task completion (success or failure), the system will trigger a
Reactive Wakeup to notify you.
CRITICAL BEHAVIORAL RULES:
1. **Fallback Timer Constraint**: You **MUST** set a background timer to
check the task status at least once every 1 minute as a safety fallback.
2. **Post-Task Inspection**: After completion, retrieve and inspect the
execution logs (`stderr_log`/`stdout_log`) to verify success.
3. **Inspecting Notes (Pro Model Only)**: If running with Pro, you can
inspect the notes/plans recorded in `notes_dir` to extract critical
execution results passed back by the subagent.
Args:
task_desc: A clear, highly detailed, actionable task description for the
mobile subagent. Since execution is autonomous and one-shot, provide
all necessary context, exact UI actions to take, and clear termination
conditions. If `model="Pro"`, you can explicitly instruct the subagent
on what specific information it must record in the notes.
conversation_id: Your current active Conversation ID. Critical for
routing the wakeup notification.
model: Must be either `"Flash"` or `"Pro"`. - Choose `"Flash"` for fast,
simple UI tasks under 30 steps without log/video analysis or
monitoring/polling needs. - Choose `"Pro"` for complex tasks requiring
planning, ADB shell commands, logs, video exploration, or continuous
monitoring and polling tasks.
locked_app_package: Optional package name of the app to lock execution
to. The agent will auto-launch and restrict actions to this app.
app_path: Optional path to a local APK to install on the device before
running the task.
expected_output_desc: Optional (Pro Model Only). If provided, a
summarization agent will aggregate the interaction history
(motivation, actions, videos) into a summary report saved as
"output.md" in `notes_dir`. Ignored if `model="Flash"`.
device_serial: Optional Android device serial number (e.g. "63191FDKX00062"
or "emulator-5554"). If specified, binds execution strictly to that device,
allowing multi-device concurrent automation. If omitted, ARTEMIS will
automatically select an available device. Prioritize letting the user select
the target device if multiple devices are attached.
"""
# 0. Validate and normalize model
if model.lower() not in ("flash", "pro"):
raise ValueError(f"Invalid model '{model}'. Must be either 'Flash' or 'Pro'.")
canonical_model = "Flash" if model.lower() == "flash" else "Pro"
# 1. Generate a unique trace_id
trace_id = str(uuid.uuid4())
# 2. Initialize the trace store directory and status.json
trace_store.init_trace(
trace_id=trace_id,
task_desc=task_desc,
model=canonical_model,
conversation_id=conversation_id,
device_serial=device_serial,
)
# 3. Auto-spawn daemon if not already running (unless standalone forced)
if os.environ.get("ARTEMIS_STANDALONE") != "1":
try:
from artemis.runtime import ensure_daemon_running
ensure_daemon_running(timeout=1.5)
except Exception:
pass
# 4. Resolve project root, python executable, and background runner module
project_root = env_utils.get_project_root()
python_exe = env_utils.resolve_python_executable(project_root)
reserve_kwargs: dict[str, Any] = {
"description": f"MCP task: {task_desc[:120]}",
"session_id": trace_id,
"ingress": "mcp",
}
if device_serial:
reserve_kwargs["device_id"] = device_serial
queue_ticket = DeviceExecutionLock.reserve(**reserve_kwargs)
# 5. Spawn the background task runner as an independent subprocess
try:
cmd = [
python_exe,
"-m",
"mcp_server.background.task_runner",
"--trace-id",
trace_id,
"--task-desc",
task_desc,
"--model",
canonical_model,
"--conversation-id",
conversation_id,
]
if locked_app_package:
cmd.extend(["--locked-app-package", locked_app_package])
if app_path:
cmd.extend(["--app-path", app_path])
if expected_output_desc and canonical_model != "Flash":
cmd.extend(["--expected-output-desc", expected_output_desc])
if device_serial:
cmd.extend(["--device-serial", device_serial])
env = os.environ.copy()
env["ARTEMIS_SESSION_ID"] = trace_id
env["ARTEMIS_TASK_INGRESS"] = "mcp"
if device_serial:
env["ADB_DEVICE_SERIAL"] = device_serial
env["ARTEMIS_DEVICE_ID"] = device_serial
env[DeviceExecutionLock.QUEUE_TICKET_ENV] = queue_ticket
proc_kwargs = env_utils.get_detached_process_kwargs()
proc = subprocess.Popen(
cmd,
env=env,
cwd=project_root,
**proc_kwargs,
)
transfer_kwargs: dict[str, Any] = {
"description": f"MCP task: {task_desc[:120]}",
"session_id": trace_id,
"ingress": "mcp",
}
if device_serial:
transfer_kwargs["device_id"] = device_serial
DeviceExecutionLock.transfer_reservation(
queue_ticket,
proc.pid,
**transfer_kwargs,
)
# 5. Record the PID of the spawned subprocess
status_data = trace_store.read_status(trace_id)
if status_data:
status_data["pid"] = proc.pid
status_data["queue_ticket"] = queue_ticket
trace_store.write_status(trace_id, status_data)
trace_dir = trace_store.get_trace_dir(trace_id)
stdout_log_path = os.path.join(trace_dir, "stdout.log")
stderr_log_path = os.path.join(trace_dir, "stderr.log")
response_dict: dict[str, Any] = {
"trace_id": trace_id,
"device_serial": device_serial or "auto-select",
"message": "Successfully started background task.",
"stdout_log": stdout_log_path,
"stderr_log": stderr_log_path,
}
if canonical_model != "Flash":
response_dict["notes_dir"] = os.path.join(trace_dir, "notes")
return response_dict
except Exception as e:
DeviceExecutionLock.cancel_reservation(queue_ticket)
trace_store.update_trace_status(
trace_id, "failed", error=f"Failed to spawn background runner: {e}"
)
return {
"trace_id": trace_id,
"status": "failed",
"message": f"Error starting background task: {e}",
}