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

+35
View File
@@ -0,0 +1,35 @@
# 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.
"""Telemetry and distributed tracing layer for ARTEMIS."""
from artemis.telemetry.models import (
SpanType,
TelemetrySpan,
TokenUsage,
)
from artemis.telemetry.tracer import (
CURRENT_PARENT_SPAN_ID,
CURRENT_TRACE_ID,
TelemetryTracer,
)
__all__ = [
"TelemetryTracer",
"TelemetrySpan",
"SpanType",
"TokenUsage",
"CURRENT_TRACE_ID",
"CURRENT_PARENT_SPAN_ID",
]
+64
View File
@@ -0,0 +1,64 @@
# 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.
"""Telemetry Data Models and Schema Definitions."""
from enum import Enum
import time
from typing import Any
from uuid import uuid4
from pydantic import BaseModel, Field
class SpanType(str, Enum):
AGENT = "agent"
LLM_CALL = "llm_call"
TOOL_EXECUTION = "tool_execution"
DEVICE_ACTION = "device_action"
SCREEN_CAPTURE = "screen_capture"
CUSTOM = "custom"
class TokenUsage(BaseModel):
"""LLM token consumption metrics."""
prompt_tokens: int = Field(default=0, description="Input prompt tokens")
completion_tokens: int = Field(default=0, description="Output generated tokens")
total_tokens: int = Field(default=0, description="Total tokens consumed")
class TelemetrySpan(BaseModel):
"""Structured telemetry span representing a single unit of agentic execution."""
span_id: str = Field(default_factory=lambda: str(uuid4()))
trace_id: str = Field(..., description="Root trace/session identifier")
parent_id: str | None = Field(default=None, description="Parent span identifier")
name: str = Field(..., description="Human-readable span operation name")
type: SpanType = Field(default=SpanType.CUSTOM, description="Span categorization")
start_time: float = Field(default_factory=time.time, description="Start timestamp")
end_time: float | None = Field(default=None, description="Completion timestamp")
duration_ms: float | None = Field(default=None, description="Elapsed execution time in ms")
status: str = Field(default="running", description="Status ('running', 'success', 'failed')")
payload: dict[str, Any] = Field(
default_factory=dict, description="Custom parameters and payloads"
)
error: str | None = Field(default=None, description="Exception message if failed")
token_usage: TokenUsage | None = Field(default=None, description="Token consumption metrics")
def finish(self, status: str = "success", error: str | None = None) -> None:
"""Closes the span and calculates duration."""
self.end_time = time.time()
self.duration_ms = (self.end_time - self.start_time) * 1000.0
self.status = status
self.error = error
+34
View File
@@ -0,0 +1,34 @@
# 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 Replay and Inspection utilities."""
from pathlib import Path
from typing import Any
from artemis.telemetry.storage.json_storage import JsonStorage
class TraceReplayer:
"""Loads and formats historic task traces for replay and visual inspection."""
def __init__(self, traces_dir: Path | str = "traces"):
self.storage = JsonStorage(storage_dir=traces_dir)
def get_trace_summary(self, trace_id: str) -> dict[str, Any]:
spans = self.storage.read_spans(trace_id)
return {
"trace_id": trace_id,
"total_spans": len(spans),
"spans": spans,
}
+47
View File
@@ -0,0 +1,47 @@
# 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.
"""JSON Lines Telemetry Storage Backend."""
import json
from pathlib import Path
from typing import Any
from artemis.telemetry.models import TelemetrySpan
class JsonStorage:
"""Persists spans into newline-delimited JSON files."""
def __init__(self, storage_dir: Path | str = "traces"):
self.storage_dir = Path(storage_dir)
def write_span(self, trace_id: str, span: TelemetrySpan) -> None:
target_dir = self.storage_dir / trace_id
target_dir.mkdir(parents=True, exist_ok=True)
log_file = target_dir / "spans.jsonl"
with open(log_file, "a", encoding="utf-8") as f:
f.write(json.dumps(span.model_dump(), ensure_ascii=False) + "\n")
def read_spans(self, trace_id: str) -> list[dict[str, Any]]:
log_file = self.storage_dir / trace_id / "spans.jsonl"
if not log_file.exists():
return []
spans = []
with open(log_file, encoding="utf-8") as f:
for line in f:
if line.strip():
spans.append(json.loads(line))
return spans
+85
View File
@@ -0,0 +1,85 @@
# 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.
"""Telemetry Tracer and Span Life Cycle Manager."""
from contextlib import asynccontextmanager
from contextvars import ContextVar
import json
from pathlib import Path
from typing import Any
from collections.abc import AsyncIterator
from uuid import uuid4
from artemis.telemetry.models import SpanType, TelemetrySpan
from artemis.utils.logger import get_logger
logger = get_logger(__name__)
CURRENT_TRACE_ID: ContextVar[str] = ContextVar("CURRENT_TRACE_ID", default="global-session")
CURRENT_PARENT_SPAN_ID: ContextVar[str | None] = ContextVar("CURRENT_PARENT_SPAN_ID", default=None)
class TelemetryTracer:
"""Central tracer for recording and persisting telemetry spans."""
def __init__(self, trace_id: str | None = None, traces_dir: Path | None = None):
self.trace_id = trace_id or str(uuid4())
self.traces_dir = traces_dir or Path("traces") / self.trace_id
self.spans: list[TelemetrySpan] = []
self._log_file: Path | None = None
def _ensure_log_file(self) -> Path:
if self._log_file is None:
self.traces_dir.mkdir(parents=True, exist_ok=True)
self._log_file = self.traces_dir / "telemetry_spans.jsonl"
return self._log_file
def record_span(self, span: TelemetrySpan) -> None:
"""Appends and persists completed span."""
self.spans.append(span)
try:
log_file = self._ensure_log_file()
with open(log_file, "a", encoding="utf-8") as f:
f.write(json.dumps(span.model_dump(), ensure_ascii=False) + "\n")
except Exception as e:
logger.warning(f"Failed to persist telemetry span {span.span_id}: {e}")
@asynccontextmanager
async def start_span(
self,
name: str,
span_type: SpanType = SpanType.CUSTOM,
payload: dict[str, Any] | None = None,
) -> AsyncIterator[TelemetrySpan]:
"""Asynchronous context manager creating and recording a span."""
parent_id = CURRENT_PARENT_SPAN_ID.get()
span = TelemetrySpan(
trace_id=self.trace_id,
parent_id=parent_id,
name=name,
type=span_type,
payload=payload or {},
)
token = CURRENT_PARENT_SPAN_ID.set(span.span_id)
try:
yield span
span.finish(status="success")
except Exception as e:
span.finish(status="failed", error=str(e))
raise
finally:
CURRENT_PARENT_SPAN_ID.reset(token)
self.record_span(span)