fix: bound task failures and release stalled workers

This commit is contained in:
chenmingxuan committed 2026-10-08 16:10:10 +08:00
1 parent 606ed94160
commit 1fdf7c0d1d
32 files changed
+1121 -93

No files matched your search

+72 -17
View File
@@ -81,7 +81,7 @@ from artemis.graph.state import State
from artemis.llm.router import ModelEndpoint
from artemis.llm.structured import ParseFailure, parse_structured
from artemis.mcp.action_executor import McpActionExecutor
from artemis.mcp.observation import observe
from artemis.mcp.observation import DeviceObservationError, require_observation
from artemis.memory.transcript import PRO_UI_LIST_MARKER, TranscriptLedger, mark_ephemeral
from artemis.services.llm import (
RobustChatModelWrapper,
@@ -380,8 +380,16 @@ class FlashRunner:
blocks.append(
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}}
)
if xml_list:
blocks.append({"type": "text", "text": f"{PRO_UI_LIST_MARKER}\n{xml_list}"})
blocks.append(
{
"type": "text",
"text": f"{PRO_UI_LIST_MARKER}\n"
+ (
xml_list
or "当前没有可用的元素索引。仅根据当前截图定位坐标,禁止使用历史画面的元素编号。"
),
}
)
ephemeral: list[int] = []
# (text, ephemeral) in tail order.
per_turn: list[tuple[str, bool]] = [(text, True) for text in (notices or [])]
@@ -604,19 +612,14 @@ class FlashRunner:
post_img_bytes = exec_result.screenshot_bytes
if post_img_bytes or name not in action_names:
return post_img_bytes
try:
obs, screenshot_bytes = await observe(self.ctx, self.controller, settle_ms=0)
except Exception as shot_err:
logger.warning(f"Failed to capture fallback screenshot in FlashRunner: {shot_err}")
return post_img_bytes
if not obs.ok:
logger.warning(f"Failed to capture fallback screenshot in FlashRunner: {obs.message}")
return post_img_bytes
if state is not None:
state.indexed_points = []
state.indexed_elements = []
state.latest_screenshot = None
obs, screenshot_bytes = await require_observation(self.ctx, self.controller, settle_ms=0)
post_img_bytes = screenshot_bytes
if not exec_result.ui_elements_text:
exec_result.ui_elements_text = obs.elements_text
if not exec_result.screenshot_path and obs.screenshot_path:
exec_result.screenshot_path = obs.screenshot_path
exec_result.ui_elements_text = obs.elements_text
exec_result.screenshot_path = obs.screenshot_path
if state is not None:
if obs.hierarchy_ok:
state.indexed_points = [el["center"] for el in obs.elements]
@@ -831,7 +834,7 @@ class FlashRunner:
turn.actions.append((name, exec_result.status, exec_result.text_summary))
# ⚡ Non-blocking dispatch of objective visual transition summarizer
if self.summarizer and name in action_names:
if self.summarizer and name in action_names and post_img_bytes:
action_sequence += 1
self.summarizer.dispatch(
step_number=action_sequence,
@@ -857,7 +860,7 @@ class FlashRunner:
if post_img_bytes:
pre_screenshot_bytes = post_img_bytes
if exec_result.ui_elements_text:
if exec_result.ui_elements_text is not None:
xml_list = exec_result.ui_elements_text
# Helper tools may return multimodal blocks (a step screenshot);
@@ -876,6 +879,8 @@ class FlashRunner:
)
)
except DeviceObservationError:
raise
except Exception as e:
logger.error(f"Error executing tool {name}: {e}")
if name in action_names:
@@ -1047,6 +1052,21 @@ class FlashRunner:
@trace(type="agent", name="FlashRunner")
async def run(self, state: State) -> dict:
try:
return await self._run(state)
except DeviceObservationError as exc:
state.indexed_points = []
state.indexed_elements = []
state.latest_screenshot = None
logger.error(f"Flash 因设备观测失败停止:{exc}")
return {
"status": "failed",
"explanation": str(exc),
"error": str(exc),
"code": "device_observation_failed",
}
async def _run(self, state: State) -> dict:
limit = self.turn_limit
logger.info(
f"Starting Artemis Flash reactive loop for goal: {self.goal}"
@@ -1073,6 +1093,7 @@ class FlashRunner:
no_tool_limit = settings.ARTEMIS_FLASH_MAX_NO_PROGRESS_ATTEMPTS
fallback_llm = None
using_tool_fallback = False
action_failure_attempts = 0
while limit is None or turns < limit:
control = getattr(self.ctx, "task_control", None)
@@ -1084,6 +1105,7 @@ class FlashRunner:
if resumed:
using_tool_fallback = False
no_tool_attempts = 0
action_failure_attempts = 0
self._llm = llm
(
shot_path,
@@ -1157,6 +1179,7 @@ class FlashRunner:
# assembled before the instruction arrived.
using_tool_fallback = False
no_tool_attempts = 0
action_failure_attempts = 0
self._llm = llm
(
shot_path,
@@ -1241,6 +1264,7 @@ class FlashRunner:
fallback_llm = get_llm(self.ctx, name="operator", use_fallback=True)
using_tool_fallback = True
no_tool_attempts = 0
action_failure_attempts = 0
logger.warning("Flash 首选模型未调用工具,当前步骤切换到兜底模型")
pending_notices.append("首选模型连续未调用工具;本步骤改用兜底模型。")
pending_notices.append(_NO_TOOL_CALL_NOTICE)
@@ -1279,6 +1303,37 @@ class FlashRunner:
using_tool_fallback = False
self._llm = llm
pending_notices.append("兜底模型已完成本步骤;后续步骤恢复首选模型。")
for _, status, _ in turn.actions:
action_failure_attempts = 0 if status == "success" else action_failure_attempts + 1
failure_limit = (
settings.ARTEMIS_TOOL_FALLBACK_MAX_NO_PROGRESS_ATTEMPTS
if using_tool_fallback
else settings.ARTEMIS_ACTION_FAILURE_ATTEMPTS
)
if action_failure_attempts >= failure_limit:
if using_tool_fallback:
final_report = {
"status": "failed",
"explanation": f"兜底模型连续 {action_failure_attempts} 次设备动作失败,已停止任务。最后原因:{turn.result()}",
}
break
# A fresh, valid observation is required before spending any
# fallback model calls. Hardware recovery is not model retry.
(
shot_path,
current_pre_screenshot_bytes,
current_xml_list,
) = await capture_screenshot_and_parse_ui(self.ctx, state, self.controller)
state.latest_screenshot = shot_path
fallback_llm = get_llm(self.ctx, name="operator", use_fallback=True)
using_tool_fallback = True
self._llm = fallback_llm
action_failure_attempts = 0
no_tool_attempts = 0
pending_notices.append(
f"首选模型连续 {settings.ARTEMIS_ACTION_FAILURE_ATTEMPTS} 次设备动作失败。"
f"最后原因:{turn.result()}。已重新观察;本步骤由兜底模型重新定位,勿重复旧动作。"
)
ledger.stage_turn(messages[turn_base:])
previous_turn = turn
if final_report_from_calls is not None:
@@ -136,9 +136,14 @@ async def capture_screenshot_and_parse_ui(
Thin adapter over :func:`artemis.mcp.observation.observe` that adds the LangGraph
``State`` write-back (indexed points/elements) the agents rely on.
"""
obs, screenshot_bytes = await observe(ctx, controller, settle_ms=0 if skip_settling else 400)
if not obs.ok:
return None, None, None
from artemis.mcp.observation import require_observation
state.indexed_points = []
state.indexed_elements = []
state.latest_screenshot = None
obs, screenshot_bytes = await require_observation(
ctx, controller, settle_ms=0 if skip_settling else 400
)
if obs.hierarchy_ok:
state.indexed_points = [el["center"] for el in obs.elements]