Skip to content
Open
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 @@ -435,6 +435,10 @@ topics:
- ActionPlan
- PlanningContext
- ExecutionSession
- ExecutionRunner
- ObservationProvider
- CommandSink
- SimulationExecutionAdapter
- StateDelta
- held_objects
- ActionBinding
Expand Down Expand Up @@ -464,6 +468,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
52 changes: 49 additions & 3 deletions agent_context/topics/atomic-actions/atomic-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,17 @@ 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 and monitors:
`ExecutionSession` owns deterministic planning progress and recovery state. It
emits at most one `JointCommand` per tick and monitors:

- joint tracking error against the previous command;
- translation/rotation drift of referenced scene entities;
Expand All @@ -88,6 +95,39 @@ 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` maps this to `JointCommand.hold_duration`; the final sample
uses its own interval 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 @@ -101,6 +141,11 @@ selection behavior. An action constructor may accept `default_options`; an
invocation's `skill_options` replaces them for that call. There is no
`ActionCfg` or built-in `*Cfg` layer.

`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 @@ -154,4 +199,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
66 changes: 59 additions & 7 deletions docs/source/overview/sim/atomic_actions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,25 @@ and whole-body control are not implemented by this module yet.
| +-- MotionGenerator / planner backend |
| +-- device and shared TrajectoryBuilder |
| |
| registered AtomicAction.plan(...) -> ActionPlan |
| registered AtomicAction.plan(request, context) |
| -> ActionPlan |
+--------------------------+----------------------------------+
|
+------------+-------------+
| |
v v
compile(...) start(...) / tick(...)
fixed projection observed closed loop
compile(...) start(...)
fixed projection ExecutionSession.tick(...)
| |
v v
CompiledTrajectory JointCommand + events
CompiledTrajectory JointCommand + recovery events
|
v
ExecutionRunner
observe / schedule / dispatch
|
v
ObservationProvider + CommandSink + Clock
```

The boundary is deliberate:
Expand All @@ -71,9 +79,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 @@ -123,6 +141,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 @@ -276,10 +295,13 @@ Both instances still borrow the same engine-owned motion generator.
|---|---|---|
| `AtomicAction.plan(request, context)` | Implementing a skill | Consumes an engine-resolved immutable request; application code normally calls it through the engine |
| `engine.plan(invocation, context)` | Planning one registered skill | Resolves the registered action, binds shared resources, and validates its plan |
| `engine.plan_action(action, invocation, context)` | Planning an unregistered configured instance | Supports multiple configurations with one `skill_id` and one engine backend |
| `engine.plan_action(action, invocation, context)` | Planning an unregistered action instance | Supports multiple default-option variants with one `skill_id` and one engine backend |
| `engine.compile(invocations, context)` | Fixed-scene/offline sequence planning | Returns one concatenated `CompiledTrajectory` and a hypothetical projected context |
| `engine.start(invocations, context)` | Observed incremental execution | Returns an `ExecutionSession`; each `tick()` emits at most one command and recovery events |
| `session.revise_current(invocation)` | Explicit runtime parameter/goal update | Requires a newer revision of the active logical invocation and replans from the latest 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 |

`AtomicAction.plan()` is therefore not a second execution API. It is the
polymorphic implementation point used by the engine. Neither it nor the engine
Expand Down Expand Up @@ -360,6 +382,34 @@ 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`; the final
sample's interval is also its 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 @@ -482,4 +532,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/tracking_error_recovery.py` — runnable runner
example with an injected tracking disturbance
Loading