fix: preserve cross-device evidence and render recorded reports
This commit is contained in:
1 parent
a66803651a
commit
1440a1f508
28 files changed
+759
-128
No files matched your search
@@ -59,3 +59,7 @@ Keep in mind that the mobile device is an asynchronous, constantly evolving envi
|
||||
- **Completion (`status="completed"`)**: ONLY call `report_task_status(status="completed", ...)` after the latest screenshot and UI elements support the claimed result. If those sources disagree during a transition, wait and observe again before concluding. Tie each claimed page title or value to an observed screenshot or UI element; mark any missing evidence as unverified.
|
||||
- **Deliver the requested output**: Completing device actions does not complete a requested report. Before finishing, check the original goal and user guidance for required deliverables. Put the actual report/answer in the explanation field, covering the requested sections with observed findings, source-specific evidence and untested or blocked scope. Do not replace it with "completed" or a promise to provide it later. The short-paragraph rule above applies to turn commentary, not to this final deliverable. Use the existing record; retrieve only missing evidence instead of repeating the whole test. Do not invent findings or treat finding a product defect as execution failure when the goal is to investigate and report it.
|
||||
- **Failure (`status="failed"`)**: If the task is genuinely impossible due to paywalls, unrecoverable crashes, or hitting an absolute dead end after sensible fallbacks, call `report_task_status(status="failed", ...)` with the exact blocking reason.
|
||||
|
||||
|
||||
## 收尾与跨端验收
|
||||
切换到另一端之前,核对当前端的必需验收动作是否已完成并留下证据。提交 report_task_status 前逐项对照用户要求,缺少必要且仍可安全取得的观察时只补该步骤,不重跑已完成部分;无法补齐时明确列出缺项。看到页脚不自动等于滚动已到底,若用户要求验证位置不变,需追加滚动并比较本端前后画面。截图来源、步骤和实际内容共同支持结论,执行成功回执本身不证明验收通过。
|
||||
@@ -63,6 +63,7 @@ from artemis.agents.validator.tool_declarations import (
|
||||
LIST_NOTES_TOOL,
|
||||
READ_NOTE_TOOL,
|
||||
REPORT_TASK_STATUS_TOOL,
|
||||
ToolExecutionResult,
|
||||
VALIDATOR_TOOLS_DECLARATION,
|
||||
capture_screenshot_and_parse_ui,
|
||||
prune_intermediate_screenshots,
|
||||
@@ -1014,7 +1015,8 @@ class FlashRunner:
|
||||
return (
|
||||
"当前使用插件观察;实际画面和控件见对应工具返回,不是安卓画面。"
|
||||
"安卓旧截图、元素编号和坐标已失效。连续操作电脑无需重复观察手机;"
|
||||
"切回安卓时先单独调用 wait_for_delay(time_in_ms=0) 读取最新手机画面,"
|
||||
"等待电脑加载可用正时长 wait_for_delay,等待本身不会刷新画面,之后调用插件观察工具;"
|
||||
"只有切回安卓时才单独调用 wait_for_delay(time_in_ms=0) 读取最新手机画面,"
|
||||
"收到后再决定手机动作。不要将电脑控件编号用于手机。"
|
||||
)
|
||||
|
||||
@@ -1027,14 +1029,17 @@ class FlashRunner:
|
||||
return image, elements
|
||||
|
||||
@staticmethod
|
||||
def _defer_tool_calls(messages: list[BaseMessage], tool_calls: list) -> None:
|
||||
def _defer_tool_calls(
|
||||
messages: list[BaseMessage], tool_calls: list, *, reason: str | None = None
|
||||
) -> None:
|
||||
"""Close every pending call without executing decisions made on stale evidence."""
|
||||
for tc in tool_calls:
|
||||
messages.append(
|
||||
ToolMessage(
|
||||
tool_call_id=tc.get("id") or str(uuid.uuid4()),
|
||||
name=tc["name"],
|
||||
content="未执行此调用:已切回安卓并重新观察。请先读取最新手机画面和控件,再重新决定动作或结论。",
|
||||
content=reason
|
||||
or "未执行此调用:已切回安卓并重新观察。请先读取最新手机画面和控件,再重新决定动作或结论。",
|
||||
status="error",
|
||||
)
|
||||
)
|
||||
@@ -1104,6 +1109,52 @@ class FlashRunner:
|
||||
args = tc.get("args") or {}
|
||||
tc_id = tc.get("id") or str(uuid.uuid4())
|
||||
|
||||
# A positive wait on the PC means elapsed time, not a phone refresh.
|
||||
if (
|
||||
getattr(self, "_android_observation_stale", False)
|
||||
and name == "wait_for_delay"
|
||||
and isinstance(args.get("time_in_ms"), (int, float))
|
||||
and 0 < args["time_in_ms"] < float("inf")
|
||||
):
|
||||
operation = asyncio.sleep(args["time_in_ms"] / 1000)
|
||||
if isinstance(control, TaskControl):
|
||||
interrupted, _ = await control.run_interruptibly(
|
||||
operation,
|
||||
on_pause=getattr(self.ctx, "on_task_pause", None),
|
||||
on_resume=getattr(self.ctx, "on_task_resume", None),
|
||||
)
|
||||
if interrupted:
|
||||
self._defer_tool_calls(
|
||||
messages,
|
||||
tool_calls[call_index:],
|
||||
reason="等待被用户打断,请重新观察当前目标再继续。",
|
||||
)
|
||||
return None, pre_screenshot_bytes, xml_list, action_sequence
|
||||
else:
|
||||
await operation
|
||||
message = f"已等待 {args['time_in_ms']} 毫秒,保持当前插件操作端;未采集新画面,请用插件观察工具核对加载结果。"
|
||||
result = ToolExecutionResult(
|
||||
tool_name=name, tool_call_id=tc_id, status="success", text_summary=message
|
||||
)
|
||||
step_id = None
|
||||
if self.ctx.data_engine:
|
||||
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))
|
||||
turn.actions.append((name, "success", message))
|
||||
messages.append(ToolMessage(tool_call_id=tc_id, name=name, content=message))
|
||||
continue
|
||||
|
||||
returning_to_android = (
|
||||
getattr(self, "_android_observation_stale", False) and name in android_tools
|
||||
)
|
||||
|
||||
@@ -276,15 +276,18 @@ class VisualStepSummarizer(StepMemoryService):
|
||||
"post_ui_elements_text": post_ui_elements_text,
|
||||
"observation_settled": observation_settled,
|
||||
}
|
||||
if receipt_only:
|
||||
unchanged = bool(pre_img_bytes and post_img_bytes and pre_img_bytes == post_img_bytes)
|
||||
if receipt_only or unchanged:
|
||||
from artemis.config.constants import PLUGIN_SUMMARY_TEXT_CHARS
|
||||
from artemis.plugins.prompt_budget import bounded_result_text
|
||||
|
||||
summary = (
|
||||
f"步骤 {step_number}:只读观察 {action_name};未执行写操作。"
|
||||
f"步骤 {step_number}:{action_name};重新采集的动作后截图与动作前截图完全相同。"
|
||||
"只能确认本次画面未变化,不能单凭此判定动作生效、已到页底或测试通过。\n"
|
||||
if unchanged
|
||||
else f"步骤 {step_number}:只读观察 {action_name};未执行写操作。"
|
||||
"以下为工具返回摘录,截图仍在本步骤证据中;不代表视觉验收通过。\n"
|
||||
+ bounded_result_text(exec_outcome, limit=PLUGIN_SUMMARY_TEXT_CHARS)
|
||||
)
|
||||
) + bounded_result_text(exec_outcome, limit=PLUGIN_SUMMARY_TEXT_CHARS)
|
||||
# Evidence has already been persisted by the caller. As with a
|
||||
# completed visual summary, do not retain every frame in memory.
|
||||
payload.update(pre_img_bytes=None, post_img_bytes=None, focus=None)
|
||||
@@ -295,7 +298,10 @@ class VisualStepSummarizer(StepMemoryService):
|
||||
if data_engine_step_id is not None and self.ctx.data_engine:
|
||||
try:
|
||||
self.ctx.data_engine.update_step_summary(
|
||||
data_engine_step_id, summary, status="ready", source="tool_receipt"
|
||||
data_engine_step_id,
|
||||
summary,
|
||||
status="ready",
|
||||
source="unchanged_capture" if unchanged else "tool_receipt",
|
||||
)
|
||||
except (OSError, ValueError, sqlite3.Error) as de_err:
|
||||
logger.debug(f"DataEngine receipt summary update skipped: {de_err}")
|
||||
@@ -442,7 +448,8 @@ class VisualStepSummarizer(StepMemoryService):
|
||||
# ACTION section and forbids synthesizing a transition.
|
||||
dual = bool(pre_bytes) and bool(post_bytes)
|
||||
template = self._prompt_template if dual else self._single_prompt_template
|
||||
rendered_prompt = Template(template).render(step_number=step_number)
|
||||
# The ordinal belongs to the dynamic input, not the cacheable rules.
|
||||
rendered_prompt = Template(template).render(step_number="N")
|
||||
phrase = self._action_phrase(action_name, action_args)
|
||||
lead_lines = [f"Step {step_number} Physical Action: {phrase}"]
|
||||
# The verbatim arguments are worth a line only when the phrase
|
||||
@@ -455,7 +462,8 @@ class VisualStepSummarizer(StepMemoryService):
|
||||
lead_lines.append(
|
||||
"Observation status: screen still changing; page state is not confirmed."
|
||||
)
|
||||
focus = input_data.get("focus")
|
||||
focus = dict(input_data.get("focus") or {})
|
||||
goal = focus.pop("goal", "")
|
||||
if isinstance(focus, dict) and focus:
|
||||
lead_lines.extend(["", render_focus_block(focus, dual=dual)])
|
||||
content_blocks: list[dict[str, Any]] = [{"type": "text", "text": "\n".join(lead_lines)}]
|
||||
@@ -513,7 +521,10 @@ class VisualStepSummarizer(StepMemoryService):
|
||||
)
|
||||
|
||||
messages: list[BaseMessage] = [
|
||||
SystemMessage(content=rendered_prompt),
|
||||
SystemMessage(
|
||||
content=rendered_prompt
|
||||
+ f"\n\nTask goal (attention context, not observed evidence):\n{goal}"
|
||||
),
|
||||
HumanMessage(content=content_blocks),
|
||||
]
|
||||
|
||||
|
||||
Reference in new issue
Block a user