droidrun 0.3.9__py3-none-any.whl → 0.3.10.dev3__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 (73) 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 +66 -40
  6. droidrun/agent/codeact/events.py +6 -3
  7. droidrun/agent/codeact/prompts.py +2 -2
  8. droidrun/agent/common/events.py +4 -2
  9. droidrun/agent/context/__init__.py +1 -3
  10. droidrun/agent/context/agent_persona.py +2 -1
  11. droidrun/agent/context/context_injection_manager.py +6 -6
  12. droidrun/agent/context/episodic_memory.py +5 -3
  13. droidrun/agent/context/personas/__init__.py +3 -3
  14. droidrun/agent/context/personas/app_starter.py +3 -3
  15. droidrun/agent/context/personas/big_agent.py +3 -3
  16. droidrun/agent/context/personas/default.py +3 -3
  17. droidrun/agent/context/personas/ui_expert.py +5 -5
  18. droidrun/agent/context/task_manager.py +15 -17
  19. droidrun/agent/droid/__init__.py +1 -1
  20. droidrun/agent/droid/droid_agent.py +327 -180
  21. droidrun/agent/droid/events.py +91 -9
  22. droidrun/agent/executor/__init__.py +13 -0
  23. droidrun/agent/executor/events.py +24 -0
  24. droidrun/agent/executor/executor_agent.py +327 -0
  25. droidrun/agent/executor/prompts.py +136 -0
  26. droidrun/agent/manager/__init__.py +18 -0
  27. droidrun/agent/manager/events.py +20 -0
  28. droidrun/agent/manager/manager_agent.py +459 -0
  29. droidrun/agent/manager/prompts.py +223 -0
  30. droidrun/agent/oneflows/app_starter_workflow.py +118 -0
  31. droidrun/agent/oneflows/text_manipulator.py +204 -0
  32. droidrun/agent/planner/__init__.py +3 -3
  33. droidrun/agent/planner/events.py +6 -3
  34. droidrun/agent/planner/planner_agent.py +27 -42
  35. droidrun/agent/planner/prompts.py +2 -2
  36. droidrun/agent/usage.py +11 -11
  37. droidrun/agent/utils/__init__.py +11 -1
  38. droidrun/agent/utils/async_utils.py +2 -1
  39. droidrun/agent/utils/chat_utils.py +48 -60
  40. droidrun/agent/utils/device_state_formatter.py +177 -0
  41. droidrun/agent/utils/executer.py +12 -11
  42. droidrun/agent/utils/inference.py +114 -0
  43. droidrun/agent/utils/llm_picker.py +2 -0
  44. droidrun/agent/utils/message_utils.py +85 -0
  45. droidrun/agent/utils/tools.py +220 -0
  46. droidrun/agent/utils/trajectory.py +8 -7
  47. droidrun/cli/__init__.py +1 -1
  48. droidrun/cli/logs.py +29 -28
  49. droidrun/cli/main.py +279 -143
  50. droidrun/config_manager/__init__.py +25 -0
  51. droidrun/config_manager/config_manager.py +583 -0
  52. droidrun/macro/__init__.py +2 -2
  53. droidrun/macro/__main__.py +1 -1
  54. droidrun/macro/cli.py +36 -34
  55. droidrun/macro/replay.py +7 -9
  56. droidrun/portal.py +1 -1
  57. droidrun/telemetry/__init__.py +2 -2
  58. droidrun/telemetry/events.py +3 -4
  59. droidrun/telemetry/phoenix.py +173 -0
  60. droidrun/telemetry/tracker.py +7 -5
  61. droidrun/tools/__init__.py +1 -1
  62. droidrun/tools/adb.py +210 -82
  63. droidrun/tools/ios.py +7 -5
  64. droidrun/tools/tools.py +25 -8
  65. {droidrun-0.3.9.dist-info → droidrun-0.3.10.dev3.dist-info}/METADATA +5 -3
  66. droidrun-0.3.10.dev3.dist-info/RECORD +70 -0
  67. droidrun/agent/common/default.py +0 -5
  68. droidrun/agent/context/reflection.py +0 -20
  69. droidrun/agent/oneflows/reflector.py +0 -265
  70. droidrun-0.3.9.dist-info/RECORD +0 -56
  71. {droidrun-0.3.9.dist-info → droidrun-0.3.10.dev3.dist-info}/WHEEL +0 -0
  72. {droidrun-0.3.9.dist-info → droidrun-0.3.10.dev3.dist-info}/entry_points.txt +0 -0
  73. {droidrun-0.3.9.dist-info → droidrun-0.3.10.dev3.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,43 +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
260
  self.max_codeact_steps = 5
169
261
 
170
- if self.reflection:
171
- self.reflector = Reflector(llm=llm, debug=debug)
262
+
263
+ # Keep planner_agent for backward compatibility (can be removed later)
264
+ self.planner_agent = None
172
265
 
173
266
  else:
174
- 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
175
270
  self.planner_agent = None
176
271
 
272
+ # Get tool names from ATOMIC_ACTION_SIGNATURES for telemetry
273
+ atomic_tools = list(ATOMIC_ACTION_SIGNATURES.keys())
274
+
177
275
  capture(
276
+ # TODO: do proper telemetry instead of this ductaped crap
178
277
  DroidAgentInitEvent(
179
278
  goal=goal,
180
- llm=llm.class_name(),
181
- tools=",".join(self.tool_list),
279
+ llm=self.llm.class_name(),
280
+ tools=",".join(atomic_tools + ["remember", "complete"]),
182
281
  personas=",".join([p.name for p in personas]),
183
282
  max_steps=max_steps,
184
283
  timeout=timeout,
185
- vision=vision,
284
+ vision=self.vision,
186
285
  reasoning=reasoning,
187
- reflection=reflection,
188
286
  enable_tracing=enable_tracing,
189
287
  debug=debug,
190
288
  save_trajectories=save_trajectories,
@@ -200,6 +298,35 @@ class DroidAgent(Workflow):
200
298
  """
201
299
  return super().run(*args, **kwargs)
202
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
+
203
330
  @step
204
331
  async def execute_task(self, ctx: Context, ev: CodeActExecuteEvent) -> CodeActResultEvent:
205
332
  """
@@ -212,19 +339,18 @@ class DroidAgent(Workflow):
212
339
  Tuple of (success, reason)
213
340
  """
214
341
  task: Task = ev.task
215
- reflection = ev.reflection if ev.reflection is not None else None
216
342
  persona = self.cim.get_persona(task.agent_type)
217
343
 
218
344
  logger.info(f"🔧 Executing task: {task.description}")
219
345
 
220
346
  try:
221
347
  codeact_agent = CodeActAgent(
222
- llm=self.llm,
348
+ llm=self.codeact_llm,
223
349
  persona=persona,
224
- vision=self.vision,
350
+ vision=self.codeact_vision,
225
351
  max_steps=self.max_codeact_steps,
226
- all_tools_list=self.tool_list,
227
352
  tools_instance=self.tools_instance,
353
+ custom_tools=self.custom_tools,
228
354
  debug=self.debug,
229
355
  timeout=self.timeout,
230
356
  )
@@ -232,7 +358,6 @@ class DroidAgent(Workflow):
232
358
  handler = codeact_agent.run(
233
359
  input=task.description,
234
360
  remembered_info=self.tools_instance.memory,
235
- reflection=reflection,
236
361
  )
237
362
 
238
363
  async for nested_ev in handler.stream_events():
@@ -266,32 +391,17 @@ class DroidAgent(Workflow):
266
391
  @step
267
392
  async def handle_codeact_execute(
268
393
  self, ctx: Context, ev: CodeActResultEvent
269
- ) -> FinalizeEvent | ReflectionEvent | ReasoningLogicEvent:
394
+ ) -> FinalizeEvent:
270
395
  try:
271
396
  task = ev.task
272
- if not self.reasoning:
273
- return FinalizeEvent(
274
- success=ev.success,
275
- reason=ev.reason,
276
- output=ev.reason,
277
- task=[task],
278
- tasks=[task],
279
- steps=ev.steps,
280
- )
281
-
282
- if self.reflection and ev.success:
283
- return ReflectionEvent(task=task)
284
-
285
- # Reasoning is enabled but reflection is disabled.
286
- # Success: mark complete and proceed to next step in reasoning loop.
287
- # Failure: mark failed and trigger planner immediately without advancing to the next queued task.
288
- if ev.success:
289
- self.task_manager.complete_task(task, message=ev.reason)
290
- return ReasoningLogicEvent()
291
- else:
292
- self.task_manager.fail_task(task, failure_reason=ev.reason)
293
- return ReasoningLogicEvent(force_planning=True)
294
-
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
+ )
295
405
  except Exception as e:
296
406
  logger.error(f"❌ Error during DroidAgent execution: {e}")
297
407
  if self.debug:
@@ -309,141 +419,178 @@ class DroidAgent(Workflow):
309
419
  )
310
420
 
311
421
  @step
312
- async def reflect(
313
- self, ctx: Context, ev: ReflectionEvent
314
- ) -> ReasoningLogicEvent | CodeActExecuteEvent:
315
- task = ev.task
316
- if ev.task.agent_type == "AppStarterExpert":
317
- self.task_manager.complete_task(task)
318
- return ReasoningLogicEvent()
319
-
320
- reflection = await self.reflector.reflect_on_episodic_memory(
321
- episodic_memory=self.current_episodic_memory, goal=task.description
322
- )
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.
323
427
 
324
- if reflection.goal_achieved:
325
- self.task_manager.complete_task(task)
326
- 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)
327
433
 
328
- else:
329
- self.task_manager.fail_task(task)
330
- 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
+ # ========================================================================
331
454
 
332
455
  @step
333
- async def handle_reasoning_logic(
456
+ async def run_manager(
334
457
  self,
335
458
  ctx: Context,
336
- ev: ReasoningLogicEvent,
337
- ) -> FinalizeEvent | CodeActExecuteEvent:
338
- try:
339
- if self.step_counter >= self.max_steps:
340
- output = f"Reached maximum number of steps ({self.max_steps})"
341
- tasks = self.task_manager.get_task_history()
342
- return FinalizeEvent(
343
- success=False,
344
- reason=output,
345
- output=output,
346
- task=tasks,
347
- tasks=tasks,
348
- steps=self.step_counter,
349
- )
350
- self.step_counter += 1
459
+ ev: ManagerInputEvent
460
+ ) -> ManagerPlanEvent | FinalizeEvent:
461
+ """
462
+ Run Manager planning phase.
351
463
 
352
- if ev.reflection:
353
- handler = self.planner_agent.run(
354
- remembered_info=self.tools_instance.memory, reflection=ev.reflection
355
- )
356
- else:
357
- if not ev.force_planning and self.task_iter:
358
- try:
359
- task = next(self.task_iter)
360
- return CodeActExecuteEvent(task=task, reflection=None)
361
- except StopIteration as e:
362
- 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
+ )
363
475
 
364
- 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})")
365
478
 
366
- handler = self.planner_agent.run(
367
- remembered_info=self.tools_instance.memory, reflection=None
368
- )
479
+ # Run Manager workflow
480
+ handler = self.manager_agent.run()
369
481
 
370
- async for nested_ev in handler.stream_events():
371
- 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)
372
485
 
373
- result = await handler
486
+ result = await handler
374
487
 
375
- self.tasks = self.task_manager.get_all_tasks()
376
- 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
+ )
377
496
 
378
- if self.task_manager.goal_completed:
379
- logger.info(f"✅ Goal completed: {self.task_manager.message}")
380
- tasks = self.task_manager.get_task_history()
381
- return FinalizeEvent(
382
- success=True,
383
- reason=self.task_manager.message,
384
- output=self.task_manager.message,
385
- task=tasks,
386
- tasks=tasks,
387
- steps=self.step_counter,
388
- )
389
- if not self.tasks:
390
- logger.warning("No tasks generated by planner")
391
- output = "Planner did not generate any tasks"
392
- tasks = self.task_manager.get_task_history()
393
- return FinalizeEvent(
394
- success=False,
395
- reason=output,
396
- output=output,
397
- task=tasks,
398
- tasks=tasks,
399
- steps=self.step_counter,
400
- )
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.
401
505
 
402
- 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
+ )
403
518
 
