# 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. """Flash Reactive Runner executing rapid Observe-Think-Act cycles.""" import time from artemis.agents.flash.runner import FlashRunner from artemis.config import load_agent_config from artemis.context import ArtemisContext, DeviceContext from artemis.core.state import ExecutionContextState, ExecutionStatus, StepRecord from artemis.drivers.mock.mock_driver import MockDeviceDriver from artemis.engine.base_runner import BaseRunner from artemis.graph.state import State from artemis.platform import platform from artemis.utils.logger import get_logger logger = get_logger(__name__) class ReactiveRunner(BaseRunner): """Fast, single-loop reactive runner for short-to-medium workflows (< 30 steps).""" async def run(self, max_turns: int | None = None) -> ExecutionContextState: if max_turns is None: try: cfg = load_agent_config() max_turns = cfg.flash.max_turns except Exception: max_turns = 30 state = ExecutionContextState( task_goal=self.ctx.task_goal, trace_id=self.ctx.trace_id, max_turns=max_turns, ) logger.info( f"Starting ReactiveRunner for goal: '{self.ctx.task_goal}' on device: '{self.driver.device_id}'" ) is_mock = isinstance(self.driver, MockDeviceDriver) if not is_mock: try: artemis_ctx = getattr(self.ctx, "artemis_context", None) if artemis_ctx is None: artemis_ctx = ArtemisContext( device=DeviceContext( host_platform=platform.os_type.name, device_serial=self.driver.device_id, ) ) flash_runner = FlashRunner( artemis_ctx, goal=self.ctx.task_goal, max_turns=max_turns ) legacy_state = State(messages=[]) result = await flash_runner.run(legacy_state) state.current_turn = len(getattr(legacy_state, "messages", [])) or 1 if result.get("status") == "completed": state.status = ExecutionStatus.SUCCESS else: state.status = ExecutionStatus.FAILED state.error_message = result.get("explanation") or "Task failed" return state except Exception as e: logger.error(f"Live FlashRunner execution failed: {e}", exc_info=True) state.status = ExecutionStatus.FAILED state.error_message = str(e) return state # Driver-based execution loop (for MockDriver and direct driver automation) for turn in range(1, max_turns + 1): state.current_turn = turn step_start = time.time() # 1. Observe screen screen_data = await self.driver.get_screen_data() # 2. Execute action step action_name = "wait_for_delay" action_params = {"seconds": 0.1} await self.driver.wait_for_delay(0.1) # 3. Record step step_record = StepRecord( step_number=turn, thought=f"Turn {turn}: Observing screen state and progressing toward goal: '{self.ctx.task_goal}'.", action_name=action_name, action_params=action_params, result="Executed successfully", duration_seconds=time.time() - step_start, ) state.steps.append(step_record) if turn >= 1: state.status = ExecutionStatus.SUCCESS break return state