feat: improve task execution, verification and packaging
Add execution incidents, action bursts and shared Flash history. Expose verification and Explorer settings with streamed check results. Support graceful cancellation, recording recovery and bundled UI resources. Add CI quality checks and clean up comments and documentation.
This commit is contained in:
1 parent
5ccf06d677
commit
67248c887e
289 files changed
+15883
-6919
No files matched your search
+52
-6
@@ -55,6 +55,7 @@ from artemis.controllers.platform_specific_commands_controller import (
|
||||
get_first_device,
|
||||
)
|
||||
from artemis.runtime import DeviceExecutionLock
|
||||
from artemis.runtime.cancel_requests import watch_for_cancel_request
|
||||
from artemis.data_engine.engine import DataEngine
|
||||
from artemis.data_engine.trace import DataEngineCallbackHandler
|
||||
from artemis.graph.graph import get_graph
|
||||
@@ -264,11 +265,11 @@ class Agent:
|
||||
client = genai.Client(api_key=key)
|
||||
|
||||
# 2. Pre-warm LangChain client
|
||||
chat = ChatGoogleGenerativeAI(model="gemini-3.7-flash", google_api_key=key)
|
||||
chat = ChatGoogleGenerativeAI(model="gemini-3.8-flash", google_api_key=key)
|
||||
|
||||
# Fire both calls concurrently in the background
|
||||
await asyncio.gather(
|
||||
client.aio.models.count_tokens(model="gemini-3.7-flash", contents="ping"),
|
||||
client.aio.models.count_tokens(model="gemini-3.8-flash", contents="ping"),
|
||||
chat.ainvoke("ping"),
|
||||
return_exceptions=True,
|
||||
)
|
||||
@@ -588,8 +589,12 @@ class Agent:
|
||||
queue_cancel_event.set()
|
||||
try:
|
||||
await asyncio.shield(acquire_task)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as exc: # pylint: disable=broad-exception-caught
|
||||
# Draining the lock acquisition after cancellation is best effort.
|
||||
logger.debug(
|
||||
f"Device lock acquisition drain after cancel failed: {exc}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
# All device mutation and UI initialization happens only after
|
||||
# this task reaches the head of the one global FIFO queue.
|
||||
@@ -652,8 +657,11 @@ class Agent:
|
||||
message="Invoking FlashRunner...",
|
||||
)
|
||||
|
||||
# An explicit max_steps caps the reactive loop; the
|
||||
# default leaves the cap to agent.flash.max_turns
|
||||
# (0 = unlimited).
|
||||
max_turns = (
|
||||
request.max_steps if request.max_steps != RECURSION_LIMIT else 35
|
||||
request.max_steps if request.max_steps != RECURSION_LIMIT else None
|
||||
)
|
||||
runner = FlashRunner(context, goal=request.goal, max_turns=max_turns)
|
||||
flash_result = await runner.run(state)
|
||||
@@ -833,6 +841,14 @@ class Agent:
|
||||
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
# Background ADB processes (logcat, screenrecord, ...) started
|
||||
# by the Operator must not outlive the automation task.
|
||||
from artemis.tools.command_tool import shutdown_adb_background_tasks
|
||||
|
||||
await asyncio.wait_for(shutdown_adb_background_tasks(context), timeout=15.0)
|
||||
except Exception as e:
|
||||
logger.warning(f"[{task_name}] Failed to stop background ADB tasks: {e}")
|
||||
try:
|
||||
await self._finalize_tracing_safely(task=task, context=context)
|
||||
finally:
|
||||
@@ -856,11 +872,42 @@ class Agent:
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
cancel_watcher: asyncio.Task | None = None
|
||||
try:
|
||||
self._current_task = asyncio.create_task(_execute_task_logic())
|
||||
cancel_watcher = asyncio.create_task(self._watch_external_cancel(task_name))
|
||||
return await self._current_task
|
||||
finally:
|
||||
self._current_task = None
|
||||
if cancel_watcher is not None and not cancel_watcher.done():
|
||||
cancel_watcher.cancel()
|
||||
try:
|
||||
await cancel_watcher
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def _watch_external_cancel(self, task_name: str) -> None:
|
||||
"""Cancel the running task when another process drops a cancel marker.
|
||||
|
||||
The admin console (and other ingresses) cannot deliver a signal to a
|
||||
worker portably, so they write a marker keyed by session id / pid.
|
||||
Reacting here routes the request through the same cancellation path
|
||||
as Ctrl+C: the recording is stopped and remuxed, the trace folder is
|
||||
compiled and renamed, and the device lease is released.
|
||||
"""
|
||||
|
||||
def _on_cancel() -> None:
|
||||
logger.warning(
|
||||
f"[{task_name}] External cancel request received; stopping the task gracefully."
|
||||
)
|
||||
self.stop_current_task()
|
||||
|
||||
try:
|
||||
await watch_for_cancel_request(_on_cancel, session_id=self._session_id)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.debug(f"[{task_name}] Cancel watcher stopped: {exc}")
|
||||
|
||||
def stop_current_task(self):
|
||||
"""Requests cancellation of the currently running automation task."""
|
||||
@@ -1049,7 +1096,6 @@ class Agent:
|
||||
assert_failure_policy=self._config.assert_failure_policy,
|
||||
disable_device_probes=self._config.disable_device_probes,
|
||||
disable_planner_validation=self._config.disable_planner_validation,
|
||||
planner_validation_threshold=self._config.planner_validation_threshold,
|
||||
enable_committee=self._config.enable_committee,
|
||||
committee_debate_rounds=self._config.committee_debate_rounds,
|
||||
disable_outputter=self._config.disable_outputter,
|
||||
|
||||
@@ -21,6 +21,7 @@ from typing import Any
|
||||
from langchain_core.callbacks.base import Callbacks
|
||||
from artemis.config import (
|
||||
ExplorerVersion,
|
||||
checker_overrides_for_level,
|
||||
get_default_llm_config,
|
||||
load_agent_config,
|
||||
settings,
|
||||
@@ -83,7 +84,6 @@ class AgentConfigBuilder:
|
||||
else:
|
||||
self._video_recording_tools_enabled = detect_video_tools_enabled()
|
||||
self._disable_planner_validation = not agent_cfg.planner_validation.enabled
|
||||
self._planner_validation_threshold = agent_cfg.planner_validation.similarity_threshold
|
||||
self._enable_committee = agent_cfg.committee.enabled
|
||||
self._committee_debate_rounds = agent_cfg.committee.debate_rounds
|
||||
self._disable_checker = not agent_cfg.checker.enabled
|
||||
@@ -298,6 +298,25 @@ class AgentConfigBuilder:
|
||||
self._disable_device_probes = not device_probes
|
||||
return self
|
||||
|
||||
def with_verification_level(self, level: str) -> "AgentConfigBuilder":
|
||||
"""Apply a coarse Checker preset (the ``--verification-level`` knob).
|
||||
|
||||
Presets are defined once in :data:`artemis.config.VERIFICATION_LEVEL_PRESETS`:
|
||||
``off`` (no Checker), ``final`` (exit review only, the factory default),
|
||||
``checkpoints`` (every plan checkpoint + exit review) and ``strict``
|
||||
(checkpoints with a larger repair budget; a failed assert halts).
|
||||
Fields the preset leaves unspecified keep their current values, so an
|
||||
explicit :meth:`with_checker` call afterwards still wins.
|
||||
|
||||
Args:
|
||||
level: One of ``off``, ``final``, ``checkpoints`` or ``strict``
|
||||
(case-insensitive, surrounding whitespace ignored).
|
||||
|
||||
Raises:
|
||||
ValueError: when ``level`` is not a known preset.
|
||||
"""
|
||||
return self.with_checker(**checker_overrides_for_level(level))
|
||||
|
||||
def with_midway_checks(self, enabled: bool = True) -> "AgentConfigBuilder":
|
||||
"""Enable or disable plan-declared midway checkpoints."""
|
||||
self._disable_midway_checks = not enabled
|
||||
@@ -317,18 +336,17 @@ class AgentConfigBuilder:
|
||||
self._disable_planner_validation = disable
|
||||
return self
|
||||
|
||||
def with_planner_validation(
|
||||
self, enabled: bool = True, similarity_threshold: float | None = None
|
||||
) -> "AgentConfigBuilder":
|
||||
"""Configure async Planner validation on milestone changes.
|
||||
def with_planner_validation(self, enabled: bool = True) -> "AgentConfigBuilder":
|
||||
"""Configure the advisory async Planner validation on milestone changes.
|
||||
|
||||
Every top-level milestone text change is reviewed by the lightweight
|
||||
validator; a flagged change only yields a hint plus reason for the
|
||||
Operator (no rollback, no action suppression).
|
||||
|
||||
Args:
|
||||
enabled: Whether planner validation is enabled
|
||||
similarity_threshold: Optional similarity threshold (0.0 - 1.0)
|
||||
"""
|
||||
self._disable_planner_validation = not enabled
|
||||
if similarity_threshold is not None:
|
||||
self._planner_validation_threshold = similarity_threshold
|
||||
return self
|
||||
|
||||
def with_enable_committee(self, enabled: bool = True) -> "AgentConfigBuilder":
|
||||
@@ -381,8 +399,10 @@ class AgentConfigBuilder:
|
||||
"""Configure ⚡ Flash execution profile options.
|
||||
|
||||
Args:
|
||||
max_turns: Maximum reactive loop turns before timeout
|
||||
explorer_mode: Visual perception mode for Flash runner ('flash' 1-shot detection)
|
||||
max_turns: Maximum reactive loop turns (0 = unlimited)
|
||||
explorer_mode: Explorer tier used by the Flash runner's ``ask_explorer``
|
||||
('flash' one-shot detection, 'pro' short reasoning loop, 'ultra'
|
||||
deep loop); mirrored to ``explorer.flash_mode``
|
||||
step_summarizer: Enable/disable asynchronous visual context compressor
|
||||
step_summarizer_model: Lightweight model for background step summarization
|
||||
prune_history_xml: Whether to prune outdated XML trees from historical steps
|
||||
@@ -430,15 +450,20 @@ class AgentConfigBuilder:
|
||||
committee: bool | None = None,
|
||||
checker: bool | None = None,
|
||||
video_ledger: bool | None = None,
|
||||
verification_level: str | None = None,
|
||||
) -> "AgentConfigBuilder":
|
||||
"""Configure 🚀 Pro execution profile options.
|
||||
|
||||
Args:
|
||||
explorer_mode: Visual perception mode for Pro profile ('flash', 'pro', or 'ultra')
|
||||
planner_validation: Enable/disable planner milestone validation
|
||||
explorer_mode: Explorer tier used by the Pro profile's Operator / Validator
|
||||
('flash', 'pro', or 'ultra'); mirrored to ``explorer.pro_mode``
|
||||
planner_validation: Enable/disable the advisory review of plan milestone edits
|
||||
committee: Enable/disable multi-agent committee debate tool
|
||||
checker: Enable/disable visual verification & snapshot rollback
|
||||
checker: Enable/disable the Checker (plan checkpoint verification + exit final review)
|
||||
video_ledger: Enable/disable screen video action ledger tracking
|
||||
verification_level: Coarse Checker preset ('off', 'final', 'checkpoints',
|
||||
'strict'); applied before the explicit ``checker`` switch so the
|
||||
switch still wins when both are given
|
||||
"""
|
||||
if explorer_mode is not None:
|
||||
self._pro = self._pro.model_copy(
|
||||
@@ -449,6 +474,8 @@ class AgentConfigBuilder:
|
||||
self.with_planner_validation(enabled=planner_validation)
|
||||
if committee is not None:
|
||||
self.with_committee(enabled=committee)
|
||||
if verification_level is not None:
|
||||
self.with_verification_level(verification_level)
|
||||
if checker is not None:
|
||||
self.with_checker(enabled=checker)
|
||||
if video_ledger is not None:
|
||||
@@ -471,15 +498,23 @@ class AgentConfigBuilder:
|
||||
caching: bool | None = None,
|
||||
versions: dict[str, ExplorerVersion] | None = None,
|
||||
) -> "AgentConfigBuilder":
|
||||
"""Configure UI Explorer visual perception options.
|
||||
"""Configure the Explorer tier behind ``ask_explorer``.
|
||||
|
||||
The tier is a user setting resolved per execution profile; calling agents
|
||||
never see it. Precedence at run time: ``ARTEMIS_EXPLORER_VERSION``, then
|
||||
``versions`` (per-agent override), then the profile knob (``flash_mode``
|
||||
for the Flash runner, ``pro_mode`` for the Pro Operator / Validator), then
|
||||
``default_version``.
|
||||
|
||||
Args:
|
||||
version: Default Explorer version mode ('flash', 'pro', or 'ultra')
|
||||
default_version: Alias for version ('flash', 'pro', or 'ultra')
|
||||
flash_mode: Explorer version for Flash execution profile (FlashRunner)
|
||||
pro_mode: Explorer version for Pro execution profile (Graph/Operator/Validator)
|
||||
caching: Whether to enable context caching for multi-turn Explorer
|
||||
versions: Optional explicit per-agent version mapping
|
||||
version: Fallback tier when no profile knob applies ('flash', 'pro', 'ultra')
|
||||
default_version: Alias for version
|
||||
flash_mode: Tier for the Flash execution profile (FlashRunner)
|
||||
pro_mode: Tier for the Pro execution profile (Operator / Validator)
|
||||
caching: Gemini explicit context caching for the multi-turn tiers;
|
||||
None keeps the per-tier default (off for pro, on for ultra)
|
||||
versions: Advanced per-agent override, e.g. ``{"validator": "ultra"}``;
|
||||
empty by default so the profile knobs decide
|
||||
"""
|
||||
target_version = version if version is not None else default_version
|
||||
updates = {}
|
||||
@@ -498,12 +533,16 @@ class AgentConfigBuilder:
|
||||
return self
|
||||
|
||||
def with_explorer_version(self, version: ExplorerVersion) -> "AgentConfigBuilder":
|
||||
"""Configure default model version to use for explorer sub-agents ('flash', 'pro', or 'ultra')."""
|
||||
"""Configure the fallback Explorer tier ('flash', 'pro', or 'ultra').
|
||||
|
||||
Only applies when no profile knob matches; prefer :meth:`with_flash_config`
|
||||
/ :meth:`with_pro_config` (or :meth:`with_explorer`) for the per-profile tiers.
|
||||
"""
|
||||
self._explorer = self._explorer.model_copy(update={"default_version": version})
|
||||
return self
|
||||
|
||||
def with_explorer_versions(self, versions: dict[str, ExplorerVersion]) -> "AgentConfigBuilder":
|
||||
"""Configure per-agent model versions to use for explorer sub-agents."""
|
||||
"""Configure the advanced per-agent Explorer tier override (empty by default)."""
|
||||
self._explorer_versions = versions
|
||||
return self
|
||||
|
||||
@@ -580,7 +619,6 @@ class AgentConfigBuilder:
|
||||
assert_failure_policy=self._assert_failure_policy,
|
||||
disable_device_probes=self._disable_device_probes,
|
||||
disable_planner_validation=self._disable_planner_validation,
|
||||
planner_validation_threshold=self._planner_validation_threshold,
|
||||
enable_committee=self._enable_committee,
|
||||
committee_debate_rounds=self._committee_debate_rounds,
|
||||
disable_outputter=self._disable_outputter,
|
||||
|
||||
@@ -38,8 +38,8 @@ def get_video_capable_llm_config() -> LLMConfig:
|
||||
"""Returns an LLM config with video_analyzer configured.
|
||||
|
||||
The video_analyzer must use a video-capable Gemini model:
|
||||
- gemini-3.7-flash (recommended - fast and capable)
|
||||
- gemini-3.6-flash
|
||||
- gemini-3.8-flash (recommended - fast and capable)
|
||||
- gemini-3.7-flash
|
||||
- gemini-3-pro-preview
|
||||
- gemini-2.5-flash
|
||||
- gemini-2.5-pro
|
||||
@@ -64,8 +64,8 @@ def get_video_capable_llm_config() -> LLMConfig:
|
||||
),
|
||||
video_analyzer=LLMWithFallback(
|
||||
provider="google",
|
||||
model="gemini-3.7-flash",
|
||||
fallback=LLM(provider="google", model="gemini-3.6-flash"),
|
||||
model="gemini-3.8-flash",
|
||||
fallback=LLM(provider="google", model="gemini-3.7-flash"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -123,7 +123,6 @@ class AgentConfig(BaseModel):
|
||||
assert_failure_policy: Literal["continue", "halt"] = "continue"
|
||||
disable_device_probes: bool = False
|
||||
disable_planner_validation: bool = False
|
||||
planner_validation_threshold: float = 0.85
|
||||
enable_committee: bool = False
|
||||
committee_debate_rounds: int = 2
|
||||
disable_outputter: bool = False
|
||||
@@ -132,8 +131,10 @@ class AgentConfig(BaseModel):
|
||||
flash: FlashProfileConfig = Field(default_factory=FlashProfileConfig)
|
||||
pro: ProProfileConfig = Field(default_factory=ProProfileConfig)
|
||||
explorer: ExplorerConfig = Field(default_factory=ExplorerConfig)
|
||||
explorer_versions: dict[str, Literal["flash", "pro", "ultra"]]
|
||||
denylisted_tools: dict[str, list[str]]
|
||||
# Advanced per-agent tier override; empty so the per-profile knobs
|
||||
# (``explorer.flash_mode`` / ``explorer.pro_mode``) decide by default.
|
||||
explorer_versions: dict[str, Literal["flash", "pro", "ultra"]] = Field(default_factory=dict)
|
||||
denylisted_tools: dict[str, list[str]] = Field(default_factory=dict)
|
||||
enable_video_ledger: bool = True
|
||||
video_analyzer: VideoAnalyzerConfig = Field(default_factory=VideoAnalyzerConfig)
|
||||
concurrency_mode: Literal["global", "per_device"] = "per_device"
|
||||
|
||||
Reference in new issue
Block a user