Skip to content

feat: add agent-triggered Go2 episode recording#3117

Open
yusufjarada wants to merge 13 commits into
dimensionalOS:mainfrom
yusufjarada:feat/agent-episode-recording
Open

feat: add agent-triggered Go2 episode recording#3117
yusufjarada wants to merge 13 commits into
dimensionalOS:mainfrom
yusufjarada:feat/agent-episode-recording

Conversation

@yusufjarada

@yusufjarada yusufjarada commented Jul 21, 2026

Copy link
Copy Markdown

Contribution path

  • Small, safe change that does not need a tracking issue

Problem

DimOS can already persist robot streams and export episodes to LeRobot/HDF5, but an agent cannot mark labeled training episodes at runtime. Existing collection workflows depend on teleop buttons.

Solution

  • Expose start_recording, stop_recording, and discard_recording directly from EpisodeMonitorModule.
  • Use one runtime EpisodeCommand enum for start/save/discard/toggle commands and normalize RPC input once.
  • Keep task-label updates and state transitions under one lock.
  • Preserve the current task label across repeated takes until a new label is supplied or the session counters are reset.
  • Make save/discard while idle explicit no-ops.
  • Add an opt-in unitree-go2-agentic-record blueprint.
  • Record Go2 camera, odometry, velocity commands, episode status, and TF into a timestamped memory2 session DB.
  • Preserve the default unitree-go2-agentic blueprint unchanged.

cmd_vel is produced by the existing MovementManager in the transitive Go2 agentic blueprint. A blueprint invariant test verifies that producer and recorder input are both present.

How to Test

dimos --simulation --viewer none run unitree-go2-agentic-record

In another terminal:

dimos mcp call start_recording --arg task_label=walk-test
dimos mcp call relative_move --arg forward=0.2
dimos mcp call stop_recording
# Omit task_label to record another take of walk-test
dimos mcp call start_recording

Verification completed locally:

  • pytest dimos/learning/collection dimos/learning/dataprep/test_core.py dimos/robot/test_all_blueprints_generation.py -q — 51 passed
  • Ruff format and lint checks passed for all changed Python files
  • go2_recorder.py focused coverage — 100%
  • MuJoCo MCP smoke test completed two start → move → save takes; both persisted the same task label and the DB contained camera, odom, cmd_vel, status, and TF records with no runtime errors

AI assistance

Claude Code with Opus 4.8 produced the initial design and implementation. OpenAI Codex with GPT-5 reviewed maintainer feedback, revised the implementation, added regression tests, resolved upstream merges, and ran verification. I reviewed and understand the resulting changes.

Checklist

  • I have read and approved the CLA.

yjarada03 and others added 8 commits July 20, 2026 13:17
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sodeMonitor

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ord blueprint

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…guidance

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds agent-triggered episode recording for the Go2 workflow. The main changes are:

  • Agent skills for starting, saving, and discarding labeled episodes.
  • Serialized episode state changes and status publication.
  • Persistent task labels across takes with reset support.
  • An opt-in Go2 recorder blueprint for camera, odometry, commands, status, and TF data.
  • Tests for concurrent transitions, idle operations, labels, and action-stream wiring.

Confidence Score: 5/5

This looks safe to merge.

  • The episode transition fixes keep labels, state changes, and publication in one ordered operation.
  • Idle save and discard calls no longer report successful transitions.
  • Counter resets restore the configured task label.
  • The recording blueprint includes both the velocity-command producer and recorder input.
  • No blocking issue remains in the reviewed fixes.

Important Files Changed

Filename Overview
dimos/learning/collection/episode_monitor.py Adds agent recording commands and serializes label, state, and publication updates.
dimos/learning/collection/go2_recorder.py Adds a recorder for Go2 observations, velocity commands, and episode status.
dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_record.py Adds an opt-in recording variant of the existing Go2 agentic blueprint.
dimos/learning/collection/test_episode_monitor.py Covers labels, idle operations, agent skills, resets, and concurrent publication order.
dimos/learning/collection/test_go2_recorder.py Checks action-stream producer wiring and odometry pose handling.
dimos/robot/all_blueprints.py Registers the new Go2 recording blueprint and recorder module.

Reviews (4): Last reviewed commit: "fix(collection): preserve transition pub..." | Re-trigger Greptile

Comment thread dimos/learning/collection/go2_recorder.py
Comment on lines +133 to +136
if event == "start" and task_label is not None:
with self._lock:
self._task_label = task_label
self._transition(cast("EpisodeCommand", event), time.time())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Label Update Races With Start

