droidrun 0.3.3__py3-none-any.whl → 0.3.4__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.
@@ -165,16 +165,18 @@ class CodeActAgent(Workflow):
165
165
  chat_history = await chat_utils.add_memory_block(self.remembered_info, chat_history)
166
166
 
167
167
  for context in self.required_context:
168
- if model == "DeepSeek":
169
- logger.warning(
170
- "[yellow]DeepSeek doesnt support images. Disabling screenshots[/]"
171
- )
172
- elif self.vision == True and context == "screenshot":
168
+ if context == "screenshot":
169
+ # if vision is disabled, screenshot should save to trajectory
173
170
  screenshot = (self.tools.take_screenshot())[1]
174
171
  ctx.write_event_to_stream(ScreenshotEvent(screenshot=screenshot))
175
172
 
176
173
  await ctx.set("screenshot", screenshot)
177
- chat_history = await chat_utils.add_screenshot_image_block(screenshot, chat_history)
174
+ if model == "DeepSeek":
175
+ logger.warning(
176
+ "[yellow]DeepSeek doesnt support images. Disabling screenshots[/]"
177
+ )
178
+ elif self.vision == True: # if vision is enabled, add screenshot to chat history
179
+ chat_history = await chat_utils.add_screenshot_image_block(screenshot, chat_history)
178
180
 
179
181
  if context == "ui_state":
180
182
  try:
@@ -248,7 +250,10 @@ class CodeActAgent(Workflow):
248
250
  try:
249
251
  self.code_exec_counter += 1
250
252
  result = await self.executor.execute(ctx, code)
251
- logger.info(f"💡 Code execution successful. Result: {result}")
253
+ logger.info(f"💡 Code execution successful. Result: {result['output']}")
254
+ screenshots = result['screenshots']
255
+ for screenshot in screenshots[:-1]: # the last screenshot will be captured by next step
256
+ ctx.write_event_to_stream(ScreenshotEvent(screenshot=screenshot))
252
257
 
253
258
  if self.tools.finished == True:
254
259
  logger.debug(" - Task completed.")
@@ -260,7 +265,7 @@ class CodeActAgent(Workflow):
260
265
 
261
266
  self.remembered_info = self.tools.memory
262
267
 
263
- event = TaskExecutionResultEvent(output=str(result))
268
+ event = TaskExecutionResultEvent(output=str(result['output']))
264
269
  ctx.write_event_to_stream(event)
265
270
  return event
266
271
 
@@ -1,5 +1,5 @@
1
1
  import os
2
- from typing import List, Dict
2
+ from typing import List, Dict, Optional
3
3
  from dataclasses import dataclass
4
4
  import copy
5
5
 
@@ -11,6 +11,9 @@ class Task:
11
11
  description: str
12
12
  status: str
13
13
  agent_type: str
14
+ # Optional fields to carry success/failure context back to the planner
15
+ message: Optional[str] = None
16
+ failure_reason: Optional[str] = None
14
17
 
15
18
 
16
19
  class TaskManager:
@@ -40,14 +43,16 @@ class TaskManager:
40
43
  def get_task_history(self):
41
44
  return self.task_history
42
45
 
43
- def complete_task(self, task: Task):
46
+ def complete_task(self, task: Task, message: Optional[str] = None):
44
47
  task = copy.deepcopy(task)
45
48
  task.status = self.STATUS_COMPLETED
49
+ task.message = message
46
50
  self.task_history.append(task)
47
51
 
48
- def fail_task(self, task: Task):
52
+ def fail_task(self, task: Task, failure_reason: Optional[str] = None):
49
53
  task = copy.deepcopy(task)
50
54
  task.status = self.STATUS_FAILED
55
+ task.failure_reason = failure_reason
51
56
  self.task_history.append(task)
52
57
 
53
58
  def get_completed_tasks(self) -> list[dict]:
@@ -68,7 +68,7 @@ A wrapper class that coordinates between PlannerAgent (creates plans) and
68
68
  reflection: bool = False,
69
69
  enable_tracing: bool = False,
70
70
  debug: bool = False,
71
- save_trajectories: bool = False,
71
+ save_trajectories: str = "none",
72
72
  excluded_tools: List[str] = None,
73
73
  *args,
74
74
  **kwargs
@@ -86,6 +86,10 @@ A wrapper class that coordinates between PlannerAgent (creates plans) and
86
86
  reflection: Whether to reflect on steps the CodeActAgent did to give the PlannerAgent advice
87
87
  enable_tracing: Whether to enable Arize Phoenix tracing
88
88
  debug: Whether to enable verbose debug logging
89
+ save_trajectories: Trajectory saving level. Can be:
90
+ - "none" (no saving)
91
+ - "step" (save per step)
92
+ - "action" (save per action)
89
93
  **kwargs: Additional keyword arguments to pass to the agents
90
94
  """
91
95
  self.user_id = kwargs.pop("user_id", None)
@@ -114,7 +118,17 @@ A wrapper class that coordinates between PlannerAgent (creates plans) and
114
118
  self.debug = debug
115
119
 
116
120
  self.event_counter = 0
117
- self.save_trajectories = save_trajectories
121
+ # Handle backward compatibility: bool -> str mapping
122
+ if isinstance(save_trajectories, bool):
123
+ self.save_trajectories = "step" if save_trajectories else "none"
124
+ else:
125
+ # Validate string values
126
+ valid_values = ["none", "step", "action"]
127
+ if save_trajectories not in valid_values:
128
+ logger.warning(f"Invalid save_trajectories value: {save_trajectories}. Using 'none' instead.")
129
+ self.save_trajectories = "none"
130
+ else:
131
+ self.save_trajectories = save_trajectories
118
132
 
119
133
  self.trajectory = Trajectory(goal=goal)
120
134
  self.task_manager = TaskManager()
@@ -125,9 +139,12 @@ A wrapper class that coordinates between PlannerAgent (creates plans) and
125
139
  self.current_episodic_memory = None
126
140
 
127
141
  logger.info("🤖 Initializing DroidAgent...")
142
+ logger.info(f"💾 Trajectory saving level: {self.save_trajectories}")
128
143
 
129
144
  self.tool_list = describe_tools(tools, excluded_tools)
130
145
  self.tools_instance = tools
146
+
147
+ self.tools_instance.save_trajectories = self.save_trajectories
131
148
 
132
149
 
133
150
  if self.reasoning:
@@ -173,11 +190,11 @@ A wrapper class that coordinates between PlannerAgent (creates plans) and
173
190
 
174
191
  logger.info("✅ DroidAgent initialized successfully.")
175
192
 
176
- def run(self) -> WorkflowHandler:
193
+ def run(self, *args, **kwargs) -> WorkflowHandler:
177
194
  """
178
195
  Run the DroidAgent workflow.
179
196
  """
180
- return super().run()
197
+ return super().run(*args, **kwargs)
181
198
 
182
199
  @step
183
200
  async def execute_task(
@@ -237,16 +254,24 @@ A wrapper class that coordinates between PlannerAgent (creates plans) and
237
254
  return CodeActResultEvent(success=False, reason=f"Error: {str(e)}", task=task, steps=[])
238
255
 
239
256
  @step
240
- async def handle_codeact_execute(self, ctx: Context, ev: CodeActResultEvent) -> FinalizeEvent | ReflectionEvent:
257
+ async def handle_codeact_execute(self, ctx: Context, ev: CodeActResultEvent) -> FinalizeEvent | ReflectionEvent | ReasoningLogicEvent:
241
258
  try:
242
259
  task = ev.task
243
260
  if not self.reasoning:
244
261
  return FinalizeEvent(success=ev.success, reason=ev.reason, output=ev.reason, task=[task], tasks=[task], steps=ev.steps)
245
262
 
246
- if self.reflection:
263
+ if self.reflection and ev.success:
247
264
  return ReflectionEvent(task=task)
248
-
249
- return ReasoningLogicEvent()
265
+
266
+ # Reasoning is enabled but reflection is disabled.
267
+ # Success: mark complete and proceed to next step in reasoning loop.
268
+ # Failure: mark failed and trigger planner immediately without advancing to the next queued task.
269
+ if ev.success:
270
+ self.task_manager.complete_task(task, message=ev.reason)
271
+ return ReasoningLogicEvent()
272
+ else:
273
+ self.task_manager.fail_task(task, failure_reason=ev.reason)
274
+ return ReasoningLogicEvent(force_planning=True)
250
275
 
251
276
  except Exception as e:
252
277
  logger.error(f"❌ Error during DroidAgent execution: {e}")
@@ -298,7 +323,7 @@ A wrapper class that coordinates between PlannerAgent (creates plans) and
298
323
  if ev.reflection:
299
324
  handler = planner_agent.run(remembered_info=self.tools_instance.memory, reflection=ev.reflection)
300
325
  else:
301
- if self.task_iter:
326
+ if not ev.force_planning and self.task_iter:
302
327
  try:
303
328
  task = next(self.task_iter)
304
329
  return CodeActExecuteEvent(task=task, reflection=None)
@@ -387,7 +412,7 @@ A wrapper class that coordinates between PlannerAgent (creates plans) and
387
412
  "steps": ev.steps,
388
413
  }
389
414
 
390
- if self.trajectory and self.save_trajectories:
415
+ if self.trajectory and self.save_trajectories != "none":
391
416
  self.trajectory.save_trajectory()
392
417
 
393
418
  return StopEvent(result)
@@ -13,6 +13,7 @@ class CodeActResultEvent(Event):
13
13
 
14
14
  class ReasoningLogicEvent(Event):
15
15
  reflection: Optional[Reflection] = None
16
+ force_planning: bool = False
16
17
 
17
18
  class FinalizeEvent(Event):
18
19
  success: bool
@@ -130,9 +130,10 @@ class PlannerAgent(Workflow):
130
130
  self.steps_counter += 1
131
131
  logger.info(f"🧠 Thinking about how to plan the goal...")
132
132
 
133
+ # if vision is disabled, screenshot should save to trajectory
134
+ screenshot = (self.tools_instance.take_screenshot())[1]
135
+ ctx.write_event_to_stream(ScreenshotEvent(screenshot=screenshot))
133
136
  if self.vision:
134
- screenshot = (self.tools_instance.take_screenshot())[1]
135
- ctx.write_event_to_stream(ScreenshotEvent(screenshot=screenshot))
136
137
  await ctx.set("screenshot", screenshot)
137
138
 
138
139
  try:
@@ -168,11 +169,15 @@ class PlannerAgent(Workflow):
168
169
  try:
169
170
  result = await self.executer.execute(ctx, code)
170
171
  logger.info(f"📝 Planning complete")
171
- logger.debug(f" - Planning code executed. Result: {result}")
172
+ logger.debug(f" - Planning code executed. Result: {result['output']}")
173
+
174
+ screenshots = result['screenshots']
175
+ for screenshot in screenshots[:-1]: # the last screenshot will be captured by next step
176
+ ctx.write_event_to_stream(ScreenshotEvent(screenshot=screenshot))
172
177
 
173
178
  await self.chat_memory.aput(
174
179
  ChatMessage(
175
- role="user", content=f"Execution Result:\n```\n{result}\n```"
180
+ role="user", content=f"Execution Result:\n```\n{result['output']}\n```"
176
181
  )
177
182
  )
178
183
 
@@ -241,15 +246,15 @@ wrap your code inside this:
241
246
  logger.debug(f" - Sending {len(chat_history)} messages to LLM.")
242
247
 
243
248
  model = self.llm.class_name()
244
- if model == "DeepSeek":
245
- logger.warning(
246
- "[yellow]DeepSeek doesnt support images. Disabling screenshots[/]"
247
- )
248
-
249
- elif self.vision == True:
250
- chat_history = await chat_utils.add_screenshot_image_block(
251
- await ctx.get("screenshot"), chat_history
252
- )
249
+ if self.vision == True:
250
+ if model == "DeepSeek":
251
+ logger.warning(
252
+ "[yellow]DeepSeek doesnt support images. Disabling screenshots[/]"
253
+ )
254
+ else:
255
+ chat_history = await chat_utils.add_screenshot_image_block(
256
+ await ctx.get("screenshot"), chat_history
257
+ )
253
258
 
254
259
 
255
260
 
@@ -208,7 +208,7 @@ async def add_task_history_block(completed_tasks: list[dict], failed_tasks: list
208
208
  all_tasks = completed_tasks + failed_tasks
209
209
 
210
210
  if all_tasks:
211
- task_history += "Task History (chronological order):\n"
211
+ task_history += "### Task Execution History (chronological):\n"
212
212
  for i, task in enumerate(all_tasks, 1):
213
213
  if hasattr(task, 'description'):
214
214
  status_indicator = "[success]" if hasattr(task, 'status') and task.status == "completed" else "[failed]"
@@ -99,10 +99,13 @@ class SimpleCodeExecutor:
99
99
  """
100
100
  # Update UI elements before execution
101
101
  self.globals['ui_state'] = await ctx.get("ui_state", None)
102
-
102
+ self.globals['step_screenshots'] = []
103
+ self.globals['step_ui_states'] = []
104
+
103
105
  if self.tools_instance and isinstance(self.tools_instance, AdbTools):
104
106
  self.tools_instance._set_context(ctx)
105
107
 
