Skip to content

Retire the TF service, tf is a standard topic, add IO[T] ports - #3169

Merged
leshy merged 14 commits into
mainfrom
ivan/feat/tf_simplification
Jul 30, 2026
Merged

Retire the TF service, tf is a standard topic, add IO[T] ports#3169
leshy merged 14 commits into
mainfrom
ivan/feat/tf_simplification

Conversation

@leshy

@leshy leshy commented Jul 24, 2026

Copy link
Copy Markdown
Member

What

Kills the magic lazily-attached self.tf service. TF is now an ordinary TFMessage stream named tf, declared and wired like any other module port. Adds the missing primitive that made this possible: a bidirectional IO[T] stream kind.

Why

  • the TF service (PubSubTF/LCMTF/ZenohTF, auto-created on first self.tf touch) implied a second protocol living next to module streams, its own pubsub stack per module, invisible to blueprints, io(), recorders, remapping and namespacing. Every use is just "subscribe to a tf topic" or "publish on it" - those are pubsub functions

  • simplifies native modules Retire the TF service, tf is a standard topic, add IO[T] ports #3169

  • recorder can easily record tf without special code

How

IO[T] ports (core/stream.py): publish and subscribe on the same topic; an IO port sees the whole topic including its own publishes (transport loopback). BlueprintAtom records direction "inout".

API:

class Odometry(Module):
    tf: Out[TFMessage]                      # publish-only port

    def on_odom(self, odom: PoseStamped) -> None:
        self.tf.publish(TFMessage(Transform.from_pose("base_link", odom)))


class Perception(Module):
    tf: IO[TFMessage]                       # publish + lookup (In[TFMessage] if read-only)

    def process(self, img: Image) -> None:
        # self.tfbuffer: lazy TF view over the tf port — subscribes on first
        # touch, buffers 10s, disposed with the module
        tf = self.tfbuffer.get("world", "camera_optical", img.ts, 0.5)  # chained/inverse lookups
        self.tfbuffer.publish(marker_tf)    # Transform varargs: stores locally + sends on the topic

    async def handle_tf(self, msg: TFMessage) -> None:
        ...                                 # optional: receive the raw stream, auto-bound to the port

Outside modules, TF(stream) works over any stream-like — a port, a raw Transport (TF(make_transport("/tf", TFMessage)) outside modules), or nothing (TF(), a purely local buffer for tests). dispose() unsubscribes.

Module pattern:

  • publish-only → tf: Out[TFMessage], self.tf.publish(TFMessage(...))
  • lookup-only → tf: In[TFMessage], lookups via self.tfbuffer
  • both → tf: IO[TFMessage] (Detection2DModule)

Deleted: TFSpec, TFConfig, PubSubTF, LCMTF, ZenohTF, tf_backend(), ModuleConfig.tf_transport, the lazy self.tf property. ~30 call sites migrated; the memory2 Recorder records through its own tf: In port; StreamTF became a plain class; docs rewritten.

Compat: wire format unchanged (/tf LCM, dimos/tf zenoh) - recordings and external listeners unaffected. Namespacing prefixes tf like any stream (per-robot trees, consistent with frame_id_prefix). tf is no longer a reserved Module attribute, which unblocks the pure-modules T14 bridge.

…orts

The magic lazily-attached self.tf service (PubSubTF/LCMTF/ZenohTF with its
own pubsub stack per module) implied a second protocol next to module
streams: invisible to blueprints, io(), recorders and remapping. tf is now
an ordinary TFMessage stream named "tf", wired like any other port.

- core: new IO[T]/RemoteIO bidirectional stream kind. Ports declared as
  tf: IO[TFMessage] publish and subscribe on one topic; BlueprintAtom
  records direction "inout". The coordinator needed no changes: same-named
  ports already share one transport, so N writers + M readers on /tf is
  native pub/sub.
- protocol/tf: TFSpec/TFConfig/PubSubTF/LCMTF/ZenohTF deleted, along with
  tf_backend() and ModuleConfig.tf_transport. What remains is MultiTBuffer
  (+ receive_tfmessage/get_pose) and TF — a disposable buffer view over any
  stream-like (IO/In port, raw Transport, or nothing for a local buffer).
