Skip to content

feat: external blueprint registration#2517

Open
TomCC7 wants to merge 17 commits into
mainfrom
cc/exp/dyn-blueprint-reg
Open

feat: external blueprint registration#2517
TomCC7 wants to merge 17 commits into
mainfrom
cc/exp/dyn-blueprint-reg

Conversation

@TomCC7

@TomCC7 TomCC7 commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Adds dynamic discovery for externally packaged DimOS blueprints via Python package entry points.

External packages can expose runnable blueprints with:

[project.entry-points."dimos.blueprints"]
go2 = "my_robot_stack.go2:go2_blueprint"

Users can then run them by namespaced name:

dimos run my-robot-stack.go2
dimos run unitree-go2 my-robot-stack.keyboard-teleop

Changes

  • Add external blueprint discovery/resolution through importlib.metadata.
  • Require external names to use <canonical-distribution-namespace>.<local-kebab-name>.
  • Keep bare names reserved for built-in DimOS blueprints/modules.
  • Support external entry point targets that are:
    • Blueprint objects
    • DimOS Module classes converted with .blueprint()
  • Reject factories and unsupported targets.
  • Add metadata-only external blueprint listing in dimos list.
  • Add namespace-aware errors for missing namespaces, missing local names, invalid metadata, load failures, ambiguous namespaces, and invalid targets.
  • Cover shared resolution paths used by CLI, Python API, and coordinator loading.
  • Update user, contributor, and coding-agent docs for built-in vs external blueprint registration.

Testing

  • Manual QA with temporary external packages:
    • dimos list
    • dimos run my-test-stack.demo --daemon
    • dimos run demo-mcp-stress-test my-test-stack.demo --daemon
    • broken external entry point load failure

@TomCC7 TomCC7 changed the title WIP: dynamic blueprint registration feat: dynamic blueprint registration Jun 17, 2026
@TomCC7 TomCC7 marked this pull request as ready for review June 17, 2026 00:18
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds dynamic discovery of externally packaged DimOS blueprints via Python package entry points (dimos.blueprints group), allowing third-party packages to register runnable blueprints without modifying the in-repo all_blueprints.py registry.

  • external_blueprints.py (new): Scans importlib.metadata for dimos.blueprints entry points, canonicalizes distribution names to namespaces, validates local names as lowercase kebab-case, and resolves targets to Blueprint objects (accepting both Blueprint instances and Module classes).
  • get_all_blueprints.py (updated): Routes dotted names to external resolution, adds a _exit_with_error helper, and switches sys.exit(1) to raise typer.Exit(1) for consistency with the rest of the CLI layer.
  • dimos.py CLI (updated): dimos list now shows built-in and external blueprints in separate labelled sections; external names are read from metadata without loading targets.

Confidence Score: 5/5

Safe to merge. The change is well-contained: external resolution only activates for dotted names, bare built-in names are unaffected, and all error paths exit cleanly.

The core _collect_external_blueprints loop handles invalid entry point names gracefully per-distribution, so a misconfigured third-party package never blocks discovery of other packages. The three issues raised in previous review threads have all been resolved in this version. Test coverage is thorough, including edge cases for colliding namespaces, empty distributions, missing metadata, and load failures.

No files require special attention.

Important Files Changed

Filename Overview
dimos/robot/external_blueprints.py New module implementing external blueprint discovery, validation, and resolution via importlib.metadata. Gracefully handles invalid entry point names per-distribution, avoids loading targets during listing, and provides distinct error messages for entry-point metadata errors vs. user-input validation errors.
dimos/robot/get_all_blueprints.py Routes dotted names to external resolution; switches sys.exit to raise typer.Exit for CLI consistency; adds _exit_with_error helper; overall clean integration of external resolution into existing lookup logic.
dimos/robot/test_external_blueprints.py Comprehensive test suite covering namespace canonicalization, listing without loading, resolution of Blueprint objects and Module classes, all error paths, invalid name handling, cross-namespace collision behavior, and bare vs. namespaced routing.
dimos/robot/cli/dimos.py list_blueprints now groups built-in and external blueprints with section headers, gracefully catches ExternalBlueprintError and exits with code 1, and skips the external section when no external blueprints are found.
dimos/robot/cli/test_dimos.py New CLI tests cover grouped output, empty external section, discovery error propagation, external resolution error display, and unknown bare blueprint error — all using CliRunner with monkeypatching.
dimos/core/module.py Minor: is_module_type now uses TypeGuard[type[Module]] return annotation, improving type narrowing for callers like _target_to_blueprint.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["dimos run name"] --> B{is_namespaced_blueprint_name?}
    B -- "No (bare name)" --> C{in all_blueprints?}
    C -- Yes --> D[get_blueprint_by_name]
    C -- No --> E{in all_modules?}
    E -- Yes --> F[get_module_by_name]
    E -- No --> G[_fail_or_exit / ValueError]
    B -- "Yes (has dot)" --> H[resolve_external_blueprint_by_name]
    H --> I[_collect_external_blueprints via importlib.metadata]
    I --> J{valid local name?}
    J -- No --> K[ExternalBlueprintError: invalid local name]
    J -- Yes --> L{namespace found?}
    L -- "No, invalid entries exist" --> M[ExternalBlueprintError: invalid entry point name in dist]
    L -- No --> N[ExternalBlueprintError: namespace not found]
    L -- Yes --> O{local name found?}
    O -- No --> P[ExternalBlueprintError: local name not found]
    O -- Yes --> Q[entry_point.load]
    Q -- ImportError --> R[ExternalBlueprintError: load failure]
    Q -- Success --> S{isinstance Blueprint?}
    S -- Yes --> T[return Blueprint]
    S -- No --> U{is_module_type?}
    U -- Yes --> V[Module.blueprint then return]
    U -- No --> W[ExternalBlueprintError: unsupported target]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["dimos run name"] --> B{is_namespaced_blueprint_name?}
    B -- "No (bare name)" --> C{in all_blueprints?}
    C -- Yes --> D[get_blueprint_by_name]
    C -- No --> E{in all_modules?}
    E -- Yes --> F[get_module_by_name]
    E -- No --> G[_fail_or_exit / ValueError]
    B -- "Yes (has dot)" --> H[resolve_external_blueprint_by_name]
    H --> I[_collect_external_blueprints via importlib.metadata]
    I --> J{valid local name?}
    J -- No --> K[ExternalBlueprintError: invalid local name]
    J -- Yes --> L{namespace found?}
    L -- "No, invalid entries exist" --> M[ExternalBlueprintError: invalid entry point name in dist]
    L -- No --> N[ExternalBlueprintError: namespace not found]
    L -- Yes --> O{local name found?}
    O -- No --> P[ExternalBlueprintError: local name not found]
    O -- Yes --> Q[entry_point.load]
    Q -- ImportError --> R[ExternalBlueprintError: load failure]
    Q -- Success --> S{isinstance Blueprint?}
    S -- Yes --> T[return Blueprint]
    S -- No --> U{is_module_type?}
    U -- Yes --> V[Module.blueprint then return]
    U -- No --> W[ExternalBlueprintError: unsupported target]
Loading

Reviews (8): Last reviewed commit: "fix: address external blueprint review c..." | Re-trigger Greptile

Comment thread dimos/robot/external_blueprints.py Outdated
Comment thread dimos/robot/external_blueprints.py Outdated
Comment thread dimos/robot/get_all_blueprints.py Outdated
@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
2228 1 2227 70
View the full list of 1 ❄️ flaky test(s)
dimos.e2e_tests.test_dimsim_walk_forward::test_walk_forward

Flake rate in main: 40.00% (Passed 9 times, Failed 6 times)

Stack Traces | 207s run time
lcm_spy = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7590a194a480>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x7590a1a01ee0>
human_input = <function human_input.<locals>.send_human_input at 0x7590a1a039c0>
dim_sim = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x7590a11a1c10>

    @pytest.mark.self_hosted_large
    def test_walk_forward(lcm_spy, start_blueprint, human_input, dim_sim) -> None:
        start_blueprint(
            "run",
            "--disable",
            "spatial-memory",
            "--disable",
            "security-module",
            "unitree-go2-agentic",
            simulator="dimsim",
        )
        lcm_spy.save_topic(".../McpClient/on_system_modules/res")
        lcm_spy.wait_for_saved_topic(".../McpClient/on_system_modules/res", timeout=1200.0)
    
        origin_x, origin_y = 1, 2
        dim_sim.set_agent_position(origin_x, origin_y)
    
        human_input("move forward 3 meter")
    
>       lcm_spy.wait_until_odom_position(origin_x + 3, origin_y, threshold=0.4, timeout=120)

dim_sim    = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x7590a11a1c10>
human_input = <function human_input.<locals>.send_human_input at 0x7590a1a039c0>
lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7590a194a480>
origin_x   = 1
origin_y   = 2
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x7590a1a01ee0>

dimos/e2e_tests/test_dimsim_walk_forward.py:37: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/e2e_tests/lcm_spy.py:182: in wait_until_odom_position
    self.wait_for_message_result(
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x7590a1a03740>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7590a194a480>
        threshold  = 0.4
        timeout    = 120
        x          = 4
        y          = 2
dimos/e2e_tests/lcm_spy.py:168: in wait_for_message_result
    self.wait_until(
        event      = <threading.Event at 0x7590a11a2e40: unset>
        fail_message = 'Failed to get to position x=4, y=2'
        listener   = <function LcmSpy.wait_for_message_result.<locals>.listener at 0x7590a1a02480>
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x7590a1a03740>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7590a194a480>
        timeout    = 120
        topic      = '/odom#geometry_msgs.PoseStamped'
        type       = <class 'dimos.msgs.geometry_msgs.PoseStamped.PoseStamped'>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7590a194a480>

    def wait_until(
        self,
        *,
        condition: Callable[[], bool],
        timeout: float,
        error_message: str,
        poll_interval: float = 0.1,
    ) -> None:
        start_time = time.time()
        while time.time() - start_time < timeout:
            if condition():
                return
            time.sleep(poll_interval)
>       raise TimeoutError(error_message)
E       TimeoutError: Failed to get to position x=4, y=2

condition  = <bound method Event.is_set of <threading.Event at 0x7590a11a2e40: unset>>
error_message = 'Failed to get to position x=4, y=2'
poll_interval = 0.1
self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7590a194a480>
start_time = 1781666435.7423742
timeout    = 120

dimos/e2e_tests/lcm_spy.py:105: TimeoutError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Comment thread dimos/robot/external_blueprints.py Outdated
@TomCC7 TomCC7 changed the title feat: dynamic blueprint registration feat: external blueprint registration Jun 17, 2026
Comment thread dimos/core/coordination/test_module_coordinator.py Outdated
Comment thread dimos/core/coordination/test_module_coordinator.py Outdated
Comment thread dimos/core/coordination/test_module_coordinator.py Outdated
Comment thread dimos/core/coordination/test_module_coordinator.py Outdated
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Jun 17, 2026
Comment thread dimos/robot/external_blueprints.py Outdated
Comment thread dimos/robot/external_blueprints.py Outdated
Comment thread docs/usage/blueprints.md Outdated
Comment thread .codecov.yml Outdated
@github-actions github-actions Bot added ready-to-merge Required CI checks have passed on this PR and removed ready-to-merge Required CI checks have passed on this PR labels Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Required CI checks have passed on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants