harmonyrun 0.4.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harmonyrun/__init__.py +186 -0
- harmonyrun/__main__.py +8 -0
- harmonyrun/agent/__init__.py +5 -0
- harmonyrun/agent/events/__init__.py +18 -0
- harmonyrun/agent/events/_render.py +258 -0
- harmonyrun/agent/events/handler.py +36 -0
- harmonyrun/agent/events/render_registry.py +41 -0
- harmonyrun/agent/execution/__init__.py +27 -0
- harmonyrun/agent/execution/anti_loop.py +410 -0
- harmonyrun/agent/execution/base_agent.py +991 -0
- harmonyrun/agent/execution/failure_classifier.py +332 -0
- harmonyrun/agent/execution/formatting.py +23 -0
- harmonyrun/agent/execution/handle.py +124 -0
- harmonyrun/agent/execution/interventions.py +89 -0
- harmonyrun/agent/execution/thresholds.py +99 -0
- harmonyrun/agent/fast_agent/__init__.py +3 -0
- harmonyrun/agent/fast_agent/fast_agent.py +972 -0
- harmonyrun/agent/harmony/__init__.py +29 -0
- harmonyrun/agent/harmony/harmony_agent.py +1413 -0
- harmonyrun/agent/harmony/resources.py +10 -0
- harmonyrun/agent/harmony/services.py +358 -0
- harmonyrun/agent/llm/__init__.py +70 -0
- harmonyrun/agent/llm/cache.py +72 -0
- harmonyrun/agent/llm/inference.py +359 -0
- harmonyrun/agent/llm/introspect.py +36 -0
- harmonyrun/agent/llm/loader.py +274 -0
- harmonyrun/agent/llm/messages.py +144 -0
- harmonyrun/agent/llm/picker.py +253 -0
- harmonyrun/agent/llm/usage.py +203 -0
- harmonyrun/agent/manager/__init__.py +9 -0
- harmonyrun/agent/manager/base_manager.py +215 -0
- harmonyrun/agent/manager/executor_agent.py +409 -0
- harmonyrun/agent/manager/manager_agent.py +487 -0
- harmonyrun/agent/memory/__init__.py +56 -0
- harmonyrun/agent/memory/distiller.py +164 -0
- harmonyrun/agent/memory/keys.py +21 -0
- harmonyrun/agent/memory/models.py +108 -0
- harmonyrun/agent/memory/recall.py +39 -0
- harmonyrun/agent/memory/store.py +158 -0
- harmonyrun/agent/oneflows/__init__.py +11 -0
- harmonyrun/agent/oneflows/app_matcher.py +85 -0
- harmonyrun/agent/oneflows/app_starter_workflow.py +177 -0
- harmonyrun/agent/oneflows/structured_output_agent.py +80 -0
- harmonyrun/agent/prompt/__init__.py +114 -0
- harmonyrun/agent/prompt/_env.py +45 -0
- harmonyrun/agent/prompt/constants.py +60 -0
- harmonyrun/agent/prompt/packing.py +367 -0
- harmonyrun/agent/prompt/resolver.py +68 -0
- harmonyrun/agent/prompt/state_user.py +402 -0
- harmonyrun/agent/prompt/system/__init__.py +27 -0
- harmonyrun/agent/prompt/system/executor.py +44 -0
- harmonyrun/agent/prompt/system/fast_agent.py +64 -0
- harmonyrun/agent/prompt/system/manager.py +194 -0
- harmonyrun/agent/prompt/templates/executor/README.md +39 -0
- harmonyrun/agent/prompt/templates/executor/system.en.j2 +63 -0
- harmonyrun/agent/prompt/templates/executor/system.zh.j2 +63 -0
- harmonyrun/agent/prompt/templates/executor/user.en.j2 +75 -0
- harmonyrun/agent/prompt/templates/executor/user.zh.j2 +75 -0
- harmonyrun/agent/prompt/templates/fast_agent/README.md +53 -0
- harmonyrun/agent/prompt/templates/fast_agent/system.en.j2 +157 -0
- harmonyrun/agent/prompt/templates/fast_agent/system.zh.j2 +156 -0
- harmonyrun/agent/prompt/templates/manager/README.md +26 -0
- harmonyrun/agent/prompt/templates/manager/system.en.j2 +164 -0
- harmonyrun/agent/prompt/templates/manager/system.zh.j2 +164 -0
- harmonyrun/agent/prompt/user_blocks/__init__.py +53 -0
- harmonyrun/agent/prompt/user_blocks/delta_trail.py +182 -0
- harmonyrun/agent/prompt/user_blocks/device_state.py +306 -0
- harmonyrun/agent/prompt/user_blocks/executor_user.py +57 -0
- harmonyrun/agent/prompt/user_blocks/goal.py +142 -0
- harmonyrun/agent/prompt/user_blocks/history.py +162 -0
- harmonyrun/agent/prompt/user_blocks/labels.py +24 -0
- harmonyrun/agent/prompt/user_blocks/next_step.py +32 -0
- harmonyrun/agent/prompt/user_blocks/task_plan.py +50 -0
- harmonyrun/agent/state/__init__.py +72 -0
- harmonyrun/agent/state/flow_states.py +85 -0
- harmonyrun/agent/state/observations.py +202 -0
- harmonyrun/agent/state/state.py +402 -0
- harmonyrun/agent/state/state_components.py +204 -0
- harmonyrun/agent/tools/__init__.py +49 -0
- harmonyrun/agent/tools/actions.py +653 -0
- harmonyrun/agent/tools/context.py +44 -0
- harmonyrun/agent/tools/dispatcher.py +85 -0
- harmonyrun/agent/tools/gestures.py +120 -0
- harmonyrun/agent/tools/registry.py +501 -0
- harmonyrun/agent/tools/result.py +16 -0
- harmonyrun/agent/tools/schemas.py +276 -0
- harmonyrun/app_cards/__init__.py +55 -0
- harmonyrun/app_cards/app_card_provider.py +26 -0
- harmonyrun/app_cards/loading.py +69 -0
- harmonyrun/app_cards/provider_factory.py +60 -0
- harmonyrun/app_cards/providers/__init__.py +7 -0
- harmonyrun/app_cards/providers/composite_provider.py +104 -0
- harmonyrun/app_cards/providers/local_provider.py +113 -0
- harmonyrun/app_cards/providers/server_provider.py +120 -0
- harmonyrun/app_cards/suite_resolve.py +112 -0
- harmonyrun/app_cards/writing.py +74 -0
- harmonyrun/batch/__init__.py +49 -0
- harmonyrun/batch/execution/__init__.py +34 -0
- harmonyrun/batch/execution/device_gate.py +73 -0
- harmonyrun/batch/execution/driver_factory.py +60 -0
- harmonyrun/batch/execution/phase.py +152 -0
- harmonyrun/batch/execution/resources.py +169 -0
- harmonyrun/batch/execution/runner.py +687 -0
- harmonyrun/batch/execution/scheduler.py +159 -0
- harmonyrun/batch/loading/__init__.py +29 -0
- harmonyrun/batch/loading/levels.py +23 -0
- harmonyrun/batch/loading/loader.py +285 -0
- harmonyrun/batch/loading/prompts/README.md +29 -0
- harmonyrun/batch/loading/prompts/__init__.py +114 -0
- harmonyrun/batch/loading/prompts/agent_goal/en.j2 +22 -0
- harmonyrun/batch/loading/prompts/agent_goal/zh.j2 +23 -0
- harmonyrun/batch/loading/prompts/postcondition/en.j2 +8 -0
- harmonyrun/batch/loading/prompts/postcondition/zh.j2 +8 -0
- harmonyrun/batch/loading/prompts/precondition/en.j2 +8 -0
- harmonyrun/batch/loading/prompts/precondition/zh.j2 +8 -0
- harmonyrun/batch/loading/prompts/test_execution/en.j2 +13 -0
- harmonyrun/batch/loading/prompts/test_execution/zh.j2 +13 -0
- harmonyrun/batch/loading/validation.py +60 -0
- harmonyrun/batch/models.py +149 -0
- harmonyrun/batch/reporting/__init__.py +49 -0
- harmonyrun/batch/reporting/_common.py +26 -0
- harmonyrun/batch/reporting/html_report.py +84 -0
- harmonyrun/batch/reporting/json_report.py +97 -0
- harmonyrun/batch/reporting/live.py +323 -0
- harmonyrun/batch/reporting/paths.py +37 -0
- harmonyrun/batch/reporting/templates/report.html +1191 -0
- harmonyrun/batch/reporting/viewer.py +309 -0
- harmonyrun/batch/schemas/report.schema.json +123 -0
- harmonyrun/batch/schemas/test_suite.schema.json +129 -0
- harmonyrun/batch/suite_runner.py +487 -0
- harmonyrun/cli/__init__.py +9 -0
- harmonyrun/cli/_capture.py +202 -0
- harmonyrun/cli/_logging.py +21 -0
- harmonyrun/cli/_runtime.py +55 -0
- harmonyrun/cli/feedback.py +60 -0
- harmonyrun/cli/main.py +332 -0
- harmonyrun/cli/mcp.py +219 -0
- harmonyrun/cli/memory_cmd.py +145 -0
- harmonyrun/cli/run.py +150 -0
- harmonyrun/cli/view.py +37 -0
- harmonyrun/config/__init__.py +88 -0
- harmonyrun/config/config_example.yaml +96 -0
- harmonyrun/config/manager.py +322 -0
- harmonyrun/config/paths.py +66 -0
- harmonyrun/config/schema/__init__.py +32 -0
- harmonyrun/config/schema/agent.py +182 -0
- harmonyrun/config/schema/llm.py +28 -0
- harmonyrun/config/schema/mcp.py +18 -0
- harmonyrun/config/schema/overrides.py +262 -0
- harmonyrun/config/schema/root.py +247 -0
- harmonyrun/config/schema/runtime.py +121 -0
- harmonyrun/diagnostics/__init__.py +13 -0
- harmonyrun/diagnostics/checks.py +498 -0
- harmonyrun/diagnostics/results.py +64 -0
- harmonyrun/diagnostics/runner.py +130 -0
- harmonyrun/env_keys.py +80 -0
- harmonyrun/events/__init__.py +34 -0
- harmonyrun/events/bus.py +83 -0
- harmonyrun/events/model/__init__.py +122 -0
- harmonyrun/events/model/agent.py +378 -0
- harmonyrun/events/model/base.py +73 -0
- harmonyrun/events/model/catalog.py +32 -0
- harmonyrun/events/model/device.py +129 -0
- harmonyrun/events/model/llm.py +53 -0
- harmonyrun/events/transports/__init__.py +15 -0
- harmonyrun/events/transports/langgraph.py +85 -0
- harmonyrun/farm/CLAUDE.md +280 -0
- harmonyrun/farm/__init__.py +27 -0
- harmonyrun/farm/_bootstrap.py +38 -0
- harmonyrun/farm/cli.py +397 -0
- harmonyrun/farm/endpoint.py +36 -0
- harmonyrun/farm/remote_driver.py +426 -0
- harmonyrun/farm/server/__init__.py +3 -0
- harmonyrun/farm/server/app.py +266 -0
- harmonyrun/farm/server/archive.py +141 -0
- harmonyrun/farm/server/auth.py +41 -0
- harmonyrun/farm/server/dispatch.py +155 -0
- harmonyrun/farm/server/pool.py +168 -0
- harmonyrun/farm/server/schemas.py +70 -0
- harmonyrun/farm/server/session.py +206 -0
- harmonyrun/feedback/__init__.py +13 -0
- harmonyrun/feedback/context.py +19 -0
- harmonyrun/feedback/discovery.py +85 -0
- harmonyrun/feedback/flow.py +111 -0
- harmonyrun/feedback/github.py +224 -0
- harmonyrun/feedback/issue.py +109 -0
- harmonyrun/feedback/packaging.py +76 -0
- harmonyrun/hdcutils/__init__.py +52 -0
- harmonyrun/hdcutils/agent/__init__.py +37 -0
- harmonyrun/hdcutils/agent/diagnostic.py +82 -0
- harmonyrun/hdcutils/agent/installer.py +189 -0
- harmonyrun/hdcutils/agent/spec.py +166 -0
- harmonyrun/hdcutils/agent/uitest_agent_1.1.12.so +0 -0
- harmonyrun/hdcutils/agent/uitest_agent_1.1.3.so +0 -0
- harmonyrun/hdcutils/agent/uitest_agent_1.1.5.so +0 -0
- harmonyrun/hdcutils/agent/uitest_agent_1.2.3.so +0 -0
- harmonyrun/hdcutils/agent/uitest_agent_x86_1.1.9.so +0 -0
- harmonyrun/hdcutils/client.py +108 -0
- harmonyrun/hdcutils/device/__init__.py +3 -0
- harmonyrun/hdcutils/device/apps.py +391 -0
- harmonyrun/hdcutils/device/core.py +181 -0
- harmonyrun/hdcutils/device/display.py +219 -0
- harmonyrun/hdcutils/device/display_state.py +589 -0
- harmonyrun/hdcutils/device/files.py +54 -0
- harmonyrun/hdcutils/device/forwards.py +90 -0
- harmonyrun/hdcutils/device/input.py +99 -0
- harmonyrun/hdcutils/device/recording.py +337 -0
- harmonyrun/hdcutils/device/screen.py +376 -0
- harmonyrun/hdcutils/device/system.py +82 -0
- harmonyrun/hdcutils/emulator/__init__.py +25 -0
- harmonyrun/hdcutils/emulator/scenario.py +297 -0
- harmonyrun/hdcutils/errors.py +41 -0
- harmonyrun/hdcutils/hilog.py +441 -0
- harmonyrun/hdcutils/keycode.py +361 -0
- harmonyrun/hdcutils/rpc/__init__.py +5 -0
- harmonyrun/hdcutils/rpc/client.py +462 -0
- harmonyrun/hdcutils/rpc/framing.py +103 -0
- harmonyrun/hdcutils/rpc/recovery.py +114 -0
- harmonyrun/hdcutils/transport/__init__.py +23 -0
- harmonyrun/hdcutils/transport/ports.py +128 -0
- harmonyrun/hdcutils/transport/recording.py +93 -0
- harmonyrun/hdcutils/transport/runner.py +204 -0
- harmonyrun/hdcutils/types.py +43 -0
- harmonyrun/hdcutils/version.py +12 -0
- harmonyrun/hdcutils/wake.py +258 -0
- harmonyrun/hypium/__init__.py +12 -0
- harmonyrun/hypium/action_mapper.py +159 -0
- harmonyrun/hypium/assertion_builder.py +280 -0
- harmonyrun/hypium/cli.py +140 -0
- harmonyrun/hypium/code_polisher.py +203 -0
- harmonyrun/hypium/component_resolver.py +221 -0
- harmonyrun/hypium/generator.py +306 -0
- harmonyrun/hypium/template.py +157 -0
- harmonyrun/hypium/trajectory_loader.py +280 -0
- harmonyrun/log_handlers.py +68 -0
- harmonyrun/mcp/__init__.py +28 -0
- harmonyrun/mcp/schemas.py +107 -0
- harmonyrun/mcp/server.py +96 -0
- harmonyrun/mcp/session.py +124 -0
- harmonyrun/mcp/tools/__init__.py +5 -0
- harmonyrun/mcp/tools/actions.py +218 -0
- harmonyrun/mcp/tools/connection.py +62 -0
- harmonyrun/mcp/tools/perception.py +233 -0
- harmonyrun/mcp/transport.py +19 -0
- harmonyrun/tools/__init__.py +79 -0
- harmonyrun/tools/contracts.py +39 -0
- harmonyrun/tools/driver/__init__.py +25 -0
- harmonyrun/tools/driver/app_catalog.py +113 -0
- harmonyrun/tools/driver/base.py +362 -0
- harmonyrun/tools/driver/factory.py +82 -0
- harmonyrun/tools/driver/harmony.py +1185 -0
- harmonyrun/tools/driver/perceiving.py +89 -0
- harmonyrun/tools/driver/proxy.py +228 -0
- harmonyrun/tools/driver/recording.py +603 -0
- harmonyrun/tools/driver/stealth.py +217 -0
- harmonyrun/tools/monitors/__init__.py +26 -0
- harmonyrun/tools/monitors/hilog_monitor.py +463 -0
- harmonyrun/tools/monitors/screen_recorder.py +208 -0
- harmonyrun/tools/perception/__init__.py +59 -0
- harmonyrun/tools/perception/processors/__init__.py +68 -0
- harmonyrun/tools/perception/processors/bundle_serde.py +139 -0
- harmonyrun/tools/perception/processors/constants.py +54 -0
- harmonyrun/tools/perception/processors/converters/__init__.py +5 -0
- harmonyrun/tools/perception/processors/converters/a11y_converter.py +116 -0
- harmonyrun/tools/perception/processors/extractors/__init__.py +5 -0
- harmonyrun/tools/perception/processors/extractors/nav_state.py +232 -0
- harmonyrun/tools/perception/processors/extractors/phone_state.py +399 -0
- harmonyrun/tools/perception/processors/filters/__init__.py +40 -0
- harmonyrun/tools/perception/processors/filters/base.py +24 -0
- harmonyrun/tools/perception/processors/filters/concise_filter.py +123 -0
- harmonyrun/tools/perception/processors/filters/detailed_filter.py +173 -0
- harmonyrun/tools/perception/processors/filters/passthrough_filter.py +23 -0
- harmonyrun/tools/perception/processors/formatters/__init__.py +10 -0
- harmonyrun/tools/perception/processors/formatters/base.py +39 -0
- harmonyrun/tools/perception/processors/formatters/indexed_formatter.py +941 -0
- harmonyrun/tools/perception/processors/pipeline.py +137 -0
- harmonyrun/tools/perception/processors/simplifiers/__init__.py +20 -0
- harmonyrun/tools/perception/processors/simplifiers/highlight_marker.py +58 -0
- harmonyrun/tools/perception/processors/simplifiers/system_ui_pruner.py +171 -0
- harmonyrun/tools/perception/processors/simplifiers/tree_simplifier.py +227 -0
- harmonyrun/tools/perception/processors/utils/__init__.py +42 -0
- harmonyrun/tools/perception/processors/utils/coordinate.py +25 -0
- harmonyrun/tools/perception/processors/utils/geometry.py +148 -0
- harmonyrun/tools/perception/processors/utils/semantic.py +58 -0
- harmonyrun/tools/perception/ui/__init__.py +47 -0
- harmonyrun/tools/perception/ui/config.py +80 -0
- harmonyrun/tools/perception/ui/offline.py +140 -0
- harmonyrun/tools/perception/ui/provider.py +341 -0
- harmonyrun/tools/perception/ui/screenshot.py +272 -0
- harmonyrun/tools/perception/ui/snapshot.py +178 -0
- harmonyrun/tools/perception/ui/state.py +261 -0
- harmonyrun/tools/perception/ui/stealth_state.py +106 -0
- harmonyrun/traces/__init__.py +50 -0
- harmonyrun/traces/case_report.py +274 -0
- harmonyrun/traces/folder.py +221 -0
- harmonyrun/traces/format.py +33 -0
- harmonyrun/traces/llm_log.py +212 -0
- harmonyrun/traces/recorder.py +293 -0
- harmonyrun/traces/serializers.py +138 -0
- harmonyrun/traces/sinks.py +60 -0
- harmonyrun/traces/telemetry/__init__.py +22 -0
- harmonyrun/traces/telemetry/tracker.py +180 -0
- harmonyrun/traces/templates/case_report.html +1908 -0
- harmonyrun/traces/writer.py +309 -0
- harmonyrun-0.4.9.dist-info/METADATA +401 -0
- harmonyrun-0.4.9.dist-info/RECORD +309 -0
- harmonyrun-0.4.9.dist-info/WHEEL +4 -0
- harmonyrun-0.4.9.dist-info/entry_points.txt +2 -0
- harmonyrun-0.4.9.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""Anti-loop hint / warning message templates (pure text rendering).
|
|
2
|
+
|
|
3
|
+
This module holds the **prompt-text** half of the anti-loop subsystem,
|
|
4
|
+
separated from the **detection logic** that lives in
|
|
5
|
+
``base/execution_agent.py``. The detection methods (``_check_stagnation`` /
|
|
6
|
+
``_check_rotation_stuck`` / streak bookkeeping) compute small status dicts;
|
|
7
|
+
the renderers here turn those into the bilingual ``<stagnation_warning>`` /
|
|
8
|
+
``<rotation_unresponsive>`` / ``<off_target_hint>`` blocks spliced into the
|
|
9
|
+
next LLM user message.
|
|
10
|
+
|
|
11
|
+
Keeping the large en/zh literal blocks here means wording / localization can
|
|
12
|
+
be edited without touching the algorithm file, and the detection logic can be
|
|
13
|
+
unit-tested without snapshot-asserting on prose.
|
|
14
|
+
|
|
15
|
+
All functions are pure (no ``self``, no device I/O): callers pass the already
|
|
16
|
+
computed values. The thresholds used for phrasing are imported from
|
|
17
|
+
``execution_agent`` to keep a single source of truth.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from harmonyrun.agent.execution.failure_classifier import FailureMode
|
|
23
|
+
from harmonyrun.agent.execution.thresholds import (
|
|
24
|
+
STAGNATION_ABORT_AT,
|
|
25
|
+
STAGNATION_WARN_AT,
|
|
26
|
+
TOOL_REPEAT_WARN_AT,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def render_stagnation_warning_block(
|
|
31
|
+
*,
|
|
32
|
+
streak: int,
|
|
33
|
+
tool_repeat: int,
|
|
34
|
+
same_page: int,
|
|
35
|
+
cause: str,
|
|
36
|
+
page: str,
|
|
37
|
+
container_hint: str,
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Render the ``<stagnation_warning>`` block.
|
|
40
|
+
|
|
41
|
+
``container_hint`` is computed by the caller (it needs device-side state)
|
|
42
|
+
and spliced in verbatim; pass ``""`` when not applicable.
|
|
43
|
+
"""
|
|
44
|
+
# 三类信号的措辞差异:
|
|
45
|
+
# - streak / tool_repeat:会触发硬终止,给出剩余步数提醒
|
|
46
|
+
# - same_page only:软信号,仅提示"页面长时间没跨",不会自动 abort
|
|
47
|
+
hard_signal = streak >= STAGNATION_WARN_AT or tool_repeat >= TOOL_REPEAT_WARN_AT
|
|
48
|
+
if hard_signal:
|
|
49
|
+
remaining = max(0, STAGNATION_ABORT_AT - streak)
|
|
50
|
+
suffix = f" (≈ {remaining} step(s) left)." if streak > 0 else "."
|
|
51
|
+
tail = (
|
|
52
|
+
"_The framework will auto-abort after "
|
|
53
|
+
f"{STAGNATION_ABORT_AT} consecutive no-progress steps"
|
|
54
|
+
f"{suffix}_\n"
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
# same_page-only:不要让 LLM 误以为会硬终止;强调"重新评估前提"
|
|
58
|
+
tail = (
|
|
59
|
+
"_This is an advisory: the framework will NOT auto-abort on "
|
|
60
|
+
"same-page stagnation alone (single-page tasks are valid). "
|
|
61
|
+
"But if you expected to navigate away, your previous tap "
|
|
62
|
+
f"likely did not work — currently on `{page or 'unknown'}` "
|
|
63
|
+
f"for {same_page} step(s)._\n"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
"\n<stagnation_warning>\n"
|
|
68
|
+
"## ⚠️ Stuck Detector\n"
|
|
69
|
+
f"You appear to be stuck: {cause}. "
|
|
70
|
+
"Re-issuing the same action will not help.\n\n"
|
|
71
|
+
"**Pick ONE of these now:**\n"
|
|
72
|
+
"1. Try a meaningfully different action — different target "
|
|
73
|
+
"element, `system_button` back, re-open the app, or scroll to "
|
|
74
|
+
"expose new UI.\n"
|
|
75
|
+
"2. Call `complete(success=false, message=\"...\")` with an "
|
|
76
|
+
"honest assessment of what's blocking you.\n\n"
|
|
77
|
+
f"{container_hint}"
|
|
78
|
+
f"{tail}"
|
|
79
|
+
"</stagnation_warning>\n"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def render_container_tap_hint(*, index: int, tool_repeat: int, bounds: str) -> str:
|
|
84
|
+
"""Render the container-tap hint appended inside ``<stagnation_warning>``.
|
|
85
|
+
|
|
86
|
+
Caller resolves ``index`` / ``bounds`` from the a11y tree first; this is
|
|
87
|
+
pure text assembly.
|
|
88
|
+
"""
|
|
89
|
+
return (
|
|
90
|
+
f"**Container-tap hint:** element [{index}] clicked "
|
|
91
|
+
f"{tool_repeat} times with no UI change. "
|
|
92
|
+
"It may be a container (Rating control / WebView / section-header "
|
|
93
|
+
f"Row) and `click({{'index': {index}}})` always lands at its "
|
|
94
|
+
"center. Switch to absolute coordinates: call "
|
|
95
|
+
f"`click_at(x, y)` with (x, y) inside bounds=({bounds}) — pick "
|
|
96
|
+
"a sub-region different from the center (e.g. right edge for a "
|
|
97
|
+
"5-star Rating, a specific visible icon inside a WebView).\n\n"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def render_rotation_stuck_block(
|
|
102
|
+
*, consecutive: int, requested: str, current: str, language: str = "en"
|
|
103
|
+
) -> str:
|
|
104
|
+
"""Render the ``<rotation_unresponsive>`` block in en/zh."""
|
|
105
|
+
if language == "zh":
|
|
106
|
+
return (
|
|
107
|
+
"\n<rotation_unresponsive>\n"
|
|
108
|
+
"## ⚠️ 设备拒绝旋转(前置条件未满足)\n"
|
|
109
|
+
f"你已经连续 {consecutive} 次调用 `set_rotation` 请求 "
|
|
110
|
+
f"`{requested}`,但 `<device_state>` 中的 **Orientation** "
|
|
111
|
+
f"字段(来源:DisplayManagerService)始终是 `{current}`。"
|
|
112
|
+
"继续重试不会改变结果——设备在当前姿态下不响应运行时旋转。\n\n"
|
|
113
|
+
"**默认动作(推荐):** 立刻调用\n"
|
|
114
|
+
"```\n"
|
|
115
|
+
"complete(\n"
|
|
116
|
+
" success=false,\n"
|
|
117
|
+
" message=\"前置条件未满足:测试要求 "
|
|
118
|
+
f"{requested} 模式,但设备拒绝 set_rotation —— "
|
|
119
|
+
f"Orientation 仍为 {current},无法在指定方向下执行视觉检查\",\n"
|
|
120
|
+
" screenshot_evidence=[\n"
|
|
121
|
+
" \"具体可见证据 1(引用 current screenshot 的内容 / "
|
|
122
|
+
"坐标 / 颜色 / 可见文本)\",\n"
|
|
123
|
+
" \"具体可见证据 2\"\n"
|
|
124
|
+
" ]\n"
|
|
125
|
+
")\n"
|
|
126
|
+
"```\n"
|
|
127
|
+
"理由:测试用例名/标题已指明目标方向(如『横屏』『竖屏』),"
|
|
128
|
+
"**该方向是测试范围的一部分**;无法进入意味着这次执行不能"
|
|
129
|
+
"证明 expected_result 在目标方向下成立——不能算 PASS。\n\n"
|
|
130
|
+
"**例外(仅当你能明确说明当前方向也算"
|
|
131
|
+
"测试通过条件时才走这条路):** 在当前 "
|
|
132
|
+
f"`{current}` 屏下做完整的视觉检查、并在 message 里写明"
|
|
133
|
+
"「测试声明需要 X 方向但设备只能在 Y 方向下检查,截图未发现遮挡,"
|
|
134
|
+
"因此**在 Y 方向上**通过」,并 `success=true`。\n\n"
|
|
135
|
+
"**禁止:** 再发任何 `set_rotation` 或 `wait`。\n"
|
|
136
|
+
"</rotation_unresponsive>\n"
|
|
137
|
+
)
|
|
138
|
+
return (
|
|
139
|
+
"\n<rotation_unresponsive>\n"
|
|
140
|
+
"## ⚠️ Device Refused Rotation (precondition unmet)\n"
|
|
141
|
+
f"You have called `set_rotation` with target `{requested}` "
|
|
142
|
+
f"{consecutive} time(s) in a row, but the **Orientation** "
|
|
143
|
+
"field in `<device_state>` (sourced from DisplayManagerService) "
|
|
144
|
+
f"still reads `{current}`. Retrying will not change that — "
|
|
145
|
+
"the device does not honor runtime rotation in its current "
|
|
146
|
+
"posture.\n\n"
|
|
147
|
+
"**Default action (recommended):** call\n"
|
|
148
|
+
"```\n"
|
|
149
|
+
"complete(\n"
|
|
150
|
+
" success=false,\n"
|
|
151
|
+
" message=\"Precondition unmet: test requires "
|
|
152
|
+
f"{requested} mode but device refused set_rotation — "
|
|
153
|
+
f"Orientation stayed at {current}; cannot run the visual "
|
|
154
|
+
"check in the orientation the test scope specifies\",\n"
|
|
155
|
+
" screenshot_evidence=[\n"
|
|
156
|
+
" \"<concrete visual anchor 1 — quote visible text / "
|
|
157
|
+
"coords / colors from current screenshot>\",\n"
|
|
158
|
+
" \"<concrete visual anchor 2>\"\n"
|
|
159
|
+
" ]\n"
|
|
160
|
+
")\n"
|
|
161
|
+
"```\n"
|
|
162
|
+
"Reason: the test name/title encodes the required "
|
|
163
|
+
"orientation (e.g. '横屏'/'竖屏', 'landscape'/'portrait') "
|
|
164
|
+
"as part of the test scope. If you cannot enter that "
|
|
165
|
+
"orientation, the run did not exercise the scenario the "
|
|
166
|
+
"test was designed for — that's a failure, not a PASS.\n\n"
|
|
167
|
+
"**Exception (only when you can argue the current "
|
|
168
|
+
"orientation also satisfies the test):** do a full visual "
|
|
169
|
+
f"sweep on the current `{current}` screen and call "
|
|
170
|
+
"`complete(success=true, ...)` with a message explicitly "
|
|
171
|
+
"stating the orientation mismatch and why portrait-mode "
|
|
172
|
+
"evidence is sufficient.\n\n"
|
|
173
|
+
"**Forbidden:** any further `set_rotation` or `wait` call.\n"
|
|
174
|
+
"</rotation_unresponsive>\n"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def render_off_target_hint_block(*, current: str, target: str, streak: int) -> str:
|
|
179
|
+
"""Render the ``<off_target_hint>`` block."""
|
|
180
|
+
return (
|
|
181
|
+
"\n<off_target_hint>\n"
|
|
182
|
+
"## 🧭 Off-Target App Detector\n"
|
|
183
|
+
f"You are currently in `{current}` (target app is `{target}`) "
|
|
184
|
+
f"for {streak} step(s).\n\n"
|
|
185
|
+
"If this is an expected handoff (browser deep-link confirmation, "
|
|
186
|
+
"system settings, share sheet, OAuth) — continue.\n"
|
|
187
|
+
f"Otherwise call `open_app(\"{target}\")` to return to the "
|
|
188
|
+
"target app before doing anything else.\n"
|
|
189
|
+
"</off_target_hint>\n"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def render_off_target_relaunch_notice(*, target: str, language: str = "en") -> str:
|
|
194
|
+
"""Render the notice injected after the harness FORCED a relaunch of the
|
|
195
|
+
target app (Tier-2). Tells the model the screen it now sees is the freshly
|
|
196
|
+
relaunched target, not its previous off-target frame."""
|
|
197
|
+
if language == "zh":
|
|
198
|
+
return (
|
|
199
|
+
"\n<harness_intervention>\n"
|
|
200
|
+
"## 🛠️ 已自动拉回目标 app\n"
|
|
201
|
+
f"你之前偏离了目标 app,harness 已强制重新启动 `{target}`。"
|
|
202
|
+
"当前 `<device_state>` / 截图反映的是**重启后的目标 app**,"
|
|
203
|
+
"请据此重新定位,不要再依据之前那一帧操作。\n"
|
|
204
|
+
"</harness_intervention>\n"
|
|
205
|
+
)
|
|
206
|
+
return (
|
|
207
|
+
"\n<harness_intervention>\n"
|
|
208
|
+
"## 🛠️ Auto-returned to target app\n"
|
|
209
|
+
f"You had drifted off-target; the harness force-relaunched `{target}`. "
|
|
210
|
+
"The current `<device_state>` / screenshot reflect the **relaunched "
|
|
211
|
+
"target app** — re-orient from this frame, not your previous one.\n"
|
|
212
|
+
"</harness_intervention>\n"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def render_stagnation_abort_reason(*, streak: int, tool_repeat: int) -> str:
|
|
217
|
+
"""Build the failure reason for a hard-abort end/finalize event."""
|
|
218
|
+
if streak >= STAGNATION_ABORT_AT:
|
|
219
|
+
return (
|
|
220
|
+
f"Aborted: device UI unchanged for {streak} "
|
|
221
|
+
"consecutive steps — the agent is stuck in a loop with no "
|
|
222
|
+
"UI progress."
|
|
223
|
+
)
|
|
224
|
+
return (
|
|
225
|
+
f"Aborted: the LLM repeated the same tool call "
|
|
226
|
+
f"{tool_repeat} times consecutively while the "
|
|
227
|
+
f"UI was also unchanged for {streak} step(s) "
|
|
228
|
+
"— truly stuck (not a legitimate repeat like scrolling a list "
|
|
229
|
+
"or incrementing a counter)."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ===========================================================================
|
|
234
|
+
# P0-1 classified failure-mode renderers
|
|
235
|
+
#
|
|
236
|
+
# Each mode owns one Tier-1 HINT renderer (budget-aware, bilingual) and one
|
|
237
|
+
# Tier-3 CONCLUDE reason. ``render_failure_hint`` / ``render_failure_conclusion``
|
|
238
|
+
# are the dispatch facades the agents call — keyed by ``FailureMode`` so adding
|
|
239
|
+
# a mode = add a renderer + register it in the dispatch dicts below (open/closed,
|
|
240
|
+
# matching the classifier's detector/policy registries).
|
|
241
|
+
# ===========================================================================
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def render_obstruction_hint(
|
|
245
|
+
*, extra: dict, attempt: int, budget: int, container_hint: str, language: str
|
|
246
|
+
) -> str:
|
|
247
|
+
modal = extra.get("modal_kind", "overlay")
|
|
248
|
+
if language == "zh":
|
|
249
|
+
return (
|
|
250
|
+
"\n<obstruction>\n"
|
|
251
|
+
"## ⚠️ 遮挡检测\n"
|
|
252
|
+
f"当前有 `{modal}` 浮层挡住目标,你上一步动作没有产生变化"
|
|
253
|
+
f"(已是第 {attempt}/{budget} 次卡在它上面)。\n\n"
|
|
254
|
+
"**先关掉它再继续**:点浮层外部 / 关闭图标 / `system_button(\"back\")`,"
|
|
255
|
+
"成功后再重试原动作。预算耗尽会判定 **Persistent Obstruction**。\n"
|
|
256
|
+
"</obstruction>\n"
|
|
257
|
+
)
|
|
258
|
+
return (
|
|
259
|
+
"\n<obstruction>\n"
|
|
260
|
+
"## ⚠️ Obstruction Detector\n"
|
|
261
|
+
f"A `{modal}` overlay is blocking the target and your last action "
|
|
262
|
+
f"produced no change (attempt {attempt}/{budget} stuck behind it).\n\n"
|
|
263
|
+
"**Dismiss it first**: tap outside / its close icon / "
|
|
264
|
+
"`system_button(\"back\")`, then retry the original action. "
|
|
265
|
+
"Exhausting the budget concludes **Persistent Obstruction**.\n"
|
|
266
|
+
"</obstruction>\n"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def render_unresponsive_element_hint(
|
|
271
|
+
*, extra: dict, attempt: int, budget: int, container_hint: str, language: str
|
|
272
|
+
) -> str:
|
|
273
|
+
if language == "zh":
|
|
274
|
+
return (
|
|
275
|
+
"\n<unresponsive_element>\n"
|
|
276
|
+
"## ⚠️ 元素无响应\n"
|
|
277
|
+
f"你反复点同一个元素但屏幕零变化(第 {attempt}/{budget} 次)。\n\n"
|
|
278
|
+
f"{container_hint}"
|
|
279
|
+
"**换招**:换一个目标元素、用坐标点子区域、或换导航路径;"
|
|
280
|
+
f"再无效将判定 **Unresponsive Element Bug**。\n"
|
|
281
|
+
"</unresponsive_element>\n"
|
|
282
|
+
)
|
|
283
|
+
return (
|
|
284
|
+
"\n<unresponsive_element>\n"
|
|
285
|
+
"## ⚠️ Unresponsive Element\n"
|
|
286
|
+
f"You tapped the same element repeatedly with zero screen change "
|
|
287
|
+
f"(attempt {attempt}/{budget}).\n\n"
|
|
288
|
+
f"{container_hint}"
|
|
289
|
+
"**Switch tactics**: a different target element, a coordinate tap on "
|
|
290
|
+
"a sub-region, or a different navigation path. Another no-op concludes "
|
|
291
|
+
"**Unresponsive Element Bug**.\n"
|
|
292
|
+
"</unresponsive_element>\n"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def render_navigation_cycle_hint(
|
|
297
|
+
*, extra: dict, attempt: int, budget: int, container_hint: str, language: str
|
|
298
|
+
) -> str:
|
|
299
|
+
page_a = extra.get("page_a", "A")
|
|
300
|
+
page_b = extra.get("page_b", "B")
|
|
301
|
+
# One side is the launcher / multitask shell → the bounce is almost always a
|
|
302
|
+
# gesture hijack (an edge swipe captured by system navigation), which the
|
|
303
|
+
# model is actively trying to recover from. Coach the reliable recovery
|
|
304
|
+
# instead of a generic "find another route".
|
|
305
|
+
if extra.get("system_surface") == "true":
|
|
306
|
+
if language == "zh":
|
|
307
|
+
return (
|
|
308
|
+
"\n<navigation_cycle>\n"
|
|
309
|
+
"## ⚠️ 手势被系统截获\n"
|
|
310
|
+
f"你在 app 与系统桌面/多任务之间来回(`{page_a}` ↔ `{page_b}`,"
|
|
311
|
+
f"第 {attempt}/{budget} 次)。你的边缘滑动很可能被系统手势导航吃掉了,"
|
|
312
|
+
"并没有在 app 内返回。\n\n"
|
|
313
|
+
"**改用可靠返回**:`system_button(\"back\")` 一定能在 app 内返回上一页;"
|
|
314
|
+
"或点页面上的返回/关闭按钮。别再重复同一条边缘滑动。\n"
|
|
315
|
+
"</navigation_cycle>\n"
|
|
316
|
+
)
|
|
317
|
+
return (
|
|
318
|
+
"\n<navigation_cycle>\n"
|
|
319
|
+
"## ⚠️ Gesture Captured by System\n"
|
|
320
|
+
f"You are bouncing between the app and the system launcher/multitask "
|
|
321
|
+
f"view (`{page_a}` ↔ `{page_b}`, attempt {attempt}/{budget}). Your "
|
|
322
|
+
"edge swipe was likely grabbed by system gesture navigation and did "
|
|
323
|
+
"not go back inside the app.\n\n"
|
|
324
|
+
"**Use a reliable back**: `system_button(\"back\")` always navigates "
|
|
325
|
+
"back inside the app; or tap the on-screen back/close button. Stop "
|
|
326
|
+
"repeating the same edge swipe.\n"
|
|
327
|
+
"</navigation_cycle>\n"
|
|
328
|
+
)
|
|
329
|
+
if language == "zh":
|
|
330
|
+
return (
|
|
331
|
+
"\n<navigation_cycle>\n"
|
|
332
|
+
"## ⚠️ 页面来回跳\n"
|
|
333
|
+
f"你在两个页面之间来回(`{page_a}` ↔ `{page_b}`,第 {attempt}/{budget} 次)。"
|
|
334
|
+
"重复同一路径不会前进。\n\n"
|
|
335
|
+
"**换一条路**:换不同的入口/元素、用搜索、或滚动暴露新 UI;"
|
|
336
|
+
"继续循环将判定 **Navigation Cycle**。\n"
|
|
337
|
+
"</navigation_cycle>\n"
|
|
338
|
+
)
|
|
339
|
+
return (
|
|
340
|
+
"\n<navigation_cycle>\n"
|
|
341
|
+
"## ⚠️ Navigation Cycle\n"
|
|
342
|
+
f"You are bouncing between two pages (`{page_a}` ↔ `{page_b}`, "
|
|
343
|
+
f"attempt {attempt}/{budget}). Repeating the same path will not "
|
|
344
|
+
"progress.\n\n"
|
|
345
|
+
"**Take a different route**: a different entry/element, search, or "
|
|
346
|
+
"scroll to expose new UI. Continuing the loop concludes "
|
|
347
|
+
"**Navigation Cycle**.\n"
|
|
348
|
+
"</navigation_cycle>\n"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# mode → Tier-1 HINT renderer
|
|
353
|
+
_HINT_RENDERERS = {
|
|
354
|
+
FailureMode.OBSTRUCTION: render_obstruction_hint,
|
|
355
|
+
FailureMode.UNRESPONSIVE_ELEMENT: render_unresponsive_element_hint,
|
|
356
|
+
FailureMode.NAVIGATION_CYCLE: render_navigation_cycle_hint,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def render_failure_hint(
|
|
361
|
+
*,
|
|
362
|
+
mode: FailureMode,
|
|
363
|
+
extra: dict,
|
|
364
|
+
attempt: int,
|
|
365
|
+
budget: int,
|
|
366
|
+
container_hint: str = "",
|
|
367
|
+
language: str = "en",
|
|
368
|
+
) -> str:
|
|
369
|
+
"""Dispatch to the mode's Tier-1 HINT block. Empty string for unmapped
|
|
370
|
+
modes (the caller then falls back to the generic stagnation block)."""
|
|
371
|
+
renderer = _HINT_RENDERERS.get(mode)
|
|
372
|
+
if renderer is None:
|
|
373
|
+
return ""
|
|
374
|
+
return renderer(
|
|
375
|
+
extra=extra,
|
|
376
|
+
attempt=attempt,
|
|
377
|
+
budget=budget,
|
|
378
|
+
container_hint=container_hint,
|
|
379
|
+
language=language,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# mode → human-readable typed conclusion noun phrase (for the CONCLUDE reason)
|
|
384
|
+
_CONCLUSION_DETAIL = {
|
|
385
|
+
FailureMode.OBSTRUCTION: (
|
|
386
|
+
"a blocking overlay (`{ref}`) kept obstructing the target after the "
|
|
387
|
+
"dismissal budget was spent"
|
|
388
|
+
),
|
|
389
|
+
FailureMode.UNRESPONSIVE_ELEMENT: (
|
|
390
|
+
"element `{ref}` produced zero observable change across every attempt "
|
|
391
|
+
"— the control is unresponsive"
|
|
392
|
+
),
|
|
393
|
+
FailureMode.NAVIGATION_CYCLE: (
|
|
394
|
+
"the agent kept bouncing between pages (`{ref}`) without progressing"
|
|
395
|
+
),
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def render_failure_conclusion(
|
|
400
|
+
*, label: str, mode: FailureMode, target_ref: str, severity: str
|
|
401
|
+
) -> str:
|
|
402
|
+
"""Build the structured typed reason for a Tier-3 CONCLUDE abort.
|
|
403
|
+
|
|
404
|
+
``label`` is the policy's conclusion noun (e.g. "Unresponsive Element Bug");
|
|
405
|
+
the detail clause is mode-specific. Fed verbatim into the FastAgentEndEvent
|
|
406
|
+
reason so the trace / evals get a machine-readable failure class.
|
|
407
|
+
"""
|
|
408
|
+
detail = _CONCLUSION_DETAIL.get(mode, "the failure budget was exhausted")
|
|
409
|
+
detail = detail.format(ref=target_ref or "?")
|
|
410
|
+
return f"Aborted [{label}, severity={severity}]: {detail}."
|