RPC calls run concurrently, but this path releases _lock after assigning _task_label and reacquires it in _transition. Two simultaneous starts can publish one caller's episode with the other caller's label, so the returned message names one task while DataPrep persists another.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. set_episode now passes the requested label into _transition, and the label assignment plus state mutation happen under the same lock. A concurrent-start regression test verifies that alpha and beta each retain their own emitted label.

Comment on lines +141 to +143
if event == "save":
return f"Episode saved. Total saved this session: {saved}."
return f"Episode discarded. Total discarded this session: {discarded}."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Idle Transitions Report Success

When save or discard arrives while idle, _transition changes no counter and DataPrep has no pending episode to commit. These unconditional responses still tell the agent that an episode was saved or discarded, creating a phantom successful operation for duplicate or out-of-order calls.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Save and discard while idle now return a clear no-op response and emit no EpisodeStatus event. Both paths have regression coverage.

Comment thread dimos/learning/collection/episode_monitor.py
Comment thread dimos/agents/system_prompt.py Outdated
- `where_am_i` gives your current street/area and nearby landmarks
- `map_query` finds places on the OSM map by description and returns coordinates
## Episode Recording

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we inject skill prompt into agent context automatically, so this might not be necessary here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the system-prompt addition. The skill docstrings provide the injected tool guidance.

@TomCC7 TomCC7 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, would you fill in the PR description so we have a better understanding of what solution you are providing here? Also it would be good if you can cleanup the modified files a bit (superpower, logs, etc)

Returns:
A human-readable status string (also safe to surface to the agent).
"""
if event not in ("start", "save", "discard"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better using enum/literal

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the public event parameter to EpisodeAction, a Literal restricted to start, save, and discard. The internal EpisodeCommand still includes toggle for button input.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you are using it here. You defined the enum but this if statement still use the bare tuple

Suggested change
if event not in ("start", "save", "discard"):
if event not in EpisodeAction:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous EpisodeAction was a typing.Literal alias, so membership against it would not work at runtime. I replaced both aliases with a real EpisodeCommand StrEnum and normalize/validate the incoming value once with EpisodeCommand(event).

from dimos.learning.collection.episode_monitor_spec import EpisodeControlSpec


class EpisodeRecordingSkillContainer(Module):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fine to just add these functions to episode monitor directly

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The three recording skills now live directly on EpisodeMonitorModule; the separate skill container and RPC spec were removed.

Comment thread MUJOCO_LOG.TXT Outdated
@@ -0,0 +1,18 @@
Wed Jul 15 12:43:01 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be committed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed MUJOCO_LOG.TXT and the other unrelated local artifacts from the branch.

@paul-nechifor

Copy link
Copy Markdown
Contributor

Please fill in the PR description.

@yusufjarada yusufjarada changed the title Feat/agent episode recording feat: add agent-triggered Go2 episode recording Jul 22, 2026
@yusufjarada

Copy link
Copy Markdown
Author

Addressed the requested changes and pushed the revision:

  • filled in the PR description and AI disclosure
  • removed logs, planning notes, and unrelated local files
  • moved recording tools directly onto EpisodeMonitorModule
  • fixed the label race, idle save/discard responses, and reset label leakage
  • added typed episode actions and regression coverage
  • verified the existing MovementManager.cmd_vel producer with a blueprint test and a successful MuJoCo recording
  • merged current main and resolved the generated blueprint conflict

Local verification: 47 collection/dataprep tests passed, generated blueprint check passed, Ruff passed, and the simulated MCP start → move → save flow produced all expected DB streams without runtime errors.

The remaining Backport check requires a maintainer to add backport:skip (the fork account cannot add labels). Ready for another review when convenient.

@yusufjarada
yusufjarada requested a review from TomCC7 July 22, 2026 00:06
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
dimos/learning/collection/go2_recorder.py 90.90% 2 Missing ⚠️
@@            Coverage Diff             @@
##             main    #3117      +/-   ##
==========================================
- Coverage   73.23%   72.98%   -0.25%     
==========================================
  Files        1037     1040       +3     
  Lines       93895    94031     +136     
  Branches     8528     8534       +6     
==========================================
- Hits        68761    68629     -132     
- Misses      22792    23085     +293     
+ Partials     2342     2317      -25     
Flag Coverage Δ
OS-ubuntu-24.04-arm 66.41% <98.59%> (+0.06%) ⬆️
OS-ubuntu-latest 68.68% <98.59%> (+0.06%) ⬆️
Py-3.10 ?
Py-3.11 68.67% <98.59%> (+0.05%) ⬆️
Py-3.12 68.68% <98.59%> (+0.06%) ⬆️
Py-3.13 68.66% <98.59%> (+0.05%) ⬆️
Py-3.14 68.68% <98.59%> (+0.06%) ⬆️
Py-3.14t 68.66% <98.59%> (+0.05%) ⬆️
SelfHosted-macOS ?

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

Files with missing lines Coverage Δ
dimos/learning/collection/episode_monitor.py 93.43% <100.00%> (+5.58%) ⬆️
dimos/learning/collection/test_episode_monitor.py 100.00% <100.00%> (+1.12%) ⬆️
dimos/learning/collection/test_go2_recorder.py 100.00% <100.00%> (ø)
dimos/robot/all_blueprints.py 100.00% <ø> (ø)
...2/blueprints/agentic/unitree_go2_agentic_record.py 100.00% <100.00%> (ø)
dimos/learning/collection/go2_recorder.py 90.90% <90.90%> (ø)

... and 25 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.

Comment on lines 42 to 45
EpisodeAction: TypeAlias = Literal["start", "save", "discard"]
# A button/keyboard press can also request `toggle`, which resolves to
# `start`/`save` based on the current state and never reaches the output.
EpisodeCommand: TypeAlias = Literal["start", "save", "discard", "toggle"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two definitions are nearly the same. Can you elaborate what's the difference between an action and a command? And is it possible to unify it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unified them into one runtime EpisodeCommand StrEnum with start, save, discard, and toggle values. The separate EpisodeAction alias is removed.

Returns:
A human-readable status string (also safe to surface to the agent).
"""
if event not in ("start", "save", "discard"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you are using it here. You defined the enum but this if statement still use the bare tuple

Suggested change
if event not in ("start", "save", "discard"):
if event not in EpisodeAction:

Comment on lines +139 to +145
if event == "start":
return (
f"Recording started (task: {task_label})." if task_label else "Recording started."
)
if event == "save":
return f"Episode saved. Total saved this session: {status.episodes_saved}."
return f"Episode discarded. Total discarded this session: {status.episodes_discarded}."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better using a match statement or just a simple dict here as these three cases are same level, and using any as fallback looks weird.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the response dispatch to an explicit match over the resolved status event, with separate start, save, and discard cases.

# (inside _transition) so it happens regardless of entry point — button,
# keyboard, or the set_episode RPC.
if event in ("save", "discard"):
self._task_label = self.config.default_task_label

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what's the settings here. Generally during data collection we do the same task multiple times instead of doing different tasks right? Let me know what you think.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this to session-level task semantics. Save and discard now preserve the active task label, and start_recording accepts an optional label so repeated takes reuse it. Supplying a new label changes tasks; reset_counters restores the configured default. The MuJoCo smoke test persisted two takes with the same label.

@yusufjarada

Copy link
Copy Markdown
Author

Pushed the follow-up review fixes in e0005448d:

  • unified action/command types into one runtime EpisodeCommand enum
  • normalized RPC inputs against that enum
  • changed response handling to match
  • preserved task labels across repeated takes and made the skill label optional
  • added direct coverage for the odometry pose setter, bringing go2_recorder.py to 100% focused coverage
  • synced the branch with current main

Local verification: 51 tests passed, Ruff passed, blueprint generation passed, and a two-take MuJoCo MCP run persisted the same label across both episodes with all expected streams. Ready for re-review.

status = self._snapshot(event, ts)
self._emit(status)
status = self._snapshot(event.value, ts)
return self._emit(status)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Preserve transition publication order

The lock serializes label assignment and snapshot creation, but _emit runs after the lock is released. If concurrent starts use alpha and beta, the alpha transition can snapshot first while beta publishes first. DataPrep then processes the starts in the wrong order and can assign the intervening episode to the wrong label. Serialize publication with the state transition or send snapshots through an ordered publisher.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 550af88. Episode state mutation/snapshotting still uses the state lock, while a separate transition lock now serializes the complete snapshot-and-publish sequence. This preserves publication order without holding the state lock during subscriber I/O. Added a deterministic concurrent-transition regression test that reproduced the alpha/beta inversion before the fix. Focused suite: 52 passed; episode monitor suite: 19 passed; Ruff and diff checks pass.

@yusufjarada
yusufjarada requested a review from TomCC7 July 23, 2026 00:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants