Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .agents/skills/add-atomic-action/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Inspect only the files relevant to the requested skill:
| Engine-owned planning resources | `embodichain/lab/sim/atomic_actions/runtime.py` |
| Reference implementations | `embodichain/lab/sim/atomic_actions/primitives/` |
| Static compiler and execution session | `engine.py`, `execution.py` |
| Controller-facing execution ports | `runner.py`, `sim_adapter.py` |

The public contract is:

Expand Down Expand Up @@ -203,8 +204,10 @@ invocation = ActionInvocation(
compiled = engine.compile((invocation,))
```

Use `engine.start(...).tick(...)` instead when dynamic scene updates or online
error recovery are required.
For dynamic scene updates or online error recovery, create a session with
`engine.start(...)`, then connect it to observation, command, and clock ports
through `ExecutionRunner`. Use non-blocking `runner.step()` in an existing event
loop or `runner.run_until_blocked()` in a simple application.

## 5. Export and document

Expand Down Expand Up @@ -250,4 +253,4 @@ then use the `pre-commit-check` skill before committing.
| Return an arm-only tensor | Embed into full robot DoF. |
| Mutate held state after planning | Declare a `StateDelta`. |
| Treat `plan_success` as physical success | Verify effects during execution. |
| Step the simulator from the action | Emit plans; let the caller own execution. |
| Step the simulator from the action | Emit plans; connect execution through `ExecutionRunner`. |
6 changes: 6 additions & 0 deletions agent_context/MAP.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,10 @@ topics:
- ActionPlan
- PlanningContext
- ExecutionSession
- ExecutionRunner
- ObservationProvider
- CommandSink
- SimulationExecutionAdapter
- StateDelta
- held_objects
- ActionBinding
Expand Down Expand Up @@ -472,6 +476,8 @@ topics:
- embodichain/lab/sim/atomic_actions/state.py
- embodichain/lab/sim/atomic_actions/plans.py
- embodichain/lab/sim/atomic_actions/execution.py
- embodichain/lab/sim/atomic_actions/runner.py
- embodichain/lab/sim/atomic_actions/sim_adapter.py
- embodichain/lab/sim/atomic_actions/engine.py
- embodichain/lab/sim/atomic_actions/trajectory.py
- embodichain/lab/sim/atomic_actions/primitives/
Expand Down
65 changes: 59 additions & 6 deletions agent_context/topics/atomic-actions/atomic-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,21 @@ snapshot every time the action plans. Its entity ID is recorded in

```python
session = engine.start(invocations, initial_context)
tick = session.tick(latest_context, effect_success=None)
runner = ExecutionRunner(
session,
observation_provider,
command_sink,
clock=execution_clock,
)
result = runner.step(effect_success=None)
```

An `ExecutionSession` emits at most one `JointCommand` per tick. The command's
per-environment `hold_duration` preserves `TimedTrajectory.dt` for the caller's
control loop: command `i` carries the arrival interval `dt[:, i]`, which is
normally zero for the initial waypoint. The session monitors:
`ExecutionSession` owns deterministic planning progress and recovery state. It
emits at most one `JointCommand` per tick. The command's per-environment
`hold_duration` schedules the next feedback cycle from `TimedTrajectory.dt`:
command `i` carries the arrival interval `dt[:, i + 1]` leading to the next
waypoint. The final command reuses its own interval as a settling window. The
session monitors:

- joint tracking error against the previous command;
- translation/rotation drift of referenced scene entities;
Expand Down Expand Up @@ -113,6 +121,40 @@ The replacement must keep the active `skill_id` and `invocation_id`. The
session resolves a new snapshot, resets that revision's recovery budgets, and
replans from the latest context.

`ExecutionRunner` owns the controller-facing lifecycle around a session:

- `ObservationProvider.observe(task_state)` supplies a fresh, monotonically
timestamped `PlanningContext` when a feedback cycle is due;
- `CommandSink.send/hold/cancel` returns a `CommandAcknowledgement` with
`accepted`, `rejected`, or `timed_out` status;
- `ExecutionClock` supplies monotonic time and backend waiting;
- non-blocking `step()` dispatches only when the current command's
`hold_duration` has elapsed;
- `run_until_blocked()` is a convenience loop that waits through the clock and
stops at a terminal state or an unhandled effect-verification boundary; the
runner remembers that boundary so a later verifier call can resume it;
- cancellation, observation/session exceptions, and negative acknowledgements
enter a best-effort cancel-then-hold path.

`TimedTrajectory.dt[:, i]` is the interval leading to sample `i`.
`ExecutionSession` dispatches sample zero immediately, then maps each following
arrival interval to the preceding command's `JointCommand.hold_duration`. The
final sample uses its own interval again as a settling window before terminal
validation. Batched execution currently advances at a synchronized barrier
using the longest active row interval.

`SimulationExecutionAdapter` implements observation, command, and clock ports
for a `SimulationManager`/`Robot` pair. Its `sleep()` advances an integral
number of physics steps, so simulation execution does not depend on wall time.
Stable context IDs are correlation identifiers; the adapter maps command rows
to simulation robot indices rather than using those IDs as array indices.
Real-device adapters should implement the same protocols and enforce the passed
acknowledgement timeout in their transport/controller layer.

The latest validated session context is retained for safe hold if the first
live observation fails. Environment IDs must remain stable and ordered for the
entire session; robot and scene timestamps and scene versions must be monotonic.

## Parameter ownership

Goal dataclasses carry only semantic task intent. They do not carry robot part
Expand All @@ -135,6 +177,11 @@ The module-level `register_action()` API is a process-wide extension-type
discovery catalog only; it neither binds actions nor changes an engine's
default built-in set.

`ExecutionRunnerCfg` is intentionally separate from action options. It
configures controller acknowledgement deadlines, scheduler cadence, and final
safe-hold behavior for one runner instance; it does not change skill planning
semantics and does not belong in `ActionInvocation` or an invocation revision.

Every `ActionBinding` value is a `RobotCfg.control_parts` key. It is not a link,
TCP-frame, joint, or scene-object name. Planning services validate those names
and resolve immutable `ResolvedControlPart` values containing full-robot joint
Expand Down Expand Up @@ -177,6 +224,11 @@ tutorial may derive a simple profile from limits explicitly.
| `coordinated_placement` | `CoordinatedPlacementGoal` | `placing`, `support` |
| `hand_over` | `GraspGoal` | `source`, `destination` |

`GraspGoal.grasp_xpos` accepts an explicit pose tensor, a late-bound
`SceneEntityPose`, or `None` for affordance sampling. A `SceneEntityPose`
registers the referenced entity as a recovery dependency, allowing an executing
`PickUp` to replan when the grasp target moves.

## Extension rules

1. Define a frozen action-owned goal dataclass with `goal_kind`.
Expand All @@ -188,4 +240,5 @@ tutorial may derive a simple profile from limits explicitly.
7. Declare symbolic changes with `StateDelta`; do not mutate context or commit
physical effects during planning.
8. Keep scene stepping, controller I/O, and task-graph/MLLM logic outside the
atomic action.
atomic action. Put execution-loop I/O behind the runner protocols rather than
calling a simulator or device from `plan()` or `ExecutionSession`.
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,23 @@ embodichain.lab.sim.atomic_actions
AtomicAction
AtomicActionEngine
ExecutionSession
ExecutionRunner
ExecutionRunnerCfg
RunnerStep
RunnerStatus
ObservationProvider
CommandSink
CommandAcknowledgement
CommandAckStatus
CommandDispatch
CommandOperation
ExecutionClock
SimulationExecutionAdapter
ExecutionTick
JointCommand
ExecutionEvent
ExecutionEventKind
ExecutionStatus

.. rubric:: Built-in goals and actions

Expand Down Expand Up @@ -151,6 +165,46 @@ Engine and execution
.. autoclass:: ExecutionSession
:members:

.. autoclass:: ExecutionRunner
:members:

.. autoclass:: ExecutionRunnerCfg
:members:
:exclude-members: __init__, copy, replace, to_dict

.. autoclass:: ObservationProvider
:members:

.. autoclass:: CommandSink
:members:

.. autoclass:: ExecutionClock
:members:

.. autoclass:: MonotonicExecutionClock
:members:

.. autoclass:: SimulationExecutionAdapter
:members:

.. autoclass:: CommandAcknowledgement
:members:

.. autoclass:: CommandAckStatus
:members:

.. autoclass:: CommandDispatch
:members:

.. autoclass:: CommandOperation
:members:

.. autoclass:: RunnerStep
:members:

.. autoclass:: RunnerStatus
:members:

.. autoclass:: ExecutionTick
:members:

Expand All @@ -160,6 +214,12 @@ Engine and execution
.. autoclass:: ExecutionEvent
:members:

.. autoclass:: ExecutionEventKind
:members:

.. autoclass:: ExecutionStatus
:members:

Semantic objects and helpers
----------------------------

Expand Down
19 changes: 11 additions & 8 deletions docs/source/overview/sim/atomic_actions/builtin_actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ entity as a recovery dependency.
| `Press.xpos` | yes | yes |
| `CoordinatedPickGoal.object_target_pose` / `object_initial_pose` | yes | yes |
| `CoordinatedPlacementGoal` placing/support poses | yes | yes |
| `PickUp` / `HandOver` semantic entity lookup | not through `SceneEntityPose` | no automatic scene dependency |
| `PickUp.grasp_xpos` | yes | yes |
| `PickUp` / `HandOver` `ObjectSemantics.entity` lookup | not through `SceneEntityPose` | no automatic scene dependency |
| `AssembleGoal` base entity lookup | not through `SceneEntityPose` | latest pose is used when replanning, but base movement alone does not trigger it |

### Parameter ownership
Expand Down Expand Up @@ -310,10 +311,12 @@ bound manipulator.
| Effect | write `HeldObjectState` for the bound manipulator and clear overlapping coordinated attachment state |
| Verification | the attachment effect must be verified during closed-loop execution |

`grasp_xpos` may be `(4, 4)` or `(B, 4, 4)`. When omitted, the action samples
valid affordance grasps, evaluates reachability, and stores the selected
`object_to_eef` transform in the expected held-object state. Later
object-centric skills reuse that transform.
`grasp_xpos` may be `(4, 4)`, `(B, 4, 4)`, or a `SceneEntityPose`. A scene
reference resolves the latest grasp pose and registers its entity as a recovery
dependency, so material target motion invalidates and replans an executing
`PickUp`. When omitted, the action samples valid affordance grasps, evaluates
reachability, and stores the selected `object_to_eef` transform in the expected
held-object state. Later object-centric skills reuse that transform.

`PickUp` requires `open` and `grasp` commands on the bound end-effector profile.
Important `PickUpOptions` fields:
Expand All @@ -327,9 +330,9 @@ Important `PickUpOptions` fields:
| `downstream_object_target_poses` | Optional future reachability constraints used in grasp selection |
| `obj_upright_direction`, `rotate_upright` | Optional orientation-selection behavior |

The semantic entity pose is read when planning occurs, but it is not currently a
`SceneEntityPose` dependency. Object motion alone therefore does not trigger
automatic dynamic-goal replanning.
Reading `ObjectSemantics.entity` remains a live planning lookup rather than an
automatic dependency. Use an explicit `SceneEntityPose` in `grasp_xpos` when
object motion should trigger dynamic-goal replanning.

**Example:** `scripts/tutorials/atomic_action/pickup.py`

Expand Down
65 changes: 59 additions & 6 deletions docs/source/overview/sim/atomic_actions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ and whole-body control are not implemented by this module yet.
| |
v v
CompiledTrajectory JointCommand + events
|
v
ExecutionRunner
observe / schedule / dispatch
|
v
ObservationProvider + CommandSink + Clock
```

The boundary is deliberate:
Expand All @@ -71,9 +78,19 @@ The boundary is deliberate:
| Perception and grounding | Agent adapter or user application | Builds scene snapshots and resource bindings, or supplies already-grounded values directly |
| Deterministic motion planning | Atomic action module | Produces an `ActionPlan` from an invocation and context |
| Motion-generation resources | `AtomicActionEngine` | Owns one robot, motion generator, planner backend, device, trajectory builder, and control-part command profiles |
| Robot/simulator stepping | Application control loop | Consumes `JointCommand`; the session never steps the simulator itself |
| Recovery state | `ExecutionSession` | Consumes fresh contexts, emits at most one `JointCommand` per tick, and owns bounded recovery/revision state |
| Scheduling and controller lifecycle | `ExecutionRunner` | Observes only when due, dispatches timed commands, records acknowledgements, and performs safe stop |
| Robot/simulator I/O | `ObservationProvider`, `CommandSink`, and `ExecutionClock` adapters | Isolates observation, command transport, and time/physics advancement from planning and session state |
| Physical-effect verification | Application observer | Verifies grasp, release, handover, and other symbolic effects |

`ExecutionRunner.step()` is non-blocking. Its convenience
`run_until_blocked()` loop waits or advances simulation through an injected
clock. Observation errors, rejected or timed-out commands, session failures,
and explicit cancellation trigger a best-effort cancel-then-hold sequence.
`SimulationExecutionAdapter` implements all three ports for a simulation robot;
real hardware integrations implement the same protocols without changing
action planning or recovery state.

### Caller entry points

The engine supports two first-class caller paths. An Action Agent emits a
Expand Down Expand Up @@ -154,6 +171,7 @@ from leaking into an Action Agent schema.
| `ActionControlOverrides` | Optional role-scoped command replacements for one invocation revision | Persistent robot configuration |
| `MotionPolicy` | Motion source, sample count, timing, limits, collision option, typed planner options | Skill semantics or robot-resource names |
| `RecoveryPolicy` | Replan/retry budgets, tracking and dynamic-goal thresholds, phase timeout | Controller state or mutable counters |
| `ExecutionRunnerCfg` | Runner-level acknowledgement deadlines, minimum feedback cadence, and completion hold policy | Skill behavior, planning resources, or invocation revision data |
| `PlanningContext` | Measured `RobotObservation`, verified `TaskState`, versioned `SceneSnapshot`, stable environment IDs | Hypothetical simulator mutation |
| `ActionPlan` | Per-environment planning result, scene-bound phases, timed trajectories, diagnostics, expected `StateDelta` | Proof that a grasp/release/contact physically succeeded |

Expand Down Expand Up @@ -344,6 +362,9 @@ by the engine after resolving an invocation:
| `AtomicAction.plan(request, context)` | Atomic-action implementer | Consumes an immutable `ResolvedActionRequest` and returns an `ActionPlan` |
| `engine.plan_action(action, invocation, context)` | Extension or isolated test | Temporarily binds and plans an unregistered action instance; built-in parameter variants should use invocation `skill_options` instead |
| `session.revise_current(invocation)` | Runtime orchestrator or Action Agent | Replaces the active logical call with a newer revision and replans from the latest observed context |
| `runner.step(effect_success=...)` | Non-blocking controller integration | Observes and dispatches only when the next timed command is due |
| `runner.run_until_blocked(...)` | Simple blocking application or tutorial | Advances the injected clock until terminal or external effect verification is required |
| `runner.cancel(reason)` | Explicit safe stop | Requests controller cancellation followed by an observed-position hold |

Application code should start with `engine.plan()`, `engine.compile()`, or
`engine.start()` unless it specifically needs one of these extension points.
Expand Down Expand Up @@ -463,6 +484,35 @@ while session.status is ExecutionStatus.RUNNING:
latest_context = observe_context()
```

For most applications, use `ExecutionRunner` to keep scheduling and controller
acknowledgement handling outside the session:

```python
adapter = SimulationExecutionAdapter(sim, robot, scene_supplier=read_scene)
initial_context = adapter.observe(
TaskState.empty(robot.get_qpos().shape[0], robot.device)
)
session = engine.start((moving_goal,), initial_context)
runner = ExecutionRunner(session, adapter, adapter, clock=adapter)
result = runner.run_until_blocked()
```

`ExecutionRunner.step()` is the non-blocking entry point for an application
that already owns its event loop. It observes only when the previous command's
`hold_duration` has elapsed, dispatches active commands through `CommandSink`,
and records accepted, rejected, or timed-out acknowledgements. Cancellation,
observation/session exceptions, and negative acknowledgements enter a
best-effort cancel-then-hold path.

`TimedTrajectory.dt[:, i]` is the interval leading to sample `i`.
`ExecutionSession` maps each following arrival interval onto the preceding
command's post-dispatch hold, while the final sample reuses its own interval as
a settling window before terminal validation. A batched runner uses the longest
active row interval as its synchronized barrier.
`SimulationExecutionAdapter.sleep()` converts that interval to an integral
number of physics steps instead of using wall-clock sleep. Stable `env_ids`
remain correlation identifiers and are not used as simulator array indices.

On each tick, the session can detect:

- joint tracking error relative to the previously emitted command;
Expand Down Expand Up @@ -495,10 +545,11 @@ retain their runtime identity.
Each emitted `JointCommand` carries a per-environment `hold_duration` derived
from the plan's `TimedTrajectory.dt`. The application control loop must respect
that timing after dispatching the command and before requesting the next
observation. `dt[:, i]` is the arrival interval for waypoint `i`, so the first
command normally carries zero duration. For a synchronized batch, the caller
should wait for the longest duration among active rows. A passive hold command
has zero duration.
observation. `dt[:, i]` is the arrival interval leading to waypoint `i`, so the
first waypoint is dispatched immediately and command `i` carries `dt[:, i + 1]`
until the next waypoint is due. The final command reuses `dt[:, -1]` as a
settling window. For a synchronized batch, the caller should wait for the
longest duration among active rows. A passive hold command has zero duration.

Use an explicit newer revision when the application or Action Agent decides to
change runtime behavior:
Expand Down Expand Up @@ -604,4 +655,6 @@ See {doc}`builtin_actions` for the shipped skill catalog and visual demos, and

- {doc}`../planners/motion_generator` — the motion generator owned by the engine
- {doc}`../sim_robot` — robot control parts and kinematic configuration
- `scripts/tutorials/atomic_action/` — focused examples for every built-in skill
- {doc}`/tutorial/atomic_actions` — static, closed-loop, and recovery examples
- `scripts/tutorials/atomic_action/moving_target_recovery.py` — runnable runner
example that visibly moves a late-bound target, replans, and picks up the cube
Loading
Loading