droidrun 0.3.10.dev3__py3-none-any.whl → 0.3.10.dev4__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 (54) hide show
  1. droidrun/agent/codeact/__init__.py +1 -4
  2. droidrun/agent/codeact/codeact_agent.py +95 -86
  3. droidrun/agent/codeact/events.py +1 -2
  4. droidrun/agent/context/__init__.py +5 -9
  5. droidrun/agent/context/episodic_memory.py +1 -3
  6. droidrun/agent/context/task_manager.py +8 -2
  7. droidrun/agent/droid/droid_agent.py +102 -141
  8. droidrun/agent/droid/events.py +45 -14
  9. droidrun/agent/executor/__init__.py +6 -4
  10. droidrun/agent/executor/events.py +29 -9
  11. droidrun/agent/executor/executor_agent.py +86 -28
  12. droidrun/agent/executor/prompts.py +8 -2
  13. droidrun/agent/manager/__init__.py +6 -7
  14. droidrun/agent/manager/events.py +16 -4
  15. droidrun/agent/manager/manager_agent.py +130 -69
  16. droidrun/agent/manager/prompts.py +1 -159
  17. droidrun/agent/utils/chat_utils.py +64 -2
  18. droidrun/agent/utils/device_state_formatter.py +54 -26
  19. droidrun/agent/utils/executer.py +66 -80
  20. droidrun/agent/utils/inference.py +11 -10
  21. droidrun/agent/utils/tools.py +58 -6
  22. droidrun/agent/utils/trajectory.py +18 -12
  23. droidrun/cli/logs.py +118 -56
  24. droidrun/cli/main.py +154 -136
  25. droidrun/config_manager/__init__.py +9 -7
  26. droidrun/config_manager/app_card_loader.py +148 -0
  27. droidrun/config_manager/config_manager.py +200 -102
  28. droidrun/config_manager/path_resolver.py +104 -0
  29. droidrun/config_manager/prompt_loader.py +75 -0
  30. droidrun/macro/__init__.py +1 -1
  31. droidrun/macro/cli.py +23 -18
  32. droidrun/telemetry/__init__.py +2 -2
  33. droidrun/telemetry/events.py +3 -3
  34. droidrun/telemetry/tracker.py +1 -1
  35. droidrun/tools/adb.py +1 -1
  36. droidrun/tools/ios.py +3 -2
  37. {droidrun-0.3.10.dev3.dist-info → droidrun-0.3.10.dev4.dist-info}/METADATA +9 -1
  38. droidrun-0.3.10.dev4.dist-info/RECORD +61 -0
  39. droidrun/agent/codeact/prompts.py +0 -26
  40. droidrun/agent/context/agent_persona.py +0 -16
  41. droidrun/agent/context/context_injection_manager.py +0 -66
  42. droidrun/agent/context/personas/__init__.py +0 -11
  43. droidrun/agent/context/personas/app_starter.py +0 -44
  44. droidrun/agent/context/personas/big_agent.py +0 -96
  45. droidrun/agent/context/personas/default.py +0 -95
  46. droidrun/agent/context/personas/ui_expert.py +0 -108
  47. droidrun/agent/planner/__init__.py +0 -13
  48. droidrun/agent/planner/events.py +0 -21
  49. droidrun/agent/planner/planner_agent.py +0 -311
  50. droidrun/agent/planner/prompts.py +0 -124
  51. droidrun-0.3.10.dev3.dist-info/RECORD +0 -70
  52. {droidrun-0.3.10.dev3.dist-info → droidrun-0.3.10.dev4.dist-info}/WHEEL +0 -0
  53. {droidrun-0.3.10.dev3.dist-info → droidrun-0.3.10.dev4.dist-info}/entry_points.txt +0 -0
  54. {droidrun-0.3.10.dev3.dist-info → droidrun-0.3.10.dev4.dist-info}/licenses/LICENSE +0 -0
