droidrun 0.2.0__py3-none-any.whl → 0.3.1__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 (55) hide show
  1. droidrun/__init__.py +16 -11
  2. droidrun/__main__.py +1 -1
  3. droidrun/adb/__init__.py +3 -3
  4. droidrun/adb/device.py +1 -1
  5. droidrun/adb/manager.py +2 -2
  6. droidrun/agent/__init__.py +6 -0
  7. droidrun/agent/codeact/__init__.py +2 -4
  8. droidrun/agent/codeact/codeact_agent.py +330 -235
  9. droidrun/agent/codeact/events.py +12 -20
  10. droidrun/agent/codeact/prompts.py +0 -52
  11. droidrun/agent/common/default.py +5 -0
  12. droidrun/agent/common/events.py +4 -0
  13. droidrun/agent/context/__init__.py +23 -0
  14. droidrun/agent/context/agent_persona.py +15 -0
  15. droidrun/agent/context/context_injection_manager.py +66 -0
  16. droidrun/agent/context/episodic_memory.py +15 -0
  17. droidrun/agent/context/personas/__init__.py +11 -0
  18. droidrun/agent/context/personas/app_starter.py +44 -0
  19. droidrun/agent/context/personas/default.py +95 -0
  20. droidrun/agent/context/personas/extractor.py +52 -0
  21. droidrun/agent/context/personas/ui_expert.py +107 -0
  22. droidrun/agent/context/reflection.py +20 -0
  23. droidrun/agent/context/task_manager.py +124 -0
  24. droidrun/agent/droid/__init__.py +2 -2
  25. droidrun/agent/droid/droid_agent.py +269 -325
  26. droidrun/agent/droid/events.py +28 -0
  27. droidrun/agent/oneflows/reflector.py +265 -0
  28. droidrun/agent/planner/__init__.py +2 -4
  29. droidrun/agent/planner/events.py +9 -13
  30. droidrun/agent/planner/planner_agent.py +288 -0
  31. droidrun/agent/planner/prompts.py +33 -53
  32. droidrun/agent/utils/__init__.py +3 -0
  33. droidrun/agent/utils/async_utils.py +1 -40
  34. droidrun/agent/utils/chat_utils.py +265 -48
  35. droidrun/agent/utils/executer.py +49 -14
  36. droidrun/agent/utils/llm_picker.py +14 -10
  37. droidrun/agent/utils/trajectory.py +184 -0
  38. droidrun/cli/__init__.py +1 -1
  39. droidrun/cli/logs.py +283 -0
  40. droidrun/cli/main.py +364 -441
  41. droidrun/tools/__init__.py +5 -10
  42. droidrun/tools/{actions.py → adb.py} +381 -412
  43. droidrun/tools/ios.py +596 -0
  44. droidrun/tools/tools.py +95 -0
  45. droidrun-0.3.1.dist-info/METADATA +150 -0
  46. droidrun-0.3.1.dist-info/RECORD +50 -0
  47. droidrun/agent/planner/task_manager.py +0 -355
  48. droidrun/agent/planner/workflow.py +0 -371
  49. droidrun/tools/device.py +0 -29
  50. droidrun/tools/loader.py +0 -60
  51. droidrun-0.2.0.dist-info/METADATA +0 -373
  52. droidrun-0.2.0.dist-info/RECORD +0 -32
  53. {droidrun-0.2.0.dist-info → droidrun-0.3.1.dist-info}/WHEEL +0 -0
  54. {droidrun-0.2.0.dist-info → droidrun-0.3.1.dist-info}/entry_points.txt +0 -0
  55. {droidrun-0.2.0.dist-info → droidrun-0.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -2,9 +2,16 @@ import io
2
2
  import contextlib
3
3
  import ast
4
4
  import traceback
5
+ import logging
5
6
  from typing import Any, Dict
6
- from .async_utils import async_to_sync
7
+ from droidrun.agent.utils.async_utils import async_to_sync
8
+ from llama_index.core.workflow import Context
7
9
  import asyncio
10
+ from asyncio import AbstractEventLoop
11
+ import threading
12
+
13
+ logger = logging.getLogger("droidrun")
14
+
8
15
 
9
16
  class SimpleCodeExecutor:
10
17
  """
@@ -16,7 +23,14 @@ class SimpleCodeExecutor:
16
23
  NOTE: not safe for production use! Use with caution.
17
24
  """
18
25
 
19
- def __init__(self, loop, locals: Dict[str, Any] = {}, globals: Dict[str, Any] = {}, tools = {}, use_same_scope: bool = True):
26
+ def __init__(
27
+ self,
28
+ loop: AbstractEventLoop,
29
+ locals: Dict[str, Any] = {},
30
+ globals: Dict[str, Any] = {},
31
+ tools={},
32
+ use_same_scope: bool = True,
33
+ ):
20
34
  """
21
35
  Initialize the code executor.
22
36
 
@@ -28,9 +42,12 @@ class SimpleCodeExecutor:
28
42
 
29
43
  # loop throught tools and add them to globals, but before that check if tool value is async, if so convert it to sync. tools is a dictionary of tool name: function
30
44
  # e.g. tools = {'tool_name': tool_function}
31
-
45
+
32
46
  # check if tools is a dictionary
33
47
  if isinstance(tools, dict):
48
+ logger.debug(
49
+ f"🔧 Initializing SimpleCodeExecutor with tools: {tools.items()}"
50
+ )
34
51
  for tool_name, tool_function in tools.items():
35
52
  if asyncio.iscoroutinefunction(tool_function):
36
53
  # If the function is async, convert it to sync
@@ -38,6 +55,7 @@ class SimpleCodeExecutor:
38
55
  # Add the tool to globals
39
56
  globals[tool_name] = tool_function
40
57
  elif isinstance(tools, list):
58
+ logger.debug(f"🔧 Initializing SimpleCodeExecutor with tools: {tools}")
41
59
  # If tools is a list, convert it to a dictionary with tool name as key and function as value
42
60
  for tool in tools:
43
61
  if asyncio.iscoroutinefunction(tool):
@@ -48,20 +66,22 @@ class SimpleCodeExecutor:
48
66
  else:
49
67
  raise ValueError("Tools must be a dictionary or a list of functions.")
50
68
 
51
-
52
- # add time to globals
53
69
  import time
54
- globals['time'] = time
55
- # State that persists between executions
70
+
71
+ globals["time"] = time
72
+
56
73
  self.globals = globals
57
74
  self.locals = locals
58
75
  self.loop = loop
59
76
  self.use_same_scope = use_same_scope
60
77
  if self.use_same_scope:
61
78
  # If using the same scope, set the globals and locals to the same dictionary
62
- self.globals = self.locals = {**self.locals, **{k: v for k, v in self.globals.items() if k not in self.locals}}
79
+ self.globals = self.locals = {
80
+ **self.locals,
81
+ **{k: v for k, v in self.globals.items() if k not in self.locals},
82
+ }
63
83
 
64
- async def execute(self, code: str) -> str:
84
+ async def execute(self, ctx: Context, code: str) -> str:
65
85
  """
66
86
  Execute Python code and capture output and return values.
67
87
 
@@ -71,6 +91,9 @@ class SimpleCodeExecutor:
71
91
  Returns:
72
92
  str: Output from the execution, including print statements.
73
93
  """
94
+ # Update UI elements before execution
95
+ self.globals['ui_state'] = await ctx.get("ui_state", None)
96
+
74
97
  # Capture stdout and stderr
75
98
  stdout = io.StringIO()
76
99
  stderr = io.StringIO()
@@ -78,20 +101,32 @@ class SimpleCodeExecutor:
78
101
  output = ""
79
102
  try:
80
103
  # Execute with captured output
81
- with contextlib.redirect_stdout(
82
- stdout
83
- ), contextlib.redirect_stderr(stderr):
104
+ thread_exception = []
105
+ with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
106
+
107
+ def execute_code():
108
+ try:
109
+ exec(code, self.globals, self.locals)
110
+ except Exception as e:
111
+ import traceback
112
+
113
+ thread_exception.append((e, traceback.format_exc()))
84
114
 
85
- exec(code, self.globals, self.locals)
115
+ t = threading.Thread(target=execute_code)
116
+ t.start()
117
+ t.join()
86
118
 
87
119
  # Get output
88
120
  output = stdout.getvalue()
89
121
  if stderr.getvalue():
90
122
  output += "\n" + stderr.getvalue()
123
+ if thread_exception:
124
+ e, tb = thread_exception[0]
125
+ output += f"\nError: {type(e).__name__}: {str(e)}\n{tb}"
91
126
 
92
127
  except Exception as e:
93
128
  # Capture exception information
94
129
  output = f"Error: {type(e).__name__}: {str(e)}\n"
95
130
  output += traceback.format_exc()
96
131
 
97
- return output
132
+ return output
@@ -3,8 +3,7 @@ import logging
3
3
  from typing import Any
4
4
  from llama_index.core.llms.llm import LLM
5
5
  # Configure logging
6
- logger = logging.getLogger(__name__)
7
- logger.addHandler(logging.NullHandler())
6
+ logger = logging.getLogger("droidrun")
8
7
 
9
8
  def load_llm(provider_name: str, **kwargs: Any) -> LLM:
10
9
  """
@@ -32,6 +31,8 @@ def load_llm(provider_name: str, **kwargs: Any) -> LLM:
32
31
  raise ValueError("provider_name cannot be empty.")
33
32
  if provider_name == "OpenAILike":
34
33
  module_provider_part = "openai_like"
34
+ elif provider_name == "GoogleGenAI":
35
+ module_provider_part = "google_genai"
35
36
  else:
36
37
  # Use lowercase for module path, handle hyphens for package name suggestion
37
38
  lower_provider_name = provider_name.lower()
@@ -44,9 +45,9 @@ def load_llm(provider_name: str, **kwargs: Any) -> LLM:
44
45
  install_package_name = f"llama-index-llms-{module_provider_part.replace('_', '-')}"
45
46
 
46
47
  try:
47
- logger.info(f"Attempting to import module: {module_path}")
48
+ logger.debug(f"Attempting to import module: {module_path}")
48
49
  llm_module = importlib.import_module(module_path)
49
- logger.info(f"Successfully imported module: {module_path}")
50
+ logger.debug(f"Successfully imported module: {module_path}")
50
51
 
51
52
  except ModuleNotFoundError:
52
53
  logger.error(f"Module '{module_path}' not found. Try: pip install {install_package_name}")
@@ -55,18 +56,21 @@ def load_llm(provider_name: str, **kwargs: Any) -> LLM:
55
56
  ) from None
56
57
 
57
58
  try:
58
- logger.info(f"Attempting to get class '{provider_name}' from module {module_path}")
59
+ logger.debug(f"Attempting to get class '{provider_name}' from module {module_path}")
59
60
  llm_class = getattr(llm_module, provider_name)
60
- logger.info(f"Found class: {llm_class.__name__}")
61
+ logger.debug(f"Found class: {llm_class.__name__}")
61
62
 
62
63
  # Verify the class is a subclass of LLM
63
64
  if not isinstance(llm_class, type) or not issubclass(llm_class, LLM):
64
65
  raise TypeError(f"Class '{provider_name}' found in '{module_path}' is not a valid LLM subclass.")
65
66
 
67
+ # Filter out None values from kwargs
68
+ filtered_kwargs = {k: v for k, v in kwargs.items() if v is not None}
69
+
66
70
  # Initialize
67
- logger.info(f"Initializing {llm_class.__name__} with kwargs: {list(kwargs.keys())}")
68
- llm_instance = llm_class(**kwargs)
69
- logger.info(f"Successfully loaded and initialized LLM: {provider_name}")
71
+ logger.debug(f"Initializing {llm_class.__name__} with kwargs: {list(filtered_kwargs.keys())}")
72
+ llm_instance = llm_class(**filtered_kwargs)
73
+ logger.debug(f"Successfully loaded and initialized LLM: {provider_name}")
70
74
  if not llm_instance:
71
75
  raise RuntimeError(f"Failed to initialize LLM instance for {provider_name}.")
72
76
  return llm_instance
@@ -81,7 +85,7 @@ def load_llm(provider_name: str, **kwargs: Any) -> LLM:
81
85
  raise # Re-raise TypeError (could be from issubclass check or __init__)
82
86
  except Exception as e:
83
87
  logger.error(f"An unexpected error occurred initializing {provider_name}: {e}")
84
- raise RuntimeError(f"Failed to initialize LLM '{provider_name}'.") from e
88
+ raise e
85
89
 
86
90
  # --- Example Usage ---
87
91
  if __name__ == "__main__":
@@ -0,0 +1,184 @@
1
+ """
2
+ Trajectory utilities for DroidRun agents.
3
+
4
+ This module provides helper functions for working with agent trajectories,
5
+ including saving, loading, and analyzing them.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ import time
12
+ from typing import Dict, List, Any
13
+ from PIL import Image
14
+ import io
15
+ from llama_index.core.workflow import Event
16
+
17
+ logger = logging.getLogger("droidrun")
18
+
19
+ class Trajectory:
20
+
21
+ def __init__(self):
22
+ """Initializes an empty trajectory class."""
23
+ self.events: List[Event] = []
24
+ self.screenshots: List[bytes] = []
25
+
26
+
27
+ def create_screenshot_gif(self, output_path: str, duration: int = 1000) -> str:
28
+ """
29
+ Create a GIF from a list of screenshots.
30
+
31
+ Args:
32
+ output_path: Base path for the GIF (without extension)
33
+ duration: Duration for each frame in milliseconds
34
+
35
+ Returns:
36
+ Path to the created GIF file
37
+ """
38
+ if len(self.screenshots) == 0:
39
+ return None
40
+
41
+ images = []
42
+ for screenshot in self.screenshots:
43
+ img_data = screenshot
44
+ img = Image.open(io.BytesIO(img_data))
45
+ images.append(img)
46
+
47
+ # Save as GIF
48
+ gif_path = f"{output_path}.gif"
49
+ images[0].save(
50
+ gif_path,
51
+ save_all=True,
52
+ append_images=images[1:],
53
+ duration=duration,
54
+ loop=0
55
+ )
56
+
57
+ return gif_path
58
+
59
+ def save_trajectory(
60
+ self,
61
+ directory: str = "trajectories",
62
+ ) -> str:
63
+ """
64
+ Save trajectory steps to a JSON file and create a GIF of screenshots if available.
65
+
66
+ Args:
67
+ directory: Directory to save the trajectory files
68
+
69
+ Returns:
70
+ Path to the saved trajectory file
71
+ """
72
+ os.makedirs(directory, exist_ok=True)
73
+
74
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
75
+ base_path = os.path.join(directory, f"trajectory_{timestamp}")
76
+
77
+ def make_serializable(obj):
78
+ """Recursively make objects JSON serializable."""
79
+ if hasattr(obj, "__class__") and obj.__class__.__name__ == "ChatMessage":
80
+ # Extract the text content from the ChatMessage
81
+ if hasattr(obj, "content") and obj.content is not None:
82
+ return {"role": obj.role.value, "content": obj.content}
83
+ # If content is not available, try extracting from blocks
84
+ elif hasattr(obj, "blocks") and obj.blocks:
85
+ text_content = ""
86
+ for block in obj.blocks:
87
+ if hasattr(block, "text"):
88
+ text_content += block.text
89
+ return {"role": obj.role.value, "content": text_content}
90
+ else:
91
+ return str(obj)
92
+ elif isinstance(obj, dict):
93
+ return {k: make_serializable(v) for k, v in obj.items()}
94
+ elif isinstance(obj, list):
95
+ return [make_serializable(item) for item in obj]
96
+ elif hasattr(obj, "__dict__"):
97
+ # Handle other custom objects by converting to dict
98
+ return {k: make_serializable(v) for k, v in obj.__dict__.items()
99
+ if not k.startswith('_')}
100
+ else:
101
+ return obj
102
+
103
+ serializable_events = []
104
+ for event in self.events:
105
+ event_dict = {
106
+ "type": event.__class__.__name__,
107
+ **{k: make_serializable(v) for k, v in event.__dict__.items()
108
+ if not k.startswith('_')}
109
+ }
110
+ serializable_events.append(event_dict)
111
+
112
+ json_path = f"{base_path}.json"
113
+ with open(json_path, "w") as f:
114
+ json.dump(serializable_events, f, indent=2)
115
+
116
+ self.create_screenshot_gif(base_path)
117
+
118
+ return json_path
119
+
120
+ def get_trajectory_statistics(trajectory_data: Dict[str, Any]) -> Dict[str, Any]:
121
+ """
122
+ Get statistics about a trajectory.
123
+
124
+ Args:
125
+ trajectory_data: The trajectory data dictionary
126
+
127
+ Returns:
128
+ Dictionary with statistics about the trajectory
129
+ """
130
+ trajectory_steps = trajectory_data.get("trajectory_steps", [])
131
+
132
+ # Count different types of steps
133
+ step_types = {}
134
+ for step in trajectory_steps:
135
+ step_type = step.get("type", "unknown")
136
+ step_types[step_type] = step_types.get(step_type, 0) + 1
137
+
138
+ # Count planning vs execution steps
139
+ planning_steps = sum(count for step_type, count in step_types.items()
140
+ if step_type.startswith("planner_"))
141
+ execution_steps = sum(count for step_type, count in step_types.items()
142
+ if step_type.startswith("codeact_"))
143
+
144
+ # Count successful vs failed executions
145
+ successful_executions = sum(1 for step in trajectory_steps
146
+ if step.get("type") == "codeact_execution"
147
+ and step.get("success", False))
148
+ failed_executions = sum(1 for step in trajectory_steps
149
+ if step.get("type") == "codeact_execution"
150
+ and not step.get("success", True))
151
+
152
+ # Return statistics
153
+ return {
154
+ "total_steps": len(trajectory_steps),
155
+ "step_types": step_types,
156
+ "planning_steps": planning_steps,
157
+ "execution_steps": execution_steps,
158
+ "successful_executions": successful_executions,
159
+ "failed_executions": failed_executions,
160
+ "goal_achieved": trajectory_data.get("success", False)
161
+ }
162
+
163
+ def print_trajectory_summary(self, trajectory_data: Dict[str, Any]) -> None:
164
+ """
165
+ Print a summary of a trajectory.
166
+
167
+ Args:
168
+ trajectory_data: The trajectory data dictionary
169
+ """
170
+ stats = self.get_trajectory_statistics(trajectory_data)
171
+
172
+ print("=== Trajectory Summary ===")
173
+ print(f"Goal: {trajectory_data.get('goal', 'Unknown')}")
174
+ print(f"Success: {trajectory_data.get('success', False)}")
175
+ print(f"Reason: {trajectory_data.get('reason', 'Unknown')}")
176
+ print(f"Total steps: {stats['total_steps']}")
177
+ print("Step breakdown:")
178
+ for step_type, count in stats['step_types'].items():
179
+ print(f" - {step_type}: {count}")
180
+ print(f"Planning steps: {stats['planning_steps']}")
181
+ print(f"Execution steps: {stats['execution_steps']}")
182
+ print(f"Successful executions: {stats['successful_executions']}")
183
+ print(f"Failed executions: {stats['failed_executions']}")
184
+ print("==========================")
droidrun/cli/__init__.py CHANGED
@@ -4,6 +4,6 @@ DroidRun CLI Module.
4
4
  This module provides command-line interfaces for interacting with Android devices.
5
5
  """
6
6
 
7
- from .main import cli
7
+ from droidrun.cli.main import cli
8
8
 
9
9
  __all__ = ["cli"]
droidrun/cli/logs.py ADDED
@@ -0,0 +1,283 @@
1
+ import logging
2
+ from rich.layout import Layout
3
+ from rich.panel import Panel
4
+ from rich.spinner import Spinner
5
+ from rich.console import Console
6
+ from rich.live import Live
7
+ from typing import List
8
+
9
+ from droidrun.agent.common.events import ScreenshotEvent
10
+ from droidrun.agent.planner.events import (
11
+ PlanInputEvent,
12
+ PlanThinkingEvent,
13
+ PlanCreatedEvent,
14
+ )
15
+ from droidrun.agent.codeact.events import (
16
+ TaskInputEvent,
17
+ TaskThinkingEvent,
18
+ TaskExecutionEvent,
19
+ TaskExecutionResultEvent,
20
+ TaskEndEvent,
21
+ )
22
+ from droidrun.agent.droid.events import (
23
+ CodeActExecuteEvent,
24
+ CodeActResultEvent,
25
+ ReasoningLogicEvent,
26
+ TaskRunnerEvent,
27
+ FinalizeEvent,
28
+ )
29
+
30
+
31
+ class LogHandler(logging.Handler):
32
+ def __init__(self, goal: str, current_step: str = "Initializing..."):
33
+ super().__init__()
34
+
35
+ self.goal = goal
36
+ self.current_step = current_step
37
+ self.is_completed = False
38
+ self.is_success = False
39
+ self.spinner = Spinner("dots")
40
+ self.console = Console()
41
+ self.layout = self._create_layout()
42
+ self.logs: List[str] = []
43
+
44
+ def emit(self, record):
45
+ msg = self.format(record)
46
+ lines = msg.splitlines()
47
+
48
+ for line in lines:
49
+ self.logs.append(line)
50
+ # Optionally, limit the log list size
51
+ if len(self.logs) > 100:
52
+ self.logs.pop(0)
53
+
54
+ self.rerender()
55
+
56
+ def render(self):
57
+ return Live(self.layout, refresh_per_second=4, console=self.console)
58
+
59
+ def rerender(self):
60
+ self._update_layout(
61
+ self.layout,
62
+ self.logs,
63
+ self.current_step,
64
+ self.goal,
65
+ self.is_completed,
66
+ self.is_success,
67
+ )
68
+
69
+ def update_step(self, step: str):
70
+ self.current_step = step
71
+ self.rerender()
72
+
73
+ def _create_layout(self):
74
+ """Create a layout with logs at top and status at bottom"""
75
+ layout = Layout()
76
+ layout.split(
77
+ Layout(name="logs"),
78
+ Layout(name="goal", size=3),
79
+ Layout(name="status", size=3),
80
+ )
81
+ return layout
82
+
83
+ def _update_layout(
84
+ self,
85
+ layout: Layout,
86
+ log_list: List[str],
87
+ step_message: str,
88
+ goal: str = None,
89
+ completed: bool = False,
90
+ success: bool = False,
91
+ ):
92
+ """Update the layout with current logs and step information"""
93
+ from rich.text import Text
94
+ import shutil
95
+
96
+ # Cache terminal size to avoid frequent recalculation
97
+ try:
98
+ terminal_height = shutil.get_terminal_size().lines
99
+ except:
100
+ terminal_height = 24 # fallback
101
+
102
+ # Reserve space for panels and borders (more conservative estimate)
103
+ other_components_height = 10 # goal panel + status panel + borders + padding
104
+ available_log_lines = max(8, terminal_height - other_components_height)
105
+
106
+ # Only show recent logs, but ensure we don't flicker
107
+ visible_logs = (
108
+ log_list[-available_log_lines:]
109
+ if len(log_list) > available_log_lines
110
+ else log_list
111
+ )
112
+
113
+ # Ensure we always have some content to prevent panel collapse
114
+ if not visible_logs:
115
+ visible_logs = ["Initializing..."]
116
+
117
+ log_content = "\n".join(visible_logs)
118
+
119
+ layout["logs"].update(
120
+ Panel(
121
+ log_content,
122
+ title=f"Activity Log ({len(log_list)} entries)",
123
+ border_style="blue",
124
+ title_align="left",
125
+ padding=(0, 1),
126
+ height=available_log_lines + 2,
127
+ )
128
+ )
129
+
130
+ if goal:
131
+ goal_text = Text(goal, style="bold")
132
+ layout["goal"].update(
133
+ Panel(
134
+ goal_text,
135
+ title="Goal",
136
+ border_style="magenta",
137
+ title_align="left",
138
+ padding=(0, 1),
139
+ height=3,
140
+ )
141
+ )
142
+
143
+ step_display = Text()
144
+
145
+ if completed:
146
+ if success:
147
+ step_display.append("✓ ", style="bold green")
148
+ panel_title = "Completed"
149
+ panel_style = "green"
150
+ else:
151
+ step_display.append("✗ ", style="bold red")
152
+ panel_title = "Failed"
153
+ panel_style = "red"
154
+ else:
155
+ step_display.append("⚡ ", style="bold yellow")
156
+ panel_title = "Status"
157
+ panel_style = "yellow"
158
+
159
+ step_display.append(step_message)
160
+
161
+ layout["status"].update(
162
+ Panel(
163
+ step_display,
164
+ title=panel_title,
165
+ border_style=panel_style,
166
+ title_align="left",
167
+ padding=(0, 1),
168
+ height=3,
169
+ )
170
+ )
171
+
172
+ def handle_event(self, event):
173
+ """Handle streaming events from the agent workflow."""
174
+ logger = logging.getLogger("droidrun")
175
+
176
+ # Log different event types with proper names
177
+ if isinstance(event, ScreenshotEvent):
178
+ logger.debug("📸 Taking screenshot...")
179
+
180
+ # Planner events
181
+ elif isinstance(event, PlanInputEvent):
182
+ self.current_step = "Planning..."
183
+ logger.info("💭 Planner receiving input...")
184
+
185
+ elif isinstance(event, PlanThinkingEvent):
186
+ if event.thoughts:
187
+ thoughts_preview = (
188
+ event.thoughts[:150] + "..."
189
+ if len(event.thoughts) > 150
190
+ else event.thoughts
191
+ )
192
+ logger.info(f"🧠 Planning: {thoughts_preview}")
193
+ if event.code:
194
+ logger.info(f"📝 Generated plan code")
195
+
196
+ elif isinstance(event, PlanCreatedEvent):
197
+ if event.tasks:
198
+ task_count = len(event.tasks) if event.tasks else 0
199
+ self.current_step = f"Plan ready ({task_count} tasks)"
200
+ logger.info(f"📋 Plan created with {task_count} tasks")
201
+ for task in event.tasks:
202
+ desc = task.description
203
+ logger.info(f"- {desc}")
204
+
205
+ # CodeAct events
206
+ elif isinstance(event, TaskInputEvent):
207
+ self.current_step = "Processing task input..."
208
+ logger.info("💬 Task input received...")
209
+
210
+ elif isinstance(event, TaskThinkingEvent):
211
+ if hasattr(event, "thoughts") and event.thoughts:
212
+ thoughts_preview = (
213
+ event.thoughts[:150] + "..."
214
+ if len(event.thoughts) > 150
215
+ else event.thoughts
216
+ )
217
+ logger.info(f"🧠 Thinking: {thoughts_preview}")
218
+ if hasattr(event, "code") and event.code:
219
+ logger.info(f"💻 Executing action code")
220
+ logger.debug(f"{event.code}")
221
+
222
+ elif isinstance(event, TaskExecutionEvent):
223
+ self.current_step = "Executing action..."
224
+ logger.info(f"⚡ Executing action...")
225
+
226
+ elif isinstance(event, TaskExecutionResultEvent):
227
+ if hasattr(event, "output") and event.output:
228
+ output = str(event.output)
229
+ if "Error" in output or "Exception" in output:
230
+ output_preview = (
231
+ output[:100] + "..." if len(output) > 100 else output
232
+ )
233
+ logger.info(f"❌ Action error: {output_preview}")
234
+ else:
235
+ output_preview = (
236
+ output[:100] + "..." if len(output) > 100 else output
237
+ )
238
+ logger.info(f"⚡ Action result: {output_preview}")
239
+
240
+ elif isinstance(event, TaskEndEvent):
241
+ if hasattr(event, "success") and hasattr(event, "reason"):
242
+ if event.success:
243
+ self.current_step = event.reason
244
+ logger.info(f"✅ Task completed: {event.reason}")
245
+ else:
246
+ self.current_step = f"Task failed"
247
+ logger.info(f"❌ Task failed: {event.reason}")
248
+
249
+ # Droid coordination events
250
+ elif isinstance(event, CodeActExecuteEvent):
251
+ self.current_step = "Executing task..."
252
+ logger.info(f"🔧 Starting task execution...")
253
+
254
+ elif isinstance(event, CodeActResultEvent):
255
+ if hasattr(event, "success") and hasattr(event, "reason"):
256
+ if event.success:
257
+ self.current_step = event.reason
258
+ logger.info(f"✅ Task completed: {event.reason}")
259
+ else:
260
+ self.current_step = f"Task failed"
261
+ logger.info(f"❌ Task failed: {event.reason}")
262
+
263
+ elif isinstance(event, ReasoningLogicEvent):
264
+ self.current_step = "Planning..."
265
+ logger.info(f"🤔 Planning next steps...")
266
+
267
+ elif isinstance(event, TaskRunnerEvent):
268
+ self.current_step = "Processing tasks..."
269
+ logger.info(f"🏃 Processing task queue...")
270
+
271
+ elif isinstance(event, FinalizeEvent):
272
+ if hasattr(event, "success") and hasattr(event, "reason"):
273
+ self.is_completed = True
274
+ self.is_success = event.success
275
+ if event.success:
276
+ self.current_step = f"Success: {event.reason}"
277
+ logger.info(f"🎉 Goal achieved: {event.reason}")
278
+ else:
279
+ self.current_step = f"Failed: {event.reason}"
280
+ logger.info(f"❌ Goal failed: {event.reason}")
281
+
282
+ else:
283
+ logger.debug(f"🔄 {event.__class__.__name__}")