Agent Skills › DexForce/EmbodiChain › add-atomic-action

add-atomic-action

GitHub

用于在EmbodiChain框架中新增模拟原子动作或运动基元,涵盖目标定义、计划实现及注册流程。

.agents/skills/add-atomic-action/SKILL.md DexForce/EmbodiChain

Trigger Scenarios

需要添加新的模拟原子动作 实现新的技能或动作规划器

Install

npx skills add DexForce/EmbodiChain --skill add-atomic-action -g -y
More Options

Non-standard path

npx skills add https://github.com/DexForce/EmbodiChain/tree/main/.agents/skills/add-atomic-action -g -y

Use without installing

npx skills use DexForce/EmbodiChain@add-atomic-action

指定 Agent (Claude Code)

npx skills add DexForce/EmbodiChain --skill add-atomic-action -a claude-code -g -y

安装 repo 全部 skill

npx skills add DexForce/EmbodiChain --all -g -y

预览 repo 内 skill

npx skills add DexForce/EmbodiChain --list

SKILL.md

Frontmatter
{
    "name": "add-atomic-action",
    "description": "Add a new simulation atomic action or motion primitive to EmbodiChain's typed planning and execution framework. Use when implementing a new skill, goal contract, action planner, symbolic effect, registration entry, documentation, and tests for AtomicActionEngine."
}

Add Atomic Action

Add an action-owned goal and a side-effect-free AtomicAction._plan() implementation. The inherited public plan() entry point binds the current collision scene before calling the skill hook. The engine owns all motion-planning resources; action constructors accept only optional typed default options. Keep task-graph/MLLM logic, simulator stepping, controller I/O, and physical-effect commits outside the action.

Read the current contracts

Inspect only the files relevant to the requested skill:

Purpose Path
Base action and descriptors embodichain/lab/sim/atomic_actions/core.py
Goals and dynamic pose references embodichain/lab/sim/atomic_actions/goals.py
Skill endpoint requirements embodichain/lab/sim/atomic_actions/requirements.py
Resolved endpoint bindings and targets embodichain/lab/sim/atomic_actions/bindings.py
Invocation, options, and resolved request embodichain/lab/sim/atomic_actions/invocation.py
Control-part semantic commands embodichain/lab/sim/atomic_actions/control.py
Invocation policies embodichain/lab/sim/atomic_actions/policies.py
Robot/task/scene state embodichain/lab/sim/atomic_actions/state.py
Dynamic scene provider contract embodichain/lab/sim/atomic_actions/scene.py
Effects and plans embodichain/lab/sim/atomic_actions/effects.py, plans.py
Runtime command frames and payloads embodichain/lab/sim/atomic_actions/runtime_commands.py
Endpoint command transports embodichain/lab/sim/atomic_actions/transports.py
Trajectory helpers embodichain/lab/sim/atomic_actions/trajectory_ops.py
Engine-owned planning resources embodichain/lab/sim/atomic_actions/runtime.py
Declarative robot resources and adapters embodichain/lab/task_program/semantics/profiles.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:

plan = engine.plan(invocation: ActionInvocation[Goal], context: PlanningContext)

Use engine.plan_action(action, invocation, context) for a configured action that is intentionally not in the stable skill registry. Never pass a motion generator to an action constructor.

Do not add compatibility code for ActionTarget, WorldState, ActionResult, execute(), or AtomicActionEngine.run().

1. Define the goal

Place a frozen action-owned dataclass beside the action. Add a stable goal_kind: ClassVar[str]. Do not inherit a marker base merely for dispatch; declare the accepted type on the action instead.

from dataclasses import dataclass
from typing import ClassVar

import torch


@dataclass(frozen=True, slots=True, eq=False)
class PushGoal:
    goal_kind: ClassVar[str] = "push"
    contact_pose: torch.Tensor

Keep only semantic intent in the goal. Do not include arm/hand names, planner options, retry counts, live state, or a generic optional field bag. Use SceneEntityPose for a pose that must be resolved again when the scene moves. Use ObjectActionGoal only when the shared semantics field is genuinely required.

2. Define runtime options and control commands

Define a frozen ActionOptions subclass only when skill behavior may vary by invocation. Examples include distances, grasp constraints, and segment split counts. If no such behavior exists, use the base ActionOptions.

Do not put strategy, planner choice, sample count, control period, velocity limits, collision policy, or recovery thresholds in skill options; those belong to MotionPolicy or RecoveryPolicy.

from dataclasses import dataclass

from embodichain.lab.sim.atomic_actions import ActionOptions


@dataclass(frozen=True, slots=True, eq=False)
class PushOptions(ActionOptions):
    push_distance: float = 0.05

Do not put arm/hand names, hand qpos, or named robot postures in options. Declare robot-independent participant slots and endpoints with SkillBindingContract; the engine or a bound robot skill profile produces the engine-owned ActionBinding. Register embodiment-specific commands such as open, grasp, or ready on ControlPartCommandProfile; use ActionControlOverrides only for one invocation revision.

3. Implement the planner

Inherit AtomicAction[PushGoal, PushOptions] directly. Declare stable metadata and an explicit, robot-independent endpoint contract. Every concrete action class must declare binding_contract in its own class body; use SkillBindingContract() for a skill that consumes no robot resource.

from typing import ClassVar

from embodichain.lab.sim.atomic_actions import (
    ActionPlan,
    AtomicAction,
    CARTESIAN_POSE_CAPABILITY,
    JointPositionTarget,
    PlanningContext,
    ResolvedActionRequest,
    SkillBindingContract,
    SkillEndpointRequirement,
    SkillResourceSlot,
    StateDelta,
)
from embodichain.lab.sim.atomic_actions.trajectory_ops import (
    build_pose_plan_states,
    to_full_robot_trajectory,
)


class Push(AtomicAction[PushGoal, PushOptions]):
    skill_id: ClassVar[str] = "push"
    GoalType: ClassVar[type] = PushGoal
    OptionsType: ClassVar[type] = PushOptions
    binding_contract: ClassVar[SkillBindingContract] = SkillBindingContract(
        slots=(
            SkillResourceSlot(
                slot_id="primary",
                endpoints=(
                    SkillEndpointRequirement(
                        endpoint_id="motion",
                        capabilities=frozenset({CARTESIAN_POSE_CAPABILITY}),
                    ),
                ),
            ),
        ),
    )

    def __init__(self, default_options: PushOptions | None = None) -> None:
        super().__init__(default_options)

    def _plan(
        self,
        request: ResolvedActionRequest[PushGoal, PushOptions],
        context: PlanningContext,
    ) -> ActionPlan:
        goal = self.require_goal(request)
        motion_target = request.binding.endpoint(
            "primary", "motion"
        ).require_target(JointPositionTarget)
        control_part = motion_target.control_part
        joint_ids = list(motion_target.joint_ids)
        start_qpos = context.robot.qpos[:, joint_ids]
        target_poses = goal.contact_pose

        # Build planner states and generate controlled-joint motion using
        # request.motion_policy. Embed it into full robot DoF.
        result = self.motion_generator.generate(
            build_pose_plan_states(target_poses),
            options=request.motion_policy.to_motion_gen_options(
                start_qpos=start_qpos,
                control_part=control_part,
            ),
        )
        success, trajectory = to_full_robot_trajectory(
            result,
            base_qpos=context.robot.qpos,
            joint_ids=joint_ids,
            env_ids=context.env_ids,
            control_dt=request.motion_policy.control_dt,
        )
        return self.build_plan(
            request,
            context,
            success=success,
            trajectory=trajectory,
            expected_effects=StateDelta(),
        )

Follow these invariants:

  • Let the engine supply self.robot and self.motion_generator; use _on_bind() only for robot/device-dependent setup.
  • Keep slot and endpoint IDs semantic and robot-independent. Declare all-of capabilities, required typed commands, and disjointness constraints in the SkillBindingContract; do not infer resources from endpoint names.
  • Resolve an endpoint with request.binding.endpoint(slot_id, endpoint_id) and call require_target(ExpectedTarget) before using target-specific fields.
  • Import pure target-shaping, interpolation, pose-translation, and full-robot embedding helpers directly from atomic_actions.trajectory_ops; keep stateful planning inside MotionGenerator.
  • Call require_goal() before planning.
  • Implement _plan() rather than overriding the framework-owned public plan() method; the latter injects the latest dynamic obstacle poses into a copied planner policy.
  • Plan from context.robot.qpos, never an implicit live robot start state.
  • For joint-backed motion, return full-robot (B, N, robot.dof) motion as a tensor or TimedTrajectory with matching env_ids through build_plan().
  • Preserve row-local planner success. build_plan() normalizes the mask and replaces unsuccessful trajectory rows with the context's observed qpos.
  • Preserve backend timing/derivatives when available.
  • For a composite trajectory, pass an ordered segment_lengths mapping with the actual returned waypoint counts. Segments are inspection metadata, not independent planning or recovery boundaries.
  • Return failed_plan(request, context, message=...) for an expected soft planning failure.
  • Never mutate the context, step simulation, send commands, or claim a physical effect occurred.
  • Declare attachment/task changes with StateDelta; the execution runtime applies them only after verification.
  • Set scene_dependencies indirectly by using SceneEntityPose in the goal; build_plan() records them for dynamic invalidation.
  • Do not add dynamic-obstacle arguments to a skill. A SceneProvider declares collision_entity_ids; supported planners receive those entity poses through the framework-owned plan() entry point.

4. Emit generic runtime commands when needed

Use build_command_plan() when a skill targets a mobile base, whole-body controller, tool, or another non-joint transport. Build immutable endpoint commands; keep live controller and device handles in the transport:

target = request.binding.endpoint("primary", "tool").require_target(ToolTarget)
frames = tuple(
    RuntimeCommandFrame(
        commands=(EndpointCommand(target=target, payload=ToolPayload(value)),),
        active_mask=torch.ones(
            context.batch_size,
            dtype=torch.bool,
            device=context.robot.qpos.device,
        ),
        env_ids=context.env_ids,
        hold_duration=torch.full(
            (context.batch_size,),
            request.motion_policy.control_dt,
            device=context.robot.qpos.device,
        ),
    )
    for value in command_values
)
return self.build_command_plan(
    request,
    context,
    success=success,
    commands=TimedCommandSequence(frames=frames, env_ids=context.env_ids),
)

For a new transport kind:

  1. Define an immutable RuntimeEndpointTarget and RuntimeCommandPayload with the same stable transport_id; both must return independently owned snapshots. Payloads also expose batch_size and device. If target-specific addressing or safe hold depends on fields beyond the exact target type, transport_id, and target_id, override address_fingerprint to include those immutable fields; frames, replans, and revisions preserve it.
  2. If declarative robot profiles select it, define a ResourceEndpoint and an exact-type ResourceEndpointAdapter that returns EndpointResolution with the runtime target and physical claim metadata.
  3. Implement EndpointCommandTransport.send(), hold(), and cancel(), then register it in EndpointCommandRouter used as the ExecutionRunner command sink. The router validates payload types before dispatch.

The default command-plan feedback mode is timed and joint_trajectory is optional. Use joint-position feedback only when a matching full-robot joint_trajectory is supplied. Test target/payload snapshot ownership, frame batch/device consistency, routing, acknowledgement, hold, and cancel behavior.

5. Register and invoke

Register an instance by its class-level skill_id:

engine.register(Push())

Use the global registry only for discoverable third-party classes:

register_action(Push)

Construct a grounded invocation explicitly:

binding = engine.bind_control_parts(
    "push",
    {"primary": {"motion": "left_arm"}},
)
invocation = ActionInvocation(
    skill_id="push",
    goal=PushGoal(contact_pose),
    binding=binding,
    motion_policy=MotionPolicy(sample_count=60),
    recovery_policy=RecoveryPolicy(max_replans=2),
)
compiled = engine.compile((invocation,))

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.

engine.bind_control_parts() is the explicit direct-core path for joint-backed control parts. When a RobotSkillProfile is installed, prefer engine.skill_profile.resolve("push", selections).action_binding so capability, command, resource-claim, and custom-adapter validation remain declarative.

6. Export and document

Export the goal, options, and action from:

  1. embodichain/lab/sim/atomic_actions/primitives/__init__.py
  2. embodichain/lab/sim/atomic_actions/__init__.py

Add the stable skill ID, goal, binding slots/endpoints, and effect to docs/source/overview/sim/atomic_actions/builtin_actions.md. Update API docs for new public classes. Do not create a compatibility re-export module or a closed built-in-goal union.

Task Program handoff

An Atomic Skill is not automatically a Task Program Semantic Call. If the user also requests high-level declarative exposure, finish and test the Atomic Skill contract first, then invoke $add-semantic-call. Prefer a registered Semantic Call extension unless the concept is intentionally promoted to a stable built-in language primitive.

If the new skill requires new logical resources, endpoints, capabilities, or commands on a reusable robot, update them through $add-embodiment-component; do not encode task or profile metadata in the Atomic Action.

7. Test behavior

Add pure pytest tests under tests/sim/atomic_actions/. Cover:

  • descriptor skill_id, GoalType, and explicit binding contract;
  • invalid goal, wrong binding owner, and missing/extra endpoint rejection;
  • per-environment planning success/failure masks;
  • full-robot trajectory shape, env_ids, timing, and failed-row hold behavior;
  • generic command target/payload ownership, frame batch/device consistency, and optional joint_trajectory behavior when the skill emits command frames;
  • side-effect-free context handling;
  • masked StateDelta application for task effects;
  • SceneEntityPose replanning when the action accepts a dynamic goal;
  • collision-world revision replanning when the action uses a dynamic-world planner;
  • effect verification when the action declares a non-empty delta.

Run focused tests, format changed Python files with the pinned Black version, then use the pre-commit-check skill before committing.

Common mistakes

Mistake Required correction
Inherit another action Inherit AtomicAction directly; compose helpers.
Add one generic target with many optional fields Define a narrow action-owned goal.
Put hardware names in the goal Declare semantic slots/endpoints and resolve an engine-owned binding.
Put arm/hand control-part names in skill options Read typed runtime targets from bound endpoints.
Declare legacy role tuples on the action Declare a class-local SkillBindingContract.
Use role-specific binding accessors Use binding.endpoint(...).require_target(...).
Construct a binding from role dictionaries Use a bound skill profile, or engine.bind_control_parts() for the direct joint path.
Pass an arbitrary joint/link/TCP name to the direct path bind_control_parts() values must be keys in RobotCfg.control_parts; add an endpoint adapter for another resource kind.
Put hand qpos or named robot postures in skill options Register semantic commands on the concrete control-part profile.
Put planner/recovery knobs in skill options Move them to invocation policies.
Pass a motion generator to each action Pass it once to AtomicActionEngine; construct actions from default options only.
Read robot.get_qpos() inside plan() Use context.robot.qpos.
Return an arm-only tensor Embed into full robot DoF.
Collapse or manually mask batched planner success Return the row-local mask; let build_plan() hold unsuccessful rows.
Model one action as independently recoverable phases Return one trajectory with optional named segment metadata.
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; connect execution through ExecutionRunner.
Put live controller handles in targets or payloads Keep immutable addressing/data in values and own handles in the transport.
Force a non-joint endpoint into a fake trajectory Emit typed frames with build_command_plan() and install its transport.
Override public plan() Implement _plan() so scene binding cannot be bypassed.

Version History

  • 6f20b06 Current 2026-09-23 01:28
  • 12178b3 2026-08-20 11:22

Same Skill Collection

.agents/skills/add-embodiment-component/SKILL.md
.agents/skills/add-functor/SKILL.md
.agents/skills/add-robot/SKILL.md
.agents/skills/add-semantic-call/SKILL.md
.agents/skills/add-solver/SKILL.md
.agents/skills/add-task-env/SKILL.md
.agents/skills/add-task-program/SKILL.md
.agents/skills/add-test/SKILL.md
.agents/skills/benchmark/SKILL.md
.agents/skills/implement-performance/SKILL.md
.agents/skills/pr/SKILL.md
.agents/skills/pre-commit-check/SKILL.md
.agents/skills/project-dev-context/SKILL.md
.agents/skills/release/SKILL.md
.agents/skills/review-pr/SKILL.md
.agents/skills/update-api-docs/SKILL.md
.claude/skills/add-atomic-action/SKILL.md
.claude/skills/add-embodiment-component/SKILL.md
.claude/skills/add-functor/SKILL.md
.claude/skills/add-robot/SKILL.md
.claude/skills/add-semantic-call/SKILL.md
.claude/skills/add-solver/SKILL.md
.claude/skills/add-task-env/SKILL.md
.claude/skills/add-task-program/SKILL.md
.claude/skills/add-test/SKILL.md
.claude/skills/benchmark/SKILL.md
.claude/skills/pr/SKILL.md
.claude/skills/pre-commit-check/SKILL.md
.claude/skills/project-dev-context/SKILL.md
.claude/skills/release/SKILL.md
.claude/skills/review-pr/SKILL.md
.claude/skills/update-api-docs/SKILL.md
.claude/skills/implement-performance/SKILL.md

Metadata

Files
0
Version
6611467
Hash
526a0b72
Indexed
2026-08-20 11:22

inicio - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-10-23 08:58
浙ICP备14020137号-1