4646from openhands .sdk .security .confirmation_policy import (
4747 ConfirmationPolicyBase ,
4848)
49+ from openhands .sdk .subagent import (
50+ AgentDefinition ,
51+ register_file_agents ,
52+ register_plugin_agents ,
53+ )
54+ from openhands .sdk .subagent .registry import register_builtins_agents
4955from openhands .sdk .tool .schema import Action , Observation
5056from openhands .sdk .utils .cipher import Cipher
5157from openhands .sdk .workspace import LocalWorkspace
@@ -310,6 +316,7 @@ def _ensure_plugins_loaded(self) -> None:
310316 return
311317
312318 all_plugin_hooks : list [HookConfig ] = []
319+ all_plugin_agents : list [AgentDefinition ] = []
313320
314321 # Load plugins if specified
315322 if self ._plugin_specs :
@@ -347,6 +354,10 @@ def _ensure_plugins_loaded(self) -> None:
347354 if plugin .hooks and not plugin .hooks .is_empty ():
348355 all_plugin_hooks .append (plugin .hooks )
349356
357+ # Collect agent definitions
358+ if plugin .agents :
359+ all_plugin_agents .extend (plugin .agents )
360+
350361 # Update agent with merged content
351362 self .agent = self .agent .model_copy (
352363 update = {
@@ -361,6 +372,10 @@ def _ensure_plugins_loaded(self) -> None:
361372
362373 logger .info (f"Loaded { len (self ._plugin_specs )} plugin(s) via Conversation" )
363374
375+ # Register file-based agents defined in plugins
376+ if all_plugin_agents :
377+ register_plugin_agents (all_plugin_agents )
378+
364379 # Combine explicit hook_config with plugin hooks
365380 # Explicit hooks run first (before plugin hooks)
366381 final_hook_config = self ._pending_hook_config
@@ -387,21 +402,44 @@ def _ensure_plugins_loaded(self) -> None:
387402
388403 self ._plugins_loaded = True
389404
405+ def _register_file_based_agents (self ) -> None :
406+ """Discover and register file-based agents into the agent registry.
407+
408+ Agents are loaded from Markdown definition files and registered via
409+ `register_agent_if_absent`, so they never overwrite agents that were
410+ already registered programmatically or by plugins.
411+
412+ Registration order (highest to lowest priority):
413+ 1. Programmatic `register_agent()` calls (already in the registry)
414+ 2. Plugin agents (registered during plugin loading, i.e.,
415+ in _ensure_plugins_loaded())
416+ 3. Project-level file agents (`{project}/.agents/agents/*.md`,
417+ then `{project}/.openhands/agents/*.md`)
418+ 4. User-level file agents (`~/.agents/agents/*.md`,
419+ then `~/.openhands/agents/*.md`)
420+ 5. SDK builtin agents (`subagent/builtins/*.md`)
421+ """
422+ # register project-level and then user-level file-based agents
423+ register_file_agents (self .workspace .working_dir )
424+ # register builtins agents
425+ register_builtins_agents ()
426+
390427 def _ensure_agent_ready (self ) -> None :
391- """Ensure agent is fully initialized with plugins loaded.
428+ """Ensure the agent is fully initialized with plugins and agents loaded.
392429
393- This method combines plugin loading and agent initialization to ensure
394- the agent is initialized exactly once with complete configuration.
430+ Performs one-time lazy initialization on the first `send_message()`
431+ or `run()` call. The steps executed (in order) are:
395432
396- Called lazily on first send_message() or run() to:
397- 1. Load plugins (if specified)
398- 2 . Initialize agent with complete plugin config and hooks
399- 3 . Register LLMs in the registry
433+ 1. Load plugins (merges skills, MCP config, and hooks).
434+ 2. Register file-based agents into the agent registry.
435+ 3 . Initialize the agent with complete plugin config and hooks.
436+ 4 . Register LLMs in the LLM registry.
400437
401438 This preserves the design principle that constructors should not perform
402439 I/O or error-prone operations, while eliminating double initialization.
403440
404- Thread-safe: Uses state lock to prevent concurrent initialization.
441+ Thread-safe: uses a double-checked lock on the conversation state to
442+ prevent concurrent initialization.
405443 """
406444 # Fast path: if already initialized, skip lock acquisition entirely.
407445 # This is crucial for concurrent send_message() calls during run(),
@@ -419,6 +457,9 @@ def _ensure_agent_ready(self) -> None:
419457 # Load plugins first (merges skills, MCP config, hooks)
420458 self ._ensure_plugins_loaded ()
421459
460+ # register file-based agents
461+ self ._register_file_based_agents ()
462+
422463 # Initialize agent with complete configuration
423464 self .agent .init_state (self ._state , on_event = self ._on_event )
424465
0 commit comments