@@ -2,10 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import threading
5
- import yaml
5
+ from dataclasses import asdict, dataclass, field
6
6
  from pathlib import Path
7
- from typing import Any, Callable, Optional, Dict
8
- from dataclasses import dataclass, field, asdict
7
+ from typing import Any, Callable, Dict, List, Optional
8
+
9
+ import yaml
10
+
11
+ from droidrun.config_manager.path_resolver import PathResolver
12
+
9
13
 
10
14
  # ---------- Helpers / defaults ----------
11
15
  def _default_config_text() -> str:
@@ -17,13 +21,44 @@ def _default_config_text() -> str:
17
21
  agent:
18
22
  # Maximum number of steps per task
19
23
  max_steps: 15
20
- # Enable vision capabilities per agent (screenshots)
21
- vision:
22
- manager: false
23
- executor: false
24
- codeact: false
25
24
  # Enable planning with reasoning mode
26
25
  reasoning: false
26
+ # Sleep duration after each action, waits for ui state to be updated (seconds)
27
+ after_sleep_action: 1.0
28
+ # Wait duration for UI to stabilize (seconds)
29
+ wait_for_stable_ui: 0.3
30
+ # Base directory for prompt templates
31
+ prompts_dir: config/prompts
32
+
33
+ # CodeAct Agent Configuration
34
+ codeact:
35
+ # Enable vision capabilities (screenshots)
36
+ vision: false
37
+ # System prompt filename (located in prompts_dir/codeact/)
38
+ system_prompt: system.md
39
+ # User prompt filename (located in prompts_dir/codeact/)
40
+ user_prompt: user.md
41
+
42
+ # Manager Agent Configuration
43
+ manager:
44
+ # Enable vision capabilities (screenshots)
45
+ vision: false
46
+ # System prompt filename (located in prompts_dir/manager/)
47
+ system_prompt: system.md
48
+
49
+ # Executor Agent Configuration
50
+ executor:
51
+ # Enable vision capabilities (screenshots)
52
+ vision: false
53
+ # System prompt filename (located in prompts_dir/executor/)
54
+ system_prompt: system.md
55
+
56
+ # App Cards Configuration
57
+ app_cards:
58
+ # Enable app-specific instruction cards
59
+ enabled: true
60
+ # Directory containing app card files
61
+ app_cards_dir: config/app_cards
27
62
 
28
63
  # === LLM Profiles ===
29
64
  # Define LLM configurations for each agent type
@@ -35,7 +70,7 @@ llm_profiles:
35
70
  temperature: 0.2
36
71
  kwargs:
37
72
  max_tokens: 8192
38
-
73
+
39
74
  # Executor: Selects and executes atomic actions
40
75
  executor:
41
76
  provider: GoogleGenAI
@@ -43,7 +78,7 @@ llm_profiles:
43
78
  temperature: 0.1
44
79
  kwargs:
45
80
  max_tokens: 4096
46
-
81
+
47
82
  # CodeAct: Generates and executes code actions
48
83
  codeact:
49
84
  provider: GoogleGenAI
@@ -51,7 +86,7 @@ llm_profiles:
51
86
  temperature: 0.2
52
87
  kwargs:
53
88
  max_tokens: 8192
54
-
89
+
55
90
  # Text Manipulator: Edits text in input fields
56
91
  text_manipulator:
57
92
  provider: GoogleGenAI
@@ -59,7 +94,7 @@ llm_profiles:
59
94
  temperature: 0.3
60
95
  kwargs:
61
96
  max_tokens: 4096
62
-
97
+
63
98
  # App Opener: Opens apps by name/description
64
99
  app_opener:
65
100
  provider: OpenAI
@@ -77,13 +112,11 @@ device:
77
112
  serial: null
78
113
  # Use TCP communication instead of content provider
79
114
  use_tcp: false
80
- # Sleep duration after each action (seconds)
81
- after_sleep_action: 1.0
82
115
 
83
116
  # === Telemetry Settings ===
84
117
  telemetry:
85
118
  # Enable anonymous telemetry
86
- enabled: true
119
+ enabled: false
87
120
 
88
121
  # === Tracing Settings ===
89
122
  tracing:
@@ -96,18 +129,14 @@ logging:
96
129
  debug: false
97
130
  # Trajectory saving level (none, step, action)
98
131
  save_trajectory: none
99
-
132
+ rich_text: false
100
133
  # === Tool Settings ===
101
134
  tools:
102
135
  # Enable drag tool
103
136
  allow_drag: false
104
137
  """
105
138
 
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"
139
+ # Removed: _default_project_config_path() - now using PathResolver
111
140
 
112
141
 
113
142
  # ---------- Config Schema ----------
@@ -120,7 +149,7 @@ class LLMProfile:
120
149
  base_url: Optional[str] = None
121
150
  api_base: Optional[str] = None
122
151
  kwargs: Dict[str, Any] = field(default_factory=dict)
123
-
152
+
124
153
  def to_load_llm_kwargs(self) -> Dict[str, Any]:
125
154
  """Convert profile to kwargs for load_llm function."""
126
155
  result = {
@@ -138,36 +167,63 @@ class LLMProfile:
138
167
 
139
168
 
140
169
  @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
- )
170
+ class CodeActConfig:
171
+ """CodeAct agent configuration."""
172
+ vision: bool = False
173
+ system_prompt: str = "system.md"
174
+ user_prompt: str = "user.md"
175
+
176
+
177
+ @dataclass
178
+ class ManagerConfig:
179
+ """Manager agent configuration."""
180
+ vision: bool = False
181
+ system_prompt: str = "system.md"
182
+
183
+
184
+ @dataclass
185
+ class ExecutorConfig:
186
+ """Executor agent configuration."""
187
+ vision: bool = False
188
+ system_prompt: str = "system.md"
189
+
190
+
191
+ @dataclass
192
+ class AppCardConfig:
193
+ """App card configuration."""
194
+ enabled: bool = True
195
+ app_cards_dir: str = "config/app_cards"
161
196
 
162
197
 
163
198
  @dataclass
164
199
  class AgentConfig:
165
200
  """Agent-related configuration."""
166
201
  max_steps: int = 15
167
- vision: VisionConfig = field(default_factory=VisionConfig)
168
202
  reasoning: bool = False
169
203
  after_sleep_action: float = 1.0
170
204
  wait_for_stable_ui: float = 0.3
205
+ prompts_dir: str = "config/prompts"
206
+
207
+ codeact: CodeActConfig = field(default_factory=CodeActConfig)
208
+ manager: ManagerConfig = field(default_factory=ManagerConfig)
209
+ executor: ExecutorConfig = field(default_factory=ExecutorConfig)
210
+ app_cards: AppCardConfig = field(default_factory=AppCardConfig)
211
+
212
+ def get_codeact_system_prompt_path(self) -> str:
213
+ """Get full path to CodeAct system prompt."""
214
+ return f"{self.prompts_dir}/codeact/{self.codeact.system_prompt}"
215
+
216
+ def get_codeact_user_prompt_path(self) -> str:
217
+ """Get full path to CodeAct user prompt."""
218
+ return f"{self.prompts_dir}/codeact/{self.codeact.user_prompt}"
219
+
220
+ def get_manager_system_prompt_path(self) -> str:
221
+ """Get full path to Manager system prompt."""
222
+ return f"{self.prompts_dir}/manager/{self.manager.system_prompt}"
223
+
224
+ def get_executor_system_prompt_path(self) -> str:
225
+ """Get full path to Executor system prompt."""
226
+ return f"{self.prompts_dir}/executor/{self.executor.system_prompt}"
171
227
 
172
228
 
173
229
  @dataclass
@@ -180,7 +236,7 @@ class DeviceConfig:
180
236
  @dataclass
181
237
  class TelemetryConfig:
182
238
  """Telemetry configuration."""
183
- enabled: bool = True
239
+ enabled: bool = False
184
240
 
185
241
 
186
242
  @dataclass
@@ -194,7 +250,7 @@ class LoggingConfig:
194
250
  """Logging configuration."""
195
251
  debug: bool = False
196
252
  save_trajectory: str = "none"
197
-
253
+ rich_text: bool = False
198
254
 
199
255
  @dataclass
200
256
  class ToolsConfig:
@@ -217,7 +273,7 @@ class DroidRunConfig:
217
273
  """Ensure default profiles exist."""
218
274
  if not self.llm_profiles:
219
275
  self.llm_profiles = self._default_profiles()
220
-
276
+
221
277
  @staticmethod
222
278
  def _default_profiles() -> Dict[str, LLMProfile]:
223
279
  """Get default agent specific LLM profiles."""
@@ -247,7 +303,7 @@ class DroidRunConfig:
247
303
  kwargs={}
248
304
  ),
249
305
  "app_opener": LLMProfile(
250
- provider="OpenAI",
306
+ provider="GoogleGenAI",
251
307
  model="models/gemini-2.5-pro",
252
308
  temperature=0.0,
253
309
  kwargs={}
@@ -261,11 +317,6 @@ class DroidRunConfig:
261
317
  result["llm_profiles"] = {
262
318
  name: asdict(profile) for name, profile in self.llm_profiles.items()
263
319
  }
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
320
  return result
270
321
 
271
322
  @classmethod
@@ -275,18 +326,34 @@ class DroidRunConfig:
275
326
  llm_profiles = {}
276
327
  for name, profile_data in data.get("llm_profiles", {}).items():
277
328
  llm_profiles[name] = LLMProfile(**profile_data)
278
-
279
- # Parse agent config with vision
329
+
330
+ # Parse agent config with sub-configs
280
331
  agent_data = data.get("agent", {})
281
- vision_data = agent_data.get("vision", {})
282
- vision_config = VisionConfig.from_dict(vision_data)
283
-
332
+
333
+ codeact_data = agent_data.get("codeact", {})
334
+ codeact_config = CodeActConfig(**codeact_data) if codeact_data else CodeActConfig()
335
+
336
+ manager_data = agent_data.get("manager", {})
337
+ manager_config = ManagerConfig(**manager_data) if manager_data else ManagerConfig()
338
+
339
+ executor_data = agent_data.get("executor", {})
340
+ executor_config = ExecutorConfig(**executor_data) if executor_data else ExecutorConfig()
341
+
342
+ app_cards_data = agent_data.get("app_cards", {})
343
+ app_cards_config = AppCardConfig(**app_cards_data) if app_cards_data else AppCardConfig()
344
+
284
345
  agent_config = AgentConfig(
285
346
  max_steps=agent_data.get("max_steps", 15),
286
- vision=vision_config,
287
347
  reasoning=agent_data.get("reasoning", False),
348
+ after_sleep_action=agent_data.get("after_sleep_action", 1.0),
349
+ wait_for_stable_ui=agent_data.get("wait_for_stable_ui", 0.3),
350
+ prompts_dir=agent_data.get("prompts_dir", "config/prompts"),
351
+ codeact=codeact_config,
352
+ manager=manager_config,
353
+ executor=executor_config,
354
+ app_cards=app_cards_config,
288
355
  )
289
-
356
+
290
357
  return cls(
291
358
  agent=agent_config,
292
359
  llm_profiles=llm_profiles,
@@ -304,17 +371,20 @@ class ConfigManager:
304
371
  Thread-safe singleton ConfigManager with typed configuration schema.
305
372
 
306
373
  Usage:
307
- from droidrun.config_manager import config
308
-
374
+ from droidrun.config_manager.config_manager import ConfigManager
375
+
376
+ # Create config instance (singleton pattern)
377
+ config = ConfigManager()
378
+
309
379
  # Access typed config objects
310
380
  print(config.agent.max_steps)
311
-
312
- # Load all 3 LLMs
381
+
382
+ # Load all LLMs
313
383
  llms = config.load_all_llms()
314
- fast_llm = llms['fast']
315
- mid_llm = llms['mid']
316
- smart_llm = llms['smart']
317
-
384
+ manager_llm = llms['manager']
385
+ executor_llm = llms['executor']
386
+ codeact_llm = llms['codeact']
387
+
318
388
  # Modify and save
319
389
  config.save()
320
390
  """
