feat: add official Codex skill and harden task execution
Install and update artemis-use with task diagnostics and plugin guidance. Preserve cross-device file receipts, review canonical reports, guard stage transitions and reduce small history summaries. Improve exploration lifecycle, request recovery and browser live connections.
This commit is contained in:
1 parent
2328a4df0c
commit
e1ea099592
128 files changed
+4944
-700
No files matched your search
@@ -121,10 +121,19 @@ async def _download(
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def mobile_workspace(include_devices: bool = True) -> dict:
|
||||
async def mobile_workspace(
|
||||
include_devices: bool = True,
|
||||
plugin_id: str | None = None,
|
||||
probe_plugin: bool = False,
|
||||
profile: str = "default",
|
||||
include_development: bool = False,
|
||||
) -> dict:
|
||||
"""List connected Android devices and the unified running/queued task state.
|
||||
|
||||
Select the user's device; if multiple devices exist and none is chosen, ask first.
|
||||
plugin_id filters the installed plugin list. probe_plugin=true performs a bounded
|
||||
connection/schema probe of that installed, authorized plugin (starts its runtime,
|
||||
executes no task action). It never installs or grants permissions.
|
||||
This does not submit tasks. On connection/device errors use mobile_diagnose.
|
||||
For computer-only work use include_devices=false to skip ADB discovery entirely.
|
||||
"""
|
||||
@@ -132,10 +141,36 @@ async def mobile_workspace(include_devices: bool = True) -> dict:
|
||||
_request("GET", "/api/status"), _request("GET", "/api/plugins")
|
||||
)
|
||||
devices = await _request("GET", "/api/devices") if include_devices else {}
|
||||
installed = plugins.get("plugins", [])
|
||||
if plugin_id:
|
||||
_id(plugin_id)
|
||||
installed = [
|
||||
item for item in installed if (item.get("manifest") or item).get("id") == plugin_id
|
||||
]
|
||||
if not installed:
|
||||
raise ValueError("插件未安装,请在网页插件页安装并授权")
|
||||
development = None
|
||||
if include_development:
|
||||
from artemis import __version__
|
||||
import sys
|
||||
|
||||
development = {
|
||||
"python_executable": sys.executable,
|
||||
"artemis_version": __version__,
|
||||
"plugin_schema_version": 2,
|
||||
"installation_root": str(ROOT_DIR),
|
||||
}
|
||||
probe = None
|
||||
if probe_plugin:
|
||||
probe = await _request(
|
||||
"POST", f"/api/plugins/{_id(plugin_id)}/diagnose", json={"profile": profile}
|
||||
)
|
||||
return {
|
||||
"devices": devices.get("devices", []),
|
||||
"queue": status,
|
||||
"plugins": plugins.get("plugins", []),
|
||||
"plugins": installed,
|
||||
"plugin_probe": probe,
|
||||
"development": development,
|
||||
"blocked_resources": plugins.get("blocked_resources", {}),
|
||||
"execution_targets": ["android", "computer", "mixed"],
|
||||
}
|
||||
@@ -298,6 +333,7 @@ async def mobile_search_history(
|
||||
end: float | None = None,
|
||||
page: int = 1,
|
||||
size: int = 30,
|
||||
batch_id: str = "",
|
||||
) -> dict:
|
||||
"""Search ALL persisted terminal tasks by name, prompt, ID or case metadata, with pagination.
|
||||
|
||||
@@ -306,6 +342,8 @@ async def mobile_search_history(
|
||||
Return items,total,page,size; inspect evidence/report before claiming a test passed.
|
||||
"""
|
||||
params = {"q": query, "status": status, "page": page, "size": size}
|
||||
if batch_id:
|
||||
params["batch_id"] = _id(batch_id)
|
||||
if start is not None:
|
||||
params["start"] = start
|
||||
if end is not None:
|
||||
|
||||
@@ -10,6 +10,8 @@ from mcp_server.tools.automation import _id, _request
|
||||
async def mobile_exploration(
|
||||
action: Literal[
|
||||
"list",
|
||||
"deletion_preview",
|
||||
"delete",
|
||||
"get",
|
||||
"create",
|
||||
"start",
|
||||
@@ -33,9 +35,12 @@ async def mobile_exploration(
|
||||
demonstration_id: str | None = None,
|
||||
after: int = 0,
|
||||
version: int | None = None,
|
||||
user_confirmed: bool = False,
|
||||
) -> Any:
|
||||
"""Explore ONE complex test with user guidance and produce a reusable case.
|
||||
|
||||
deletion_preview lists affected conversation runs; delete requires user_confirmed=true
|
||||
after the user requests deletion. Independent validation runs are preserved.
|
||||
Read-only list/get/case never execute tasks. create stores {goal,config?,task_config?};
|
||||
start/continue explicitly enqueue or resume Pro exploration. Independent defaults:
|
||||
gpt-6.1-sol/high, 1800 active seconds; timeout pauses, never fails by itself.
|
||||
@@ -49,11 +54,19 @@ async def mobile_exploration(
|
||||
poll get.generations for completion. validate payload={version,profile,task_config?}
|
||||
explicitly runs the saved version in Flash/Pro; batch additionally accepts name and
|
||||
creates a DRAFT batch, never starts it. Exploration pass and rerun pass are separate.
|
||||
Use stable request_id for every mutation except pause/stop, reusing it on transport
|
||||
Use stable request_id for every mutation except pause/stop/delete, reusing it on transport
|
||||
retries. No API keys; configure secrets in the Web UI. mobile_config(section='exploration')
|
||||
controls non-secret defaults. After errors call mobile_diagnose before resubmitting.
|
||||
"""
|
||||
base = "/api/explorations"
|
||||
if action in {"deletion_preview", "delete"}:
|
||||
path = f"{base}/{_id(exploration_id)}"
|
||||
if action == "deletion_preview":
|
||||
return await _request("GET", path + "/deletion-preview")
|
||||
if not user_confirmed:
|
||||
raise ValueError("删除探索需要用户明确要求;先用 deletion_preview 核对影响范围")
|
||||
return await _request("DELETE", path)
|
||||
|
||||
if action in {"list", "get", "case"}:
|
||||
if payload or request_id:
|
||||
raise ValueError("查询不接受写入内容")
|
||||
|
||||
@@ -57,6 +57,8 @@ async def mobile_inspect_trace(
|
||||
query: str | None = None,
|
||||
step_range: list[int] | None = None,
|
||||
max_results: int = 5,
|
||||
offset: int = 0,
|
||||
limit: int = 20,
|
||||
) -> Any:
|
||||
"""Retrieves execution details of a mobile automation task (running or finished).
|
||||
|
||||
@@ -64,8 +66,12 @@ async def mobile_inspect_trace(
|
||||
both Flash and Pro tasks. Every result includes the assigned `device_serial`.
|
||||
|
||||
### Actions
|
||||
- view_report / view_usage / view_requests / view_logs / view_evidence:
|
||||
bounded read-only report, measured token/cache totals, request phases and IDs,
|
||||
redacted log tails, or source-specific evidence index. offset/limit paginate;
|
||||
start with view_report, then inspect only relevant evidence. No export is generated.
|
||||
- **'view_summary'**: High-level execution summary across all steps.
|
||||
Pro: hierarchical task plan with per-step status; Flash: full execution
|
||||
Pro: hierarchical task plan with per-step status; Flash: paginated execution
|
||||
chain with each step's reasoning and action.
|
||||
- **'search'**: Deterministic keyword / step-range lookup over the full
|
||||
stored history (screen descriptions, exact actions and results,
|
||||
@@ -74,15 +80,15 @@ async def mobile_inspect_trace(
|
||||
its step number; a `step_range` also returns that range's per-step
|
||||
action ledger. Requires `query` and/or `step_range`.
|
||||
- **'view_step_screenshots'**: Local file paths of one step's screenshots:
|
||||
`before_screenshot` (what the agent saw), `after_screenshot` (only set
|
||||
when the action failed/was intercepted, else null), and
|
||||
`before_screenshot` (what the agent saw), `after_screenshot` (when actually captured, otherwise null), and
|
||||
`action_overlay_screenshot` (the action visually marked — e.g. red circle
|
||||
for taps — key for verifying the agent tapped the right element).
|
||||
- **'view_step_details'**: Stored details of one step exactly as the agent's
|
||||
own context showed it: what the screen showed, its reasoning, every tool
|
||||
call it made (name, arguments, result — e.g. what the explorer or OCR
|
||||
reported), the planned action, any safety-net interception and the
|
||||
execution result. Coordinates are normalized [x, y]. Works for Flash and
|
||||
execution result. Coordinate units depend on the tool: Cua uses screenshot
|
||||
pixels; consult the recorded source geometry, never reuse another window. Works for Flash and
|
||||
Pro tasks.
|
||||
|
||||
Args:
|
||||
@@ -97,6 +103,17 @@ async def mobile_inspect_trace(
|
||||
`"search"`.
|
||||
max_results: Maximum `"search"` hits to return (server-side cap applies).
|
||||
"""
|
||||
from mcp_server.tools.automation import _id, _request
|
||||
|
||||
_id(trace_id)
|
||||
if offset < 0 or not 1 <= limit <= 100:
|
||||
raise ValueError("offset 必须非负,limit 必须在 1–100 之间")
|
||||
if action in {"view_report", "view_usage", "view_requests", "view_logs", "view_evidence"}:
|
||||
return await _request(
|
||||
"GET",
|
||||
f"/api/sessions/{trace_id}/inspection",
|
||||
params={"view": action.removeprefix("view_"), "offset": offset, "limit": limit},
|
||||
)
|
||||
project_root = env_utils.get_project_root()
|
||||
db_path = os.path.join(trace_store.TRACES_DIR, "data_engine.db")
|
||||
if not os.path.exists(db_path):
|
||||
@@ -135,8 +152,8 @@ async def mobile_inspect_trace(
|
||||
session_start_time = session_row["start_time"] if session_row else None
|
||||
|
||||
cursor.execute(
|
||||
"SELECT * FROM steps WHERE session_id = ? ORDER BY step_number ASC",
|
||||
(trace_id,),
|
||||
"SELECT * FROM steps WHERE session_id = ? ORDER BY step_number ASC LIMIT ? OFFSET ?",
|
||||
(trace_id, limit, offset),
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
@@ -252,7 +269,10 @@ async def mobile_inspect_trace(
|
||||
summary_lines.append(f"### Step {s_num} (at {rel_t}, duration {dur_str})")
|
||||
summary_lines.append(f"- **Thinking / Motivation:** {thought}")
|
||||
summary_lines.append(f"- **Action Taken:** `{act_str}` -> `{res}`\n")
|
||||
return "\n".join(summary_lines)
|
||||
return (
|
||||
"\n".join(summary_lines)[:40000]
|
||||
+ f"\n\nPage offset={offset}, limit={limit}; use offset={offset + limit} for more steps."
|
||||
)
|
||||
|
||||
try:
|
||||
summary_markdown = build_plan_and_history(
|
||||
@@ -268,7 +288,10 @@ async def mobile_inspect_trace(
|
||||
f"**Session ID:** `{trace_id}` | **Model:** Pro{device_info_str}\n\n"
|
||||
+ summary_markdown
|
||||
)
|
||||
return summary_markdown
|
||||
return (
|
||||
summary_markdown[:40000]
|
||||
+ f"\n\nPage offset={offset}, limit={limit}; use offset={offset + limit} for more steps."
|
||||
)
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": "Formatting error",
|
||||
|
||||
@@ -254,18 +254,28 @@ def _mark_liveness_failure(trace_id: str, status_data: dict[str, Any]) -> None:
|
||||
@mcp.tool()
|
||||
def mobile_manage_task(
|
||||
action: str,
|
||||
trace_id: str,
|
||||
trace_id: str = "",
|
||||
instruction: str | None = None,
|
||||
release_loop: bool = False,
|
||||
request_id: str | None = None,
|
||||
review_status: str | None = None,
|
||||
review_reason: str = "",
|
||||
user_confirmed: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""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` 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.
|
||||
`mobile_run_task` or the web workspace. Poll with reasonable intervals while
|
||||
following a running task; use request diagnostics for unexplained waits.
|
||||
You can also steer a runner that is stuck or off-track, or stop a task.
|
||||
|
||||
### Actions
|
||||
- lookup: Resolve a submission by request_id without starting or resubmitting it.
|
||||
- deletion_preview / delete: Show affected session IDs, then delete only when
|
||||
the user requested it (user_confirmed=true). Active tasks cannot be deleted.
|
||||
An exploration deletes its conversation runs, preserving independent reruns.
|
||||
- review: Record ONLY the user's explicitly stated passed/failed decision with
|
||||
review_reason and user_confirmed=true. Never label the agent's verdict as human.
|
||||
- **'status'**: Returns `trace_id`, `status` ('running'/'pausing'/'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
|
||||
@@ -311,6 +321,31 @@ def mobile_manage_task(
|
||||
that authorizes the subagent to complete continuous monitoring
|
||||
loops and finish the task gracefully.
|
||||
"""
|
||||
from mcp_server.tools.automation import _id, _request
|
||||
import asyncio
|
||||
|
||||
if action == "lookup":
|
||||
from mcp_server.task_requests import lookup
|
||||
|
||||
return lookup(_id(request_id))
|
||||
_id(trace_id)
|
||||
if action in {"deletion_preview", "delete", "review"}:
|
||||
if action != "deletion_preview" and not user_confirmed:
|
||||
raise ValueError("此动作需要用户明确要求;AI 自行判断不能记作人工复验")
|
||||
if action == "deletion_preview":
|
||||
return asyncio.run(_request("GET", f"/api/sessions/{trace_id}/deletion-preview"))
|
||||
if action == "delete":
|
||||
return asyncio.run(_request("POST", f"/api/sessions/{trace_id}/delete"))
|
||||
if review_status not in {"passed", "failed"}:
|
||||
raise ValueError("review_status 必须为 passed 或 failed")
|
||||
result = asyncio.run(
|
||||
_request(
|
||||
"PUT",
|
||||
f"/api/automation/reports/{trace_id}/review",
|
||||
json={"status": review_status, "reason": review_reason},
|
||||
)
|
||||
)
|
||||
return {"trace_id": trace_id, "test_result": result.get("test_result")}
|
||||
status_data = trace_store.read_status(trace_id) or _web_session_status(trace_id)
|
||||
if not status_data:
|
||||
return {
|
||||
@@ -438,6 +473,15 @@ def mobile_manage_task(
|
||||
test_result = resolve_case_result(None, current_status, response.get("error") or "")
|
||||
response["test_result"] = test_result.model_dump() if test_result else None
|
||||
|
||||
from apps.admin_console.services.case_review import reviewed_result
|
||||
from apps.admin_console.services.automation_store import AutomationStore
|
||||
from pathlib import Path
|
||||
|
||||
review_store = AutomationStore(Path(db_path))
|
||||
response["test_result"] = reviewed_result(
|
||||
trace_id, response.get("test_result"), review_store
|
||||
)
|
||||
|
||||
if current_status in ("running", "pending", "task_paused", "resuming") and is_alive:
|
||||
progress: dict[str, Any] = {}
|
||||
|
||||
@@ -472,12 +516,13 @@ def mobile_manage_task(
|
||||
conn2.row_factory = sqlite3.Row
|
||||
cur2 = conn2.cursor()
|
||||
cur2.execute(
|
||||
"SELECT count(*) as cnt FROM traces WHERE session_id = ? AND type = 'llm_call'",
|
||||
"SELECT coalesce(max(step_number),0) as cnt FROM steps WHERE session_id = ?",
|
||||
(session_id,),
|
||||
)
|
||||
row_cnt = cur2.fetchone()
|
||||
if row_cnt:
|
||||
progress["current_turn"] = row_cnt["cnt"]
|
||||
progress["current_step"] = row_cnt["cnt"]
|
||||
|
||||
cur2.execute(
|
||||
"SELECT payload FROM traces WHERE session_id = ? AND type in ('raw_thinking', 'llm_call') "
|
||||
|
||||
@@ -267,14 +267,15 @@ def mobile_run_task(
|
||||
verification_level: str | None = None,
|
||||
explorer_mode: str | None = None,
|
||||
runtime_overrides: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Starts an autonomous mobile UI automation subagent on a connected Android device.
|
||||
"""Submit a Flash/Pro Android, computer or mixed task to the user-started ARTEMIS service.
|
||||
|
||||
Delegates a mobile workflow to a background agent. Non-blocking: returns
|
||||
immediately with `trace_id` (for `mobile_manage_task` / `mobile_inspect_trace`),
|
||||
`device_serial`, `stdout_log`/`stderr_log` paths, and `notes_dir` (Pro only).
|
||||
On completion (success or failure) a Reactive Wakeup notifies you — but only
|
||||
when `conversation_id` was provided; without it, rely on your fallback timer.
|
||||
Completion notifications depend on the caller's configured notification backend;
|
||||
conversation_id alone does not guarantee the agent client receives a wakeup.
|
||||
|
||||
### Model selection (routing)
|
||||
- **Flash** (default, preferred): simple, deterministic tasks with a clear
|
||||
@@ -291,23 +292,25 @@ def mobile_run_task(
|
||||
checkpoints, multi-step planning, or detailed written output (via notes
|
||||
and `expected_output_desc`).
|
||||
|
||||
Timing is not precise: the agent's own inference adds ~5 s per step (Flash)
|
||||
or ~30 s per turn (Pro) on top of any requested waits — account for this
|
||||
when the task involves waiting. For recurring workflows, run once to
|
||||
discover the path, then author a deterministic script instead of
|
||||
re-delegating.
|
||||
For interactive case design use mobile_exploration: user guidance, demonstration
|
||||
analysis and reusable case generation. It has independent model settings.
|
||||
Inference time varies by model, network and device; do not subtract a fixed
|
||||
per-step estimate from required waits. Deterministic test code needs explicit
|
||||
timing and state-based waits verified against the real application.
|
||||
|
||||
### Follow-up protocol (CRITICAL)
|
||||
1. **Fallback timer**: you MUST set a background timer and check the task
|
||||
status via `mobile_manage_task` at least once every 1 minute — mobile
|
||||
tasks can stall silently, so never rely on the completion wakeup alone.
|
||||
2. **Post-task inspection**: after completion, inspect the execution logs
|
||||
to verify success (`stderr_log` carries critical failures and Python
|
||||
tracebacks — essential for debugging).
|
||||
3. **Pro notes**: for Pro runs, read `notes_dir` to extract the results and
|
||||
plans the subagent recorded.
|
||||
### Follow-up
|
||||
1. Poll mobile_manage_task at reasonable intervals (usually 30–60 seconds)
|
||||
while following a task; do not assume an unconfigured background notification.
|
||||
2. Read test_result and view_report after completion. Inspect relevant request,
|
||||
log or step evidence for failures and unexplained waits.
|
||||
3. Read Pro notes when its plan or detailed output is relevant to the user.
|
||||
|
||||
Reuse request_id for retries of the same submission; lookup with
|
||||
mobile_manage_task(action="lookup", request_id=...). Never repeat a task
|
||||
with a fresh ID merely because its admission reply was lost.
|
||||
|
||||
Args:
|
||||
request_id: Stable caller-generated ID (letters, numbers, _ or -, up to 100).
|
||||
task_desc: Clear, self-contained task description with all context,
|
||||
exact UI actions, and termination conditions (execution is one-shot).
|
||||
For Pro, may also specify what the subagent must record in its notes.
|
||||
@@ -351,6 +354,39 @@ def mobile_run_task(
|
||||
fps, h264_kbps, h265_kbps; it does not enable recording on its own.
|
||||
Unknown fields and credentials are rejected.
|
||||
"""
|
||||
parameters = dict(
|
||||
task_desc=task_desc,
|
||||
conversation_id=conversation_id,
|
||||
model=model,
|
||||
locked_app_package=locked_app_package,
|
||||
app_path=app_path,
|
||||
expected_output_desc=expected_output_desc,
|
||||
device_serial=device_serial,
|
||||
verification_level=verification_level,
|
||||
explorer_mode=explorer_mode,
|
||||
runtime_overrides=runtime_overrides,
|
||||
)
|
||||
if request_id is not None:
|
||||
from mcp_server.task_requests import run_once
|
||||
|
||||
return run_once(request_id, parameters, lambda sid: _run_task(trace_id=sid, **parameters))
|
||||
return _run_task(trace_id=str(uuid.uuid4()), **parameters)
|
||||
|
||||
|
||||
def _run_task(
|
||||
*,
|
||||
trace_id: str,
|
||||
task_desc: str,
|
||||
conversation_id: str | None,
|
||||
model: str,
|
||||
locked_app_package: str | None,
|
||||
app_path: str | None,
|
||||
expected_output_desc: str | None,
|
||||
device_serial: str | None,
|
||||
verification_level: str | None,
|
||||
explorer_mode: str | None,
|
||||
runtime_overrides: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
if runtime_overrides:
|
||||
from apps.admin_console.services.task_import import TaskOptions
|
||||
|
||||
@@ -370,9 +406,6 @@ def mobile_run_task(
|
||||
# plain tool error rather than a failed trace on disk.
|
||||
verification_level, explorer_mode = _normalize_pro_tuning(verification_level, explorer_mode)
|
||||
|
||||
# 1. Generate a unique trace_id
|
||||
trace_id = str(uuid.uuid4())
|
||||
|
||||
# 2. Initialize the trace store directory and status.json
|
||||
trace_store.init_trace(
|
||||
trace_id=trace_id,
|
||||
|
||||
Reference in new issue
Block a user