droidrun 0.3.10.dev10__tar.gz → 0.3.10.dev12__tar.gz

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 (133) hide show
  1. droidrun-0.3.10.dev12/.github/black.yml +19 -0
  2. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/PKG-INFO +1 -1
  3. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/codeact/codeact_agent.py +15 -2
  4. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/droid/droid_agent.py +142 -18
  5. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/droid/events.py +36 -20
  6. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/executor/executor_agent.py +8 -3
  7. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/manager/manager_agent.py +20 -1
  8. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/manager/prompts.py +20 -11
  9. droidrun-0.3.10.dev12/droidrun/agent/scripter/__init__.py +7 -0
  10. droidrun-0.3.10.dev12/droidrun/agent/scripter/events.py +36 -0
  11. droidrun-0.3.10.dev12/droidrun/agent/scripter/scripter_agent.py +281 -0
  12. droidrun-0.3.10.dev12/droidrun/agent/utils/async_utils.py +42 -0
  13. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/utils/executer.py +40 -5
  14. droidrun-0.3.10.dev12/droidrun/agent/utils/llm_loader.py +180 -0
  15. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/utils/tools.py +43 -2
  16. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/cli/main.py +137 -298
  17. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config/prompts/executor/system.jinja2 +1 -1
  18. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config/prompts/manager/rev1.jinja2 +55 -0
  19. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config/prompts/manager/system.jinja2 +51 -0
  20. droidrun-0.3.10.dev12/droidrun/config/prompts/scripter/system.jinja2 +86 -0
  21. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config_example.yaml +79 -0
  22. droidrun-0.3.10.dev12/droidrun/config_manager/__init__.py +52 -0
  23. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config_manager/config_manager.py +123 -1
  24. droidrun-0.3.10.dev12/droidrun/config_manager/safe_execution.py +226 -0
  25. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/pyproject.toml +1 -1
  26. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/uv.lock +1 -1
  27. droidrun-0.3.10.dev10/droidrun/agent/utils/async_utils.py +0 -18
  28. droidrun-0.3.10.dev10/droidrun/config_manager/__init__.py +0 -25
  29. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/.github/workflows/bounty.yml +0 -0
  30. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/.github/workflows/publish.yml +0 -0
  31. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/.gitignore +0 -0
  32. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/.python-version +0 -0
  33. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/CHANGELOG.md +0 -0
  34. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/CONTRIBUTING.md +0 -0
  35. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/LICENSE +0 -0
  36. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/MANIFEST.in +0 -0
  37. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/README.md +0 -0
  38. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/.generated-files.txt +0 -0
  39. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/docs.json +0 -0
  40. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/favicon.png +0 -0
  41. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/logo/dark.svg +0 -0
  42. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/logo/light.svg +0 -0
  43. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v1/concepts/agent.mdx +0 -0
  44. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v1/concepts/android-control.mdx +0 -0
  45. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v1/concepts/portal-app.mdx +0 -0
  46. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v1/overview.mdx +0 -0
  47. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v1/quickstart.mdx +0 -0
  48. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v2/concepts/agent.mdx +0 -0
  49. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v2/concepts/android-control.mdx +0 -0
  50. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v2/concepts/planning.mdx +0 -0
  51. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v2/concepts/portal-app.mdx +0 -0
  52. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v2/concepts/tracing.mdx +0 -0
  53. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v2/overview.mdx +0 -0
  54. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v2/quickstart.mdx +0 -0
  55. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/concepts/agent.mdx +0 -0
  56. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/concepts/android-tools.mdx +0 -0
  57. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/concepts/models.mdx +0 -0
  58. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/concepts/portal-app.mdx +0 -0
  59. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/guides/cli.mdx +0 -0
  60. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/guides/gemini.mdx +0 -0
  61. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/guides/ollama.mdx +0 -0
  62. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/guides/openailike.mdx +0 -0
  63. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/guides/overview.mdx +0 -0
  64. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/guides/telemetry.mdx +0 -0
  65. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/images/portal_apk.png +0 -0
  66. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/overview.mdx +0 -0
  67. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/quickstart.mdx +0 -0
  68. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/sdk/adb-tools.mdx +0 -0
  69. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/sdk/base-tools.mdx +0 -0
  70. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/sdk/droid-agent.mdx +0 -0
  71. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/docs/v3/sdk/ios-tools.mdx +0 -0
  72. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/__init__.py +0 -0
  73. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/__main__.py +0 -0
  74. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/__init__.py +0 -0
  75. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/codeact/__init__.py +0 -0
  76. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/codeact/events.py +0 -0
  77. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/common/__init__.py +0 -0
  78. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/common/constants.py +0 -0
  79. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/common/events.py +0 -0
  80. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/context/__init__.py +0 -0
  81. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/context/episodic_memory.py +0 -0
  82. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/context/task_manager.py +0 -0
  83. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/droid/__init__.py +0 -0
  84. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/executor/__init__.py +0 -0
  85. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/executor/events.py +0 -0
  86. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/executor/prompts.py +0 -0
  87. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/manager/__init__.py +0 -0
  88. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/manager/events.py +0 -0
  89. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/oneflows/__init__.py +0 -0
  90. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/oneflows/app_starter_workflow.py +0 -0
  91. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/oneflows/text_manipulator.py +0 -0
  92. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/usage.py +0 -0
  93. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/utils/__init__.py +0 -0
  94. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/utils/chat_utils.py +0 -0
  95. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/utils/device_state_formatter.py +0 -0
  96. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/utils/inference.py +0 -0
  97. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/utils/llm_picker.py +0 -0
  98. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/utils/message_utils.py +0 -0
  99. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/agent/utils/trajectory.py +0 -0
  100. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/app_cards/__init__.py +0 -0
  101. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/app_cards/app_card_provider.py +0 -0
  102. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/app_cards/providers/__init__.py +0 -0
  103. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/app_cards/providers/composite_provider.py +0 -0
  104. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/app_cards/providers/local_provider.py +0 -0
  105. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/app_cards/providers/server_provider.py +0 -0
  106. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/cli/__init__.py +0 -0
  107. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/cli/logs.py +0 -0
  108. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config/app_cards/README.md +0 -0
  109. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config/app_cards/app_cards.json +0 -0
  110. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config/app_cards/gmail.md +0 -0
  111. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config/prompts/codeact/system.jinja2 +0 -0
  112. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config/prompts/codeact/user.jinja2 +0 -0
  113. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config/prompts/executor/rev1.jinja2 +0 -0
  114. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config_manager/path_resolver.py +0 -0
  115. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/config_manager/prompt_loader.py +0 -0
  116. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/macro/__init__.py +0 -0
  117. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/macro/__main__.py +0 -0
  118. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/macro/cli.py +0 -0
  119. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/macro/replay.py +0 -0
  120. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/portal.py +0 -0
  121. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/telemetry/__init__.py +0 -0
  122. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/telemetry/events.py +0 -0
  123. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/telemetry/phoenix.py +0 -0
  124. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/telemetry/tracker.py +0 -0
  125. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/tools/__init__.py +0 -0
  126. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/tools/adb.py +0 -0
  127. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/tools/ios.py +0 -0
  128. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/tools/portal_client.py +0 -0
  129. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/droidrun/tools/tools.py +0 -0
  130. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/gen-docs-sdk-ref.sh +0 -0
  131. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/setup.py +0 -0
  132. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/static/droidrun-dark.png +0 -0
  133. {droidrun-0.3.10.dev10 → droidrun-0.3.10.dev12}/static/droidrun.png +0 -0
@@ -0,0 +1,19 @@
1
+ name: Lint
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ lint:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+
11
+ - uses: actions/setup-python@v5
12
+ with:
13
+ python-version: "3.13"
14
+
15
+ - uses: psf/black@stable
16
+ with:
17
+ options: "--check --diff --verbose"
18
+ src: "."
19
+ version: "25.9.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: droidrun
3
- Version: 0.3.10.dev10
3
+ Version: 0.3.10.dev12
4
4
  Summary: A framework for controlling Android devices through LLM agents
5
5
  Project-URL: Homepage, https://github.com/droidrun/droidrun
6
6
  Project-URL: Bug Tracker, https://github.com/droidrun/droidrun/issues
@@ -53,8 +53,10 @@ class CodeActAgent(Workflow):
53
53
  agent_config: AgentConfig,
54
54
  tools_instance: "Tools",
55
55
  custom_tools: dict = None,
56
+ atomic_tools: dict = None,
56
57
  debug: bool = False,
57
58
  shared_state: Optional["DroidAgentState"] = None,
59
+ safe_execution_config=None,
58
60
  *args,
59
61
  **kwargs,
60
62
  ):
@@ -77,8 +79,10 @@ class CodeActAgent(Workflow):
77
79
  self.goal = None
78
80
  self.code_exec_counter = 0