- modules: publishers declare tf: Out[TFMessage] and publish TFMessage;
  lookup users declare tf: In[TFMessage] and build self._tf = TF(self.tf)
  in start(); Detection2DModule uses IO for both. Recorder records its own
  tf In port instead of introspecting the service's pubsub. StreamTF is a
  plain class.

Wire format is unchanged (/tf on LCM, dimos/tf on zenoh), so recordings
and external listeners are unaffected. End-to-end coverage in
test_io_port_autoconnects_and_flows_both_ways: shared topic, IO both
directions, loopback.
@leshy leshy changed the title feat(tf): retire the TF service — tf is a standard topic, add IO[T] ports Retire the TF service, tf is a standard topic, add IO[T] ports Jul 24, 2026
@mintlify

mintlify Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
dimensional 🟢 Ready View Preview Jul 24, 2026, 4:25 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR replaces the implicit TF service with ordinary typed stream ports and adds bidirectional IO[T] ports.

  • Declares and wires TF streams through module blueprints using In, Out, or IO.
  • Introduces stream-backed TF buffers with explicit subscription disposal during module shutdown.
  • Migrates TF publishers, consumers, recorder support, tests, and documentation to the new model.

Confidence Score: 5/5

The PR appears safe to merge, with no remaining blocking failure related to the prior TF subscription-cleanup thread.

The module shutdown path now disposes the conventional TF view before stopping its port transports, removing its subscription while the transport remains active.

Important Files Changed

Filename Overview
dimos/core/module.py Adds IO-port lifecycle and introspection support and explicitly disposes the lazy TF buffer before stopping port transports.
dimos/core/stream.py Adds local and remote bidirectional stream primitives that publish and subscribe through one transport.
dimos/protocol/tf/tf.py Reworks TF into a stream-backed or local buffer with explicit subscription disposal.
dimos/core/coordination/blueprints.py Teaches blueprint extraction to represent bidirectional ports with the inout direction.
dimos/memory2/module.py Records TF through a declared input port and uses the stream-backed TF view for pose lookups.

Sequence Diagram

sequenceDiagram
    participant Module
    participant TFView as TF Buffer
    participant Port as TF In/IO Port
    participant Transport
    Module->>TFView: first tfbuffer access
    TFView->>Port: subscribe(receive_tfmessage)
    Port->>Transport: subscribe callback
    Transport-->>TFView: TFMessage
    Module->>TFView: dispose during shutdown
    TFView->>Transport: unsubscribe callback
    Module->>Port: stop
    Port->>Transport: stop
Loading

Reviews (9): Last reviewed commit: "Merge branch 'main' into ivan/feat/tf_si..." | Re-trigger Greptile

Comment thread dimos/protocol/tf/tf.py
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.48348% with 55 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
dimos/memory2/module.py 18.75% 13 Missing ⚠️
dimos/core/stream.py 75.86% 6 Missing and 1 partial ⚠️
dimos/core/module.py 90.90% 1 Missing and 2 partials ⚠️
...n/cmu_nav/modules/simple_planner/simple_planner.py 40.00% 3 Missing ⚠️
dimos/perception/test_spatial_memory_module.py 50.00% 3 Missing ⚠️
dimos/agents/skills/person_follow.py 50.00% 2 Missing ⚠️
dimos/perception/detection/module2D.py 60.00% 2 Missing ⚠️
dimos/perception/detection/module3D.py 0.00% 2 Missing ⚠️
...ception/fiducial/marker_detection_stream_module.py 60.00% 2 Missing ⚠️
dimos/perception/spatial_perception.py 50.00% 2 Missing ⚠️
... and 14 more
@@            Coverage Diff             @@
##             main    #3169      +/-   ##
==========================================
+ Coverage   75.20%   75.23%   +0.02%     
==========================================
  Files        1128     1128              
  Lines      107954   108040      +86     
  Branches     9751     9748       -3     