404
- except Exception as e:
405
- logger.error(f" Error during DroidAgent execution: {e}")
406
- if self.debug:
407
- 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)
408
522
 
409
- logger.error(traceback.format_exc())
410
- tasks = self.task_manager.get_task_history()
411
- return FinalizeEvent(
412
- success=False,
413
- reason=str(e),
414
- output=str(e),
415
- task=tasks,
416
- tasks=tasks,
417
- steps=self.step_counter,
418
- )
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
+ )
419
562
 
420
563
  @step
421
- async def start_handler(
422
- self, ctx: Context, ev: StartEvent
423
- ) -> CodeActExecuteEvent | ReasoningLogicEvent:
564
+ async def handle_executor_result(
565
+ self,
566
+ ctx: Context,
567
+ ev: ExecutorResultEvent
568
+ ) -> ManagerInputEvent:
424
569
  """
425
- Main execution loop that coordinates between planning and execution.
570
+ Process Executor result and continue.
426
571
 
427
- Returns:
428
- 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.
429
574
  """
430
- logger.info(f"🚀 Running DroidAgent to achieve goal: {self.goal}")
431
- ctx.write_event_to_stream(ev)
575
+ # Check error escalation
576
+ err_thresh = self.shared_state.err_to_manager_thresh
432
577
 
433
- self.step_counter = 0
434
- 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
435
584
 
436
- if not self.reasoning:
437
- logger.info(f"🔄 Direct execution mode - executing goal: {self.goal}")
438
- task = Task(
439
- description=self.goal,
440
- status=self.task_manager.STATUS_PENDING,
441
- agent_type="Default",
442
- )
585
+ self.step_counter += 1
586
+ logger.info(f"🔄 Step {self.step_counter}/{self.max_steps} complete, looping to Manager")
443
587
 
444
- return CodeActExecuteEvent(task=task, reflection=None)
588
+ # Always loop back to Manager (it will check max steps in pre-flight)
589
+ return ManagerInputEvent()
445
590
 
446
- return ReasoningLogicEvent()
591
+ # ========================================================================
592
+ # End Manager/Executor Workflow Steps
593
+ # ========================================================================
447
594
 
448
595
  @step
449
596
  async def finalize(self, ctx: Context, ev: FinalizeEvent) -> StopEvent: