feat: extract in-process MCP action layer, concurrent task dispatch, and spawn/reconcile hardening

- Extract device action execution into artemis/mcp action layer:
  typed ActionResult, capability manifest with startup validation,
  AdbActuator as the single physical implementation, in-memory
  ActionSession (replaces stdio subprocess MCP in validator), and
  McpActionExecutor preserving the legacy executor contract.
  adb_server keeps its external stdio/SSE surface unchanged and now
  reuses the actuator implementation; import-time side effects moved
  into configure_stdio_mode().
- Capability-gated prompt assembly (prompt_assembly.py): tool enums and
  teaching segments render per available backend tools; full-set output
  is byte-identical to the previous static text (locked by tests).
- Admin console task queue: serial worker replaced by a per-device
  concurrent dispatcher (ARTEMIS_CONCURRENCY_MODE=global restores
  serial semantics), per-session cancellation, strict device binding on
  enqueue, unified stdio piping with tee to trace stdout.log, and
  shutdown cleanup of all active runs. Fix _pid_exists exception order
  so PermissionError is treated as alive.
- mcp_server reliability: spawn watchdog kills runners stuck before
  interpreter startup and releases device reservations; task state
  reconcile uses psutil liveness, prefers runner-reported pids, and
  adds a 45s startup grace to stop false failure reports on Windows;
  conversation_id is now optional end-to-end; explicit device_serial is
  validated at submission.
- CLI install now writes ~/.claude/rules/artemis.md for Claude Code and
  migrates/removes the legacy CLAUDE.md injection block; ~/.claude.json
  is never rewritten when it fails to parse.
- Tests: Windows-portable stdio lifecycle and path assertions; new
  suites for action layer, prompt assembly, spawn watchdog, and task
  reconcile (895 passed, 3 skipped).
This commit is contained in:
weilinliu55 committed 2026-08-27 18:25:27 -07:00
1 parent 60312abbb0
commit cbdca1497c
52 files changed
+5735 -1649

No files matched your search

+24 -8
View File
@@ -36,10 +36,8 @@ from langchain_core.messages import (
from artemis.agents.flash.context_compressor import compress_flash_messages
from artemis.agents.flash.summarizer import VisualStepSummarizer
from artemis.agents.validator.tool_declarations import (
ACTION_TOOL_NAMES,
ASK_EXPLORER_TOOL,
CLICK_SEQUENCE_TOOL,
MobileActionExecutor,
REPORT_TASK_STATUS_TOOL,
VALIDATOR_TOOLS_DECLARATION,
capture_screenshot_and_parse_ui,
@@ -51,6 +49,7 @@ from artemis.controllers.unified_controller import UnifiedMobileController
from artemis.data_engine.trace import trace
from artemis.graph.perception import _check_injected_instruction_file
from artemis.graph.state import State
from artemis.mcp.action_executor import McpActionExecutor
from artemis.services.llm import (
RobustChatModelWrapper,
get_google_llm,
@@ -78,7 +77,7 @@ class FlashRunner:
self.step_summarizer_cfg = StepSummarizerConfig()
self.controller = UnifiedMobileController(ctx)
self.executor = MobileActionExecutor(ctx, self.controller)
self.executor = McpActionExecutor(ctx, self.controller)
self.summarizer = (
VisualStepSummarizer(ctx, model_name=self.step_summarizer_cfg.model)
if self.step_summarizer_cfg.enabled
@@ -90,6 +89,14 @@ class FlashRunner:
tools.insert(1, CLICK_SEQUENCE_TOOL)
tools.append(ASK_EXPLORER_TOOL)
tools.append(REPORT_TASK_STATUS_TOOL)
# With an actuator installed, drop declarations for device actions the
# backend does not implement (and append its extension tools); without
# one, the full historical declaration set is kept.
actuator = getattr(self.ctx, "actuator", None)
if actuator is not None:
from artemis.mcp.action_manifest import filter_declarations
tools = filter_declarations(tools, actuator, "flash")
return tools
def _prune_intermediate_screenshots(self, messages: list[BaseMessage]) -> None:
@@ -144,10 +151,15 @@ class FlashRunner:
}
)
# Render System Prompt
# Render System Prompt. Tool-teaching segments are gated on the available
# tool set so an absent tool leaves no trace in the prompt; until an actuator
# is wired in, the full manifest set reproduces the historical prompt.
prompt_path = Path(__file__).parent / "flash_runner.md"
prompt_template = prompt_path.read_text(encoding="utf-8")
system_prompt = Template(prompt_template).render(goal=self.goal)
available_tools = frozenset(t.name for t in tools_declaration)
system_prompt = Template(prompt_template).render(
goal=self.goal, available_tools=available_tools
)
messages: list[BaseMessage] = [
SystemMessage(content=system_prompt),
@@ -432,8 +444,12 @@ class FlashRunner:
try:
exec_result = await self.executor.execute(name, args, tc_id, state)
# Dynamic dispatch set: manifest device actions plus any backend
# extension tools, so extension steps are recorded like actions.
action_names = self.executor.action_tool_names
post_img_bytes = exec_result.screenshot_bytes
if not post_img_bytes and name in ACTION_TOOL_NAMES:
if not post_img_bytes and name in action_names:
try:
controller = UnifiedMobileController(self.ctx)
screen_data = await controller.get_screen_data()
@@ -447,7 +463,7 @@ class FlashRunner:
# Record telemetry / step in DataEngine
recorded_step_id = None
if self.ctx.data_engine and name in ACTION_TOOL_NAMES:
if self.ctx.data_engine and name in action_names:
try:
if self.ctx.data_engine.current_step_id is None:
self.ctx.data_engine.allocate_step_id()
@@ -517,7 +533,7 @@ class FlashRunner:
logger.warning(f"Error recording step in FlashRunner: {step_err}")
# ⚡ Non-blocking dispatch of objective visual transition summarizer
if self.summarizer and name in ACTION_TOOL_NAMES:
if self.summarizer and name in action_names:
action_sequence += 1
self.summarizer.dispatch(
step_number=action_sequence,