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:
somew1nd committed 2026-09-02 21:45:51 -07:00
1 parent 5ccf06d677
commit 67248c887e
289 files changed
+15883 -6919

No files matched your search

+9 -1
View File
@@ -149,7 +149,9 @@ def _get_session_video_sync(session_id: str):
v_url = media_service.resolve_video_url(row_dict, video_rec_map, video_idx)
video_segments = media_service.resolve_video_segments(v_url)
if v_url:
version = int(float((recording or {}).get("end_time") or row_dict.get("end_time") or 0) * 1000)
version = int(
float((recording or {}).get("end_time") or row_dict.get("end_time") or 0) * 1000
)
separator = "&" if "?" in v_url else "?"
versioned_url = f"{v_url}{separator}v={version}" if version else v_url
for segment in video_segments:
@@ -188,3 +190,9 @@ async def get_task_plan(session_id: str):
@router.get("/api/sessions/{session_id}/notes")
async def get_all_notes(session_id: str):
return {"notes": media_service.get_session_notes_content(session_id)}
@router.get("/api/sessions/{session_id}/checks")
async def get_session_checks(session_id: str):
"""Checker verdict ledger + run outcome (backfill for the Checker panel)."""
return media_service.get_session_checks(session_id)
+2 -3
View File
@@ -64,11 +64,10 @@ def _list_sessions_sync():
try:
from artemis.runtime import DeviceExecutionLock
active_owners = DeviceExecutionLock.get_active_owners()
active_owner_sids = {
str(owner.session_id)
for owner in active_owners.values()
if owner.session_id
str(owner.session_id) for owner in active_owners.values() if owner.session_id
}
except Exception:
active_owner_sids = set()
+7 -3
View File
@@ -297,7 +297,11 @@ async def get_credentials():
providers = ("google", "openai", "anthropic", "openrouter", "ocr")
status = {name: bool(settings.get_api_key(name)) for name in providers}
status["gemini"] = status["google"]
return {"providers": [{"name": name, "configured": configured} for name, configured in status.items()]}
return {
"providers": [
{"name": name, "configured": configured} for name, configured in status.items()
]
}
@router.post("/credentials/test")
@@ -370,7 +374,7 @@ async def update_credentials(request: UpdateCredentialsRequest):
async def get_model_config_and_env():
"""Retrieve the current active artemis.jsonc configuration and .env status for custom setup."""
import os
from artemis.config.paths import ROOT_DIR, get_config_path
from artemis.config.paths import get_config_path, get_env_file
from artemis.config import settings
from artemis.utils.file import load_jsonc
@@ -390,7 +394,7 @@ async def get_model_config_and_env():
config_content = f"// Error reading config: {e}"
# 2. Env file resolution
env_path = ROOT_DIR / ".env"
env_path = get_env_file()
# 3. Relevant Env Variables status
def get_real_env_value(key_name: str, provider: str) -> str | None:
+45 -25
View File
@@ -90,8 +90,7 @@ async def run_task(request: RunRequest):
(
item
for item in state.queue_items
if isinstance(item, dict)
and str(item.get("session_id")) == requested_sid
if isinstance(item, dict) and str(item.get("session_id")) == requested_sid
),
None,
)
@@ -122,9 +121,7 @@ async def run_task(request: RunRequest):
# fail downstream with a clear error instead.
if request.device_serial:
try:
rejection = await device_pool.validate_explicit_serial_async(
request.device_serial
)
rejection = await device_pool.validate_explicit_serial_async(request.device_serial)
except Exception:
rejection = None
if rejection:
@@ -144,19 +141,19 @@ async def run_task(request: RunRequest):
# prefers the diagnostics target preference, then any unlocked ready
# device); the verified serial is bound below.
target_serial = request.device_serial
device_probe = await readiness_engine.run_device_submission_probe(
target_serial=target_serial
)
device_probe = await readiness_engine.run_device_submission_probe(target_serial=target_serial)
if device_probe and device_probe.summary in {"Device Locked", "Lock State Unknown"}:
locked_serial = (
device_probe.metadata.get("active_device", {}).get("serial")
or target_serial
or ""
device_probe.metadata.get("active_device", {}).get("serial") or target_serial or ""
)
detail = (
f"Android device {locked_serial} is locked. Unlock it and enter the home screen before running a task.".replace(" ", " ").strip()
f"Android device {locked_serial} is locked. Unlock it and enter the home screen before running a task.".replace(
" ", " "
).strip()
if device_probe.summary == "Device Locked"
else f"Android device {locked_serial} lock state could not be verified. Keep it unlocked on the home screen and try again.".replace(" ", " ").strip()
else f"Android device {locked_serial} lock state could not be verified. Keep it unlocked on the home screen and try again.".replace(
" ", " "
).strip()
)
raise HTTPException(status_code=409, detail=detail)
@@ -173,6 +170,8 @@ async def run_task(request: RunRequest):
profile=request.profile or "flash",
expected_output=request.expected_output,
enable_outputter=request.enable_outputter,
verification_level=request.verification_level,
explorer_mode=request.explorer_mode,
locked_app_package=request.locked_app_package,
app_path=request.app_path,
device_serial=target_serial,
@@ -182,6 +181,22 @@ async def run_task(request: RunRequest):
)
@router.get("/api/run/defaults")
async def get_run_defaults():
"""Effective Pro-profile tuning defaults from the agent config.
The launcher's sliders start here so they reflect ``artemis.jsonc`` (and the
``ARTEMIS_EXPLORER_VERSION`` override) instead of a hard-coded guess.
"""
from artemis.config import load_agent_config, verification_level_for_checker
agent_cfg = load_agent_config()
return {
"verification_level": verification_level_for_checker(agent_cfg.checker),
"explorer_mode": agent_cfg.explorer.resolve(profile="pro"),
}
@router.get("/api/devices")
async def list_devices():
"""List all connected Android devices with their busy / idle status."""
@@ -223,7 +238,6 @@ async def stop_task(
return {"status": "no_running_task"}
@router.post("/api/resume")
async def resume_task():
resumed = task_queue_service.resume_task()
@@ -269,12 +283,14 @@ async def get_status():
(global_owner.session_id if global_owner else None)
or state.active_session_id
or (running_task.get("session_id") if running_task else None)
or (active_tasks[0]["session_id"] if active_tasks and active_tasks[0].get("session_id") else None)
or (
active_tasks[0]["session_id"]
if active_tasks and active_tasks[0].get("session_id")
else None
)
or (session_repo.get_running_session_id() if is_running else None)
)
owner_connection = (
state.active_connections.get(str(running_sid), {}) if running_sid else {}
)
owner_connection = state.active_connections.get(str(running_sid), {}) if running_sid else {}
running_goal = (
owner_connection.get("goal")
or (global_owner.description if global_owner else None)
@@ -283,8 +299,10 @@ async def get_status():
or (active_tasks[0]["goal"] if active_tasks else None)
)
active_profile = owner_connection.get("profile") or state.current_profile or (
running_task.get("profile") if running_task and not global_owner else None
active_profile = (
owner_connection.get("profile")
or state.current_profile
or (running_task.get("profile") if running_task and not global_owner else None)
)
if not active_profile and (running_sid or latest_session_id):
check_sid = running_sid or latest_session_id
@@ -407,7 +425,11 @@ async def stream_events(session_id: str = "active", client: str | None = None):
active_sid = state.active_session_id
if not active_sid:
running_item = next(
(t for t in state.queue_items if isinstance(t, dict) and t.get("status") == "running"),
(
t
for t in state.queue_items
if isinstance(t, dict) and t.get("status") == "running"
),
None,
)
if running_item:
@@ -435,16 +457,14 @@ async def stream_events(session_id: str = "active", client: str | None = None):
recorded_steps = step_repo.get_session_steps(str(active_sid))
for step_dict in recorded_steps:
yield (
"event: step_recorded\n"
f"data: {json.dumps(step_dict, default=str)}\n\n"
f"event: step_recorded\ndata: {json.dumps(step_dict, default=str)}\n\n"
)
except Exception as exc:
print(f"[Stream] Could not replay active steps: {exc}")
else:
for progress_event in state.get_startup_progress(session_id):
yield (
"event: startup_progress\n"
f"data: {json.dumps(progress_event, default=str)}\n\n"
f"event: startup_progress\ndata: {json.dumps(progress_event, default=str)}\n\n"
)
shutdown_waiter = asyncio.create_task(state.shutdown_event.wait())