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.
- droidrun/__init__.py +2 -3
- droidrun/__main__.py +1 -1
- droidrun/agent/__init__.py +1 -1
- droidrun/agent/codeact/__init__.py +1 -4
- droidrun/agent/codeact/codeact_agent.py +112 -48
- droidrun/agent/codeact/events.py +6 -3
- droidrun/agent/codeact/prompts.py +2 -2
- droidrun/agent/common/constants.py +2 -0
- droidrun/agent/common/events.py +5 -3
- droidrun/agent/context/__init__.py +1 -3
- droidrun/agent/context/agent_persona.py +2 -1
- droidrun/agent/context/context_injection_manager.py +6 -6
- droidrun/agent/context/episodic_memory.py +5 -3
- droidrun/agent/context/personas/__init__.py +3 -3
- droidrun/agent/context/personas/app_starter.py +3 -3
- droidrun/agent/context/personas/big_agent.py +3 -3
- droidrun/agent/context/personas/default.py +3 -3
- droidrun/agent/context/personas/ui_expert.py +5 -5
- droidrun/agent/context/task_manager.py +15 -17
- droidrun/agent/droid/__init__.py +1 -1
- droidrun/agent/droid/droid_agent.py +327 -182
- droidrun/agent/droid/events.py +91 -9
- droidrun/agent/executor/__init__.py +13 -0
- droidrun/agent/executor/events.py +24 -0
- droidrun/agent/executor/executor_agent.py +327 -0
- droidrun/agent/executor/prompts.py +136 -0
- droidrun/agent/manager/__init__.py +18 -0
- droidrun/agent/manager/events.py +20 -0
- droidrun/agent/manager/manager_agent.py +459 -0
- droidrun/agent/manager/prompts.py +223 -0
- droidrun/agent/oneflows/app_starter_workflow.py +118 -0
- droidrun/agent/oneflows/text_manipulator.py +204 -0
- droidrun/agent/planner/__init__.py +3 -3
- droidrun/agent/planner/events.py +6 -3
- droidrun/agent/planner/planner_agent.py +60 -53
- droidrun/agent/planner/prompts.py +2 -2
- droidrun/agent/usage.py +15 -13
- droidrun/agent/utils/__init__.py +11 -1
- droidrun/agent/utils/async_utils.py +2 -1
- droidrun/agent/utils/chat_utils.py +48 -60
- droidrun/agent/utils/device_state_formatter.py +177 -0
- droidrun/agent/utils/executer.py +13 -12
- droidrun/agent/utils/inference.py +114 -0
- droidrun/agent/utils/llm_picker.py +2 -0
- droidrun/agent/utils/message_utils.py +85 -0
- droidrun/agent/utils/tools.py +220 -0
- droidrun/agent/utils/trajectory.py +8 -7
- droidrun/cli/__init__.py +1 -1
- droidrun/cli/logs.py +29 -28
- droidrun/cli/main.py +279 -143
- droidrun/config_manager/__init__.py +25 -0
- droidrun/config_manager/config_manager.py +583 -0
- droidrun/macro/__init__.py +2 -2
- droidrun/macro/__main__.py +1 -1
- droidrun/macro/cli.py +36 -34
- droidrun/macro/replay.py +7 -9
- droidrun/portal.py +1 -1
- droidrun/telemetry/__init__.py +2 -2
- droidrun/telemetry/events.py +3 -4
- droidrun/telemetry/phoenix.py +173 -0
- droidrun/telemetry/tracker.py +7 -5
- droidrun/tools/__init__.py +1 -1
- droidrun/tools/adb.py +210 -82
- droidrun/tools/ios.py +7 -5
- droidrun/tools/tools.py +25 -8
- {droidrun-0.3.8.dist-info → droidrun-0.3.10.dev2.dist-info}/METADATA +13 -7
- droidrun-0.3.10.dev2.dist-info/RECORD +70 -0
- droidrun/agent/common/default.py +0 -5
- droidrun/agent/context/reflection.py +0 -20
- droidrun/agent/oneflows/reflector.py +0 -265
- droidrun-0.3.8.dist-info/RECORD +0 -55
- {droidrun-0.3.8.dist-info → droidrun-0.3.10.dev2.dist-info}/WHEEL +0 -0
- {droidrun-0.3.8.dist-info → droidrun-0.3.10.dev2.dist-info}/entry_points.txt +0 -0
- {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
|
15
|
+
from llama_index.core.workflow import Context, StartEvent, StopEvent, Workflow, step
|
11
16
|
from llama_index.core.workflow.handler import WorkflowHandler
|
12
|
-
from
|
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.
|
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.
|
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
|
38
|
-
|
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
|
-
|
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
|
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
|
-
|
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(
|
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
|
158
|
-
self.
|
159
|
-
|
160
|
-
|
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
|
-
|
172
|
-
|
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("🚫
|
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(
|
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.
|
348
|
+
llm=self.codeact_llm,
|
224
349
|
persona=persona,
|
225
|
-
vision=self.
|
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
|
394
|
+
) -> FinalizeEvent:
|
271
395
|
try:
|
272
396
|
task = ev.task
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
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
|
314
|
-
self, ctx: Context, ev:
|
315
|
-
) ->
|
316
|
-
|
317
|
-
|
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
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
330
|
-
|
331
|
-
|
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
|
456
|
+
async def run_manager(
|
335
457
|
self,
|
336
458
|
ctx: Context,
|
337
|
-
ev:
|
338
|
-
|
339
|
-
|
340
|
-
|
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
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
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
|
-
|
476
|
+
# Continue with Manager execution
|
477
|
+
logger.info(f"📋 Running Manager for planning... (step {self.step_counter}/{self.max_steps})")
|
367
478
|
|
368
|
-
|
369
|
-
|
370
|
-
)
|
479
|
+
# Run Manager workflow
|
480
|
+
handler = self.manager_agent.run()
|
371
481
|
|
372
|
-
|
373
|
-
|
482
|
+
# Stream nested events
|
483
|
+
async for nested_ev in handler.stream_events():
|
484
|
+
self.handle_stream_event(nested_ev, ctx)
|
374
485
|
|
375
|
-
|
486
|
+
result = await handler
|
376
487
|
|
377
|
-
|
378
|
-
|
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
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
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
|
-
|
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
|
-
|
407
|
-
|
408
|
-
|
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
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
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
|
424
|
-
self,
|
425
|
-
|
564
|
+
async def handle_executor_result(
|
565
|
+
self,
|
566
|
+
ctx: Context,
|
567
|
+
ev: ExecutorResultEvent
|
568
|
+
) -> ManagerInputEvent:
|
426
569
|
"""
|
427
|
-
|
570
|
+
Process Executor result and continue.
|
428
571
|
|
429
|
-
|
430
|
-
|
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
|
-
|
433
|
-
|
575
|
+
# Check error escalation
|
576
|
+
err_thresh = self.shared_state.err_to_manager_thresh
|
434
577
|
|
435
|
-
self.
|
436
|
-
|
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
|
-
|
439
|
-
|
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
|
-
|
588
|
+
# Always loop back to Manager (it will check max steps in pre-flight)
|
589
|
+
return ManagerInputEvent()
|
447
590
|
|
448
|
-
|
591
|
+
# ========================================================================
|
592
|
+
# End Manager/Executor Workflow Steps
|
593
|
+
# ========================================================================
|
449
594
|
|
450
595
|
@step
|
451
596
|
async def finalize(self, ctx: Context, ev: FinalizeEvent) -> StopEvent:
|