feat: add prompt-first batches and pausable task guidance
This commit is contained in:
1 parent
cec9898e4c
commit
057f0c3050
44 files changed
+1080
-191
No files matched your search
@@ -34,6 +34,13 @@ from artemis.runtime import (
|
||||
trace_store,
|
||||
)
|
||||
from artemis.runtime.process_probe import pid_is_alive
|
||||
from artemis.runtime.task_control import (
|
||||
add_instruction,
|
||||
control_dir,
|
||||
read_state,
|
||||
request_pause,
|
||||
request_resume,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,6 +58,34 @@ def _find_data_engine_db() -> str | None:
|
||||
return db_path if os.path.exists(db_path) else None
|
||||
|
||||
|
||||
def _web_session_status(trace_id: str) -> dict[str, Any] | None:
|
||||
"""Web submissions have a session row but no MCP launcher status.json."""
|
||||
db_path = _find_data_engine_db()
|
||||
if not db_path:
|
||||
return None
|
||||
try:
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
row = conn.execute(
|
||||
"SELECT status, pid, start_time, end_time, device_info "
|
||||
"FROM sessions WHERE session_id = ? ORDER BY start_time DESC LIMIT 1",
|
||||
(trace_id,),
|
||||
).fetchone()
|
||||
except sqlite3.Error as exc:
|
||||
logger.warning("Could not read web session %s: %s", trace_id, exc)
|
||||
return None
|
||||
if not row:
|
||||
return None
|
||||
data = dict(row)
|
||||
if data.get("device_info"):
|
||||
try:
|
||||
device = json.loads(data["device_info"])
|
||||
data["device_serial"] = device.get("device_id") if isinstance(device, dict) else None
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _pid_alive(pid: int | None) -> bool:
|
||||
if not pid:
|
||||
return False
|
||||
@@ -143,8 +178,8 @@ def _reconcile_task_state(
|
||||
liveness_failed = (
|
||||
current_status == "failed" and status_data.get("error") == _LIVENESS_FAILURE_ERROR
|
||||
)
|
||||
if db_status in ("completed", "success", "failed", "cancelled") and (
|
||||
current_status in ("running", "pending") or liveness_failed
|
||||
if db_status in ("completed", "success", "failed", "cancelled", "interrupted") and (
|
||||
current_status in ("running", "pending", "task_paused", "resuming") or liveness_failed
|
||||
):
|
||||
canonical = "completed" if db_status in ("completed", "success") else db_status
|
||||
if current_status != canonical:
|
||||
@@ -156,7 +191,7 @@ def _reconcile_task_state(
|
||||
status_data["end_time"] = time.time()
|
||||
dirty = True
|
||||
is_alive = False
|
||||
elif current_status in ("running", "pending"):
|
||||
elif current_status in ("running", "pending", "task_paused", "resuming"):
|
||||
if not pid:
|
||||
# No pid recorded (daemon dispatch never writes one): liveness cannot be
|
||||
# inferred, so the task is assumed alive -- matching the historical
|
||||
@@ -225,13 +260,13 @@ def mobile_manage_task(
|
||||
"""Manages the lifecycle and retrieves the status of a background mobile automation task.
|
||||
|
||||
This is your primary diagnostic and control tool for tasks started by
|
||||
`mobile_run_task`. You MUST use it to poll the task status when your
|
||||
`mobile_run_task` or the web workspace. You MUST use it to poll the task status when your
|
||||
1-minute fallback timer triggers (as required by `mobile_run_task`). You
|
||||
can also steer a subagent that is stuck or off-track, or abort a task.
|
||||
|
||||
### Actions
|
||||
- **'status'**: Returns `trace_id`, `status` ('running'/'completed'/'failed'/
|
||||
'cancelled'), `device_serial` (which phone owns this task in multi-device
|
||||
- **'status'**: Returns `trace_id`, `status` ('running'/'task_paused'/
|
||||
'resuming'/'completed'/'failed'/'cancelled'/'interrupted'), `device_serial` (which phone owns this task in multi-device
|
||||
setups), `task_desc`, `model`, `elapsed_seconds`, a `test_summary` (when
|
||||
the run declared verification check items: machine-readable
|
||||
passed/failed/inconclusive/unchecked counts plus failed-item details —
|
||||
@@ -239,6 +274,9 @@ def mobile_manage_task(
|
||||
current turn, latest thought/action — intervene if turns climb without
|
||||
progress or the thought indicates it is stuck; Pro: the active task plan
|
||||
— check it still aligns with your goal).
|
||||
- **'pause'**: Requests a pause after the current action, releasing the device.
|
||||
- **'resume'**: Queues a paused task for its device again; optional `instruction`
|
||||
provides guidance before the next action.
|
||||
- **'inject_instruction'**: Injects real-time guidance mid-flight when the
|
||||
subagent errs, stalls, or loops. Applied at the start of the next
|
||||
planning turn (Pro) or the next reactive loop (Flash). Requires
|
||||
@@ -254,15 +292,15 @@ def mobile_manage_task(
|
||||
should wind down cleanly instead of being killed.
|
||||
|
||||
Args:
|
||||
action: `"status"`, `"inject_instruction"`, or `"stop"`.
|
||||
trace_id: The task's session identifier from `mobile_run_task`.
|
||||
action: `"status"`, `"pause"`, `"resume"`, `"inject_instruction"`, or `"stop"`.
|
||||
trace_id: The task's session identifier from `mobile_run_task` or the web UI.
|
||||
instruction: Guidance string; required for `inject_instruction`
|
||||
(optional when `release_loop=True`), omit otherwise.
|
||||
release_loop: With `inject_instruction`: explicit user stop signal
|
||||
that authorizes the subagent to complete continuous monitoring
|
||||
loops and finish the task gracefully.
|
||||
"""
|
||||
status_data = trace_store.read_status(trace_id)
|
||||
status_data = trace_store.read_status(trace_id) or _web_session_status(trace_id)
|
||||
if not status_data:
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
@@ -271,6 +309,9 @@ def mobile_manage_task(
|
||||
}
|
||||
|
||||
current_status, pid, is_alive = _reconcile_task_state(trace_id, status_data)
|
||||
control_status = read_state(trace_id).get("status") if is_alive else None
|
||||
if control_status in ("task_paused", "resuming") and current_status in ("running", "pending"):
|
||||
current_status = control_status
|
||||
|
||||
trace_dir = trace_store.get_trace_dir(trace_id)
|
||||
|
||||
@@ -368,7 +409,7 @@ def mobile_manage_task(
|
||||
pass
|
||||
break
|
||||
|
||||
if current_status in ("running", "pending") and is_alive:
|
||||
if current_status in ("running", "pending", "task_paused", "resuming") and is_alive:
|
||||
progress: dict[str, Any] = {}
|
||||
|
||||
project_root = env_utils.get_project_root()
|
||||
@@ -483,8 +524,38 @@ def mobile_manage_task(
|
||||
|
||||
return response
|
||||
|
||||
elif action == "pause":
|
||||
if current_status not in ("running", "pending", "resuming") or not is_alive:
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
"status": current_status,
|
||||
"message": "Task is not running.",
|
||||
}
|
||||
request_pause(trace_id)
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
"status": "pausing",
|
||||
"message": "Pauses after the current action and releases the device.",
|
||||
}
|
||||
|
||||
elif action == "resume":
|
||||
if current_status != "task_paused" or not is_alive:
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
"status": current_status,
|
||||
"message": "Task is not paused.",
|
||||
}
|
||||
if instruction:
|
||||
add_instruction(trace_id, instruction, source="mcp")
|
||||
request_resume(trace_id)
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
"status": "resuming",
|
||||
"message": "Queued for the device again.",
|
||||
}
|
||||
|
||||
elif action == "stop":
|
||||
if current_status not in ("running", "pending"):
|
||||
if current_status not in ("running", "pending", "task_paused", "resuming"):
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
"status": current_status,
|
||||
@@ -579,7 +650,7 @@ def mobile_manage_task(
|
||||
"message": "Missing required argument 'instruction' for action 'inject_instruction'.",
|
||||
}
|
||||
|
||||
if current_status not in ("running", "pending"):
|
||||
if current_status not in ("running", "pending", "task_paused", "resuming"):
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
"status": current_status,
|
||||
@@ -593,19 +664,13 @@ def mobile_manage_task(
|
||||
"message": f"Trace directory not found: {trace_dir}",
|
||||
}
|
||||
|
||||
instruction_path = os.path.join(trace_dir, "injected_instruction.json")
|
||||
try:
|
||||
with open(instruction_path, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{
|
||||
"instruction": instruction,
|
||||
"release_loop": bool(release_loop),
|
||||
"timestamp": time.time(),
|
||||
"status": "pending",
|
||||
},
|
||||
f,
|
||||
indent=2,
|
||||
)
|
||||
add_instruction(trace_id, instruction, release_loop=release_loop, source="mcp")
|
||||
if (
|
||||
current_status == "task_paused"
|
||||
or (control_dir(trace_id) / "pause.request").exists()
|
||||
):
|
||||
request_resume(trace_id)
|
||||
suffix = " (with release_loop stop signal)" if release_loop else ""
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
@@ -623,5 +688,5 @@ def mobile_manage_task(
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
"status": current_status,
|
||||
"message": f"Invalid action '{action}'. Supported actions are 'status', 'stop', and 'inject_instruction'.",
|
||||
"message": f"Invalid action '{action}'. Supported actions are 'status', 'pause', 'resume', 'stop', and 'inject_instruction'.",
|
||||
}
|
||||
Reference in new issue
Block a user