@@ -335,18 +405,19 @@ class ConfigManager:
335
405
 
336
406
  self._lock = threading.RLock()
337
407
 
338
- # resolution order:
339
- # 1) explicit path arg
408
+ # Resolution order:
409
+ # 1) Explicit path arg
340
410
  # 2) DROIDRUN_CONFIG env var
341
- # 3) module-relative project_root/config.yaml (two parents up)
411
+ # 3) Default "config.yaml" (checks working dir, then project dir)
342
412
  if path:
343
- self.path = Path(path).expanduser().resolve()
413
+ self.path = PathResolver.resolve(path)
344
414
  else:
345
415
  env = os.environ.get("DROIDRUN_CONFIG")
346
416
  if env:
347
- self.path = Path(env).expanduser().resolve()
417
+ self.path = PathResolver.resolve(env)
348
418
  else:
349
- self.path = _default_project_config_path().resolve()
419
+ # Default: checks CWD first, then project dir
420
+ self.path = PathResolver.resolve("config.yaml")
350
421
 
351
422
  # Initialize with default config
352
423
  self._config = DroidRunConfig()
@@ -393,24 +464,24 @@ class ConfigManager:
393
464
  """Access tools configuration."""
394
465
  with self._lock:
395
466
  return self._config.tools
396
-
467
+
397
468
  @property
398
469
  def llm_profiles(self) -> Dict[str, LLMProfile]:
399
470
  """Access LLM profiles."""
400
471
  with self._lock:
401
472
  return self._config.llm_profiles
402
-
473
+
403
474
  # ---------------- LLM Profile Helpers ----------------
404
475
  def get_llm_profile(self, profile_name: str) -> LLMProfile:
405
476
  """
406
477
  Get an LLM profile by name.
407
-
478
+
408
479
  Args:
409
480
  profile_name: Name of the profile (fast, mid, smart, custom, etc.)
410
-
481
+
411
482
  Returns:
412
483
  LLMProfile object
413
-
484
+
414
485
  Raises:
415
486
  KeyError: If profile_name doesn't exist
416
487
  """
@@ -420,62 +491,62 @@ class ConfigManager:
420
491
  f"LLM profile '{profile_name}' not found. "
421
492
  f"Available profiles: {list(self._config.llm_profiles.keys())}"
422
493
  )
423
-
494
+
424
495
  return self._config.llm_profiles[profile_name]
425
-
496
+
426
497
  def load_llm_from_profile(self, profile_name: str, **override_kwargs):
427
498
  """
428
499
  Load an LLM using a profile configuration.
429
-
500
+
430
501
  Args:
431
502
  profile_name: Name of the profile to use (fast, mid, smart, custom)
432
503
  **override_kwargs: Additional kwargs to override profile settings
433
-
504
+
434
505
  Returns:
435
506
  Initialized LLM instance
436
-
507
+
437
508
  Example:
438
509
  # Use specific profile
439
510
  llm = config.load_llm_from_profile("smart")
440
-
511
+
441
512
  # Override specific settings
442
513
  llm = config.load_llm_from_profile("fast", temperature=0.5)
443
514
  """
444
515
  from droidrun.agent.utils.llm_picker import load_llm
445
-
516
+
446
517
  profile = self.get_llm_profile(profile_name)
447
-
518
+
448
519
  # Get kwargs from profile
449
520
  kwargs = profile.to_load_llm_kwargs()
450
-
521
+
451
522
  # Override with any provided kwargs
452
523
  kwargs.update(override_kwargs)
453
-
524
+
454
525
  # Load the LLM
455
526
  return load_llm(provider_name=profile.provider, **kwargs)
456
-
527
+
457
528
  def load_all_llms(self, profile_names: Optional[list[str]] = None, **override_kwargs_per_profile):
458
529
  """
459
530
  Load multiple LLMs from profiles for different use cases.
460
-
531
+
461
532
  Args:
462
533
  profile_names: List of profile names to load. If None, loads agent-specific profiles
463
534
  **override_kwargs_per_profile: Dict of profile-specific overrides
464
535
  Example: manager={'temperature': 0.1}, executor={'max_tokens': 8000}
465
-
536
+
466
537
  Returns:
467
538
  Dict mapping profile names to initialized LLM instances
468
-
539
+
469
540
  Example:
470
541
  # Load all agent-specific profiles
471
542
  llms = config.load_all_llms()
472
543
  manager_llm = llms['manager']
473
544
  executor_llm = llms['executor']
474
545
  codeact_llm = llms['codeact']
475
-
546
+
476
547
  # Load specific profiles
477
548
  llms = config.load_all_llms(['manager', 'executor'])
478
-
549
+
479
550
  # Load with overrides
480
551
  llms = config.load_all_llms(
481
552
  manager={'temperature': 0.1},
@@ -483,24 +554,24 @@ class ConfigManager:
483
554
  )
484
555
  """
485
556
  from droidrun.agent.utils.llm_picker import load_llm
486
-
557
+
487
558
  if profile_names is None:
488
559
  profile_names = ["manager", "executor", "codeact", "text_manipulator", "app_opener"]
489
-
560
+
490
561
  llms = {}
491
562
  for profile_name in profile_names:
492
563
  profile = self.get_llm_profile(profile_name)
493
-
564
+
494
565
  # Get kwargs from profile
495
566
  kwargs = profile.to_load_llm_kwargs()
496
-
567
+
497
568
  # Apply profile-specific overrides if provided
498
569
  if profile_name in override_kwargs_per_profile:
499
570
  kwargs.update(override_kwargs_per_profile[profile_name])
500
-
571
+
501
572
  # Load the LLM
502
573
  llms[profile_name] = load_llm(provider_name=profile.provider, **kwargs)
503
-
574
+
504
575
  return llms
505
576
 
506
577
  # ---------------- I/O ----------------
@@ -569,6 +640,35 @@ class ConfigManager:
569
640
  import copy
570
641
  return copy.deepcopy(self._config.to_dict())
571
642
 
643
+ # Implemented for for config webiu so we can have dropdown prompt selection. but canceled webui plan.
644
+ def list_available_prompts(self, agent_type: str) -> List[str]:
645
+ """
646
+ List all available prompt files for a given agent type.
647
+
648
+ Args:
649
+ agent_type: One of "codeact", "manager", "executor"
650
+
651
+ Returns:
652
+ List of prompt filenames available in the agent's prompts directory
653
+
654
+ Example:
655
+ >>> config.list_available_prompts("manager")
656
+ ['system.md', 'experimental.md', 'minimal.md']
657
+ """
658
+ agent_type = agent_type.lower()
659
+ if agent_type not in ["codeact", "manager", "executor"]:
660
+ raise ValueError(f"Invalid agent_type: {agent_type}. Must be one of: codeact, manager, executor")
661
+
662
+ # Resolve prompts directory
663
+ prompts_path = f"{self.agent.prompts_dir}/{agent_type}"
664
+ prompts_dir = PathResolver.resolve(prompts_path)
665
+
666
+ if not prompts_dir.exists():
667
+ return []
668
+
669
+ # List all .md files in the directory
670
+ return sorted([f.name for f in prompts_dir.glob("*.md")])
671
+
572
672
  # useful for tests to reset singleton state
573
673
  @classmethod
574
674
  def _reset_instance_for_testing(cls) -> None:
@@ -579,5 +679,3 @@ class ConfigManager:
579
679
  return f"<ConfigManager path={self.path!s}>"
580
680
 
581
681
 
582
- # ---------- global singleton ----------
583
- config = ConfigManager()
@@ -0,0 +1,104 @@
1
+ """
2
+ Unified path resolution for DroidRun.
3
+
4
+ This module provides a single path resolver that handles all file path resolution
5
+ with consistent priority: working directory first, then project directory.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Union
10
+
11
+
12
+ class PathResolver:
13
+ """
14
+ Unified path resolver for all DroidRun file operations.
15
+
16
+ Resolution order:
17
+ 1. Absolute paths → use as-is
18
+ 2. Relative paths → check working dir first, then project dir
19
+ 3. For creation → prefer working dir
20
+ """
21
+
22
+ @staticmethod
23
+ def get_project_root() -> Path:
24
+ """
25
+ Get the project root directory (where config.yaml lives).
26
+
27
+ This is 2 parents up from this file's location:
28
+ droidrun/config_manager/path_resolver.py -> droidrun/ (project root)
29
+ """
30
+ return Path(__file__).resolve().parents[2]
31
+
32
+ @staticmethod
33
+ def resolve(
34
+ path: Union[str, Path],
35
+ create_if_missing: bool = False,
36
+ must_exist: bool = False
37
+ ) -> Path:
38
+ """
39
+ Universal path resolver for all file operations.
40
+
41
+ Resolution order:
42
+ 1. Absolute path → use as-is
43
+ 2. Relative path:
44
+ - If creating: prefer working directory
45
+ - If reading: check working dir first, then project dir
46
+ 3. If must_exist and not found → raise FileNotFoundError
47
+
48
+ Args:
49
+ path: Path to resolve (str or Path object)
50
+ create_if_missing: If True, prefer working dir for relative paths (output mode)
51
+ must_exist: If True, raise FileNotFoundError if path doesn't exist
52
+
53
+ Returns:
54
+ Resolved Path object
55
+
56
+ Raises:
57
+ FileNotFoundError: If must_exist=True and path not found in any location
58
+
59
+ Examples:
60
+ # Reading config (checks CWD first, then project dir)
61
+ config_path = PathResolver.resolve("config.yaml")
62
+
63
+ # Creating output (creates in CWD)
64
+ output_dir = PathResolver.resolve("trajectories", create_if_missing=True)
65
+
66
+ # Loading prompts (must exist, checks both locations)
67
+ prompt = PathResolver.resolve("config/prompts/system.md", must_exist=True)
68
+
69
+ # Absolute path (used as-is)
70
+ abs_path = PathResolver.resolve("/tmp/output")
71
+ """
72
+ # Convert to Path and expand user home directory (~/)
73
+ path = Path(path).expanduser()
74
+
75
+ # Absolute paths: use as-is
76
+ if path.is_absolute():
77
+ if must_exist and not path.exists():
78
+ raise FileNotFoundError(f"Path not found: {path}")
79
+ return path
80
+
81
+ # Relative paths: check working dir and project dir
82
+ cwd_path = Path.cwd() / path
83
+ project_path = PathResolver.get_project_root() / path
84
+
85
+ # For creation, always prefer working directory (user's context)
86
+ if create_if_missing:
87
+ return cwd_path
88
+
89
+ # For reading, check both locations (working dir first)
90
+ if cwd_path.exists():
91
+ return cwd_path
92
+ if project_path.exists():
93
+ return project_path
94
+
95
+ # Not found in either location
96
+ if must_exist:
97
+ raise FileNotFoundError(
98
+ f"Path not found in:\n"
99
+ f" - Working dir: {cwd_path}\n"
100
+ f" - Project dir: {project_path}"
101
+ )
102
+
103
+ # Default to working dir (user's context)
104
+ return cwd_path