feat: add tool-call fallback and resilient batch exports

This commit is contained in:
chenmingxuan committed 2026-09-29 19:53:20 +08:00
1 parent ab066733b8
commit d31eb2a19d
32 files changed
+538 -72

No files matched your search

+46 -11
View File
@@ -1071,6 +1071,8 @@ class FlashRunner:
pending_notices: list[str] = []
no_tool_attempts = 0
no_tool_limit = settings.ARTEMIS_FLASH_MAX_NO_PROGRESS_ATTEMPTS
fallback_llm = None
using_tool_fallback = False
while limit is None or turns < limit:
control = getattr(self.ctx, "task_control", None)
@@ -1080,6 +1082,9 @@ class FlashRunner:
on_resume=getattr(self.ctx, "on_task_resume", None),
)
if resumed:
using_tool_fallback = False
no_tool_attempts = 0
self._llm = llm
(
shot_path,
current_pre_screenshot_bytes,
@@ -1131,14 +1136,18 @@ class FlashRunner:
if isinstance(control, TaskControl):
interrupted, response = await control.run_interruptibly(
self._invoke_model(llm, current_tools, messages),
self._invoke_model(
fallback_llm if using_tool_fallback else llm, current_tools, messages
),
on_pause=getattr(self.ctx, "on_task_pause", None),
on_resume=getattr(self.ctx, "on_task_resume", None),
)
else:
interrupted, response = (
False,
await self._invoke_model(llm, current_tools, messages),
await self._invoke_model(
fallback_llm if using_tool_fallback else llm, current_tools, messages
),
)
if interrupted or (
@@ -1146,6 +1155,9 @@ class FlashRunner:
):
# Do not dispatch a stale action or final answer from a prompt
# assembled before the instruction arrived.
using_tool_fallback = False
no_tool_attempts = 0
self._llm = llm
(
shot_path,
current_pre_screenshot_bytes,
@@ -1186,6 +1198,9 @@ class FlashRunner:
tool_calls = self._resolve_tool_calls(response, raw_text)
if isinstance(control, TaskControl) and has_pending_instruction(control.directory):
using_tool_fallback = False
no_tool_attempts = 0
self._llm = llm
(
shot_path,
current_pre_screenshot_bytes,
@@ -1207,15 +1222,27 @@ class FlashRunner:
final_report = {"status": "failed", "explanation": raw_text}
break
no_tool_attempts += 1
if no_tool_attempts >= no_tool_limit:
final_report = {
"status": "failed",
"explanation": (
f"模型连续 {no_tool_attempts} 次未调用工具,"
"已停止任务以避免空转和重复计费;未执行新的设备动作。"
),
}
break
active_limit = (
settings.ARTEMIS_TOOL_FALLBACK_MAX_NO_PROGRESS_ATTEMPTS
if using_tool_fallback
else no_tool_limit
)
if no_tool_attempts >= active_limit:
if using_tool_fallback:
final_report = {
"status": "failed",
"explanation": (
f"首选模型连续 {no_tool_limit} 次、兜底模型连续 "
f"{no_tool_attempts} 次未调用工具,已停止任务;"
"未执行新的设备动作。"
),
}
break
fallback_llm = get_llm(self.ctx, name="operator", use_fallback=True)
using_tool_fallback = True
no_tool_attempts = 0
logger.warning("Flash 首选模型未调用工具,当前步骤切换到兜底模型")
pending_notices.append("首选模型连续未调用工具;本步骤改用兜底模型。")
pending_notices.append(_NO_TOOL_CALL_NOTICE)
# Keep the observation and any injected user instruction, but
# do not feed the model its own unproductive prose on retry.
@@ -1224,6 +1251,10 @@ class FlashRunner:
continue
no_tool_attempts = 0
if using_tool_fallback:
# A tool-producing turn completes the fallback's scope. The next
# observation goes back to the configured primary model.
self._llm = fallback_llm
# Process tool calls
turn = _TurnRecord()
(
@@ -1244,6 +1275,10 @@ class FlashRunner:
injected=injected,
native_text=native_text,
)
if using_tool_fallback and any(status == "success" for _, status, _ in turn.actions):
using_tool_fallback = False
self._llm = llm
pending_notices.append("兜底模型已完成本步骤;后续步骤恢复首选模型。")
ledger.stage_turn(messages[turn_base:])
previous_turn = turn
if final_report_from_calls is not None: