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:
chenmingxuan committed 2026-10-18 14:18:37 +08:00
1 parent 2328a4df0c
commit e1ea099592
128 files changed
+4944 -700

No files matched your search

+40 -2
View File
@@ -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:
+14 -1
View File
@@ -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("查询不接受写入内容")
+31 -8
View File
@@ -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",
+50 -5
View File
@@ -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') "
+53 -20
View File
@@ -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,