droidrun 0.3.9__py3-none-any.whl → 0.3.10.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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.dev2.dist-info}/METADATA +6 -3
  66. droidrun-0.3.10.dev2.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.dev2.dist-info}/WHEEL +0 -0
  72. {droidrun-0.3.9.dist-info → droidrun-0.3.10.dev2.dist-info}/entry_points.txt +0 -0
  73. {droidrun-0.3.9.dist-info → droidrun-0.3.10.dev2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,583 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import threading
5
+ import yaml
6
+ from pathlib import Path
7
+ from typing import Any, Callable, Optional, Dict
8
+ from dataclasses import dataclass, field, asdict
9
+
10
+ # ---------- Helpers / defaults ----------
11
+ def _default_config_text() -> str:
12
+ """Generate default config.yaml content with all settings."""
13
+ return """# DroidRun Configuration File
14
+ # This file is auto-generated. Edit values as needed.
15
+
16
+ # === Agent Settings ===
17
+ agent:
18
+ # Maximum number of steps per task
19
+ max_steps: 15
20
+ # Enable vision capabilities per agent (screenshots)
21
+ vision:
22
+ manager: false
23
+ executor: false
24
+ codeact: false
25
+ # Enable planning with reasoning mode
26
+ reasoning: false
27
+
28
+ # === LLM Profiles ===
29
+ # Define LLM configurations for each agent type
30
+ llm_profiles:
31
+ # Manager: Plans and reasons about task progress
32
+ manager:
33
+ provider: GoogleGenAI
34
+ model: models/gemini-2.5-pro
35
+ temperature: 0.2
36
+ kwargs:
37
+ max_tokens: 8192
38
+
39
+ # Executor: Selects and executes atomic actions
40
+ executor:
41
+ provider: GoogleGenAI
42
+ model: models/gemini-2.5-pro
43
+ temperature: 0.1
44
+ kwargs:
45
+ max_tokens: 4096
46
+
47
+ # CodeAct: Generates and executes code actions
48
+ codeact:
49
+ provider: GoogleGenAI
50
+ model: models/gemini-2.5-pro
51
+ temperature: 0.2
52
+ kwargs:
53
+ max_tokens: 8192
54
+
55
+ # Text Manipulator: Edits text in input fields
56
+ text_manipulator:
57
+ provider: GoogleGenAI
58
+ model: models/gemini-2.5-pro
59
+ temperature: 0.3
60
+ kwargs:
61
+ max_tokens: 4096
62
+
63
+ # App Opener: Opens apps by name/description
64
+ app_opener:
65
+ provider: OpenAI
66
+ model: gpt-4o-mini
67
+ temperature: 0.0
68
+ base_url: null
69
+ api_base: null
70
+ kwargs:
71
+ max_tokens: 512
72
+ api_key: YOUR_API_KEY
73
+
74
+ # === Device Settings ===
75
+ device:
76
+ # Default device serial (null = auto-detect)
77
+ serial: null
78
+ # Use TCP communication instead of content provider
79
+ use_tcp: false
80
+ # Sleep duration after each action (seconds)
81
+ after_sleep_action: 1.0
82
+
83
+ # === Telemetry Settings ===
84
+ telemetry:
85
+ # Enable anonymous telemetry
86
+ enabled: true
87
+
88
+ # === Tracing Settings ===
89
+ tracing:
90
+ # Enable Arize Phoenix tracing
91
+ enabled: false
92
+
93
+ # === Logging Settings ===
94
+ logging:
95
+ # Enable debug logging
96
+ debug: false
97
+ # Trajectory saving level (none, step, action)
98
+ save_trajectory: none
99
+
100
+ # === Tool Settings ===
101
+ tools:
102
+ # Enable drag tool
103
+ allow_drag: false
104
+ """
105
+
106
+ def _default_project_config_path() -> Path:
107
+ """
108
+ Use module-relative resolution: two parents above this file -> project root.
109
+ """
110
+ return Path(__file__).resolve().parents[2] / "config.yaml"
111
+
112
+
113
+ # ---------- Config Schema ----------
114
+ @dataclass
115
+ class LLMProfile:
116
+ """LLM profile configuration."""
117
+ provider: str = "GoogleGenAI"
118
+ model: str = "models/gemini-2.0-flash-exp"
119
+ temperature: float = 0.2
120
+ base_url: Optional[str] = None
121
+ api_base: Optional[str] = None
122
+ kwargs: Dict[str, Any] = field(default_factory=dict)
123
+
124
+ def to_load_llm_kwargs(self) -> Dict[str, Any]:
125
+ """Convert profile to kwargs for load_llm function."""
126
+ result = {
127
+ "model": self.model,
128
+ "temperature": self.temperature,
129
+ }
130
+ # Add optional URL parameters
131
+ if self.base_url:
132
+ result["base_url"] = self.base_url
133
+ if self.api_base:
134
+ result["api_base"] = self.api_base
135
+ # Merge additional kwargs
136
+ result.update(self.kwargs)
137
+ return result
138
+
139
+
140
+ @dataclass
141
+ class VisionConfig:
142
+ """Per-agent vision settings."""
143
+ manager: bool = False
144
+ executor: bool = False
145
+ codeact: bool = False
146
+
147
+ def to_dict(self) -> Dict[str, bool]:
148
+ return {"manager": self.manager, "executor": self.executor, "codeact": self.codeact}
149
+
150
+ @classmethod
151
+ def from_dict(cls, data: Dict[str, Any]) -> "VisionConfig":
152
+ """Create VisionConfig from dictionary or bool."""
153
+ if isinstance(data, bool):
154
+ # Support single bool → apply to all agents
155
+ return cls(manager=data, executor=data, codeact=data)
156
+ return cls(
157
+ manager=data.get("manager", False),
158
+ executor=data.get("executor", False),
159
+ codeact=data.get("codeact", False),
160
+ )
161
+
162
+
163
+ @dataclass
164
+ class AgentConfig:
165
+ """Agent-related configuration."""
166
+ max_steps: int = 15
167
+ vision: VisionConfig = field(default_factory=VisionConfig)
168
+ reasoning: bool = False
169
+ after_sleep_action: float = 1.0
170
+ wait_for_stable_ui: float = 0.3
171
+
172
+
173
+ @dataclass
174
+ class DeviceConfig:
175
+ """Device-related configuration."""
176
+ serial: Optional[str] = None
177
+ use_tcp: bool = False
178
+
179
+
180
+ @dataclass
181
+ class TelemetryConfig:
182
+ """Telemetry configuration."""
183
+ enabled: bool = True
184
+
185
+
186
+ @dataclass
187
+ class TracingConfig:
188
+ """Tracing configuration."""
189
+ enabled: bool = False
190
+
191
+
192
+ @dataclass
193
+ class LoggingConfig:
194
+ """Logging configuration."""
195
+ debug: bool = False
196
+ save_trajectory: str = "none"
197
+
198
+
199
+ @dataclass
200
+ class ToolsConfig:
201
+ """Tools configuration."""
202
+ allow_drag: bool = False
203
+
204
+
205
+ @dataclass
206
+ class DroidRunConfig:
207
+ """Complete DroidRun configuration schema."""
208
+ agent: AgentConfig = field(default_factory=AgentConfig)
209
+ llm_profiles: Dict[str, LLMProfile] = field(default_factory=dict)
210
+ device: DeviceConfig = field(default_factory=DeviceConfig)
211
+ telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
212
+ tracing: TracingConfig = field(default_factory=TracingConfig)
213
+ logging: LoggingConfig = field(default_factory=LoggingConfig)
214
+ tools: ToolsConfig = field(default_factory=ToolsConfig)
215
+
216
+ def __post_init__(self):
217
+ """Ensure default profiles exist."""
218
+ if not self.llm_profiles:
219
+ self.llm_profiles = self._default_profiles()
220
+
221
+ @staticmethod
222
+ def _default_profiles() -> Dict[str, LLMProfile]:
223
+ """Get default agent specific LLM profiles."""
224
+ return {
225
+ "manager": LLMProfile(
226
+ provider="GoogleGenAI",
227
+ model="models/gemini-2.5-pro",
228
+ temperature=0.2,
229
+ kwargs={}
230
+ ),
231
+ "executor": LLMProfile(
232
+ provider="GoogleGenAI",
233
+ model="models/gemini-2.5-pro",
234
+ temperature=0.1,
235
+ kwargs={}
236
+ ),
237
+ "codeact": LLMProfile(
238
+ provider="GoogleGenAI",
239
+ model="models/gemini-2.5-pro",
240
+ temperature=0.2,
241
+ kwargs={"max_tokens": 8192 }
242
+ ),
243
+ "text_manipulator": LLMProfile(
244
+ provider="GoogleGenAI",
245
+ model="models/gemini-2.5-pro",
246
+ temperature=0.3,
247
+ kwargs={}
248
+ ),
249
+ "app_opener": LLMProfile(
250
+ provider="OpenAI",
251
+ model="models/gemini-2.5-pro",
252
+ temperature=0.0,
253
+ kwargs={}
254
+ ),
255
+ }
256
+
257
+ def to_dict(self) -> Dict[str, Any]:
258
+ """Convert config to dictionary."""
259
+ result = asdict(self)
260
+ # Convert LLMProfile objects to dicts
261
+ result["llm_profiles"] = {
262
+ name: asdict(profile) for name, profile in self.llm_profiles.items()
263
+ }
264
+ # Convert VisionConfig to dict
265
+ if isinstance(result["agent"]["vision"], dict):
266
+ pass # Already a dict from asdict
267
+ else:
268
+ result["agent"]["vision"] = self.agent.vision.to_dict()
269
+ return result
270
+
271
+ @classmethod
272
+ def from_dict(cls, data: Dict[str, Any]) -> "DroidRunConfig":
273
+ """Create config from dictionary."""
274
+ # Parse LLM profiles
275
+ llm_profiles = {}
276
+ for name, profile_data in data.get("llm_profiles", {}).items():
277
+ llm_profiles[name] = LLMProfile(**profile_data)
278
+
279
+ # Parse agent config with vision
280
+ agent_data = data.get("agent", {})
281
+ vision_data = agent_data.get("vision", {})
282
+ vision_config = VisionConfig.from_dict(vision_data)
283
+
284
+ agent_config = AgentConfig(
285
+ max_steps=agent_data.get("max_steps", 15),
286
+ vision=vision_config,
287
+ reasoning=agent_data.get("reasoning", False),
288
+ )
289
+
290
+ return cls(
291
+ agent=agent_config,
292
+ llm_profiles=llm_profiles,
293
+ device=DeviceConfig(**data.get("device", {})),
294
+ telemetry=TelemetryConfig(**data.get("telemetry", {})),
295
+ tracing=TracingConfig(**data.get("tracing", {})),
296
+ logging=LoggingConfig(**data.get("logging", {})),
297
+ tools=ToolsConfig(**data.get("tools", {})),
298
+ )
299
+
300
+
301
+ # ---------- ConfigManager ----------
302
+ class ConfigManager:
303
+ """
304
+ Thread-safe singleton ConfigManager with typed configuration schema.
305
+
306
+ Usage:
307
+ from droidrun.config_manager import config
308
+
309
+ # Access typed config objects
310
+ print(config.agent.max_steps)
311
+
312
+ # Load all 3 LLMs
313
+ llms = config.load_all_llms()
314
+ fast_llm = llms['fast']
315
+ mid_llm = llms['mid']
316
+ smart_llm = llms['smart']
317
+
318
+ # Modify and save
319
+ config.save()
320
+ """
321
+ _instance: Optional["ConfigManager"] = None
322
+ _instance_lock = threading.Lock()
323
+
324
+ def __new__(cls, path: Optional[str] = None):
325
+ # ensure singleton
326
+ with cls._instance_lock:
327
+ if cls._instance is None:
328
+ cls._instance = super().__new__(cls)
329
+ cls._instance._initialized = False
330
+ return cls._instance
331
+
332
+ def __init__(self, path: Optional[str] = None):
333
+ if getattr(self, "_initialized", False):
334
+ return
335
+
336
+ self._lock = threading.RLock()
337
+
338
+ # resolution order:
339
+ # 1) explicit path arg
340
+ # 2) DROIDRUN_CONFIG env var
341
+ # 3) module-relative project_root/config.yaml (two parents up)
342
+ if path:
343
+ self.path = Path(path).expanduser().resolve()
344
+ else:
345
+ env = os.environ.get("DROIDRUN_CONFIG")
346
+ if env:
347
+ self.path = Path(env).expanduser().resolve()
348
+ else:
349
+ self.path = _default_project_config_path().resolve()
350
+
351
+ # Initialize with default config
352
+ self._config = DroidRunConfig()
353
+ self.validate_fn: Optional[Callable[[DroidRunConfig], None]] = None
354
+
355
+ self._ensure_file_exists()
356
+ self.load_config()
357
+
358
+ self._initialized = True
359
+
360
+ # ---------------- Typed property access ----------------
361
+ @property
362
+ def agent(self) -> AgentConfig:
363
+ """Access agent configuration."""
364
+ with self._lock:
365
+ return self._config.agent
366
+
367
+ @property
368
+ def device(self) -> DeviceConfig:
369
+ """Access device configuration."""
370
+ with self._lock:
371
+ return self._config.device
372
+
373
+ @property
374
+ def telemetry(self) -> TelemetryConfig:
375
+ """Access telemetry configuration."""
376
+ with self._lock:
377
+ return self._config.telemetry
378
+
379
+ @property
380
+ def tracing(self) -> TracingConfig:
381
+ """Access tracing configuration."""
382
+ with self._lock:
383
+ return self._config.tracing
384
+
385
+ @property
386
+ def logging(self) -> LoggingConfig:
387
+ """Access logging configuration."""
388
+ with self._lock:
389
+ return self._config.logging
390
+
391
+ @property
392
+ def tools(self) -> ToolsConfig:
393
+ """Access tools configuration."""
394
+ with self._lock:
395
+ return self._config.tools
396
+
397
+ @property
398
+ def llm_profiles(self) -> Dict[str, LLMProfile]:
399
+ """Access LLM profiles."""
400
+ with self._lock:
401
+ return self._config.llm_profiles
402
+
403
+ # ---------------- LLM Profile Helpers ----------------
404
+ def get_llm_profile(self, profile_name: str) -> LLMProfile:
405
+ """
406
+ Get an LLM profile by name.
407
+
408
+ Args:
409
+ profile_name: Name of the profile (fast, mid, smart, custom, etc.)
410
+
411
+ Returns:
412
+ LLMProfile object
413
+
414
+ Raises:
415
+ KeyError: If profile_name doesn't exist
416
+ """
417
+ with self._lock:
418
+ if profile_name not in self._config.llm_profiles:
419
+ raise KeyError(
420
+ f"LLM profile '{profile_name}' not found. "
421
+ f"Available profiles: {list(self._config.llm_profiles.keys())}"
422
+ )
423
+
424
+ return self._config.llm_profiles[profile_name]
425
+
426
+ def load_llm_from_profile(self, profile_name: str, **override_kwargs):
427
+ """
428
+ Load an LLM using a profile configuration.
429
+
430
+ Args:
431
+ profile_name: Name of the profile to use (fast, mid, smart, custom)
432
+ **override_kwargs: Additional kwargs to override profile settings
433
+
434
+ Returns:
435
+ Initialized LLM instance
436
+
437
+ Example:
438
+ # Use specific profile
439
+ llm = config.load_llm_from_profile("smart")
440
+
441
+ # Override specific settings
442
+ llm = config.load_llm_from_profile("fast", temperature=0.5)
443
+ """
444
+ from droidrun.agent.utils.llm_picker import load_llm
445
+
446
+ profile = self.get_llm_profile(profile_name)
447
+
448
+ # Get kwargs from profile
449
+ kwargs = profile.to_load_llm_kwargs()
450
+
451
+ # Override with any provided kwargs
452
+ kwargs.update(override_kwargs)
453
+
454
+ # Load the LLM
455
+ return load_llm(provider_name=profile.provider, **kwargs)
456
+
457
+ def load_all_llms(self, profile_names: Optional[list[str]] = None, **override_kwargs_per_profile):
458
+ """
459
+ Load multiple LLMs from profiles for different use cases.
460
+
461
+ Args:
462
+ profile_names: List of profile names to load. If None, loads agent-specific profiles
463
+ **override_kwargs_per_profile: Dict of profile-specific overrides
464
+ Example: manager={'temperature': 0.1}, executor={'max_tokens': 8000}
465
+
466
+ Returns:
467
+ Dict mapping profile names to initialized LLM instances
468
+
469
+ Example:
470
+ # Load all agent-specific profiles
471
+ llms = config.load_all_llms()
472
+ manager_llm = llms['manager']
473
+ executor_llm = llms['executor']
474
+ codeact_llm = llms['codeact']
475
+
476
+ # Load specific profiles
477
+ llms = config.load_all_llms(['manager', 'executor'])
478
+
479
+ # Load with overrides
480
+ llms = config.load_all_llms(
481
+ manager={'temperature': 0.1},
482
+ executor={'max_tokens': 8000}
483
+ )
484
+ """
485
+ from droidrun.agent.utils.llm_picker import load_llm
486
+
487
+ if profile_names is None:
488
+ profile_names = ["manager", "executor", "codeact", "text_manipulator", "app_opener"]
489
+
490
+ llms = {}
491
+ for profile_name in profile_names:
492
+ profile = self.get_llm_profile(profile_name)
493
+
494
+ # Get kwargs from profile
495
+ kwargs = profile.to_load_llm_kwargs()
496
+
497
+ # Apply profile-specific overrides if provided
498
+ if profile_name in override_kwargs_per_profile:
499
+ kwargs.update(override_kwargs_per_profile[profile_name])
500
+
501
+ # Load the LLM
502
+ llms[profile_name] = load_llm(provider_name=profile.provider, **kwargs)
503
+
504
+ return llms
505
+
506
+ # ---------------- I/O ----------------
507
+ def _ensure_file_exists(self) -> None:
508
+ parent = self.path.parent
509
+ parent.mkdir(parents=True, exist_ok=True)
510
+ if not self.path.exists():
511
+ with open(self.path, "w", encoding="utf-8") as f:
512
+ f.write(_default_config_text())
513
+
514
+ def load_config(self) -> None:
515
+ """Load YAML from file into memory. Runs validator if registered."""
516
+ with self._lock:
517
+ if not self.path.exists():
518
+ # create starter file and set default config
519
+ self._ensure_file_exists()
520
+ self._config = DroidRunConfig()
521
+ return
522
+
523
+ with open(self.path, "r", encoding="utf-8") as f:
524
+ data = yaml.safe_load(f)
525
+ if data:
526
+ try:
527
+ self._config = DroidRunConfig.from_dict(data)
528
+ except Exception as e:
529
+ # If parsing fails, use defaults and log warning
530
+ import logging
531
+ logger = logging.getLogger("droidrun")
532
+ logger.warning(f"Failed to parse config, using defaults: {e}")
533
+ self._config = DroidRunConfig()
534
+ else:
535
+ self._config = DroidRunConfig()
536
+ self._run_validation()
537
+
538
+ def save(self) -> None:
539
+ """Persist current in-memory config to YAML file."""
540
+ with self._lock:
541
+ with open(self.path, "w", encoding="utf-8") as f:
542
+ yaml.dump(self._config.to_dict(), f, sort_keys=False, default_flow_style=False)
543
+
544
+ def reload(self) -> None:
545
+ """Reload config from disk (useful when edited externally or via UI)."""
546
+ self.load_config()
547
+
548
+ # ---------------- Validation ----------------
549
+ def register_validator(self, fn: Callable[[DroidRunConfig], None]) -> None:
550
+ """
551
+ Register a validation function that takes the config object and raises
552
+ an exception if invalid. The validator is run immediately on registration.
553
+ """
554
+ with self._lock:
555
+ self.validate_fn = fn
556
+ self._run_validation()
557
+
558
+ def _run_validation(self) -> None:
559
+ if self.validate_fn is None:
560
+ return
561
+ try:
562
+ self.validate_fn(self._config)
563
+ except Exception as exc:
564
+ raise Exception(f"Validation failed: {exc}") from exc
565
+
566
+ def as_dict(self) -> Dict[str, Any]:
567
+ """Return a deep copy of the config dict to avoid accidental mutation."""
568
+ with self._lock:
569
+ import copy
570
+ return copy.deepcopy(self._config.to_dict())
571
+
572
+ # useful for tests to reset singleton state
573
+ @classmethod
574
+ def _reset_instance_for_testing(cls) -> None:
575
+ with cls._instance_lock:
576
+ cls._instance = None
577
+
578
+ def __repr__(self) -> str:
579
+ return f"<ConfigManager path={self.path!s}>"
580
+
581
+
582
+ # ---------- global singleton ----------
583
+ config = ConfigManager()
@@ -9,6 +9,6 @@ from .replay import MacroPlayer, replay_macro_file, replay_macro_folder
9
9
 
10
10
  __all__ = [
11
11
  "MacroPlayer",
12
- "replay_macro_file",
12
+ "replay_macro_file",
13
13
  "replay_macro_folder"
14
- ]
14
+ ]
@@ -7,4 +7,4 @@ Usage: python -m droidrun.macro <command>
7
7
  from droidrun.macro.cli import macro_cli
8
8
 
9
9
  if __name__ == "__main__":
10
- macro_cli()
10
+ macro_cli()