droidrun 0.3.8__py3-none-any.whl → 0.3.10.dev2__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 (74) hide show
  1. droidrun/__init__.py +2 -3
  2. droidrun/__main__.py +1 -1
  3. droidrun/agent/__init__.py +1 -1
  4. droidrun/agent/codeact/__init__.py +1 -4
  5. droidrun/agent/codeact/codeact_agent.py +112 -48
  6. droidrun/agent/codeact/events.py +6 -3
  7. droidrun/agent/codeact/prompts.py +2 -2
  8. droidrun/agent/common/constants.py +2 -0
  9. droidrun/agent/common/events.py +5 -3
  10. droidrun/agent/context/__init__.py +1 -3
  11. droidrun/agent/context/agent_persona.py +2 -1
  12. droidrun/agent/context/context_injection_manager.py +6 -6
  13. droidrun/agent/context/episodic_memory.py +5 -3
  14. droidrun/agent/context/personas/__init__.py +3 -3
  15. droidrun/agent/context/personas/app_starter.py +3 -3
  16. droidrun/agent/context/personas/big_agent.py +3 -3
  17. droidrun/agent/context/personas/default.py +3 -3
  18. droidrun/agent/context/personas/ui_expert.py +5 -5
  19. droidrun/agent/context/task_manager.py +15 -17
  20. droidrun/agent/droid/__init__.py +1 -1
  21. droidrun/agent/droid/droid_agent.py +327 -182
  22. droidrun/agent/droid/events.py +91 -9
  23. droidrun/agent/executor/__init__.py +13 -0
  24. droidrun/agent/executor/events.py +24 -0
  25. droidrun/agent/executor/executor_agent.py +327 -0
  26. droidrun/agent/executor/prompts.py +136 -0
  27. droidrun/agent/manager/__init__.py +18 -0
  28. droidrun/agent/manager/events.py +20 -0
  29. droidrun/agent/manager/manager_agent.py +459 -0
  30. droidrun/agent/manager/prompts.py +223 -0
  31. droidrun/agent/oneflows/app_starter_workflow.py +118 -0
  32. droidrun/agent/oneflows/text_manipulator.py +204 -0
  33. droidrun/agent/planner/__init__.py +3 -3
  34. droidrun/agent/planner/events.py +6 -3
  35. droidrun/agent/planner/planner_agent.py +60 -53
  36. droidrun/agent/planner/prompts.py +2 -2
  37. droidrun/agent/usage.py +15 -13
  38. droidrun/agent/utils/__init__.py +11 -1
  39. droidrun/agent/utils/async_utils.py +2 -1
  40. droidrun/agent/utils/chat_utils.py +48 -60
  41. droidrun/agent/utils/device_state_formatter.py +177 -0
  42. droidrun/agent/utils/executer.py +13 -12
  43. droidrun/agent/utils/inference.py +114 -0
  44. droidrun/agent/utils/llm_picker.py +2 -0
  45. droidrun/agent/utils/message_utils.py +85 -0
  46. droidrun/agent/utils/tools.py +220 -0
  47. droidrun/agent/utils/trajectory.py +8 -7
  48. droidrun/cli/__init__.py +1 -1
  49. droidrun/cli/logs.py +29 -28
  50. droidrun/cli/main.py +279 -143
  51. droidrun/config_manager/__init__.py +25 -0
  52. droidrun/config_manager/config_manager.py +583 -0
  53. droidrun/macro/__init__.py +2 -2
  54. droidrun/macro/__main__.py +1 -1
  55. droidrun/macro/cli.py +36 -34
  56. droidrun/macro/replay.py +7 -9
  57. droidrun/portal.py +1 -1
  58. droidrun/telemetry/__init__.py +2 -2
  59. droidrun/telemetry/events.py +3 -4
  60. droidrun/telemetry/phoenix.py +173 -0
  61. droidrun/telemetry/tracker.py +7 -5
  62. droidrun/tools/__init__.py +1 -1
  63. droidrun/tools/adb.py +210 -82
  64. droidrun/tools/ios.py +7 -5
  65. droidrun/tools/tools.py +25 -8
  66. {droidrun-0.3.8.dist-info → droidrun-0.3.10.dev2.dist-info}/METADATA +13 -7
  67. droidrun-0.3.10.dev2.dist-info/RECORD +70 -0
  68. droidrun/agent/common/default.py +0 -5
  69. droidrun/agent/context/reflection.py +0 -20
  70. droidrun/agent/oneflows/reflector.py +0 -265
  71. droidrun-0.3.8.dist-info/RECORD +0 -55
  72. {droidrun-0.3.8.dist-info → droidrun-0.3.10.dev2.dist-info}/WHEEL +0 -0
  73. {droidrun-0.3.8.dist-info → droidrun-0.3.10.dev2.dist-info}/entry_points.txt +0 -0
  74. {droidrun-0.3.8.dist-info → droidrun-0.3.10.dev2.dist-info}/licenses/LICENSE +0 -0
@@ -1,41 +1,63 @@
1
1
  """
2
2
  DroidAgent - A wrapper class that coordinates the planning and execution of tasks
3
3
  to achieve a user's goal on an Android device.
4
+
5
+ Architecture:
6
+ - When reasoning=False: Uses CodeActAgent directly
7
+ - When reasoning=True: Uses Manager (planning) + Executor (action) workflows
4
8
  """
5
9
 
6
10
  import logging
7
11
  from typing import List
8
12
 
13
+ import llama_index.core
9
14
  from llama_index.core.llms.llm import LLM
10
- from llama_index.core.workflow import step, StartEvent, StopEvent, Workflow, Context
15
+ from llama_index.core.workflow import Context, StartEvent, StopEvent, Workflow, step
11
16
  from llama_index.core.workflow.handler import WorkflowHandler
12
- from droidrun.agent.droid.events import *
17
+ from workflows.events import Event
18
+
19
+ from droidrun.config_manager.config_manager import VisionConfig
20
+
13
21
  from droidrun.agent.codeact import CodeActAgent
14
22
  from droidrun.agent.codeact.events import EpisodicMemoryEvent
15
- from droidrun.agent.planner import PlannerAgent
16
- from droidrun.agent.context.task_manager import TaskManager
17
- from droidrun.agent.utils.trajectory import Trajectory
18
- from droidrun.tools import Tools, describe_tools
19
- from droidrun.agent.common.events import ScreenshotEvent, MacroEvent, RecordUIStateEvent
20
- from droidrun.agent.common.default import MockWorkflow
23
+ from droidrun.agent.common.events import MacroEvent, RecordUIStateEvent, ScreenshotEvent
21
24
  from droidrun.agent.context import ContextInjectionManager
22
25
  from droidrun.agent.context.agent_persona import AgentPersona
23
26
  from droidrun.agent.context.personas import DEFAULT
24
- from droidrun.agent.oneflows.reflector import Reflector
27
+ from droidrun.agent.context.task_manager import Task, TaskManager
28
+ from droidrun.agent.droid.events import (
29
+ CodeActExecuteEvent,
30
+ CodeActResultEvent,
31
+ DroidAgentState,
32
+ ExecutorInputEvent,
33
+ ExecutorResultEvent,
34
+ FinalizeEvent,
35
+ ManagerInputEvent,
36
+ ManagerPlanEvent,
37
+ )
38
+ from droidrun.agent.executor import ExecutorAgent
39
+ from droidrun.agent.manager import ManagerAgent
40
+ from droidrun.agent.utils.tools import ATOMIC_ACTION_SIGNATURES
41
+ from droidrun.agent.utils.trajectory import Trajectory
25
42
  from droidrun.telemetry import (
43
+ DroidAgentFinalizeEvent,
44
+ DroidAgentInitEvent,
26
45
  capture,
27
46
  flush,
28
- DroidAgentInitEvent,
29
- DroidAgentFinalizeEvent,
30
47
  )
48
+ from droidrun.telemetry.phoenix import arize_phoenix_callback_handler
49
+ from droidrun.tools import Tools
31
50
 
32
51
  logger = logging.getLogger("droidrun")
33
52
 
34
53
 
35
54
  class DroidAgent(Workflow):
36
55
  """
37
- A wrapper class that coordinates between PlannerAgent (creates plans) and
38
- CodeActAgent (executes tasks) to achieve a user's goal.
56
+ A wrapper class that coordinates between agents to achieve a user's goal.
57
+
58
+ Reasoning modes:
59
+ - reasoning=False: Uses CodeActAgent directly for immediate execution
60
+ - reasoning=True: Uses ManagerAgent (planning) + ExecutorAgent (actions)
39
61
  """
40
62
 
41
63
  @staticmethod
@@ -63,18 +85,18 @@ class DroidAgent(Workflow):
63
85
  def __init__(
64
86
  self,
65
87
  goal: str,
66
- llm: LLM,
88
+ llms: dict[str, LLM] | LLM,
67
89
  tools: Tools,
68
- personas: List[AgentPersona] = [DEFAULT],
90
+ personas: List[AgentPersona] = [DEFAULT], # noqa: B006
69
91
  max_steps: int = 15,
70
92
  timeout: int = 1000,
71
- vision: bool = False,
93
+ vision: "VisionConfig | dict | bool" = False,
72
94
  reasoning: bool = False,
73
- reflection: bool = False,
74
95
  enable_tracing: bool = False,
75
96
  debug: bool = False,
76
97
  save_trajectories: str = "none",
77
98
  excluded_tools: List[str] = None,
99
+ custom_tools: dict = None,
78
100
  *args,
79
101
  **kwargs,
80
102
  ):
@@ -86,42 +108,97 @@ class DroidAgent(Workflow):
86
108
  llm: The language model to use for both agents
87
109
  max_steps: Maximum number of steps for both agents
88
110
  timeout: Timeout for agent execution in seconds
89
- reasoning: Whether to use the PlannerAgent for complex reasoning (True)
111
+ reasoning: Whether to use Manager+Executor for complex reasoning (True)
90
112
  or send tasks directly to CodeActAgent (False)
91
- reflection: Whether to reflect on steps the CodeActAgent did to give the PlannerAgent advice
92
113
  enable_tracing: Whether to enable Arize Phoenix tracing
93
114
  debug: Whether to enable verbose debug logging
94
115
  save_trajectories: Trajectory saving level. Can be:
95
116
  - "none" (no saving)
96
117
  - "step" (save per step)
97
118
  - "action" (save per action)
119
+ custom_tools: Dictionary of custom tools in ATOMIC_ACTION_SIGNATURES format:
120
+ {
121
+ "tool_name": {
122
+ "arguments": ["arg1", "arg2"],
123
+ "description": "Tool description with usage example",
124
+ "function": callable
125
+ }
126
+ }
98
127
  **kwargs: Additional keyword arguments to pass to the agents
99
128
  """
100
129
  self.user_id = kwargs.pop("user_id", None)
101
- super().__init__(timeout=timeout, *args, **kwargs)
130
+ super().__init__(timeout=timeout, *args, **kwargs) # noqa: B026
102
131
  # Configure default logging if not already configured
103
132
  self._configure_default_logging(debug=debug)
104
133
 
105
134
  # Setup global tracing first if enabled
106
135
  if enable_tracing:
107
136
  try:
108
- from llama_index.core import set_global_handler
109
-
110
- set_global_handler("arize_phoenix")
137
+ handler = arize_phoenix_callback_handler()
138
+ llama_index.core.global_handler = handler
111
139
  logger.info("🔍 Arize Phoenix tracing enabled globally")
112
140
  except ImportError:
113
- logger.warning("⚠️ Arize Phoenix package not found, tracing disabled")
141
+ logger.warning(
142
+ "⚠️ Arize Phoenix is not installed.\n"
143
+ " To enable Phoenix integration, install with:\n"
144
+ " • If installed via tool: `uv tool install droidrun[phoenix]`"
145
+ " • If installed via pip: `uv pip install droidrun[phoenix]`\n"
146
+ )
114
147
  enable_tracing = False
115
148
 
116
149
  self.goal = goal
117
- self.llm = llm
118
- self.vision = vision
119
150
  self.max_steps = max_steps
120
151
  self.max_codeact_steps = max_steps
121
152
  self.timeout = timeout
122
153
  self.reasoning = reasoning
123
- self.reflection = reflection
124
154
  self.debug = debug
155
+ self.custom_tools = custom_tools or {}
156
+
157
+ # ====================================================================
158
+ # Handle LLM parameter - support both dict and single LLM
159
+ # ====================================================================
160
+ if isinstance(llms, dict):
161
+ self.manager_llm = llms.get('manager')
162
+ self.executor_llm = llms.get('executor')
163
+ self.codeact_llm = llms.get('codeact')
164
+ self.text_manipulator_llm = llms.get('text_manipulator')
165
+ self.app_opener_llm = llms.get('app_opener')
166
+
167
+ # Validate required LLMs are present
168
+ if reasoning and (not self.manager_llm or not self.executor_llm):
169
+ raise ValueError("When reasoning=True, 'manager' and 'executor' LLMs must be provided in llms dict")
170
+ if not self.codeact_llm:
171
+ raise ValueError("'codeact' LLM must be provided in llms dict")
172
+
173
+ logger.info("📚 Using agent-specific LLMs from dictionary")
174
+ else:
175
+ # single LLM for all agents
176
+ logger.info("📚 Using single LLM for all agents (backward compatibility mode)")
177
+ self.manager_llm = llms
178
+ self.executor_llm = llms
179
+ self.codeact_llm = llms
180
+ self.text_manipulator_llm = llms
181
+ self.app_opener_llm = llms
182
+
183
+ # ====================================================================
184
+ # Handle vision parameter - support VisionConfig, dict, or bool
185
+ # ====================================================================
186
+ if isinstance(vision, VisionConfig):
187
+ self.vision_config = vision
188
+ elif isinstance(vision, dict):
189
+ self.vision_config = VisionConfig.from_dict(vision)
190
+ elif isinstance(vision, bool):
191
+ # Backward compatibility: single bool for all agents
192
+ logger.info(f"👁️ Using vision={vision} for all agents (backward compatibility mode)")
193
+ self.vision_config = VisionConfig(manager=vision, executor=vision, codeact=vision)
194
+ else:
195
+ raise TypeError(f"vision must be VisionConfig, dict, or bool, got {type(vision)}")
196
+
197
+ # Store individual vision flags for easy access
198
+ self.manager_vision = self.vision_config.manager
199
+ self.executor_vision = self.vision_config.executor
200
+ self.codeact_vision = self.vision_config.codeact
201
+
125
202
 
126
203
  self.event_counter = 0
127
204
  # Handle backward compatibility: bool -> str mapping
@@ -148,44 +225,64 @@ class DroidAgent(Workflow):
148
225
  logger.info("🤖 Initializing DroidAgent...")
149
226
  logger.info(f"💾 Trajectory saving level: {self.save_trajectories}")
150
227
 
151
- self.tool_list = describe_tools(tools, excluded_tools)
152
228
  self.tools_instance = tools
153
229
 
154
230
  self.tools_instance.save_trajectories = self.save_trajectories
155
231
 
232
+ # Create shared state instance for Manager/Executor workflows
233
+ self.shared_state = DroidAgentState(
234
+ instruction=goal,
235
+ err_to_manager_thresh=2
236
+ )
237
+
156
238
  if self.reasoning:
157
- logger.info("📝 Initializing Planner Agent...")
158
- self.planner_agent = PlannerAgent(
159
- goal=goal,
160
- llm=llm,
161
- vision=vision,
239
+ logger.info("📝 Initializing Manager and Executor Agents...")
240
+ self.manager_agent = ManagerAgent(
241
+ llm=self.manager_llm,
242
+ vision=self.manager_vision,
162
243
  personas=personas,
163
- task_manager=self.task_manager,
164
244
  tools_instance=tools,
245
+ shared_state=self.shared_state,
246
+ custom_tools=self.custom_tools,
247
+ timeout=timeout,
248
+ debug=debug,
249
+ )
250
+ self.executor_agent = ExecutorAgent(
251
+ llm=self.executor_llm,
252
+ vision=self.executor_vision,
253
+ tools_instance=tools,
254
+ shared_state=self.shared_state,
255
+ persona=None, # Need to figure this out
256
+ custom_tools=self.custom_tools,
165
257
  timeout=timeout,
166
258
  debug=debug,
167
259
  )
168
- self.add_workflows(planner_agent=self.planner_agent)
169
260
  self.max_codeact_steps = 5
170
261
 
171
- if self.reflection:
172
- self.reflector = Reflector(llm=llm, debug=debug)
262
+
263
+ # Keep planner_agent for backward compatibility (can be removed later)
264
+ self.planner_agent = None
173
265
 
174
266
  else:
175
- logger.debug("🚫 Planning disabled - will execute tasks directly with CodeActAgent")
267
+ logger.debug("🚫 Reasoning disabled - will execute tasks directly with CodeActAgent")
268
+ self.manager_agent = None
269
+ self.executor_agent = None
176
270
  self.planner_agent = None
177
271
 
272
+ # Get tool names from ATOMIC_ACTION_SIGNATURES for telemetry
273
+ atomic_tools = list(ATOMIC_ACTION_SIGNATURES.keys())
274
+
178
275
  capture(
276
+ # TODO: do proper telemetry instead of this ductaped crap
179
277
  DroidAgentInitEvent(
180
278
  goal=goal,
181
- llm=llm.class_name(),
182
- tools=",".join(self.tool_list),
279
+ llm=self.llm.class_name(),
280
+ tools=",".join(atomic_tools + ["remember", "complete"]),
183
281
  personas=",".join([p.name for p in personas]),
184
282
  max_steps=max_steps,
185
283
  timeout=timeout,
186
- vision=vision,
284
+ vision=self.vision,
187
285
  reasoning=reasoning,
188
- reflection=reflection,
189
286
  enable_tracing=enable_tracing,
190
287
  debug=debug,
191
288
  save_trajectories=save_trajectories,
@@ -201,6 +298,35 @@ class DroidAgent(Workflow):
201
298
  """
202
299
  return super().run(*args, **kwargs)
203
300
 
301
+ def _create_finalize_event(
302
+ self,
303
+ success: bool,
304
+ reason: str,
305
+ output: str
306
+ ) -> FinalizeEvent:
307
+ """
308
+ Single source of truth for creating FinalizeEvent.
309
+
310
+ This helper ensures all FinalizeEvent creation is consistent
311
+ across the workflow.
312
+
313
+ Args:
314
+ success: Whether the task succeeded
315
+ reason: Reason for completion (deprecated, use output)
316
+ output: Output message
317
+
318
+ Returns:
319
+ FinalizeEvent ready to be returned
320
+ """
321
+ return FinalizeEvent(
322
+ success=success,
323
+ reason=reason,
324
+ output=output,
325
+ task=[], # TODO: use the final plan as the tasks and the goal as task
326
+ tasks=[],
327
+ steps=self.step_counter
328
+ )
329
+
204
330
  @step
205
331
  async def execute_task(self, ctx: Context, ev: CodeActExecuteEvent) -> CodeActResultEvent:
206
332
  """
@@ -213,19 +339,18 @@ class DroidAgent(Workflow):
213
339
  Tuple of (success, reason)
214
340
  """
215
341
  task: Task = ev.task
216
- reflection = ev.reflection if ev.reflection is not None else None
217
342
  persona = self.cim.get_persona(task.agent_type)
218
343
 
219
344
  logger.info(f"🔧 Executing task: {task.description}")
220
345
 
221
346
  try:
222
347
  codeact_agent = CodeActAgent(
223
- llm=self.llm,
348
+ llm=self.codeact_llm,
224
349
  persona=persona,
225
- vision=self.vision,
350
+ vision=self.codeact_vision,
226
351
  max_steps=self.max_codeact_steps,
227
- all_tools_list=self.tool_list,
228
352
  tools_instance=self.tools_instance,
353
+ custom_tools=self.custom_tools,
229
354
  debug=self.debug,
230
355
  timeout=self.timeout,
231
356
  )
@@ -233,7 +358,6 @@ class DroidAgent(Workflow):
233
358
  handler = codeact_agent.run(
234
359
  input=task.description,
235
360
  remembered_info=self.tools_instance.memory,
236
- reflection=reflection,
237
361
  )
238
362
 
239
363
  async for nested_ev in handler.stream_events():
@@ -267,32 +391,17 @@ class DroidAgent(Workflow):
267
391
  @step
268
392
  async def handle_codeact_execute(
269
393
  self, ctx: Context, ev: CodeActResultEvent
270
- ) -> FinalizeEvent | ReflectionEvent | ReasoningLogicEvent:
394
+ ) -> FinalizeEvent:
271
395
  try:
272
396
  task = ev.task
273
- if not self.reasoning:
274
- return FinalizeEvent(
275
- success=ev.success,
276
- reason=ev.reason,
277
- output=ev.reason,
278
- task=[task],
279
- tasks=[task],
280
- steps=ev.steps,
281
- )
282
-
283
- if self.reflection and ev.success:
284
- return ReflectionEvent(task=task)
285
-
286
- # Reasoning is enabled but reflection is disabled.
287
- # Success: mark complete and proceed to next step in reasoning loop.
288
- # Failure: mark failed and trigger planner immediately without advancing to the next queued task.
289
- if ev.success:
290
- self.task_manager.complete_task(task, message=ev.reason)
291
- return ReasoningLogicEvent()
292
- else:
293
- self.task_manager.fail_task(task, failure_reason=ev.reason)
294
- return ReasoningLogicEvent(force_planning=True)
295
-
397
+ return FinalizeEvent(
398
+ success=ev.success,
399
+ reason=ev.reason,
400
+ output=ev.reason,
401
+ task=[task],
402
+ tasks=[task],
403
+ steps=ev.steps,
404
+ )
296
405
  except Exception as e:
297
406
  logger.error(f"❌ Error during DroidAgent execution: {e}")
298
407
  if self.debug:
@@ -310,142 +419,178 @@ class DroidAgent(Workflow):
310
419
  )
311
420
 
312
421
  @step
313
- async def reflect(
314
- self, ctx: Context, ev: ReflectionEvent
315
- ) -> ReasoningLogicEvent | CodeActExecuteEvent:
316
- task = ev.task
317
- if ev.task.agent_type == "AppStarterExpert":
318
- self.task_manager.complete_task(task)
319
- return ReasoningLogicEvent()
320
-
321
- reflection = await self.reflector.reflect_on_episodic_memory(
322
- episodic_memory=self.current_episodic_memory, goal=task.description
323
- )
422
+ async def start_handler(
423
+ self, ctx: Context, ev: StartEvent
424
+ ) -> CodeActExecuteEvent | ManagerInputEvent:
425
+ """
426
+ Main execution loop that coordinates between planning and execution.
324
427
 
325
- if reflection.goal_achieved:
326
- self.task_manager.complete_task(task)
327
- return ReasoningLogicEvent()
428
+ Returns:
429
+ Event to trigger next step based on reasoning mode
430
+ """
431
+ logger.info(f"🚀 Running DroidAgent to achieve goal: {self.goal}")
432
+ ctx.write_event_to_stream(ev)
328
433
 
329
- else:
330
- self.task_manager.fail_task(task)
331
- return ReasoningLogicEvent(reflection=reflection)
434
+ self.step_counter = 0
435
+ self.retry_counter = 0
436
+
437
+ if not self.reasoning:
438
+ logger.info(f"🔄 Direct execution mode - executing goal: {self.goal}")
439
+ task = Task(
440
+ description=self.goal,
441
+ status=self.task_manager.STATUS_PENDING,
442
+ agent_type="Default",
443
+ )
444
+
445
+ return CodeActExecuteEvent(task=task)
446
+
447
+ # Reasoning mode - state already initialized in __init__, start with Manager
448
+ logger.info("🧠 Reasoning mode - initializing Manager/Executor workflow")
449
+ return ManagerInputEvent()
450
+
451
+ # ========================================================================
452
+ # Manager/Executor Workflow Steps
453
+ # ========================================================================
332
454
 
333
455
  @step
334
- async def handle_reasoning_logic(
456
+ async def run_manager(
335
457
  self,
336
458
  ctx: Context,
337
- ev: ReasoningLogicEvent,
338
- planner_agent: Workflow = MockWorkflow(),
339
- ) -> FinalizeEvent | CodeActExecuteEvent:
340
- try:
341
- if self.step_counter >= self.max_steps:
342
- output = f"Reached maximum number of steps ({self.max_steps})"
343
- tasks = self.task_manager.get_task_history()
344
- return FinalizeEvent(
345
- success=False,
346
- reason=output,
347
- output=output,
348
- task=tasks,
349
- tasks=tasks,
350
- steps=self.step_counter,
351
- )
352
- self.step_counter += 1
459
+ ev: ManagerInputEvent
460
+ ) -> ManagerPlanEvent | FinalizeEvent:
461
+ """
462
+ Run Manager planning phase.
353
463
 
354
- if ev.reflection:
355
- handler = planner_agent.run(
356
- remembered_info=self.tools_instance.memory, reflection=ev.reflection
357
- )
358
- else:
359
- if not ev.force_planning and self.task_iter:
360
- try:
361
- task = next(self.task_iter)
362
- return CodeActExecuteEvent(task=task, reflection=None)
363
- except StopIteration as e:
364
- logger.info("Planning next steps...")
464
+ Pre-flight checks for termination before running manager.
465
+ The Manager analyzes current state and creates a plan with subgoals.
466
+ """
467
+ # Check if we've reached the maximum number of steps
468
+ if self.step_counter >= self.max_steps:
469
+ logger.warning(f"⚠️ Reached maximum steps ({self.max_steps})")
470
+ return self._create_finalize_event(
471
+ success=False,
472
+ reason=f"Reached maximum steps ({self.max_steps})",
473
+ output=f"Reached maximum steps ({self.max_steps})"
474
+ )
365
475
 
366
- logger.debug(f"Planning step {self.step_counter}/{self.max_steps}")
476
+ # Continue with Manager execution
477
+ logger.info(f"📋 Running Manager for planning... (step {self.step_counter}/{self.max_steps})")
367
478
 
368
- handler = planner_agent.run(
369
- remembered_info=self.tools_instance.memory, reflection=None
370
- )
479
+ # Run Manager workflow
480
+ handler = self.manager_agent.run()
371
481
 
372
- async for nested_ev in handler.stream_events():
373
- self.handle_stream_event(nested_ev, ctx)
482
+ # Stream nested events
483
+ async for nested_ev in handler.stream_events():
484
+ self.handle_stream_event(nested_ev, ctx)
374
485
 
375
- result = await handler
486
+ result = await handler
376
487
 
377
- self.tasks = self.task_manager.get_all_tasks()
378
- self.task_iter = iter(self.tasks)
488
+ # Manager already updated shared_state, just return event with results
489
+ return ManagerPlanEvent(
490
+ plan=result["plan"],
491
+ current_subgoal=result["current_subgoal"],
492
+ completed_plan=result["completed_plan"],
493
+ thought=result["thought"],
494
+ manager_answer=result.get("manager_answer", "")
495
+ )
379
496
 
380
- if self.task_manager.goal_completed:
381
- logger.info(f"✅ Goal completed: {self.task_manager.message}")
382
- tasks = self.task_manager.get_task_history()
383
- return FinalizeEvent(
384
- success=True,
385
- reason=self.task_manager.message,
386
- output=self.task_manager.message,
387
- task=tasks,
388
- tasks=tasks,
389
- steps=self.step_counter,
390
- )
391
- if not self.tasks:
392
- logger.warning("No tasks generated by planner")
393
- output = "Planner did not generate any tasks"
394
- tasks = self.task_manager.get_task_history()
395
- return FinalizeEvent(
396
- success=False,
397
- reason=output,
398
- output=output,
399
- task=tasks,
400
- tasks=tasks,
401
- steps=self.step_counter,
402
- )
497
+ @step
498
+ async def handle_manager_plan(
499
+ self,
500
+ ctx: Context,
501
+ ev: ManagerPlanEvent
502
+ ) -> ExecutorInputEvent | FinalizeEvent:
503
+ """
504
+ Process Manager output and decide next step.
403
505
 
404
- return CodeActExecuteEvent(task=next(self.task_iter), reflection=None)
506
+ Checks if task is complete or if Executor should take action.
507
+ """
508
+ # Check for answer-type termination
509
+ if ev.manager_answer.strip():
510
+ logger.info(f"💬 Manager provided answer: {ev.manager_answer}")
511
+ self.shared_state.progress_status = f"Answer: {ev.manager_answer}"
512
+
513
+ return self._create_finalize_event(
514
+ success=True,
515
+ reason=ev.manager_answer,
516
+ output=ev.manager_answer
517
+ )
405
518
 
406
- except Exception as e:
407
- logger.error(f" Error during DroidAgent execution: {e}")
408
- if self.debug:
409
- import traceback
519
+ # Continue to Executor with current subgoal
520
+ logger.info(f"▶️ Proceeding to Executor with subgoal: {ev.current_subgoal}")
521
+ return ExecutorInputEvent(current_subgoal=ev.current_subgoal)
410
522
 
411
- logger.error(traceback.format_exc())
412
- tasks = self.task_manager.get_task_history()
413
- return FinalizeEvent(
414
- success=False,
415
- reason=str(e),
416
- output=str(e),
417
- task=tasks,
418
- tasks=tasks,
419
- steps=self.step_counter,
420
- )
523
+ @step
524
+ async def run_executor(
525
+ self,
526
+ ctx: Context,
527
+ ev: ExecutorInputEvent
528
+ ) -> ExecutorResultEvent:
529
+ """
530
+ Run Executor action phase.
531
+
532
+ The Executor selects and executes a specific action for the current subgoal.
533
+ """
534
+ logger.info("⚡ Running Executor for action...")
535
+
536
+ # Run Executor workflow (Executor will update shared_state directly)
537
+ handler = self.executor_agent.run(subgoal=ev.current_subgoal)
538
+
539
+ # Stream nested events
540
+ async for nested_ev in handler.stream_events():
541
+ self.handle_stream_event(nested_ev, ctx)
542
+
543
+ result = await handler
544
+
545
+ # Update coordination state after execution
546
+ self.shared_state.action_history.append(result["action"])
547
+ self.shared_state.summary_history.append(result["summary"])
548
+ self.shared_state.action_outcomes.append(result["outcome"])
549
+ self.shared_state.error_descriptions.append(result["error"])
550
+ self.shared_state.last_action = result["action"]
551
+ self.shared_state.last_summary = result["summary"]
552
+ self.shared_state.last_action_thought = result.get("thought", "")
553
+ self.shared_state.action_pool.append(result["action_json"])
554
+ self.shared_state.progress_status = self.shared_state.completed_plan
555
+
556
+ return ExecutorResultEvent(
557
+ action=result["action"],
558
+ outcome=result["outcome"],
559
+ error=result["error"],
560
+ summary=result["summary"]
561
+ )
421
562
 
422
563
  @step
423
- async def start_handler(
424
- self, ctx: Context, ev: StartEvent
425
- ) -> CodeActExecuteEvent | ReasoningLogicEvent:
564
+ async def handle_executor_result(
565
+ self,
566
+ ctx: Context,
567
+ ev: ExecutorResultEvent
568
+ ) -> ManagerInputEvent:
426
569
  """
427
- Main execution loop that coordinates between planning and execution.
570
+ Process Executor result and continue.
428
571
 
429
- Returns:
430
- Dict containing the execution result
572
+ Checks for error escalation and loops back to Manager.
573
+ Note: Max steps check is now done in run_manager pre-flight.
431
574
  """
432
- logger.info(f"🚀 Running DroidAgent to achieve goal: {self.goal}")
433
- ctx.write_event_to_stream(ev)
575
+ # Check error escalation
576
+ err_thresh = self.shared_state.err_to_manager_thresh
434
577
 
435
- self.step_counter = 0
436
- self.retry_counter = 0
578
+ if len(self.shared_state.action_outcomes) >= err_thresh:
579
+ latest = self.shared_state.action_outcomes[-err_thresh:]
580
+ error_count = sum(1 for o in latest if not o)
581
+ if error_count == err_thresh:
582
+ logger.warning(f"⚠️ Error escalation: {err_thresh} consecutive errors")
583
+ self.shared_state.error_flag_plan = True
437
584
 
438
- if not self.reasoning:
439
- logger.info(f"🔄 Direct execution mode - executing goal: {self.goal}")
440
- task = Task(
441
- description=self.goal,
442
- status=self.task_manager.STATUS_PENDING,
443
- agent_type="Default",
444
- )
585
+ self.step_counter += 1
586
+ logger.info(f"🔄 Step {self.step_counter}/{self.max_steps} complete, looping to Manager")
445
587
 
446
- return CodeActExecuteEvent(task=task, reflection=None)
588
+ # Always loop back to Manager (it will check max steps in pre-flight)
589
+ return ManagerInputEvent()
447
590
 
448
- return ReasoningLogicEvent()
591
+ # ========================================================================
592
+ # End Manager/Executor Workflow Steps
593
+ # ========================================================================
449
594
 
450
595
  @step
451
596
  async def finalize(self, ctx: Context, ev: FinalizeEvent) -> StopEvent: