feat(control): add extension sideloading#2547
Conversation
Greptile SummaryThis PR adds a public extension facade (
Confidence Score: 5/5Safe to merge — all changes are additive, the new public facade delegates cleanly to existing registries, and the normalization fix makes register/create consistent across all three hardware registries. The extension surface is well-scoped: new code adds to registries rather than modifying coordinator internals, the duplicate-registration guard prevents silent overwrites, and the shared normalization helpers eliminate the register/create asymmetry that was previously flagged. Tests cover all hardware types, idempotent re-registration, conflict rejection, padded-name round-trips, and full coordinator integration. No correctness issues were found. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[External Package\nimports blueprints.py] --> B[register_extensions called\nat module level]
B --> C[register_hardware_adapter\nhardware_type, adapter_type, factory]
B --> D[register_control_task\ntask_type, factory_path]
C --> E[normalize_adapter_name\nstrip + lower]
E --> F{hardware_type\nmatch}
F -->|MANIPULATOR| G[adapter_registry.register]
F -->|BASE| H[twist_base_adapter_registry.register]
F -->|WHOLE_BODY| I[whole_body_adapter_registry.register]
G & H & I --> J{existing\nentry?}
J -->|same factory| K[idempotent return]
J -->|different factory| L[raise ValueError]
J -->|none| M[store in _adapters]
D --> N[normalize_task_name\nstrip + lower]
N --> O[validate_task_factory_path\nmodule:attr format]
O --> P[control_task_registry.register_path]
P --> Q{existing\npath?}
Q -->|same path| R[overwrite same value]
Q -->|different path| S[raise ValueError]
Q -->|none| T[store in _factory_paths]
U[ControlCoordinator.start] --> V[HardwareComponent\nadapter_type lookup]
V --> W[registry.create\nnormalize_adapter_name]
W --> M
U --> X[TaskConfig type lookup]
X --> Y[registry.create\nnormalize_task_name]
Y --> Z[_resolve_factory\nlazy importlib.import_module]
Z --> T
%%{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[External Package\nimports blueprints.py] --> B[register_extensions called\nat module level]
B --> C[register_hardware_adapter\nhardware_type, adapter_type, factory]
B --> D[register_control_task\ntask_type, factory_path]
C --> E[normalize_adapter_name\nstrip + lower]
E --> F{hardware_type\nmatch}
F -->|MANIPULATOR| G[adapter_registry.register]
F -->|BASE| H[twist_base_adapter_registry.register]
F -->|WHOLE_BODY| I[whole_body_adapter_registry.register]
G & H & I --> J{existing\nentry?}
J -->|same factory| K[idempotent return]
J -->|different factory| L[raise ValueError]
J -->|none| M[store in _adapters]
D --> N[normalize_task_name\nstrip + lower]
N --> O[validate_task_factory_path\nmodule:attr format]
O --> P[control_task_registry.register_path]
P --> Q{existing\npath?}
Q -->|same path| R[overwrite same value]
Q -->|different path| S[raise ValueError]
Q -->|none| T[store in _factory_paths]
U[ControlCoordinator.start] --> V[HardwareComponent\nadapter_type lookup]
V --> W[registry.create\nnormalize_adapter_name]
W --> M
U --> X[TaskConfig type lookup]
X --> Y[registry.create\nnormalize_task_name]
Y --> Z[_resolve_factory\nlazy importlib.import_module]
Z --> T
Reviews (5): Last reviewed commit: "chore(docs): fix external package guide ..." | Re-trigger Greptile |
Codecov Report❌ Patch coverage is @@ Coverage Diff @@
## main #2547 +/- ##
==========================================
- Coverage 70.86% 70.57% -0.30%
==========================================
Files 852 869 +17
Lines 77118 77686 +568
Branches 6855 6923 +68
==========================================
+ Hits 54647 54824 +177
- Misses 20669 21081 +412
+ Partials 1802 1781 -21
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 33 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Co-authored-by: cc <55869557+TomCC7@users.noreply.github.com>
Problem
External robot packages need a supported way to register ControlCoordinator hardware adapter names and control task types without placing files inside the DimOS source tree. Without this, an outside package can define a blueprint but still cannot use
HardwareComponent(adapter_type=...)orTaskConfig(type=...)for custom hardware/control unless DimOS itself is modified.Closes DIM-1031
Solution
Add a public
dimos.control.extensionsfacade for explicit extension registration:register_hardware_adapter(HardwareType, adapter_type, factory)dispatches to the existing manipulator, base, or whole-body adapter registry.register_control_task(task_type, factory_path)registers lazy control task factories without importing the target module at registration time.This keeps blueprint usage unchanged: external packages register before coordinator construction, then use normal
HardwareComponentandTaskConfignames.How to Test
Manual QA:
Expected output includes
[external_test_robot]logs for:BASE/external_test_baseexternal_test_driveExternalTestBaseAdapterExternalTestDriveTaskContributor License Agreement