79
81
 
80
- # Build tool list
81
- merged_signatures = {**ATOMIC_ACTION_SIGNATURES, **(custom_tools or {})}
82
+ if atomic_tools is None:
83
+ atomic_tools = ATOMIC_ACTION_SIGNATURES
84
+
85
+ merged_signatures = {**atomic_tools, **(custom_tools or {})}
82
86
 
83
87
  self.tool_list = {}
84
88
  for action_name, signature in merged_signatures.items():
@@ -104,11 +108,20 @@ class CodeActAgent(Workflow):
104
108
  )
105
109
  self.system_prompt = ChatMessage(role="system", content=system_prompt_text)
106
110
 
111
+ # Get safety settings
112
+ safe_mode = self.config.safe_execution
113
+ safe_config = safe_execution_config
114
+
107
115
  self.executor = SimpleCodeExecutor(
108
116
  loop=asyncio.get_event_loop(),
109
117
  locals={},
110
118
  tools=self.tool_list,
111
119
  globals={"__builtins__": __builtins__},
120
+ safe_mode=safe_mode,
121
+ allowed_modules=safe_config.get_allowed_modules() if safe_config and safe_mode else None,
122
+ blocked_modules=safe_config.get_blocked_modules() if safe_config and safe_mode else None,
123
+ allowed_builtins=safe_config.get_allowed_builtins() if safe_config and safe_mode else None,
124
+ blocked_builtins=safe_config.get_blocked_builtins() if safe_config and safe_mode else None,
112
125
  )
113
126
 
114
127
  logger.info("✅ CodeActAgent initialized successfully.")
@@ -7,9 +7,10 @@ Architecture:
7
7
  - When reasoning=True: Uses Manager (planning) + Executor (action) workflows
8
8
  """
9
9
 
10
+ import asyncio
10
11
  import logging
11
12
  from typing import List
12
-
13
+ from droidrun.agent.utils.tools import create_tools_from_config
13
14
  import llama_index.core
14
15
  from llama_index.core.llms.llm import LLM
15
16
  from llama_index.core.workflow import Context, StartEvent, StopEvent, Workflow, step
@@ -29,9 +30,13 @@ from droidrun.agent.droid.events import (
29
30
  FinalizeEvent,
30
31
  ManagerInputEvent,
31
32
  ManagerPlanEvent,
33
+ ScripterExecutorInputEvent,
34
+ ScripterExecutorResultEvent,
32
35
  )
33
36
  from droidrun.agent.executor import ExecutorAgent
34
37
  from droidrun.agent.manager import ManagerAgent
38
+ from droidrun.agent.scripter import ScripterAgent
39
+ from droidrun.agent.utils.async_utils import wrap_async_tools
35
40
  from droidrun.agent.utils.tools import ATOMIC_ACTION_SIGNATURES, open_app
36
41
  from droidrun.agent.utils.trajectory import Trajectory
37
42
  from droidrun.config_manager.config_manager import (
@@ -51,6 +56,7 @@ from droidrun.telemetry import (
51
56
  )
52
57
  from droidrun.telemetry.phoenix import arize_phoenix_callback_handler
53
58
  from droidrun.tools import Tools
59
+ from droidrun.agent.utils.llm_loader import load_agent_llms, validate_llm_dict
54
60
 
55
61
  logger = logging.getLogger("droidrun")
56
62
 
@@ -89,9 +95,8 @@ class DroidAgent(Workflow):
89
95
  def __init__(
90
96
  self,
91
97
  goal: str,
92
- llms: dict[str, LLM] | LLM,
93
- tools: Tools,
94
98
  config: DroidRunConfig | None = None,
99
+ llms: dict[str, LLM] | LLM | None = None,
95
100
  agent_config: AgentConfig | None = None,
96
101
  device_config: DeviceConfig | None = None,
97
102
  tools_config: ToolsConfig | None = None,
@@ -109,9 +114,9 @@ class DroidAgent(Workflow):
109
114
 
110
115
  Args:
111
116
  goal: User's goal or command
112
- llms: Dict of agent-specific LLMs or single LLM for all
113
- tools: Tools instance (AdbTools or IOSTools)
114
- config: Full config override (optional)
117
+ config: Full config (required if llms not provided)
118
+ llms: Optional dict of agent-specific LLMs or single LLM for all.
119
+ If not provided, LLMs will be loaded from config profiles.
115
120
  agent_config: Agent config override (optional)
116
121
  device_config: Device config override (optional)
117
122
  tools_config: Tools config override (optional)
@@ -141,10 +146,31 @@ class DroidAgent(Workflow):
141
146
  llm_profiles=base_config.llm_profiles,
142
147
  )
143
148
 
149
+ # Create tools from config
150
+ tools = create_tools_from_config(self.config.device)
151
+
144
152
  super().__init__(*args, timeout=timeout, **kwargs)
145
153
 
146
154
  self._configure_default_logging(debug=self.config.logging.debug)
147
155
 
156
+ # Load LLMs if not provided
157
+ if llms is None:
158
+ if config is None:
159
+ raise ValueError(
160
+ "Either 'llms' or 'config' must be provided. "
161
+ "If llms is not provided, config is required to load LLMs from profiles."
162
+ )
163
+
164
+
165
+ logger.info("🔄 Loading LLMs from config (llms not provided)...")
166
+
167
+
168
+ llms = load_agent_llms(
169
+ config=self.config,
170
+ **kwargs
171
+ )
172
+ validate_llm_dict(self.config, llms)
173
+
148
174
  if self.config.tracing.enabled:
149
175
  try:
150
176
  handler = arize_phoenix_callback_handler()
@@ -167,11 +193,7 @@ class DroidAgent(Workflow):
167
193
  self.codeact_llm = llms.get('codeact')
168
194
  self.text_manipulator_llm = llms.get('text_manipulator')
169
195
  self.app_opener_llm = llms.get('app_opener')
170
-
171
- if self.config.agent.reasoning and (not self.manager_llm or not self.executor_llm):
172
- raise ValueError("When reasoning=True, 'manager' and 'executor' LLMs must be provided in llms dict")
173
- if not self.codeact_llm:
174
- raise ValueError("'codeact' LLM must be provided in llms dict")
196
+ self.scripter_llm = llms.get('scripter', self.codeact_llm)
175
197
 
176
198
  logger.info("📚 Using agent-specific LLMs from dictionary")
177
199
  else:
@@ -181,6 +203,7 @@ class DroidAgent(Workflow):
181
203
  self.codeact_llm = llms
182
204
  self.text_manipulator_llm = llms
183
205
  self.app_opener_llm = llms
206
+ self.scripter_llm = llms
184
207
 
185
208
 
186
209
  self.trajectory = Trajectory(goal=self.shared_state.instruction)
@@ -188,12 +211,13 @@ class DroidAgent(Workflow):
188
211
  self.task_iter = None
189
212
  self.current_episodic_memory = None
190
213
 
214
+ self.atomic_tools = ATOMIC_ACTION_SIGNATURES.copy()
215
+
191
216
  open_app_tool = {
192
217
  "arguments": ["text"],
193
218
  "description": "Open an app by name. Usage example: {\"action\": \"open_app\", \"text\": \"the name of app\"}",
194
219
  "function": open_app,
195
220
  }
196
- # Merge with user-provided custom tools
197
221
  self.custom_tools = {**self.custom_tools, "open_app": open_app_tool}
198
222
 
199
223
  logger.info("🤖 Initializing DroidAgent...")
@@ -201,8 +225,9 @@ class DroidAgent(Workflow):
201
225
 
202
226
  self.tools_instance = tools
203
227
  self.tools_instance.save_trajectories = self.config.logging.save_trajectory
204
- # Set app_opener_llm on tools instance for open_app custom tool
228
+ # Set LLMs on tools instance for helper tools
205
229
  self.tools_instance.app_opener_llm = self.app_opener_llm
230
+ self.tools_instance.text_manipulator_llm = self.text_manipulator_llm
206
231
 
207
232
 
208
233
  if self.config.agent.reasoning:
@@ -289,8 +314,10 @@ class DroidAgent(Workflow):
289
314
  agent_config=self.config.agent,
290
315
  tools_instance=self.tools_instance,
291
316
  custom_tools=self.custom_tools,
317
+ atomic_tools=self.atomic_tools,
292
318
  debug=self.config.logging.debug,
293
319
  shared_state=self.shared_state,
320
+ safe_execution_config=self.config.safe_execution,
294
321
  timeout=self.timeout,
295
322
  )
296
323
 
@@ -356,6 +383,13 @@ class DroidAgent(Workflow):
356
383
  logger.info(f"🚀 Running DroidAgent to achieve goal: {self.shared_state.instruction}")
357
384
  ctx.write_event_to_stream(ev)
358
385
 
386
+ if not hasattr(self, '_tools_wrapped') and not self.config.agent.reasoning:
387
+
388
+ self.atomic_tools = wrap_async_tools(self.atomic_tools)
389
+ self.custom_tools = wrap_async_tools(self.custom_tools)
390
+
391
+ self._tools_wrapped = True
392
+ logger.debug("✅ Async tools wrapped for synchronous execution contexts")
359
393
 
360
394
  if not self.config.agent.reasoning:
361
395
  logger.info(f"🔄 Direct execution mode - executing goal: {self.shared_state.instruction}")
@@ -416,11 +450,11 @@ class DroidAgent(Workflow):
416
450
  self,
417
451
  ctx: Context,
418
452
  ev: ManagerPlanEvent
419
- ) -> ExecutorInputEvent | FinalizeEvent:
453
+ ) -> ExecutorInputEvent | ScripterExecutorInputEvent | FinalizeEvent:
420
454
  """
421
455
  Process Manager output and decide next step.
422
456
 
423
- Checks if task is complete or if Executor should take action.
457
+ Checks if task is complete, if ScripterAgent should run, or if Executor should take action.
424
458
  """
425
459
  # Check for answer-type termination
426
460
  if ev.manager_answer.strip():
@@ -432,6 +466,21 @@ class DroidAgent(Workflow):
432
466
  reason=ev.manager_answer
433
467
  )
434
468
 
469
+ # Check for <script> tag in current_subgoal, then extract from full plan
470
+ if '<script>' in ev.current_subgoal:
471
+ # Found script tag in subgoal - now search the entire plan
472
+ start_idx = ev.plan.find('<script>')
473
+ end_idx = ev.plan.find('</script>')
474
+
475
+ if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
476
+ # Extract content between first <script> and first </script> in plan
477
+ task = ev.plan[start_idx + len('<script>'):end_idx].strip()
478
+ logger.info(f"🐍 Routing to ScripterAgent: {task[:80]}...")
479
+ return ScripterExecutorInputEvent(task=task)
480
+ else:
481
+ # <script> found in subgoal but not properly closed in plan - log warning
482
+ logger.warning("⚠️ Found <script> in subgoal but not properly closed in plan, treating as regular subgoal")
483
+
435
484
  # Continue to Executor with current subgoal
436
485
  logger.info(f"▶️ Proceeding to Executor with subgoal: {ev.current_subgoal}")
437
486
  return ExecutorInputEvent(current_subgoal=ev.current_subgoal)
@@ -507,7 +556,82 @@ class DroidAgent(Workflow):
507
556
  return ManagerInputEvent()
508
557
 
509
558
  # ========================================================================
510
- # End Manager/Executor Workflow Steps
559
+ # Script Executor Workflow Steps
560
+ # ========================================================================
561
+
562
+ @step
563
+ async def run_scripter(
564
+ self,
565
+ ctx: Context,
566
+ ev: ScripterExecutorInputEvent
567
+ ) -> ScripterExecutorResultEvent:
568
+ """
569
+ Instantiate and run ScripterAgent for off-device operations.
570
+ """
571
+ logger.info(f"🐍 Starting ScripterAgent for task: {ev.task[:2000]}...")
572
+
573
+ # Create fresh ScripterAgent instance for this task
574
+ scripter_agent = ScripterAgent(
575
+ llm=self.scripter_llm,
576
+ agent_config=self.config.agent,
577
+ shared_state=self.shared_state,
578
+ task=ev.task,
579
+ safe_execution_config=self.config.safe_execution,
580
+ timeout=self.timeout
581
+ )
582
+
583
+ # Run ScripterAgent workflow
584
+ handler = scripter_agent.run()
585
+
586
+ # Stream nested events
587
+ async for nested_ev in handler.stream_events():
588
+ ctx.write_event_to_stream(nested_ev)
589
+
590
+ result = await handler
591
+
592
+ # Store in shared state
593
+ script_record = {
594
+ 'task': ev.task,
595
+ 'message': result['message'],
596
+ 'success': result['success'],
597
+ 'code_executions': result.get('code_executions', 0)
598
+ }
599
+ self.shared_state.scripter_history.append(script_record)
600
+ self.shared_state.last_scripter_message = result['message']
601
+ self.shared_state.last_scripter_success = result['success']
602
+
603
+ logger.info(f"🐍 ScripterAgent finished: {result['message'][:2000]}...")
604
+
605
+ return ScripterExecutorResultEvent(
606
+ task=ev.task,
607
+ message=result['message'],
608
+ success=result['success'],
609
+ code_executions=result.get('code_executions', 0)
610
+ )
611
+
612
+ @step
613
+ async def handle_scripter_result(
614
+ self,
615
+ ctx: Context,
616
+ ev: ScripterExecutorResultEvent
617
+ ) -> ManagerInputEvent:
618
+ """
619
+ Process ScripterAgent result and loop back to Manager.
620
+ """
621
+ if ev.success:
622
+ logger.info(f"✅ Script completed successfully in {ev.code_executions} steps")
623
+ else:
624
+ logger.warning(f"⚠️ Script failed or reached max steps: {ev.message}")
625
+
626
+ # Increment DroidAgent step counter
627
+ self.shared_state.step_number += 1
628
+ logger.info(f"🔄 Step {self.shared_state.step_number}/{self.config.agent.max_steps} complete, looping to Manager")
629
+
630
+ # Loop back to Manager (script result in shared_state)
631
+ return ManagerInputEvent()
632
+
633
+ # ========================================================================
634
+ # End Manager/Executor/Script Workflow Steps
511
635
  # ========================================================================
512
636
 
513
637
  @step
@@ -518,8 +642,8 @@ class DroidAgent(Workflow):
518
642
  success=ev.success,
519
643
  reason=ev.reason,
520
644
  steps=self.shared_state.step_number,
521
- unique_packages_count=len(self.shared_state._visited_packages),
522
- unique_activities_count=len(self.shared_state._visited_activities),
645
+ unique_packages_count=len(self.shared_state.visited_packages),
646
+ unique_activities_count=len(self.shared_state.visited_activities),
523
647
  ),
524
648
  self.user_id,
525
649
  )
@@ -60,10 +60,10 @@ class DroidAgentState(BaseModel):
60
60
  phone_state: Dict = Field(default_factory=dict)
61
61
 
62
62
  # Private fields
63
- _current_package_name: str = ""
64
- _current_activity_name: str = ""
65
- _visited_packages: set = Field(default_factory=set)
66
- _visited_activities: set = Field(default_factory=set)
63
+ current_package_name: str = ""
64
+ current_activity_name: str = ""
65
+ visited_packages: set = Field(default_factory=set)
66
+ visited_activities: set = Field(default_factory=set)
67
67
 
68
68
  # Previous device state (for before/after comparison in Manager)
69
69
  previous_formatted_device_state: str = ""
@@ -103,19 +103,14 @@ class DroidAgentState(BaseModel):
103
103
  error_flag_plan: bool = False
104
104
  err_to_manager_thresh: int = 2
105
105
 
106
+ # Script execution tracking
107
+ scripter_history: List[Dict] = Field(default_factory=list)
108
+ last_scripter_message: str = ""
109
+ last_scripter_success: bool = True
110
+
106
111
  # Output
107
112
  output_dir: str = ""
108
113
 
109
- @property
110
- def current_package_name(self) -> str:
111
- """Get current package name"""
112
- return self._current_package_name
113
-
114
- @property
115
- def current_activity_name(self) -> str:
116
- """Get current activity name"""
117
- return self._current_activity_name
118
-
119
114
  def update_current_app(self, package_name: str, activity_name: str):
120
115
  """
121
116
  Update package and activity together, capturing telemetry event only once.
@@ -123,21 +118,21 @@ class DroidAgentState(BaseModel):
123
118
  This prevents duplicate PackageVisitEvents when both package and activity change.
124
119
  """
125
120
  # Check if either changed
126
- package_changed = package_name != self._current_package_name
127
- activity_changed = activity_name != self._current_activity_name
121
+ package_changed = package_name != self.current_package_name
122
+ activity_changed = activity_name != self.current_activity_name
128
123
 
129
124
  if not (package_changed or activity_changed):
130
125
  return # No change, nothing to do
131
126
 
132
127
  # Update tracking sets
133
128
  if package_changed and package_name:
134
- self._visited_packages.add(package_name)
129
+ self.visited_packages.add(package_name)
135
130
  if activity_changed and activity_name:
136
- self._visited_activities.add(activity_name)
131
+ self.visited_activities.add(activity_name)
137
132
 
138
133
  # Update values
139
- self._current_package_name = package_name
140
- self._current_activity_name = activity_name
134
+ self.current_package_name = package_name
135
+ self.current_activity_name = activity_name
141
136
 
142
137
  # Capture telemetry event for any change
143
138
  # This ensures we track when apps close or transitions to empty state occur
@@ -189,3 +184,24 @@ class ExecutorResultEvent(Event):
189
184
  outcome: bool
190
185
  error: str
191
186
  summary: str
187
+
188
+
189
+ # ============================================================================
190
+ # Script executor coordination events
191
+ # ============================================================================
192
+
193
+ class ScripterExecutorInputEvent(Event):
194
+ """Trigger ScripterAgent workflow for off-device operations"""
195
+ task: str
196
+
197
+
198
+ class ScripterExecutorResultEvent(Event):
199
+ """
200
+ Coordination event from ScripterAgent to DroidAgent.
201
+
202
+ Used for workflow step routing only (NOT streamed to frontend).
203
+ """
204
+ task: str
205
+ message: str # Response from response() function
206
+ success: bool
207
+ code_executions: int
@@ -67,7 +67,12 @@ class ExecutorAgent(Workflow):
67
67
  self.vision = agent_config.executor.vision
68
68
  self.tools_instance = tools_instance
69
69
  self.shared_state = shared_state
70
- self.custom_tools = custom_tools or {}
70
+
71
+ # Merge custom_tools with atomic actions (same as CodeActAgent)
72
+ atomic_tools = ATOMIC_ACTION_SIGNATURES
73
+ merged_signatures = {**atomic_tools, **(custom_tools or {})}
74
+ self.all_actions = merged_signatures # Store merged dict for prompt
75
+ self.custom_tools = custom_tools or {} # Keep for execution lookup
71
76
 
72
77
  logger.info("✅ ExecutorAgent initialized successfully.")
73
78
 
@@ -120,7 +125,7 @@ class ExecutorAgent(Workflow):
120
125
  "plan": self.shared_state.plan,
121
126
  "subgoal": subgoal,
122
127
  "progress_status": self.shared_state.progress_status,
123
- "atomic_actions": ATOMIC_ACTION_SIGNATURES,
128
+ "atomic_actions": self.all_actions, # Now includes custom tools!
124
129
  "action_history": action_history
125
130
  }
126
131
  )
@@ -296,7 +301,7 @@ class ExecutorAgent(Workflow):
296
301
  if text is None:
297
302
  return False, "Missing 'text' parameter", "Failed: open_app requires text"
298
303
 
299
- result = open_app(self.tools_instance, text)
304
+ result = await open_app(self.tools_instance, text)
300
305
  return True, "None", f"Opened app: {text}"
301
306
 
302
307
  else:
@@ -161,7 +161,9 @@ class ManagerAgent(Workflow):
161
161
  "important_notes": "", # TODO: implement
162
162
  "error_history": error_history,
163
163
  "text_manipulation_enabled": has_text_to_modify,
164
- "custom_tools_descriptions": build_custom_tool_descriptions(self.custom_tools)
164
+ "custom_tools_descriptions": build_custom_tool_descriptions(self.custom_tools),
165
+ "scripter_execution_enabled": self.agent_config.scripter.enabled,
166
+ "scripter_max_steps": self.agent_config.scripter.max_steps,
165
167
  }
166
168
  )
167
169
 
@@ -219,6 +221,23 @@ class ManagerAgent(Workflow):
219
221
  if screenshot and self.vision:
220
222
  messages[last_user_idx]['content'].append({"image": screenshot})
221
223
 
224
+ # Add script result if available
225
+ if self.shared_state.last_scripter_message:
226
+ status = "SUCCESS" if self.shared_state.last_scripter_success else "FAILED"
227
+ script_context = (
228
+ f"\n<script_result status=\"{status}\">\n"
229
+ f"{self.shared_state.last_scripter_message}\n"
230
+ f"</script_result>\n"
231
+ )
232
+
233
+ if messages[last_user_idx]['content'] and 'text' in messages[last_user_idx]['content'][0]:
234
+ messages[last_user_idx]['content'][0]['text'] += script_context
235
+ else:
236
+ messages[last_user_idx]['content'].insert(0, {"text": script_context})
237
+
238
+ # Clear after injection (avoid duplicate injection)
239
+ self.shared_state.last_scripter_message = ""
240
+
222
241
  # Add PREVIOUS device state to SECOND-TO-LAST user message (if exists)
223
242
  if len(user_indices) >= 2:
224
243
  second_last_user_idx = user_indices[-2]
@@ -17,6 +17,7 @@ def parse_manager_response(response: str) -> dict:
17
17
 
18
18
  Also derives:
19
19
  - current_subgoal: first line of plan (with list markers removed)
20
+ - If first item is <script> tag, extract script content as current_subgoal
20
21
 
21
22
  Args:
22
23
  response: Raw LLM response text
@@ -26,7 +27,7 @@ def parse_manager_response(response: str) -> dict:
26
27
  - thought: str
27
28
  - memory: str
28
29
  - plan: str
29
- - current_subgoal: str (first line of plan, cleaned)
30
+ - current_subgoal: str (first line of plan, cleaned, or script content)
30
31
  - request_accomplished: str (from request_accomplished tag)
31
32
  """
32
33
  def extract(tag: str) -> str:
@@ -42,19 +43,27 @@ def parse_manager_response(response: str) -> dict:
42
43
 
43
44
  # Parse current subgoal from first line of plan
44
45
  current_goal_text = plan
45
- # Prefer newline-separated plans; take the first non-empty line
46
- plan_lines = [line.strip() for line in current_goal_text.splitlines() if line.strip()]
47
- if plan_lines:
48
- first_line = plan_lines[0]
46
+
47
+ # Check if first item is a <script> tag
48
+ script_match = re.search(r'^\s*<script>(.*?)</script>', current_goal_text, re.DOTALL)
49
+
50
+ if script_match:
51
+ # Script is first task - extract script content with tag
52
+ current_subgoal = f"<script>{script_match.group(1).strip()}</script>"
49
53
  else:
50
- first_line = current_goal_text.strip()
54
+ # Regular subgoal - use existing logic
55
+ plan_lines = [line.strip() for line in current_goal_text.splitlines() if line.strip()]
56
+ if plan_lines:
57
+ first_line = plan_lines[0]
58
+ else:
59
+ first_line = current_goal_text.strip()
51
60
 
52
- # Remove common list markers like "1.", "-", "*", or bullet characters
53
- first_line = re.sub(r"^\s*\d+\.\s*", "", first_line) # Remove "1. ", "2. ", etc.
54
- first_line = re.sub(r"^\s*[-*]\s*", "", first_line) # Remove "- " or "* "
55
- first_line = re.sub(r"^\s*•\s*", "", first_line) # Remove bullet "• "
61
+ # Remove common list markers like "1.", "-", "*", or bullet characters
62
+ first_line = re.sub(r"^\s*\d+\.\s*", "", first_line) # Remove "1. ", "2. ", etc.
63
+ first_line = re.sub(r"^\s*[-*]\s*", "", first_line) # Remove "- " or "* "
64
+ first_line = re.sub(r"^\s*•\s*", "", first_line) # Remove bullet "• "
56
65
 
57
- current_subgoal = first_line.strip()
66
+ current_subgoal = first_line.strip()
58
67
 
59
68
  return {
60
69
  "thought": thought,
@@ -0,0 +1,7 @@
1
+ """
2
+ ScripterAgent - ReAct agent for executing Python scripts (off-device operations).
3
+ """
4
+
5
+ from droidrun.agent.scripter.scripter_agent import ScripterAgent
6
+
7
+ __all__ = ["ScripterAgent"]
@@ -0,0 +1,36 @@
1
+ """
2
+ Events for ScripterAgent workflow.
3
+ """
4
+
5
+ from typing import List, Optional
6
+
7
+ from llama_index.core.workflow import Event
8
+
9
+
10
+ class ScripterInputEvent(Event):
11
+ """Input to LLM (chat history)."""
12
+ input: List # List of ChatMessages
13
+
14
+
15
+ class ScripterThinkingEvent(Event):
16
+ """LLM generated thought + code."""
17
+ thoughts: str
18
+ code: Optional[str] = None
19
+ full_response: str = "" # Full LLM response (for fallback when no code)
20
+
21
+
22
+ class ScripterExecutionEvent(Event):
23
+ """Trigger code execution."""
24
+ code: str
25
+
26
+
27
+ class ScripterExecutionResultEvent(Event):
28
+ """Code execution result."""
29
+ output: str
30
+
31
+
32
+ class ScripterEndEvent(Event):
33
+ """Script agent finished."""
34
+ message: str # Message to Manager
35
+ success: bool # True if response() called, False if max_steps
36
+ code_executions: int = 0