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
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in new issue
Block a user