merge(runtime): reconcile task state and device cleanup

This commit is contained in:
somew1nd committed 2026-09-01 11:56:17 -07:00
commit 5907a28e14
43 files changed
+931 -2938

No files matched your search

+5 -35
View File
@@ -12,40 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Core framework contracts and lifecycle primitives."""
"""Core framework primitives.
from artemis.core.context import ExecutionContext, DeviceInfoContext
from artemis.core.state import ExecutionContextState, ExecutionStatus, StepRecord
from artemis.core.events import Event, EventBus, HookType, global_event_bus
from artemis.core.exceptions import (
ArtemisException,
DeviceDriverException,
DeviceNotFoundException,
AgentExecutionException,
ToolExecutionException,
LLMProviderException,
StateTransitionException,
)
from artemis.core.registry import AgentRegistry, ToolRegistry, DriverRegistry
Live submodules: ``artemis.core.tool_declaration`` and ``artemis.core.diagnostics``
(imported directly by module path by their consumers).
"""
__all__ = [
"ExecutionContext",
"DeviceInfoContext",
"ExecutionContextState",
"ExecutionStatus",
"StepRecord",
"Event",
"EventBus",
"HookType",
"global_event_bus",
"ArtemisException",
"DeviceDriverException",
"DeviceNotFoundException",
"AgentExecutionException",
"ToolExecutionException",
"LLMProviderException",
"StateTransitionException",
"AgentRegistry",
"ToolRegistry",
"DriverRegistry",
]
__all__: list[str] = []
-62
View File
@@ -1,62 +0,0 @@
# 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.
"""Clean Execution Context for ARTEMIS."""
from pathlib import Path
from typing import Any
from uuid import uuid4
from pydantic import BaseModel, Field
class DeviceInfoContext(BaseModel):
"""Device metadata and configuration."""
device_id: str = Field(default="default-device", description="ADB serial or device identifier")
platform: str = Field(
default="android", description="Platform type ('android', 'cloud', 'mock')"
)
width: int = Field(default=1080, description="Screen width in pixels")
height: int = Field(default=2400, description="Screen height in pixels")
class ExecutionContext:
"""Central context holding device driver, session identifiers, and telemetry tracer."""
def __init__(
self,
task_goal: str,
device_id: str = "default-device",
trace_id: str | None = None,
traces_path: Path | str | None = None,
locked_package: str | None = None,
platform: str = "android",
):
self.task_goal = task_goal
self.trace_id = trace_id or str(uuid4())
self.traces_path = Path(traces_path) if traces_path else Path("traces") / self.trace_id
self.locked_package = locked_package
self.device = DeviceInfoContext(device_id=device_id, platform=platform)
self.driver: Any | None = None
self.telemetry: Any | None = None
# Fully-wired ArtemisContext (data_engine, adb clients, execution setup).
# Required by the live (non-mock) paths of GraphRunner/ReactiveRunner.
self.artemis_context: Any | None = None
self.custom_attributes: dict[str, Any] = {}
def set_attribute(self, key: str, value: Any) -> None:
self.custom_attributes[key] = value
def get_attribute(self, key: str, default: Any = None) -> Any:
return self.custom_attributes.get(key, default)
-77
View File
@@ -1,77 +0,0 @@
# 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.
"""Event Bus and Lifecycle Hooks System for ARTEMIS."""
from collections import defaultdict
from collections.abc import Callable
from enum import Enum
import inspect
import time
from typing import Any
from pydantic import BaseModel, Field
from artemis.utils.logger import get_logger
logger = get_logger(__name__)
class HookType(str, Enum):
BEFORE_TASK = "before_task"
AFTER_TASK = "after_task"
BEFORE_STEP = "before_step"
AFTER_STEP = "after_step"
ON_ACTION = "on_action"
ON_ERROR = "on_error"
ON_SCREEN_CHANGE = "on_screen_change"
class Event(BaseModel):
"""Encapsulates an event payload emitted through the event bus."""
event_type: HookType = Field(..., description="Type of the lifecycle event")
timestamp: float = Field(default_factory=time.time, description="Event occurrence timestamp")
trace_id: str = Field(..., description="Active session trace identifier")
payload: dict[str, Any] = Field(default_factory=dict, description="Event data dictionary")
class EventBus:
"""Asynchronous event bus supporting subscription and broadcast of lifecycle events."""
def __init__(self):
self._subscribers: dict[HookType, list[Callable[..., Any]]] = defaultdict(list)
def subscribe(self, hook: HookType, callback: Callable[..., Any]) -> None:
"""Registers a listener callback for a specific lifecycle event."""
self._subscribers[hook].append(callback)
def unsubscribe(self, hook: HookType, callback: Callable[..., Any]) -> None:
"""Removes a registered callback."""
if callback in self._subscribers[hook]:
self._subscribers[hook].remove(callback)
async def emit(self, event: Event) -> None:
"""Asynchronously dispatches an event to all subscribed listeners."""
listeners = self._subscribers.get(event.event_type, [])
for listener in listeners:
try:
if inspect.iscoroutinefunction(listener):
await listener(event)
else:
listener(event)
except Exception as e:
logger.warning(f"Error executing event listener for {event.event_type}: {e}")
# Global singleton event bus instance
global_event_bus = EventBus()
-57
View File
@@ -1,57 +0,0 @@
# 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.
"""Standardized exception hierarchy for ARTEMIS."""
class ArtemisException(Exception):
"""Base exception for all ARTEMIS framework errors."""
pass
class DeviceDriverException(ArtemisException):
"""Raised when hardware, emulator, or ADB interactions fail."""
pass
class DeviceNotFoundException(DeviceDriverException):
"""Raised when the specified target device cannot be located or connected."""
pass
class AgentExecutionException(ArtemisException):
"""Raised when an autonomous agent fails during reasoning or execution."""
pass
class ToolExecutionException(ArtemisException):
"""Raised when a tool action fails or receives invalid arguments."""
pass
class LLMProviderException(ArtemisException):
"""Raised when an upstream LLM/VLM API call fails or times out."""
pass
class StateTransitionException(ArtemisException):
"""Raised when graph execution encounters an invalid state transition."""
pass
-46
View File
@@ -1,46 +0,0 @@
# 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.
"""Global Plugin and Component Registry for Agents, Tools, and Drivers."""
from typing import Any, TypeVar
T = TypeVar("T")
class ComponentRegistry:
"""Generic typed component registry supporting runtime dynamic plugin loading."""
def __init__(self, name: str):
self.name = name
self._items: dict[str, Any] = {}
def register(self, key: str, item: Any) -> Any:
self._items[key.lower()] = item
return item
def get(self, key: str) -> Any | None:
return self._items.get(key.lower())
def list_all(self) -> dict[str, Any]:
return dict(self._items)
def contains(self, key: str) -> bool:
return key.lower() in self._items
# Global Registries
AgentRegistry = ComponentRegistry("Agents")
ToolRegistry = ComponentRegistry("Tools")
DriverRegistry = ComponentRegistry("Drivers")
-70
View File
@@ -1,70 +0,0 @@
# 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.
"""Core Execution State and Lifecycle Data Structures."""
from enum import Enum
import time
from typing import Any
from pydantic import BaseModel, Field
class ExecutionStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
PAUSED = "paused"
INTERRUPTED = "interrupted"
class StepRecord(BaseModel):
"""Encapsulates a single step's observation, thought, and action."""
step_number: int = Field(..., description="1-indexed step turn counter")
thought: str | None = Field(default=None, description="Reasoning text")
action_name: str | None = Field(default=None, description="Triggered action name")
action_params: dict[str, Any] = Field(default_factory=dict, description="Action arguments")
result: str | None = Field(default=None, description="Action execution result output")
screenshot_path: str | None = Field(default=None, description="Saved step screenshot file URI")
timestamp: float = Field(default_factory=time.time, description="Timestamp of step start")
duration_seconds: float = Field(default=0.0, description="Step processing duration in seconds")
class ExecutionContextState(BaseModel):
"""Full execution state passed across agents and runners."""
task_goal: str = Field(..., description="Initial user task objective")
trace_id: str = Field(..., description="Unique task trace identifier")
status: ExecutionStatus = Field(
default=ExecutionStatus.RUNNING, description="Current execution lifecycle status"
)
current_turn: int = Field(default=0, description="Current step turn count")
max_turns: int = Field(default=30, description="Maximum allowed step turns before timeout")
steps: list[StepRecord] = Field(
default_factory=list, description="Chronological execution step history"
)
active_plan: list[str] = Field(
default_factory=list, description="Decomposed milestone plan items"
)
current_step_index: int = Field(default=0, description="Current index within active_plan")
metadata: dict[str, Any] = Field(
default_factory=dict, description="Custom metadata and state parameters"
)
error_message: str | None = Field(
default=None, description="Failure reason if status is FAILED"
)
final_output: Any | None = Field(
default=None, description="Final synthesized structured output from Outputter or runner"
)