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

+111 -9
View File
@@ -56,6 +56,7 @@ from langchain_core.messages import (
)
from artemis.agents.flash.summarizer import VisualStepSummarizer, build_focus_context
from artemis.agents.flash.progress import PROGRESS_TOOL, TaskProgress
from artemis.agents.operator.prompts import REASONING_REMINDER, UserGuidance, render_user_guidance
from artemis.agents.validator.tool_declarations import (
ASK_EXPLORER_TOOL,
@@ -207,8 +208,7 @@ class FlashRunner:
return getattr(setup, "video_recording_tools_enabled", False) is True
def _get_tools(self) -> list:
# Flash has no tool that writes notes and its prompt never teaches
# them, so the Validator set's note readers are left out here.
# Task-local deliverables use the same notes and history as Pro.
tools = [
t
for t in VALIDATOR_TOOLS_DECLARATION
@@ -216,6 +216,24 @@ class FlashRunner:
]
tools.insert(1, CLICK_SEQUENCE_TOOL)
tools.append(ASK_EXPLORER_TOOL)
if self.ctx.data_engine is not None:
from artemis.core.tool_declaration import ToolDeclaration
from artemis.tools.scratchpad import SaveNoteArgs
tools.extend([READ_NOTE_TOOL, LIST_NOTES_TOOL])
tools.append(
ToolDeclaration(
"save_note",
"Save a requested text deliverable to this task's trace/notes/<key>.md. "
"Use a plain key such as report or reusable_case, never an absolute path. "
"Returns the actual path, size, SHA-256 and read-back verification. Existing "
"notes with this key are overwritten. The output key is reserved for the "
"automatically saved final response. Use read_note to inspect saved content. "
"Do not save/rewrite a plan every turn; only persist requested deliverables "
"or important findings not already retained in history.",
SaveNoteArgs.model_json_schema(),
)
)
# Helper tools shared with the Pro operator: the history tools are
# declared from the very same args schemas the LangChain tools bind
# (same availability gates: a DataEngine session, recall config).
@@ -246,6 +264,7 @@ class FlashRunner:
tools = filter_declarations(tools, actuator, "flash")
if getattr(self.ctx, "plugins", None):
tools.extend(self.ctx.plugins.declarations())
tools.append(PROGRESS_TOOL)
return tools
def _prune_intermediate_screenshots(self, messages: list[BaseMessage]) -> None:
@@ -594,9 +613,31 @@ class FlashRunner:
xml_list,
messages: list[BaseMessage],
native_text: str | None = None,
) -> dict:
) -> dict | None:
"""Records the final step, acknowledges the tool call, and flushes."""
final_report = args
final_report = dict(args)
if args.get("report_note_key"):
from artemis.utils.notes import read_note_content
try:
key = args["report_note_key"]
if not isinstance(key, str) or not re.fullmatch(r"[\w-]{1,128}", key):
raise ValueError("report_note_key 必须是已保存报告的普通笔记名称")
body = read_note_content(self.ctx.data_engine.base_dir, key)
if not body.strip():
raise ValueError("报告正文为空")
final_report["explanation"] = body
except (OSError, ValueError, AttributeError) as exc:
messages.append(
ToolMessage(
tool_call_id=tc_id,
name=name,
status="error",
content=f"最终报告未提交:{exc}。修正报告名称或直接提供完整 explanation。",
)
)
return None
args = final_report
self.final_report_received = True
if self.ctx.data_engine:
try:
@@ -645,7 +686,7 @@ class FlashRunner:
return post_img_bytes
# File operations have their own verified receipts. Do not add a UI
# observation/visual summary that could obscure that non-visual evidence.
if post_img_bytes or name not in action_names or name == "android_file":
if post_img_bytes or name not in action_names or name in {"android_file", "save_note"}:
return post_img_bytes
if state is not None:
state.indexed_points = []
@@ -764,7 +805,7 @@ class FlashRunner:
)
action_dict.pop("coordinates", None)
action_dict.pop(COORDINATE_SPACE_KEY, None)
if name == "android_file":
if name in {"android_file", "save_note"}:
action_dict.pop("coordinates", None)
action_dict.pop(COORDINATE_SPACE_KEY, None)
return action_dict
@@ -863,7 +904,7 @@ class FlashRunner:
# extension tools, so extension steps are recorded like actions.
# File helpers perform real work too. Persist their receipts as steps,
# otherwise helper-only turns vanish from the final checker's evidence.
action_names = self.executor.action_tool_names | {"android_file"}
action_names = self.executor.action_tool_names | {"android_file", "save_note"}
plugin_action = False
try:
operation = self.executor.execute(
@@ -929,8 +970,8 @@ class FlashRunner:
exec_result,
raw_text,
step_token_usage,
pre_screenshot_bytes,
xml_list,
None if name == "save_note" else pre_screenshot_bytes,
"" if name == "save_note" else xml_list,
post_img_bytes,
injected=injected,
native_text=native_text,
@@ -1104,6 +1145,8 @@ class FlashRunner:
from artemis.plugins.runtime import get_plugin_runtime
plugins = get_plugin_runtime(self.ctx)
if not hasattr(self, "_progress"):
self._progress = TaskProgress()
android_tools = action_names | {"ask_explorer"}
if plugins:
android_tools -= plugins.tool_names
@@ -1116,6 +1159,63 @@ class FlashRunner:
args = tc.get("args") or {}
tc_id = tc.get("id") or str(uuid.uuid4())
if name == "update_task_progress":
try:
resources = (
{"android"}
if getattr(self.ctx, "execution_target", None) != "computer"
else set()
)
resources.update(plugins.packages if plugins else [])
directory = getattr(self.ctx.data_engine, "base_dir", None)
text = self._progress.update(args, resources=resources, directory=directory)
status = "success"
except (ValueError, OSError) as exc:
text, status = str(exc), "error"
messages.append(
ToolMessage(tool_call_id=tc_id, name=name, status=status, content=text)
)
if self.ctx.data_engine:
result = ToolExecutionResult(
tool_name=name, tool_call_id=tc_id, status=status, text_summary=text
)
step_id = self._record_action_step(
name,
args,
result,
raw_text,
step_token_usage,
None,
"",
None,
injected=injected,
native_text=native_text,
)
turn.step_keys.append(str(step_id or tc_id))
if status == "error":
self._defer_tool_calls(
messages,
tool_calls[call_index + 1 :],
reason="进度更新失败,先修正后再执行关联动作。",
)
return None, pre_screenshot_bytes, xml_list, action_sequence
continue
resource = None
if plugins and name in plugins.bindings:
if not plugins.is_read_only(name):
resource = plugins.bindings[name][0]
elif name in android_tools and name not in {"wait_for_delay", "ask_explorer"}:
if name != "android_file" or args.get("action") in {"write_text", "push"}:
resource = "android"
blocked = self._progress.guard(
resource,
completed=name == "report_task_status" and args.get("status") == "completed",
)
if blocked:
self._defer_tool_calls(messages, tool_calls[call_index:], reason=blocked)
return None, pre_screenshot_bytes, xml_list, action_sequence
# A positive wait on the PC means elapsed time, not a phone refresh.
if (
getattr(self, "_android_observation_stale", False)
@@ -1204,6 +1304,8 @@ class FlashRunner:
native_text=native_text,
index_elements=index_elements,
)
if resource:
self._progress.last_resource = resource
if returning_to_android:
# wait_for_delay has no stale target and its normal post-action
# observation is fresh. The model must see it before any further