108
+ # Capture stdout and stderr
106
109
  stdout = io.StringIO()
107
110
  stderr = io.StringIO()
108
111
 
@@ -137,4 +140,9 @@ class SimpleCodeExecutor:
137
140
  output = f"Error: {type(e).__name__}: {str(e)}\n"
138
141
  output += traceback.format_exc()
139
142
 
140
- return output
143
+ result = {
144
+ 'output': output,
145
+ 'screenshots': self.globals['step_screenshots'],
146
+ 'ui_states': self.globals['step_ui_states']
147
+ }
148
+ return result
droidrun/cli/main.py CHANGED
@@ -22,6 +22,8 @@ from droidrun.portal import (
22
22
  enable_portal_accessibility,
23
23
  PORTAL_PACKAGE_NAME,
24
24
  ping_portal,
25
+ ping_portal_tcp,
26
+ ping_portal_content,
25
27
  )
26
28
  from droidrun.macro.cli import macro_cli
27
29
 
@@ -78,9 +80,9 @@ async def run_command(
78
80
  reasoning: bool,
79
81
  reflection: bool,
80
82
  tracing: bool,
81
- debug: bool,
83
+ debug: bool,
82
84
  use_tcp: bool,
83
- save_trajectory: bool = False,
85
+ save_trajectory: str = "none",
84
86
  ios: bool = False,
85
87
  allow_drag: bool = False,
86
88
  **kwargs,
@@ -104,7 +106,7 @@ async def run_command(
104
106
  # Device setup
105
107
  if device is None and not ios:
106
108
  logger.info("🔍 Finding connected device...")
107
-
109
+
108
110
  devices = adb.list()
109
111
  if not devices:
110
112
  raise ValueError("No connected devices found.")
@@ -117,10 +119,14 @@ async def run_command(
117
119
  else:
118
120
  logger.info(f"📱 Using device: {device}")
119
121
 
120
- tools = AdbTools(serial=device, use_tcp=use_tcp) if not ios else IOSTools(url=device)
122
+ tools = (
123
+ AdbTools(serial=device, use_tcp=use_tcp)
124
+ if not ios
125
+ else IOSTools(url=device)
126
+ )
121
127
  # Set excluded tools based on CLI flags
122
128
  excluded_tools = [] if allow_drag else ["drag"]
123
-
129
+
124
130
  # Select personas based on --drag flag
125
131
  personas = [BIG_AGENT] if allow_drag else [DEFAULT]
126
132
 
@@ -144,7 +150,6 @@ async def run_command(
144
150
  if tracing:
145
151
  logger.info("🔍 Tracing enabled")
146
152
 
147
-
148
153
  droid_agent = DroidAgent(
149
154
  goal=command,
150
155
  llm=llm,
@@ -254,13 +259,16 @@ class DroidRunCLI(click.Group):
254
259
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
255
260
  )
256
261
  @click.option(
257
- "--use-tcp", is_flag=True, help="Use TCP communication for device control", default=False
262
+ "--use-tcp",
263
+ is_flag=True,
264
+ help="Use TCP communication for device control",
265
+ default=False,
258
266
  )
259
267
  @click.option(
260
268
  "--save-trajectory",
261
- is_flag=True,
262
- help="Save agent trajectory to file",
263
- default=False,
269
+ type=click.Choice(["none", "step", "action"]),
270
+ help="Trajectory saving level: none (no saving), step (save per step), action (save per action)",
271
+ default="none",
264
272
  )
265
273
  @click.group(cls=DroidRunCLI)
266
274
  def cli(
@@ -277,7 +285,7 @@ def cli(
277
285
  tracing: bool,
278
286
  debug: bool,
279
287
  use_tcp: bool,
280
- save_trajectory: bool,
288
+ save_trajectory: str,
281
289
  ):
282
290
  """DroidRun - Control your Android device through LLM agents."""
283
291
  pass
@@ -333,13 +341,16 @@ def cli(
333
341
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
334
342
  )
335
343
  @click.option(
336
- "--use-tcp", is_flag=True, help="Use TCP communication for device control", default=False
344
+ "--use-tcp",
345
+ is_flag=True,
346
+ help="Use TCP communication for device control",
347
+ default=False,
337
348
  )
338
349
  @click.option(
339
350
  "--save-trajectory",
340
- is_flag=True,
341
- help="Save agent trajectory to file",
342
- default=False,
351
+ type=click.Choice(["none", "step", "action"]),
352
+ help="Trajectory saving level: none (no saving), step (save per step), action (save per action)",
353
+ default="none",
343
354
  )
344
355
  @click.option(
345
356
  "--drag",
@@ -364,7 +375,7 @@ def run(
364
375
  tracing: bool,
365
376
  debug: bool,
366
377
  use_tcp: bool,
367
- save_trajectory: bool,
378
+ save_trajectory: str,
368
379
  allow_drag: bool,
369
380
  ios: bool,
370
381
  ):
@@ -437,7 +448,11 @@ def disconnect(serial: str):
437
448
 
438
449
  @cli.command()
439
450
  @click.option("--device", "-d", help="Device serial number or IP address", default=None)
440
- @click.option("--path", help="Path to the Droidrun Portal APK to install on the device. If not provided, the latest portal apk version will be downloaded and installed.", default=None)
451
+ @click.option(
452
+ "--path",
453
+ help="Path to the Droidrun Portal APK to install on the device. If not provided, the latest portal apk version will be downloaded and installed.",
454
+ default=None,
455
+ )
441
456
  @click.option(
442
457
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
443
458
  )
@@ -474,7 +489,9 @@ def setup(path: str | None, device: str | None, debug: bool):
474
489
 
475
490
  console.print(f"[bold blue]Step 1/2: Installing APK:[/] {apk_path}")
476
491
  try:
477
- device_obj.install(apk_path, uninstall=True, flags=["-g"], silent=not debug)
492
+ device_obj.install(
493
+ apk_path, uninstall=True, flags=["-g"], silent=not debug
494
+ )
478
495
  except Exception as e:
479
496
  console.print(f"[bold red]Installation failed:[/] {e}")
480
497
  return
@@ -499,9 +516,7 @@ def setup(path: str | None, device: str | None, debug: bool):
499
516
  "[yellow]Opening accessibility settings for manual configuration...[/]"
500
517
  )
501
518
 
502
- device_obj.shell(
503
- "am start -a android.settings.ACCESSIBILITY_SETTINGS"
504
- )
519
+ device_obj.shell("am start -a android.settings.ACCESSIBILITY_SETTINGS")
505
520
 
506
521
  console.print(
507
522
  "\n[yellow]Please complete the following steps on your device:[/]"
@@ -530,10 +545,16 @@ def setup(path: str | None, device: str | None, debug: bool):
530
545
 
531
546
  @cli.command()
532
547
  @click.option("--device", "-d", help="Device serial number or IP address", default=None)
548
+ @click.option(
549
+ "--use-tcp",
550
+ is_flag=True,
551
+ help="Use TCP communication for device control",
552
+ default=False,
553
+ )
533
554
  @click.option(
534
555
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
535
556
  )
536
- def ping(device: str | None, debug: bool):
557
+ def ping(device: str | None, use_tcp: bool, debug: bool):
537
558
  """Ping a device to check if it is ready and accessible."""
538
559
  try:
539
560
  device_obj = adb.device(device)
@@ -542,6 +563,12 @@ def ping(device: str | None, debug: bool):
542
563
  return
543
564
 
544
565
  ping_portal(device_obj, debug)
566
+
567
+ if use_tcp:
568
+ ping_portal_tcp(device_obj, debug)
569
+ else:
570
+ ping_portal_content(device_obj, debug)
571
+
545
572
  console.print(
546
573
  "[bold green]Portal is installed and accessible. You're good to go![/]"
547
574
  )
droidrun/macro/cli.py CHANGED
@@ -215,7 +215,7 @@ def list(directory: str, debug: bool):
215
215
 
216
216
  # Still use console for table display as it's structured data
217
217
  console.print(table)
218
- logger.info(f"💡 Use 'droidrun-macro replay {directory}/<folder>' to replay a trajectory")
218
+ logger.info(f"💡 Use 'droidrun macro replay {directory}/<folder>' to replay a trajectory")
219
219
 
220
220
  except Exception as e:
221
221
  logger.error(f"💥 Error: {e}")
droidrun/portal.py CHANGED
@@ -3,6 +3,8 @@ import tempfile
3
3
  import os
4
4
  import contextlib
5
5
  from adbutils import adb, AdbDevice
6
+ from droidrun.tools import AdbTools
7
+ from rich.console import Console
6
8
 
7
9
  REPO = "droidrun/droidrun-portal"
8
10
  ASSET_NAME = "droidrun-portal"
@@ -36,8 +38,10 @@ def get_latest_release_assets(debug: bool = False):
36
38
 
37
39
  @contextlib.contextmanager
38
40
  def download_portal_apk(debug: bool = False):
41
+ console = Console()
39
42
  assets = get_latest_release_assets(debug)
40
43
 
44
+ asset_version = None
41
45
  asset_url = None
42
46
  for asset in assets:
43
47
  if (
@@ -46,11 +50,15 @@ def download_portal_apk(debug: bool = False):
46
50
  and asset["name"].startswith(ASSET_NAME)
47
51
  ):
48
52
  asset_url = asset["browser_download_url"]
53
+ asset_version = asset["name"].split("-")[-1]
54
+ asset_version = asset_version.removesuffix(".apk")
49
55
  break
50
56
  elif "downloadUrl" in asset and os.path.basename(
51
57
  asset["downloadUrl"]
52
58
  ).startswith(ASSET_NAME):
53
59
  asset_url = asset["downloadUrl"]
60
+ asset_version: str = asset["name"].split("-")[-1]
61
+ asset_version = asset_version.removesuffix(".apk")
54
62
  break
55
63
  else:
56
64
  if debug:
@@ -59,6 +67,10 @@ def download_portal_apk(debug: bool = False):
59
67
  if not asset_url:
60
68
  raise Exception(f"Asset named '{ASSET_NAME}' not found in the latest release.")
61
69
 
70
+ console.print(f"Found Portal APK [bold]{asset_version}[/bold]")
71
+ if debug:
72
+ console.print(f"Asset URL: {asset_url}")
73
+
62
74
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".apk")
63
75
  try:
64
76
  r = requests.get(asset_url, stream=True)
@@ -81,9 +93,7 @@ def enable_portal_accessibility(device: AdbDevice):
81
93
 
82
94
 
83
95
  def check_portal_accessibility(device: AdbDevice, debug: bool = False) -> bool:
84
- a11y_services = device.shell(
85
- "settings get secure enabled_accessibility_services"
86
- )
96
+ a11y_services = device.shell("settings get secure enabled_accessibility_services")
87
97
  if not A11Y_SERVICE_NAME in a11y_services:
88
98
  if debug:
89
99
  print(a11y_services)
@@ -118,17 +128,23 @@ def ping_portal(device: AdbDevice, debug: bool = False):
118
128
  "Droidrun Portal is not enabled as an accessibility service on the device"
119
129
  )
120
130
 
131
+
132
+ def ping_portal_content(device: AdbDevice, debug: bool = False):
121
133
  try:
122
- state = device.shell(
123
- "content query --uri content://com.droidrun.portal/state"
124
- )
134
+ state = device.shell("content query --uri content://com.droidrun.portal/state")
125
135
  if not "Row: 0 result=" in state:
126
136
  raise Exception("Failed to get state from Droidrun Portal")
127
-
128
137
  except Exception as e:
129
138
  raise Exception(f"Droidrun Portal is not reachable: {e}")
130
139
 
131
140
 
141
+ def ping_portal_tcp(device: AdbDevice, debug: bool = False):
142
+ try:
143
+ tools = AdbTools(serial=device.serial, use_tcp=True)
144
+ except Exception as e:
145
+ raise Exception(f"Failed to setup TCP forwarding: {e}")
146
+
147
+
132
148
  def test():
133
149
  device = adb.device()
134
150
  ping_portal(device, debug=False)
@@ -17,7 +17,7 @@ class DroidAgentInitEvent(TelemetryEvent):
17
17
  reflection: bool
18
18
  enable_tracing: bool
19
19
  debug: bool
20
- save_trajectories: bool
20
+ save_trajectories: str
21
21
 
22
22
 
23
23
  class DroidAgentFinalizeEvent(TelemetryEvent):
droidrun/tools/adb.py CHANGED
@@ -8,7 +8,7 @@ import json
8
8
  import time
9
9
  import logging
10
10
  from llama_index.core.workflow import Context
11
- from typing_extensions import Optional, Dict, Tuple, List, Any, Type, Self
11
+ from typing import Optional, Dict, Tuple, List, Any
12
12
  from droidrun.agent.common.events import (
13
13
  InputTextActionEvent,
14
14
  KeyPressActionEvent,
@@ -23,13 +23,17 @@ import requests
23
23
  import base64
24
24
 
25
25
  logger = logging.getLogger("droidrun-tools")
26
+ PORTAL_DEFAULT_TCP_PORT = 8080
26
27
 
27
28
 
28
29
  class AdbTools(Tools):
29
30
  """Core UI interaction tools for Android device control."""
30
31
 
31
32
  def __init__(
32
- self, serial: str | None = None, use_tcp: bool = False, tcp_port: int = 8080
33
+ self,
34
+ serial: str | None = None,
35
+ use_tcp: bool = False,
36
+ remote_tcp_port: int = PORTAL_DEFAULT_TCP_PORT,
33
37
  ) -> None:
34
38
  """Initialize the AdbTools instance.
35
39
 
@@ -40,8 +44,7 @@ class AdbTools(Tools):
40
44
  """
41
45
  self.device = adb.device(serial=serial)
42
46
  self.use_tcp = use_tcp
43
- self.tcp_port = tcp_port
44
- self.tcp_base_url = f"http://localhost:{tcp_port}"
47
+ self.remote_tcp_port = remote_tcp_port
45
48
  self.tcp_forwarded = False
46
49
 
47
50
  self._ctx = None
@@ -55,6 +58,8 @@ class AdbTools(Tools):
55
58
  self.memory: List[str] = []
56
59
  # Store all screenshots with timestamps
57
60
  self.screenshots: List[Dict[str, Any]] = []
61
+ # Trajectory saving level
62
+ self.save_trajectories = "none"
58
63
 
59
64
  # Set up TCP forwarding if requested
60
65
  if self.use_tcp:
@@ -69,18 +74,21 @@ class AdbTools(Tools):
69
74
  """
70
75
  try:
71
76
  logger.debug(
72
- f"Setting up TCP port forwarding: tcp:{self.tcp_port} tcp:{self.tcp_port}"
77
+ f"Setting up TCP port forwarding for port tcp:{self.remote_tcp_port} on device {self.device.serial}"
73
78
  )
74
79
  # Use adb forward command to set up port forwarding
75
- result = self.device.forward(f"tcp:{self.tcp_port}", f"tcp:{self.tcp_port}")
76
- self.tcp_forwarded = True
77
- logger.debug(f"TCP port forwarding set up successfully: {result}")
80
+ self.local_tcp_port = self.device.forward_port(self.remote_tcp_port)
81
+ self.tcp_base_url = f"http://localhost:{self.local_tcp_port}"
82
+ logger.debug(
83
+ f"TCP port forwarding set up successfully to {self.tcp_base_url}"
84
+ )
78
85
 
79
86
  # Test the connection with a ping
80
87
  try:
81
88
  response = requests.get(f"{self.tcp_base_url}/ping", timeout=5)
82
89
  if response.status_code == 200:
83
90
  logger.debug("TCP connection test successful")
91
+ self.tcp_forwarded = True
84
92
  return True
85
93
  else:
86
94
  logger.warning(
@@ -105,10 +113,17 @@ class AdbTools(Tools):
105
113
  """
106
114
  try:
107
115
  if self.tcp_forwarded:
108
- logger.debug(f"Removing TCP port forwarding for port {self.tcp_port}")
109
- result = self.device.forward_remove(f"tcp:{self.tcp_port}")
116
+ logger.debug(
117
+ f"Removing TCP port forwarding for port {self.local_tcp_port}"
118
+ )
119
+ # remove forwarding
120
+ cmd = f"killforward:tcp:{self.local_tcp_port}"
121
+ logger.debug(f"Removing TCP port forwarding: {cmd}")
122
+ c = self.device.open_transport(cmd)
123
+ c.close()
124
+
110
125
  self.tcp_forwarded = False
111
- logger.debug(f"TCP port forwarding removed: {result}")
126
+ logger.debug(f"TCP port forwarding removed")
112
127
  return True
113
128
  return True
114
129
  except Exception as e:
@@ -170,6 +185,7 @@ class AdbTools(Tools):
170
185
  except json.JSONDecodeError:
171
186
  return None
172
187
 
188
+ @Tools.ui_action
173
189
  def tap_by_index(self, index: int) -> str:
174
190
  """
175
191
  Tap on a UI element by its index.
@@ -328,6 +344,7 @@ class AdbTools(Tools):
328
344
  """
329
345
  return self.tap_by_index(index)
330
346
 
347
+ @Tools.ui_action
331
348
  def swipe(
332
349
  self,
333
350
  start_x: int,
@@ -372,6 +389,7 @@ class AdbTools(Tools):
372
389
  print(f"Error: {str(e)}")
373
390
  return False
374
391
 
392
+ @Tools.ui_action
375
393
  def drag(
376
394
  self, start_x: int, start_y: int, end_x: int, end_y: int, duration: float = 3
377
395
  ) -> bool:
@@ -413,6 +431,7 @@ class AdbTools(Tools):
413
431
  print(f"Error: {str(e)}")
414
432
  return False
415
433
 
434
+ @Tools.ui_action
416
435
  def input_text(self, text: str) -> str:
417
436
  """
418
437
  Input text on the device.
@@ -427,59 +446,59 @@ class AdbTools(Tools):
427
446
  try:
428
447
  logger.debug(f"Inputting text: {text}")
429
448
 
430
- if self.use_tcp and self.tcp_forwarded:
431
- # Use TCP communication
432
- encoded_text = base64.b64encode(text.encode()).decode()
433
-
434
- payload = {"base64_text": encoded_text}
435
- response = requests.post(
436
- f"{self.tcp_base_url}/keyboard/input",
437
- json=payload,
438
- headers={"Content-Type": "application/json"},
439
- timeout=10,
440
- )
441
-
442
- logger.debug(
443
- f"Keyboard input TCP response: {response.status_code}, {response.text}"
444
- )
445
-
446
- if response.status_code != 200:
447
- return f"Error: HTTP request failed with status {response.status_code}: {response.text}"
448
-
449
- else:
450
- # Fallback to content provider method
451
- # Save the current keyboard
452
- original_ime = self.device.shell(
453
- "settings get secure default_input_method"
454
- )
455
- original_ime = original_ime.strip()
449
+ # if self.use_tcp and self.tcp_forwarded:
450
+ # # Use TCP communication
451
+ # encoded_text = base64.b64encode(text.encode()).decode()
452
+
453
+ # payload = {"base64_text": encoded_text}
454
+ # response = requests.post(
455
+ # f"{self.tcp_base_url}/keyboard/input",
456
+ # json=payload,
457
+ # headers={"Content-Type": "application/json"},
458
+ # timeout=10,
459
+ # )
460
+
461
+ # logger.debug(
462
+ # f"Keyboard input TCP response: {response.status_code}, {response.text}"
463
+ # )
464
+
465
+ # if response.status_code != 200:
466
+ # return f"Error: HTTP request failed with status {response.status_code}: {response.text}"
467
+
468
+ # else:
469
+ # Fallback to content provider method
470
+ # Save the current keyboard
471
+ original_ime = self.device.shell(
472
+ "settings get secure default_input_method"
473
+ )
474
+ original_ime = original_ime.strip()
456
475
 
457
- # Enable the Droidrun keyboard
458
- self.device.shell("ime enable com.droidrun.portal/.DroidrunKeyboardIME")
476
+ # Enable the Droidrun keyboard
477
+ self.device.shell("ime enable com.droidrun.portal/.DroidrunKeyboardIME")
459
478
 
460
- # Set the Droidrun keyboard as the default
461
- self.device.shell("ime set com.droidrun.portal/.DroidrunKeyboardIME")
479
+ # Set the Droidrun keyboard as the default
480
+ self.device.shell("ime set com.droidrun.portal/.DroidrunKeyboardIME")
462
481
 
463
- # Wait for keyboard to change
464
- time.sleep(1)
482
+ # Wait for keyboard to change
483
+ time.sleep(1)
465
484
 
466
- # Encode the text to Base64
467
- encoded_text = base64.b64encode(text.encode()).decode()
485
+ # Encode the text to Base64
486
+ encoded_text = base64.b64encode(text.encode()).decode()
468
487
 
469
- cmd = f'content insert --uri "content://com.droidrun.portal/keyboard/input" --bind base64_text:s:"{encoded_text}"'
470
- self.device.shell(cmd)
488
+ cmd = f'content insert --uri "content://com.droidrun.portal/keyboard/input" --bind base64_text:s:"{encoded_text}"'
489
+ self.device.shell(cmd)
471
490
 
472
- # Wait for text input to complete
473
- time.sleep(0.5)
491
+ # Wait for text input to complete
492
+ time.sleep(0.5)
474
493
 
475
- # Restore the original keyboard
476
- if original_ime and "com.droidrun.portal" not in original_ime:
477
- self.device.shell(f"ime set {original_ime}")
494
+ # Restore the original keyboard
495
+ if original_ime and "com.droidrun.portal" not in original_ime:
496
+ self.device.shell(f"ime set {original_ime}")
478
497
 
479
- logger.debug(
480
- f"Text input completed: {text[:50]}{'...' if len(text) > 50 else ''}"
481
- )
482
- return f"Text input completed: {text[:50]}{'...' if len(text) > 50 else ''}"
498
+ logger.debug(
499
+ f"Text input completed: {text[:50]}{'...' if len(text) > 50 else ''}"
500
+ )
501
+ return f"Text input completed: {text[:50]}{'...' if len(text) > 50 else ''}"
483
502
 
484
503
  if self._ctx:
485
504
  input_event = InputTextActionEvent(
@@ -501,6 +520,7 @@ class AdbTools(Tools):
501
520
  except Exception as e:
502
521
  return f"Error sending text input: {str(e)}"
503
522
 
523
+ @Tools.ui_action
504
524
  def back(self) -> str:
505
525
  """
506
526
  Go back on the current view.
@@ -523,6 +543,7 @@ class AdbTools(Tools):
523
543
  except ValueError as e:
524
544
  return f"Error: {str(e)}"
525
545
 
546
+ @Tools.ui_action
526
547
  def press_key(self, keycode: int) -> str:
527
548
  """
528
549
  Press a key on the Android device.
@@ -561,6 +582,7 @@ class AdbTools(Tools):
561
582
  except ValueError as e:
562
583
  return f"Error: {str(e)}"
563
584
 
585
+ @Tools.ui_action
564
586
  def start_app(self, package: str, activity: str | None = None) -> str:
565
587
  """
566
588
  Start an app on the device.
@@ -634,48 +656,13 @@ class AdbTools(Tools):
634
656
  try:
635
657
  logger.debug("Taking screenshot")
636
658
 
637
- if self.use_tcp and self.tcp_forwarded:
638
- # Use TCP communication
639
- response = requests.get(f"{self.tcp_base_url}/screenshot", timeout=15)
640
-
641
- if response.status_code == 200:
642
- tcp_response = response.json()
643
-
644
- # Check if response has the expected format with data field
645
- if isinstance(tcp_response, dict) and "data" in tcp_response:
646
- base64_data = tcp_response["data"]
647
- try:
648
- # Decode base64 to get image bytes
649
- image_bytes = base64.b64decode(base64_data)
650
- img_format = "PNG" # Assuming PNG format from TCP endpoint
651
- logger.debug("Screenshot taken via TCP")
652
- except Exception as e:
653
- raise ValueError(
654
- f"Failed to decode base64 screenshot data: {str(e)}"
655
- )
656
- else:
657
- # Fallback: assume direct base64 format
658
- try:
659
- image_bytes = base64.b64decode(tcp_response)
660
- img_format = "PNG"
661
- logger.debug("Screenshot taken via TCP (direct base64)")
662
- except Exception as e:
663
- raise ValueError(
664
- f"Failed to decode screenshot response: {str(e)}"
665
- )
666
- else:
667
- raise ValueError(
668
- f"HTTP request failed with status {response.status_code}: {response.text}"
669
- )
670
-
671
- else:
672
- # Fallback to ADB screenshot method
673
- img = self.device.screenshot()
674
- img_buf = io.BytesIO()
675
- img_format = "PNG"
676
- img.save(img_buf, format=img_format)
677
- image_bytes = img_buf.getvalue()
678
- logger.debug("Screenshot taken via ADB")
659
+ # Fallback to ADB screenshot method
660
+ img = self.device.screenshot()
661
+ img_buf = io.BytesIO()
662
+ img_format = "PNG"
663
+ img.save(img_buf, format=img_format)
664
+ image_bytes = img_buf.getvalue()
665
+ logger.debug("Screenshot taken via ADB")
679
666
 
680
667
  # Store screenshot with timestamp
681
668
  self.screenshots.append(
@@ -710,6 +697,7 @@ class AdbTools(Tools):
710
697
  except ValueError as e:
711
698
  raise ValueError(f"Error listing packages: {str(e)}")
712
699
 
700
+ @Tools.ui_action
713
701
  def complete(self, success: bool, reason: str = ""):
714
702
  """
715
703
  Mark the task as finished.
droidrun/tools/tools.py CHANGED
@@ -2,6 +2,8 @@ from abc import ABC, abstractmethod
2
2
  from typing import List, Optional, Dict, Any
3
3
  import logging
4
4
  from typing import Tuple, Dict, Callable, Any, Optional
5
+ from functools import wraps
6
+ import sys
5
7
 
6
8
  # Get a logger for this module
7
9
  logger = logging.getLogger(__name__)
@@ -13,6 +15,32 @@ class Tools(ABC):
13
15
  This class provides a common interface for all tools to implement.
14
16
  """
15
17
 
18
+ @staticmethod
19
+ def ui_action(func):
20
+ """"
21
+ Decorator to capture screenshots and UI states for actions that modify the UI.
22
+ """
23
+ @wraps(func)
24
+ def wrapper(*args, **kwargs):
25
+ self = args[0]
26
+ result = func(*args, **kwargs)
27
+
28
+ # Check if save_trajectories attribute exists and is set to "action"
29
+ if hasattr(self, 'save_trajectories') and self.save_trajectories == "action":
30
+ frame = sys._getframe(1)
31
+ caller_globals = frame.f_globals
32
+
33
+ step_screenshots = caller_globals.get('step_screenshots')
34
+ step_ui_states = caller_globals.get('step_ui_states')
35
+
36
+ if step_screenshots is not None:
37
+ step_screenshots.append(self.take_screenshot()[1])
38
+ if step_ui_states is not None:
39
+ step_ui_states.append(self.get_state())
40
+
41
+ return result
42
+ return wrapper
43
+
16
44
  @abstractmethod
17
45
  def get_state(self) -> Dict[str, Any]:
18
46
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: droidrun
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: A framework for controlling Android devices through LLM agents
5
5
  Project-URL: Homepage, https://github.com/droidrun/droidrun
6
6
  Project-URL: Bug Tracker, https://github.com/droidrun/droidrun/issues
@@ -46,7 +46,6 @@ Requires-Dist: posthog==6.0.2
46
46
  Requires-Dist: pydantic>=2.0.0
47
47
  Requires-Dist: python-dotenv>=1.0.0
48
48
  Requires-Dist: rich>=13.0.0
49
- Requires-Dist: typing-extensions
50
49
  Provides-Extra: dev
51
50
  Requires-Dist: bandit>=1.7.0; extra == 'dev'
52
51
  Requires-Dist: black>=23.0.0; extra == 'dev'
@@ -1,9 +1,9 @@
1
1
  droidrun/__init__.py,sha256=Cqt4NXZ-753220dlRZXglMkFT2ygcXSErMmqupGsi9A,622
2
2
  droidrun/__main__.py,sha256=78o1Wr_Z-NrZy9yLWmEfNfRRhAiJGBr4Xi3lmbgkx3w,105
3
- droidrun/portal.py,sha256=C0gZK9GHigsM6Vq2fQP_DKaDXqtnQSA2NUrxClVe6hw,3931
3
+ droidrun/portal.py,sha256=Bg3kVoNLJ2bTdk-4VuUr_i77yWJT2ZNLfT5WMumnZV8,4679
4
4
  droidrun/agent/__init__.py,sha256=4SqTJeGDvr_wT8rtN9J8hnN6P-pae663mkYr-JmzH4w,208
5
5
  droidrun/agent/codeact/__init__.py,sha256=ZLDGT_lTTzyNm7pcBzdyRIGHJ2ZgbInJdhXZRbJLhSQ,278
6
- droidrun/agent/codeact/codeact_agent.py,sha256=XYyRrDAj2MoNb4WTtCzY8bqlI1WDy9TUIRku4NMUezk,16087
6
+ droidrun/agent/codeact/codeact_agent.py,sha256=6Hltg6J4mdb5pOr5sFQVY-6SMWIcxWp6RZjct5nkiCc,16506
7
7
  droidrun/agent/codeact/events.py,sha256=skCfZ-5SR0YhhzZVxx8_VkUjfILk8rCv47k9pHNYhdc,634
8
8
  droidrun/agent/codeact/prompts.py,sha256=28HflWMNkC1ky0hGCzAxhJftjU2IIU1ZRUfya3S7M6I,1006
9
9
  droidrun/agent/common/default.py,sha256=P07el-PrHsoqQMYsYxSSln6mFl-QY75vzwp1Dll_XmY,259
@@ -13,42 +13,42 @@ droidrun/agent/context/agent_persona.py,sha256=Mxd4HTyirWD-aqNlka1hQBdS-0I-lXJr2
13
13
  droidrun/agent/context/context_injection_manager.py,sha256=sA33q2KPtX_4Yap8wM11T6ewlZC_0FIbKPEc400SHrE,2188
14
14
  droidrun/agent/context/episodic_memory.py,sha256=1ImeR3jAWOpKwkQt3bMlXVOBiQbIli5fBIlBq2waREQ,394
15
15
  droidrun/agent/context/reflection.py,sha256=0hJluOz0hTlHHhReKpIJ9HU5aJbaJsvrjMfraQ84D-M,652
16
- droidrun/agent/context/task_manager.py,sha256=ESLs4kR6VNYiYQsc4V7WAeoSLwbaZPSWBXpveOfOv8c,4343
16
+ droidrun/agent/context/task_manager.py,sha256=RqIBBLcw_hLotaO0JFsm8HOdJ_UdNttSGRGl_3GqRY4,4648
17
17
  droidrun/agent/context/personas/__init__.py,sha256=oSRa8g_xngX7JPIRPu7fLO33m3r7fdEQzIuORuqcw5M,232
18
18
  droidrun/agent/context/personas/app_starter.py,sha256=dHeknznxGEPJ7S6VPyEG_MB-HvAvQwUOnRWaShaV8Xo,1585
19
19
  droidrun/agent/context/personas/big_agent.py,sha256=Gl_y4ykz3apGc203-KG2UbSOwf7gDUiWh7GOVyiLn-Y,5091
20
20
  droidrun/agent/context/personas/default.py,sha256=Xm07YCWoKjvlHAbQRtzE3vn7BVcz6wYcSVeg4FiojJQ,5060
21
21
  droidrun/agent/context/personas/ui_expert.py,sha256=j0OKfN1jQSrREHcVeomMTDPCWLsZVX4aeuWN4Y-x3z0,4739
22
22
  droidrun/agent/droid/__init__.py,sha256=3BfUVZiUQ8ATAJ_JmqQZQx53WoERRpQ4AyHW5WOgbRI,297
23
- droidrun/agent/droid/droid_agent.py,sha256=_XtGkkQM27qfxDO8V2MwehWz931Z10CBhtOIoGNsWBk,15252
24
- droidrun/agent/droid/events.py,sha256=Ks2D6lX5P1rpZ4nIAPXSC83z4AT5OzKt3isP5yk25F4,689
23
+ droidrun/agent/droid/droid_agent.py,sha256=nKr8gV6aV_S5T4llhKokSBqL2uKF9ldxcnE29FOGTL8,16737
24
+ droidrun/agent/droid/events.py,sha256=hjYpWcSffqP83rNv_GyOEc3CNSrdvlVPdUkaRU6QDJc,722
25
25
  droidrun/agent/oneflows/reflector.py,sha256=I_tE0PBjvwWbS6SA8Qd41etxJglFgn8oScuKUxc9LEE,11621
26
26
  droidrun/agent/planner/__init__.py,sha256=Fu0Ewtd-dIRLgHIL1DB_9EEKvQS_f1vjB8jgO5TbJXg,364
27
27
  droidrun/agent/planner/events.py,sha256=oyt2FNrA2uVyUeVT65-N0AC6sWBFxSnwNEqWtnRYoFM,390
28
- droidrun/agent/planner/planner_agent.py,sha256=fxLeLeN_2kmaWHXLaDLvXOR_uvP_ult_I8SPZbgL_AU,10482
28
+ droidrun/agent/planner/planner_agent.py,sha256=gqXMj1PcTdmKpSGM1b6T_q7ojFptPgx39wAXuW3ZQDQ,10854
29
29
  droidrun/agent/planner/prompts.py,sha256=Ci7Oeu3J4TAhx-tKGPZ9l6Wb3a81FSqC8cWW4jW73HI,6046
30
30
  droidrun/agent/utils/__init__.py,sha256=JK6ygRjw7gzcQSG0HBEYLoVGH54QQAxJJ7HpIS5mgyc,44
31
31
  droidrun/agent/utils/async_utils.py,sha256=IQBcWPwevm89B7R_UdMXk0unWeNCBA232b5kQGqoxNI,336
32
- droidrun/agent/utils/chat_utils.py,sha256=5oqP2nmKs8sHWP1H_TK82yaxrxWf7FdEbFKASKpR60g,13000
33
- droidrun/agent/utils/executer.py,sha256=UKVd7asr3RvO-YgRflGdG12FFZxDvU6hFxrdV1sWLcU,4892
32
+ droidrun/agent/utils/chat_utils.py,sha256=SNn-wJSyxiirDWDqAeLehHw4ByNX_6dD5Bx3Inc6Sj0,13008
33
+ droidrun/agent/utils/executer.py,sha256=-iwdZpmpF0w116D7A_eDgeV0ZNXSuotVgBkM5lc0BNI,5202
34
34
  droidrun/agent/utils/llm_picker.py,sha256=16tNkNhEM9gD_uivzxLvuaa6s0Tz7Igu-3fxMP2lAtY,5968
35
35
  droidrun/agent/utils/trajectory.py,sha256=PU3nI3Zru580_bK0TvqUaf-5kiWvj6hFoedXkDgTqdc,17047
36
36
  droidrun/cli/__init__.py,sha256=DuwSRtZ8WILPd-nf-fZ7BaBsRgtofoInOF3JtJ9wag0,167
37
37
  droidrun/cli/logs.py,sha256=PsT_VbnOa_sOLXK4KkEJk4AsYCpscqrVoryMmLVwPG0,9714
38
- droidrun/cli/main.py,sha256=UnTocHNd8za9Fj-ppnvJSD8DT3RQ1aqVPPTvAsWRX5g,17734
38
+ droidrun/cli/main.py,sha256=3RWvY19zY9ewzcasndxpul3BUPpMq4yhrtnuAIBkufM,18311
39
39
  droidrun/macro/__init__.py,sha256=333sMt19mA8sfQa4qPKbbCmr8Ej3nvpdxGXUZtVTEqM,344
40
40
  droidrun/macro/__main__.py,sha256=-zj42Bj7309oLPZbNsxZeNwIDaEe7Th1I4zF8yAHasw,193
41
- droidrun/macro/cli.py,sha256=fecato896z9OxZsAzCX7FWgF2TXwM5EnR85EcSGzc4U,9057
41
+ droidrun/macro/cli.py,sha256=GaL1wVWVE_AT0OxHgOJyW-q_kVu4PQ76KZKeYMQdoEk,9057
42
42
  droidrun/macro/replay.py,sha256=q_3ZcHVjvsdDfS2xyt_vuuwXGt9_1t38JD1cPsjzIfU,10764
43
43
  droidrun/telemetry/__init__.py,sha256=D4Mp02iGJH2Tjpv42Bzyo6_WC3NWj9Qy9hQPWFaCkhA,234
44
- droidrun/telemetry/events.py,sha256=S6r6_c2bGTZt6F88m_vREDD_MDhw3Pz4I53lLmsI764,518
44
+ droidrun/telemetry/events.py,sha256=FtPMMDAhxvOiCssJLCCr5FX93ulnPCbiFMhkPh8NLw4,517
45
45
  droidrun/telemetry/tracker.py,sha256=Ljue6zivX8KnadXI9DivrayuWxAnUwbJKvCvNtY1Y4Y,2717
46
46
  droidrun/tools/__init__.py,sha256=9ReauavtSKDQG9ya9_Fr9O0TQnDFixgOPaP5n82_iEk,271
47
- droidrun/tools/adb.py,sha256=XRa9TnuS_0-vqMJvn6RDjCRzu-MvSbdtBVAuhVHdSzo,42083
47
+ droidrun/tools/adb.py,sha256=zFnVydQi42GZ1vh-4HT79dCQVBeCS6VObywyaIUUCmw,40832
48
48
  droidrun/tools/ios.py,sha256=imzojiS6gqz4IKexUEz1ga7-flSOaC5QRpHIJTwcgSQ,21807
49
- droidrun/tools/tools.py,sha256=eXCaFjb_FNJFPrIfKi9W1WghxfrfnsbfutW3f0XlTzw,3720
50
- droidrun-0.3.3.dist-info/METADATA,sha256=9aqH7cPVJ24Snsta0Fkyv8tG9b7dMLZ1H3ZhjcvEScM,6718
51
- droidrun-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
52
- droidrun-0.3.3.dist-info/entry_points.txt,sha256=o259U66js8TIybQ7zs814Oe_LQ_GpZsp6a9Cr-xm5zE,51
53
- droidrun-0.3.3.dist-info/licenses/LICENSE,sha256=s-uxn9qChu-kFdRXUp6v_0HhsaJ_5OANmfNOFVm2zdk,1069
54
- droidrun-0.3.3.dist-info/RECORD,,
49
+ droidrun/tools/tools.py,sha256=rqDe2gRyR45HVM15SwsW1aCTVZo5eleTxlh2hGqCHys,4785
50
+ droidrun-0.3.4.dist-info/METADATA,sha256=C7rh3674hF4xvKES0DfXTQaErn1RckdUBmrNcrito_o,6685
51
+ droidrun-0.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
52
+ droidrun-0.3.4.dist-info/entry_points.txt,sha256=o259U66js8TIybQ7zs814Oe_LQ_GpZsp6a9Cr-xm5zE,51
53
+ droidrun-0.3.4.dist-info/licenses/LICENSE,sha256=s-uxn9qChu-kFdRXUp6v_0HhsaJ_5OANmfNOFVm2zdk,1069
54
+ droidrun-0.3.4.dist-info/RECORD,,