harmonyrun 0.0.29__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.
Files changed (158) hide show
  1. droidrun/__init__.py +92 -0
  2. droidrun/__main__.py +8 -0
  3. droidrun/agent/__init__.py +3 -0
  4. droidrun/agent/action_context.py +41 -0
  5. droidrun/agent/action_result.py +16 -0
  6. droidrun/agent/common/__init__.py +0 -0
  7. droidrun/agent/common/constants.py +11 -0
  8. droidrun/agent/common/events.py +139 -0
  9. droidrun/agent/common/ui_state_snapshot.py +188 -0
  10. droidrun/agent/droid/__init__.py +10 -0
  11. droidrun/agent/droid/droid_agent.py +1404 -0
  12. droidrun/agent/droid/events.py +110 -0
  13. droidrun/agent/droid/state.py +243 -0
  14. droidrun/agent/executor/__init__.py +22 -0
  15. droidrun/agent/executor/events.py +47 -0
  16. droidrun/agent/executor/executor_agent.py +303 -0
  17. droidrun/agent/executor/prompts.py +51 -0
  18. droidrun/agent/fast_agent/__init__.py +3 -0
  19. droidrun/agent/fast_agent/events.py +52 -0
  20. droidrun/agent/fast_agent/fast_agent.py +679 -0
  21. droidrun/agent/fast_agent/xml_parser.py +191 -0
  22. droidrun/agent/manager/__init__.py +28 -0
  23. droidrun/agent/manager/events.py +40 -0
  24. droidrun/agent/manager/manager_agent.py +599 -0
  25. droidrun/agent/manager/prompts.py +106 -0
  26. droidrun/agent/manager/stateless_manager_agent.py +399 -0
  27. droidrun/agent/oneflows/__init__.py +0 -0
  28. droidrun/agent/oneflows/app_starter_workflow.py +141 -0
  29. droidrun/agent/oneflows/structured_output_agent.py +87 -0
  30. droidrun/agent/tool_registry.py +336 -0
  31. droidrun/agent/trajectory/__init__.py +7 -0
  32. droidrun/agent/trajectory/llm_interaction_log.py +206 -0
  33. droidrun/agent/trajectory/writer.py +498 -0
  34. droidrun/agent/usage.py +252 -0
  35. droidrun/agent/utils/__init__.py +33 -0
  36. droidrun/agent/utils/actions.py +468 -0
  37. droidrun/agent/utils/chat_utils.py +90 -0
  38. droidrun/agent/utils/inference.py +499 -0
  39. droidrun/agent/utils/llm_loader.py +256 -0
  40. droidrun/agent/utils/llm_picker.py +263 -0
  41. droidrun/agent/utils/prompt_resolver.py +66 -0
  42. droidrun/agent/utils/signatures.py +205 -0
  43. droidrun/agent/utils/trajectory.py +287 -0
  44. droidrun/app_cards/__init__.py +0 -0
  45. droidrun/app_cards/app_card_provider.py +26 -0
  46. droidrun/app_cards/loading.py +57 -0
  47. droidrun/app_cards/provider_factory.py +54 -0
  48. droidrun/app_cards/providers/__init__.py +7 -0
  49. droidrun/app_cards/providers/composite_provider.py +101 -0
  50. droidrun/app_cards/providers/local_provider.py +114 -0
  51. droidrun/app_cards/providers/server_provider.py +120 -0
  52. droidrun/app_cards/suite_resolve.py +112 -0
  53. droidrun/batch/__init__.py +41 -0
  54. droidrun/batch/levels.py +23 -0
  55. droidrun/batch/loader.py +232 -0
  56. droidrun/batch/models.py +94 -0
  57. droidrun/batch/prompt_templates.py +21 -0
  58. droidrun/batch/report.py +617 -0
  59. droidrun/batch/resource_manager.py +189 -0
  60. droidrun/batch/runner.py +558 -0
  61. droidrun/batch/scheduler.py +105 -0
  62. droidrun/batch/schema_validation.py +58 -0
  63. droidrun/batch/schemas/suite_report.schema.json +121 -0
  64. droidrun/batch/schemas/test_suite.schema.json +103 -0
  65. droidrun/batch/suite_runner.py +451 -0
  66. droidrun/cli/__init__.py +9 -0
  67. droidrun/cli/doctor.py +663 -0
  68. droidrun/cli/event_handler.py +178 -0
  69. droidrun/cli/logs.py +9 -0
  70. droidrun/cli/main.py +585 -0
  71. droidrun/config/app_cards/README.md +139 -0
  72. droidrun/config/app_cards/app_cards.json +3 -0
  73. droidrun/config/app_cards/gmail.md +35 -0
  74. droidrun/config/config_example.yaml +202 -0
  75. droidrun/config/credentials_example.yaml +106 -0
  76. droidrun/config/prompts/batch/agent_goal.jinja2 +27 -0
  77. droidrun/config/prompts/batch/postcondition_goal.jinja2 +8 -0
  78. droidrun/config/prompts/batch/precondition_goal.jinja2 +8 -0
  79. droidrun/config/prompts/batch/suite_report.jinja2 +62 -0
  80. droidrun/config/prompts/batch/test_execution_goal.jinja2 +13 -0
  81. droidrun/config/prompts/executor/rev1.jinja2 +103 -0
  82. droidrun/config/prompts/executor/system.jinja2 +173 -0
  83. droidrun/config/prompts/fast_agent/system.jinja2 +214 -0
  84. droidrun/config/prompts/fast_agent/user.jinja2 +7 -0
  85. droidrun/config/prompts/manager/rev1.jinja2 +131 -0
  86. droidrun/config/prompts/manager/stateless.jinja2 +181 -0
  87. droidrun/config/prompts/manager/system.jinja2 +179 -0
  88. droidrun/config/prompts/manager/trained.jinja2 +67 -0
  89. droidrun/config/test_cases_example.json +51 -0
  90. droidrun/config_manager/__init__.py +37 -0
  91. droidrun/config_manager/config_manager.py +345 -0
  92. droidrun/config_manager/env_keys.py +51 -0
  93. droidrun/config_manager/loader.py +78 -0
  94. droidrun/config_manager/path_resolver.py +112 -0
  95. droidrun/config_manager/prompt_loader.py +94 -0
  96. droidrun/credential_manager/__init__.py +13 -0
  97. droidrun/credential_manager/credential_manager.py +38 -0
  98. droidrun/credential_manager/file_credential_manager.py +137 -0
  99. droidrun/hdcutils/__init__.py +34 -0
  100. droidrun/hdcutils/_agent_so.py +420 -0
  101. droidrun/hdcutils/_device.py +1417 -0
  102. droidrun/hdcutils/_hdc.py +97 -0
  103. droidrun/hdcutils/_hilog.py +217 -0
  104. droidrun/hdcutils/_proto.py +43 -0
  105. droidrun/hdcutils/_recording.py +93 -0
  106. droidrun/hdcutils/_uitest_rpc.py +1100 -0
  107. droidrun/hdcutils/_utils.py +223 -0
  108. droidrun/hdcutils/_version.py +6 -0
  109. droidrun/hdcutils/agents/uitest_agent_v1.1.10.so +0 -0
  110. droidrun/hdcutils/agents/uitest_agent_v1.1.3.so +0 -0
  111. droidrun/hdcutils/agents/uitest_agent_v1.1.5.so +0 -0
  112. droidrun/hdcutils/agents/uitest_agent_v1.1.9.x86_64_so +0 -0
  113. droidrun/hdcutils/agents/uitest_agent_v1.2.2.so +0 -0
  114. droidrun/hdcutils/errors.py +28 -0
  115. droidrun/hdcutils/harmony_wake.py +314 -0
  116. droidrun/hdcutils/keycode.py +361 -0
  117. droidrun/hypium/__init__.py +11 -0
  118. droidrun/hypium/action_mapper.py +180 -0
  119. droidrun/hypium/assertion_builder.py +279 -0
  120. droidrun/hypium/cli.py +148 -0
  121. droidrun/hypium/code_polisher.py +207 -0
  122. droidrun/hypium/component_resolver.py +221 -0
  123. droidrun/hypium/generator.py +559 -0
  124. droidrun/hypium/template.py +157 -0
  125. droidrun/log_handlers.py +53 -0
  126. droidrun/telemetry/__init__.py +15 -0
  127. droidrun/telemetry/events.py +53 -0
  128. droidrun/telemetry/tracker.py +151 -0
  129. droidrun/tools/__init__.py +18 -0
  130. droidrun/tools/driver/__init__.py +14 -0
  131. droidrun/tools/driver/base.py +157 -0
  132. droidrun/tools/driver/harmony.py +1071 -0
  133. droidrun/tools/driver/recording.py +376 -0
  134. droidrun/tools/driver/stealth.py +211 -0
  135. droidrun/tools/filters/__init__.py +31 -0
  136. droidrun/tools/filters/base.py +42 -0
  137. droidrun/tools/filters/concise_filter.py +120 -0
  138. droidrun/tools/filters/detailed_filter.py +168 -0
  139. droidrun/tools/filters/passthrough_filter.py +23 -0
  140. droidrun/tools/formatters/__init__.py +7 -0
  141. droidrun/tools/formatters/base.py +27 -0
  142. droidrun/tools/formatters/indexed_formatter.py +663 -0
  143. droidrun/tools/helpers/__init__.py +18 -0
  144. droidrun/tools/helpers/coordinate.py +25 -0
  145. droidrun/tools/helpers/element_search.py +635 -0
  146. droidrun/tools/helpers/geometry.py +49 -0
  147. droidrun/tools/hilog_monitor.py +284 -0
  148. droidrun/tools/screen_recorder.py +246 -0
  149. droidrun/tools/ui/__init__.py +13 -0
  150. droidrun/tools/ui/provider.py +239 -0
  151. droidrun/tools/ui/screenshot_annotator.py +196 -0
  152. droidrun/tools/ui/state.py +168 -0
  153. droidrun/tools/ui/stealth_state.py +104 -0
  154. harmonyrun-0.0.29.dist-info/METADATA +142 -0
  155. harmonyrun-0.0.29.dist-info/RECORD +158 -0
  156. harmonyrun-0.0.29.dist-info/WHEEL +4 -0
  157. harmonyrun-0.0.29.dist-info/entry_points.txt +3 -0
  158. harmonyrun-0.0.29.dist-info/licenses/LICENSE +21 -0
droidrun/__init__.py ADDED
@@ -0,0 +1,92 @@
1
+ """
2
+ HarmonyRun - A framework for automating HarmonyOS devices through LLM agents.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from importlib import import_module
9
+ from importlib.metadata import PackageNotFoundError, version
10
+
11
+ try:
12
+ __version__ = version("harmonyrun")
13
+ except PackageNotFoundError:
14
+ # Running from a checkout without installing the distribution (e.g. scripts).
15
+ __version__ = "0.0.0+unknown"
16
+
17
+ # Attach a default CLILogHandler so that every consumer (CLI, SDK, tools-only)
18
+ # gets visible output without explicit setup. CLI may replace this via
19
+ # ``configure_logging()``.
20
+ _logger = logging.getLogger("droidrun")
21
+ try:
22
+ from droidrun.log_handlers import CLILogHandler
23
+
24
+ _logger.addHandler(CLILogHandler())
25
+ _logger.setLevel(logging.INFO)
26
+ _logger.propagate = False
27
+ except ImportError:
28
+ # e.g. ``rich`` not installed — skip; subpackages like ``hdcutils`` still import.
29
+ pass
30
+
31
+ _LAZY_EXPORTS: dict[str, tuple[str, str]] = {
32
+ "ResultEvent": ("droidrun.agent", "ResultEvent"),
33
+ "DroidAgent": ("droidrun.agent.droid", "DroidAgent"),
34
+ "load_llm": ("droidrun.agent.utils.llm_picker", "load_llm"),
35
+ "AgentConfig": ("droidrun.config_manager", "AgentConfig"),
36
+ "AppCardConfig": ("droidrun.config_manager", "AppCardConfig"),
37
+ "FastAgentConfig": ("droidrun.config_manager", "FastAgentConfig"),
38
+ "CredentialsConfig": ("droidrun.config_manager", "CredentialsConfig"),
39
+ "DeviceConfig": ("droidrun.config_manager", "DeviceConfig"),
40
+ "DroidrunConfig": ("droidrun.config_manager", "DroidrunConfig"),
41
+ "ExecutorConfig": ("droidrun.config_manager", "ExecutorConfig"),
42
+ "LLMProfile": ("droidrun.config_manager", "LLMProfile"),
43
+ "LoggingConfig": ("droidrun.config_manager", "LoggingConfig"),
44
+ "ManagerConfig": ("droidrun.config_manager", "ManagerConfig"),
45
+ "TelemetryConfig": ("droidrun.config_manager", "TelemetryConfig"),
46
+ "ToolsConfig": ("droidrun.config_manager", "ToolsConfig"),
47
+ "TracingConfig": ("droidrun.config_manager", "TracingConfig"),
48
+ "DeviceDriver": ("droidrun.tools", "DeviceDriver"),
49
+ "TreeStateProvider": ("droidrun.tools", "TreeStateProvider"),
50
+ "HarmonyDriver": ("droidrun.tools", "HarmonyDriver"),
51
+ "RecordingDriver": ("droidrun.tools", "RecordingDriver"),
52
+ }
53
+
54
+
55
+ def __getattr__(name: str) -> object:
56
+ spec = _LAZY_EXPORTS.get(name)
57
+ if spec is None:
58
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
59
+ mod_name, attr = spec
60
+ return getattr(import_module(mod_name), attr)
61
+
62
+
63
+ def __dir__() -> list[str]:
64
+ return sorted({*globals().keys(), *_LAZY_EXPORTS.keys(), "__version__"})
65
+
66
+
67
+ # Make main components available at package level
68
+ __all__ = [
69
+ # Agent
70
+ "DroidAgent",
71
+ "load_llm",
72
+ "ResultEvent",
73
+ # Tools / Drivers
74
+ "DeviceDriver",
75
+ "TreeStateProvider",
76
+ "HarmonyDriver",
77
+ "RecordingDriver",
78
+ # Configuration
79
+ "DroidrunConfig",
80
+ "AgentConfig",
81
+ "FastAgentConfig",
82
+ "ManagerConfig",
83
+ "ExecutorConfig",
84
+ "AppCardConfig",
85
+ "DeviceConfig",
86
+ "LoggingConfig",
87
+ "TracingConfig",
88
+ "TelemetryConfig",
89
+ "ToolsConfig",
90
+ "CredentialsConfig",
91
+ "LLMProfile",
92
+ ]
droidrun/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ DroidRun main entry point
3
+ """
4
+
5
+ from droidrun.cli.main import cli
6
+
7
+ if __name__ == "__main__":
8
+ cli()
@@ -0,0 +1,3 @@
1
+ from droidrun.agent.droid.events import ResultEvent
2
+
3
+ __all__ = ["ResultEvent"]
@@ -0,0 +1,41 @@
1
+ """ActionContext — composed bag of dependencies for action functions.
2
+
3
+ Replaces the ``tools=tools_instance`` parameter that action functions
4
+ previously received.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Optional
10
+
11
+ if TYPE_CHECKING:
12
+ from droidrun.agent.droid.state import DroidAgentState
13
+ from droidrun.credential_manager import CredentialManager
14
+ from droidrun.tools.driver.base import DeviceDriver
15
+ from droidrun.tools.ui.provider import StateProvider
16
+ from droidrun.tools.ui.state import UIState
17
+
18
+
19
+ class ActionContext:
20
+ """Everything an action function needs to interact with the device."""
21
+
22
+ def __init__(
23
+ self,
24
+ driver: "DeviceDriver",
25
+ ui: "Optional[UIState]",
26
+ shared_state: "DroidAgentState",
27
+ state_provider: "StateProvider",
28
+ app_opener_llm=None,
29
+ credential_manager: "Optional[CredentialManager]" = None,
30
+ streaming: bool = False,
31
+ swipe_fast_default: bool = False,
32
+ ) -> None:
33
+ self.driver = driver
34
+ self.ui = ui # refreshed each step before tool execution
35
+ self.shared_state = shared_state
36
+ self.state_provider = state_provider
37
+ self.app_opener_llm = app_opener_llm
38
+ self.credential_manager = credential_manager
39
+ self.streaming = streaming
40
+ #: When the ``swipe`` action omits ``fast``, use this (from ``tools.swipe_fast_default``).
41
+ self.swipe_fast_default = swipe_fast_default
@@ -0,0 +1,16 @@
1
+ """ActionResult — structured return type from action functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class ActionResult:
10
+ """What the agent sees after an action runs."""
11
+
12
+ success: bool
13
+ summary: str
14
+
15
+ def __str__(self) -> str:
16
+ return self.summary
File without changes
@@ -0,0 +1,11 @@
1
+ """Shared constants used across agent and batch packages."""
2
+
3
+ LLM_HISTORY_LIMIT = 100
4
+ """Max number of recent conversation steps to include in LLM prompt."""
5
+
6
+ # Canonical suite-managed resource names.
7
+ # Used by TestCaseResourceManager (batch) and DroidAgent (agent) to coordinate
8
+ # which resources are managed at suite level vs. per-phase level.
9
+ RESOURCE_SCREEN_RECORDING = "screen_recording"
10
+ RESOURCE_HILOG_MONITOR = "hilog_monitor"
11
+ RESOURCE_DEVICE_DRIVER = "device_driver"
@@ -0,0 +1,139 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+ from llama_index.core.workflow import Event
4
+ from pydantic import Field
5
+
6
+
7
+ class TimedEvent(Event):
8
+ """Base event with optional per-event timestamps.
9
+
10
+ Every event that flows through DroidAgent gets ``ts_unix`` and
11
+ ``ts_iso`` stamped by ``DroidAgent.handle_stream_event`` right before
12
+ it is appended to the trajectory. Keeping the fields declared as
13
+ real pydantic fields ensures they end up in ``__dict__`` and are
14
+ therefore picked up by the trajectory serializer.
15
+ """
16
+
17
+ ts_unix: Optional[float] = None
18
+ ts_iso: Optional[str] = None
19
+
20
+
21
+ class ScreenshotEvent(TimedEvent):
22
+ screenshot: bytes
23
+
24
+
25
+ class RecordUIStateEvent(TimedEvent):
26
+ """Trajectory UI snapshot.
27
+
28
+ Carries the fully-formatted device-state string that was surfaced to
29
+ the model (phone state + indexed UI tree). Previously this event
30
+ also transported the raw element list (``ui_state``); that field was
31
+ removed because the raw snapshot isn't used for debugging and it
32
+ bloated trajectory disk output. The flattened element tree still
33
+ lives in memory on ``UIState.elements`` for the duration of a step.
34
+ """
35
+
36
+ #: Same string as ``UIState.formatted_text`` / ``shared_state.formatted_device_state``
37
+ #: (phone state + indexed UI tree) — what goes into manager/executor prompts.
38
+ formatted_device_state: str = ""
39
+
40
+
41
+ class ToolExecutionEvent(TimedEvent):
42
+ """Emitted after every tool call dispatched through ToolRegistry."""
43
+
44
+ tool_name: str
45
+ tool_args: Dict[str, Any]
46
+ success: bool
47
+ summary: str
48
+ # Latency in milliseconds; None if not measured.
49
+ latency_ms: Optional[float] = None
50
+ # Repr of the exception when ``success`` is False.
51
+ error: Optional[str] = None
52
+ # Fast-agent turn index this tool call belongs to (1-based). Lets us
53
+ # group tool calls by LLM turn when post-analyzing trajectories.
54
+ step_index: Optional[int] = None
55
+
56
+
57
+ class DeviceActionEvent(TimedEvent):
58
+ """A single mutating or read operation routed through RecordingDriver.
59
+
60
+ Device operations are embedded directly in ``trajectory.jsonl`` so one
61
+ event stream can reconstruct both agent decisions and device I/O.
62
+ """
63
+
64
+ op: str
65
+ args: Dict[str, Any] = Field(default_factory=dict)
66
+ success: bool = True
67
+ error: Optional[str] = None
68
+ duration_ms: Optional[float] = None
69
+ # True for pure observation operations (screenshot, get_ui_tree, …).
70
+ readonly: bool = False
71
+ # Compact summaries for large read-only results such as screenshots/UI trees.
72
+ result_summary: Optional[Dict[str, Any]] = None
73
+
74
+
75
+ _DEVICE_ENTRY_ARG_FIELDS: tuple[str, ...] = (
76
+ "subcmd",
77
+ "cmd",
78
+ "serial",
79
+ "returncode",
80
+ "stdout_preview",
81
+ "stderr_preview",
82
+ "timed_out",
83
+ "layer",
84
+ )
85
+
86
+
87
+ def device_action_event_from_entry(entry: Dict[str, Any]) -> DeviceActionEvent:
88
+ """Build the compact trajectory event for a recorded device operation.
89
+
90
+ The old ``device_ops.jsonl`` record carried some raw-hdc fields at top level.
91
+ To keep trajectory rows compact and consistent, those details are nested
92
+ under ``args`` only for operations that actually provide them.
93
+ """
94
+ args = dict(entry.get("args") or {})
95
+ for key in _DEVICE_ENTRY_ARG_FIELDS:
96
+ if key in entry and entry[key] is not None:
97
+ args.setdefault(key, entry[key])
98
+
99
+ return DeviceActionEvent(
100
+ op=entry.get("op", "unknown"),
101
+ args=args,
102
+ success=bool(entry.get("success", True)),
103
+ error=entry.get("error"),
104
+ duration_ms=entry.get("duration_ms"),
105
+ readonly=bool(entry.get("readonly", False)),
106
+ result_summary=entry.get("result_summary"),
107
+ ts_unix=entry.get("ts_unix"),
108
+ ts_iso=entry.get("ts_iso"),
109
+ )
110
+
111
+
112
+ class ErrorEvent(TimedEvent):
113
+ """Top-level exception captured at driver/tool/workflow boundaries.
114
+
115
+ Use this to record unexpected failures so you can later correlate
116
+ them with the surrounding step / tool call. The message/traceback
117
+ are raw strings — keep them succinct enough to be readable in
118
+ ``trajectory.jsonl``.
119
+ """
120
+
121
+ where: str
122
+ exception_type: str
123
+ message: str
124
+ traceback: Optional[str] = None
125
+ step_index: Optional[int] = None
126
+
127
+
128
+ class PhaseStartEvent(TimedEvent):
129
+ """Marks the beginning of a test phase (setup/preconditions/test_execution/postconditions/teardown)."""
130
+
131
+ phase: str
132
+
133
+
134
+ class PhaseEndEvent(TimedEvent):
135
+ """Marks the end of a test phase."""
136
+
137
+ phase: str
138
+ success: bool
139
+ reason: str = ""
@@ -0,0 +1,188 @@
1
+ """Build UI trajectory snapshots aligned with LLM-facing context.
2
+
3
+ All blocks use a consistent Markdown-inside-XML-anchor shape so the model
4
+ sees the same structure across fast_agent / manager / executor: each block
5
+ is wrapped in an XML tag (``<device_state>``, ``<previous_device_state>``,
6
+ etc.) for machine parseability, and the body is standard Markdown with
7
+ ``### Phone`` / ``### UI Tree`` sub-sections produced by the formatter.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Dict, List
13
+
14
+ MANAGER_DEVICE_STATE_OPEN = "\n<device_state>\n## Device State\n\n"
15
+ MANAGER_DEVICE_STATE_CLOSE = "\n</device_state>\n"
16
+
17
+
18
+ def manager_device_state_block(formatted_inner: str) -> str:
19
+ inner = (formatted_inner or "").strip()
20
+ if not inner:
21
+ return ""
22
+ return MANAGER_DEVICE_STATE_OPEN + inner + MANAGER_DEVICE_STATE_CLOSE
23
+
24
+
25
+ def manager_memory_block(manager_memory: str) -> str:
26
+ m = (manager_memory or "").strip()
27
+ if not m:
28
+ return ""
29
+ return f"\n<memory>\n{m}\n</memory>\n"
30
+
31
+
32
+ def manager_previous_device_state_block(previous_formatted: str) -> str:
33
+ prev = (previous_formatted or "").strip()
34
+ if not prev:
35
+ return ""
36
+ return (
37
+ "\n<previous_device_state>\n"
38
+ "## Previous Device State\n\n"
39
+ f"{prev}\n"
40
+ "</previous_device_state>\n"
41
+ )
42
+
43
+
44
+ def fast_agent_ui_sections(shared_state: Any, step: int, max_steps: int) -> Dict[str, str]:
45
+ """Fragments FastAgent appends to chat (excluding the image block).
46
+
47
+ Must stay in sync with the injection order in ``fast_agent.py`` so that
48
+ what we snapshot into the trajectory matches what the LLM actually saw.
49
+
50
+ Does not drain ``device_events``; caller drains after appending ``device_events``.
51
+ """
52
+ out: Dict[str, str] = {}
53
+ remaining = max_steps - step
54
+ out["step_progress"] = (
55
+ f"\n> **Progress:** step {step} / {max_steps} · {remaining} remaining\n"
56
+ )
57
+
58
+ events_text = shared_state.format_device_events()
59
+ if events_text:
60
+ out["device_events"] = (
61
+ "\n<device_events>\n"
62
+ "### Device Events\n"
63
+ "_Transient UI events captured via system-level monitoring. "
64
+ "Use this as the source of truth for toast verification._\n\n"
65
+ f"{events_text}\n"
66
+ "</device_events>\n"
67
+ )
68
+
69
+ app_card_text = (shared_state.app_card or "").strip()
70
+ if app_card_text:
71
+ out["app_card"] = f"\n<app_card>\n{app_card_text}\n</app_card>\n"
72
+
73
+ current_state = (shared_state.formatted_device_state or "").strip()
74
+ if current_state:
75
+ out["device_state"] = (
76
+ "\n<device_state>\n"
77
+ "## Device State\n\n"
78
+ f"{current_state}\n"
79
+ "</device_state>\n"
80
+ )
81
+
82
+ prev = (shared_state.previous_formatted_device_state or "").strip()
83
+ if prev:
84
+ out["previous_device_state"] = manager_previous_device_state_block(prev)
85
+
86
+ return out
87
+
88
+
89
+ def build_fast_agent_ui_snapshot(
90
+ shared_state: Any,
91
+ step: int,
92
+ max_steps: int,
93
+ elements: List[Dict[str, Any]],
94
+ phone_state: Dict[str, Any],
95
+ focused_text: str,
96
+ ) -> Dict[str, Any]:
97
+ sections = fast_agent_ui_sections(shared_state, step, max_steps)
98
+ # Must match injection order in ``fast_agent.handle_llm_input``:
99
+ # progress → events → app_card → device_state (freshest closest to model head).
100
+ last_user = "".join(
101
+ sections.get(k, "")
102
+ for k in ("step_progress", "device_events", "app_card", "device_state")
103
+ )
104
+ return {
105
+ "schema_version": 2,
106
+ "agent": "fast_agent",
107
+ "elements": elements,
108
+ "phone_state": dict(phone_state) if phone_state else {},
109
+ "focused_text": focused_text or "",
110
+ "formatted_device_state": (shared_state.formatted_device_state or "").strip(),
111
+ "llm_text_sections": sections,
112
+ "llm_last_user_message_text": last_user,
113
+ "llm_second_to_last_user_message_text": sections.get(
114
+ "previous_device_state", ""
115
+ ),
116
+ }
117
+
118
+
119
+ def build_manager_ui_snapshot(
120
+ shared_state: Any,
121
+ elements: List[Dict[str, Any]],
122
+ phone_state: Dict[str, Any],
123
+ focused_text: str,
124
+ manager_user_message_content: str,
125
+ user_message_count_before_append: int,
126
+ vision: bool,
127
+ ) -> Dict[str, Any]:
128
+ current = (shared_state.formatted_device_state or "").strip()
129
+ prev_plain = (shared_state.previous_formatted_device_state or "").strip()
130
+
131
+ memory_block = manager_memory_block(shared_state.manager_memory or "")
132
+ device_block = manager_device_state_block(shared_state.formatted_device_state or "")
133
+
134
+ second_augment = ""
135
+ if user_message_count_before_append >= 1 and prev_plain:
136
+ second_augment = manager_previous_device_state_block(
137
+ shared_state.previous_formatted_device_state or ""
138
+ )
139
+
140
+ return {
141
+ "schema_version": 2,
142
+ "agent": "manager",
143
+ "elements": elements,
144
+ "phone_state": dict(phone_state) if phone_state else {},
145
+ "focused_text": focused_text or "",
146
+ "formatted_device_state": current,
147
+ "previous_formatted_device_state": prev_plain,
148
+ "app_card": (shared_state.app_card or "").strip(),
149
+ "manager_user_message_content": manager_user_message_content,
150
+ "llm_last_user_text_appendix": memory_block + device_block,
151
+ "llm_second_to_last_user_text_appendix": second_augment,
152
+ "vision_includes_current_screenshot": bool(vision),
153
+ }
154
+
155
+
156
+ def build_stateless_manager_ui_snapshot(
157
+ shared_state: Any,
158
+ full_user_message_text: str,
159
+ ) -> Dict[str, Any]:
160
+ return {
161
+ "schema_version": 2,
162
+ "agent": "stateless_manager",
163
+ "elements": list(shared_state.a11y_tree),
164
+ "phone_state": dict(shared_state.phone_state) if shared_state.phone_state else {},
165
+ "focused_text": shared_state.focused_text or "",
166
+ "formatted_device_state": (shared_state.formatted_device_state or "").strip(),
167
+ "previous_formatted_device_state": (
168
+ shared_state.previous_formatted_device_state or ""
169
+ ).strip(),
170
+ "app_card": (shared_state.app_card or "").strip(),
171
+ "full_user_message_text": full_user_message_text,
172
+ }
173
+
174
+
175
+ def build_final_ui_snapshot(
176
+ elements: List[Dict[str, Any]],
177
+ phone_state: Dict[str, Any],
178
+ focused_text: str,
179
+ formatted_text: str,
180
+ ) -> Dict[str, Any]:
181
+ return {
182
+ "schema_version": 2,
183
+ "agent": "capture_final",
184
+ "elements": elements,
185
+ "phone_state": dict(phone_state) if phone_state else {},
186
+ "focused_text": focused_text or "",
187
+ "formatted_device_state": (formatted_text or "").strip(),
188
+ }
@@ -0,0 +1,10 @@
1
+ """
2
+ Droidrun Agent Module.
3
+
4
+ This module provides a ReAct agent for automating HarmonyOS devices using reasoning and acting.
5
+ """
6
+
7
+ from droidrun.agent.droid.droid_agent import DroidAgent
8
+ from droidrun.agent.droid.state import DroidAgentState
9
+
10
+ __all__ = ["DroidAgent", "DroidAgentState"]