feat: initial release of ARTEMIS Android Agent & Automation Platform

ARTEMIS is an autonomous multimodal Android agent and mobile UI automation
platform engineered for physical devices and emulators. It combines state-of-the-art
multimodal Vision-Language Models (VLMs) with hierarchical closed-loop planning,
resilient self-healing, and deep Android subsystem integration.

Key Features & Architectural Highlights:
- Dual-Engine Orchestration:
  * Pro Engine: Multi-agent closed-loop (Planner -> Operator -> Validator/Failure Analyzer -> Summarizer) powered by LangGraph.
  * Flash Engine: Ultra-fast reactive loop for deterministic, low-latency UI execution (<35 steps).
- Multimodal Perception & Grounding:
  * Hybrid visual screenshot grounding, real-time OCR extraction, and XML accessibility hierarchy parsing.
  * Dynamic-First, Coordinate-Fallback locating strategy for maximum resilience against UI drift.
- Autonomous Self-Healing & Diagnostics:
  * Failure analyzer and safety net for popup dismissal, crash recovery, and auto-retry.
  * Diagnostic sub-agents for log analysis, video recording inspection, and object detection.
- Unified CLI & Python SDK:
  * Rich CLI with commands: init, doctor, run, trace, batch, bench, server, and mcp.
  * Fluent Python SDK with Pydantic structured output models and package locking.
- Multi-Provider LLM Architecture:
  * Support for Google Gemini, OpenAI, Anthropic Claude, Vertex AI, OpenRouter, xAI Grok, and local endpoints (Ollama/vLLM).
  * Centralized configuration in config/artemis.jsonc.
- Native MCP & Observability:
  * Model Context Protocol (MCP) server endpoints for AI IDEs.
  * Full trace recording with before/after screenshots, action overlays, and execution replays.

Change-Id: I430b93ee2b76b54cc5a3c6eeccd15d2cf29830a1
This commit is contained in:
somew1nd committed 2026-08-13 11:46:47 -07:00
commit 206a9ce921
229 files changed
+49653

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
# 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.
"""Trace inspection and replay commands (artemis trace)."""
import datetime
import json
from pathlib import Path
from typing import Annotated
from artemis.config import settings
from artemis.utils.logger import get_logger
from rich.console import Console
from rich.table import Table
import typer
logger = get_logger(__name__)
trace_app = typer.Typer(help="Inspect, list, and query task execution traces.")
@trace_app.command("list")
def list_traces(
traces_path: Annotated[
Path | None,
typer.Option("--path", "-p", help="Custom traces directory path."),
] = None,
limit: Annotated[int, typer.Option("--limit", "-l", help="Max sessions to list.")] = 20,
) -> None:
"""List recorded execution trace sessions."""
base_dir = traces_path or settings.TRACES_PATH
if not base_dir.exists():
typer.secho(f"No traces found at: {base_dir}", fg=typer.colors.YELLOW)
return
sessions = [d for d in base_dir.iterdir() if d.is_dir() and not d.name.startswith(".")]
sessions.sort(key=lambda x: x.stat().st_mtime, reverse=True)
console = Console()
table = Table(title=f"Recorded Traces ({len(sessions)} total)")
table.add_column("Session Name", style="cyan")
table.add_column("Last Modified", style="green")
table.add_column("Artifacts", style="white")
for s in sessions[:limit]:
artifacts = []
if (s / "recording.mp4").exists() or (s / "recording.mkv").exists():
artifacts.append("🎬 Video")
if (s / "steps.json").exists():
artifacts.append("📋 Steps")
if (s / "notes").exists():
artifacts.append("📝 Notes")
mtime = time_str = time_to_str(s.stat().st_mtime)
table.add_row(s.name, mtime, ", ".join(artifacts) or "Screenshots")
console.print(table)
def time_to_str(ts: float) -> str:
return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
@trace_app.command("view")
def view_trace(
session_name: Annotated[str, typer.Argument(help="Name of the trace session directory.")],
traces_path: Annotated[Path | None, typer.Option("--path", "-p")] = None,
) -> None:
"""View step-by-step summary of a recorded trace session."""
base_dir = traces_path or settings.TRACES_PATH
session_dir = base_dir / session_name
if not session_dir.exists():
typer.secho(f"Session '{session_name}' not found in {base_dir}", fg=typer.colors.RED)
raise typer.Exit(1)
steps_file = session_dir / "steps.json"
console = Console()
if steps_file.exists():
try:
steps_data = json.loads(steps_file.read_text(encoding="utf-8"))
table = Table(title=f"Trace Steps: {session_name}")
table.add_column("Step", justify="right", style="cyan")
table.add_column("Action", style="green")
table.add_column("Reasoning / Motivation", style="white")
for step in steps_data:
table.add_row(
str(step.get("step", "-")),
step.get("action", "-"),
step.get("motivation", step.get("thought", "-")),
)
console.print(table)
return
except Exception as e:
logger.warning(f"Could not parse steps.json: {e}")
# If steps.json not present, list files
typer.secho(f"Session files in {session_dir}:", fg=typer.colors.CYAN)
for f in session_dir.iterdir():
typer.echo(f" - {f.name}")