==========================================
+ Hits        81188    81282      +94     
+ Misses      23955    23952       -3     
+ Partials     2811     2806       -5     
Flag Coverage Δ
OS-ubuntu-24.04-arm 68.88% <81.68%> (+0.02%) ⬆️
OS-ubuntu-latest 70.95% <81.68%> (+0.03%) ⬆️
Py-3.10 70.95% <81.68%> (+0.02%) ⬆️
Py-3.11 70.94% <81.68%> (+0.02%) ⬆️
Py-3.12 70.94% <81.68%> (+0.03%) ⬆️
Py-3.13 70.94% <81.68%> (+0.02%) ⬆️
Py-3.14 70.94% <81.68%> (+0.02%) ⬆️
Py-3.14t 70.94% <81.68%> (+0.02%) ⬆️
SelfHosted-Large 29.10% <43.27%> (+0.01%) ⬆️
SelfHosted-Linux 35.67% <52.13%> (+0.01%) ⬆️
SelfHosted-macOS 34.74% <51.80%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
dimos/core/coordination/blueprints.py 88.61% <100.00%> (ø)
dimos/core/coordination/test_module_coordinator.py 99.60% <100.00%> (+0.04%) ⬆️
dimos/core/test_transport_factory.py 100.00% <ø> (ø)
dimos/core/transport.py 68.58% <100.00%> (+0.57%) ⬆️
dimos/core/transport_factory.py 100.00% <ø> (ø)
dimos/hardware/sensors/lidar/fastlio2/module.py 89.41% <100.00%> (+0.25%) ⬆️
dimos/hardware/sensors/lidar/pointlio/module.py 92.10% <100.00%> (+0.14%) ⬆️
dimos/memory2/tf.py 97.77% <100.00%> (+1.62%) ⬆️
...nav/modules/far_planner/test_far_planner_rosbag.py 97.50% <100.00%> (ø)
dimos/navigation/cmu_nav/modules/pgo/pgo.py 85.71% <100.00%> (+0.52%) ⬆️
... and 41 more

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Restores the old ergonomics without the old magic: the port stays an
explicit, blueprint-visible declaration; only the buffer view is implicit.
Built on first touch (raises clearly without a tf port or transport),
disposed with the module, backed by the _tf slot so tests can still inject
fakes. Drops the per-module `_tf = TF(self.tf)` boilerplate and None
guards across all lookup modules.
Comment thread dimos/core/coordination/test_module_coordinator.py Outdated
Comment thread dimos/core/module.py Outdated
Comment thread dimos/core/module.py Outdated
Comment thread dimos/core/stream.py Outdated
Comment thread dimos/core/stream.py Outdated
Comment thread dimos/hardware/sensors/lidar/fastlio2/module.py Outdated
Comment thread dimos/hardware/sensors/lidar/pointlio/module.py Outdated
Comment thread dimos/memory2/module.py Outdated
Co-authored-by: Paul Nechifor <paul@nechifor.net>
@leshy
leshy requested a review from Dreamsorcerer as a code owner July 25, 2026 11:46
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Jul 25, 2026
@leshy
leshy added this pull request to the merge queue Jul 29, 2026
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Jul 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Jul 29, 2026
…cation

# Conflicts:
#	dimos/core/test_transport_factory.py
#	dimos/core/transport_factory.py
Main's #3176 replaced the pubsub-based ZenohRPC with a native queryable
implementation that has no topicgen; keys are built as dimos/rpc/<name>,
so the no-leading-slash assertion is now structural.
paul-nechifor
paul-nechifor previously approved these changes Jul 29, 2026
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Jul 29, 2026
paul-nechifor
paul-nechifor previously approved these changes Jul 29, 2026
…cation

# Conflicts:
#	dimos/manipulation/manipulation_module.py
@leshy
leshy added this pull request to the merge queue Jul 30, 2026
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Jul 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 30, 2026
@paul-nechifor
paul-nechifor added this pull request to the merge queue Jul 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 30, 2026
@leshy
leshy added this pull request to the merge queue Jul 30, 2026
Merged via the queue into main with commit ac8afff Jul 30, 2026
36 checks passed
@leshy
leshy deleted the ivan/feat/tf_simplification branch July 30, 2026 07:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants