(robot-skill-profiles)=

# Robot skill profiles

```{currentmodule} embodichain.lab.task_program.semantics
```

A {class}`RobotSkillProfile` describes how robot-independent atomic-skill
requirements map onto one robot embodiment. Configure the robot's resources,
semantic commands, default choices, and policy presets once; task code can then
select skill-local participants instead of constructing an `ActionBinding` from
robot-specific control-part names.

The model is deliberately generic. It does not define global `arm` and `tool`
fields. Each atomic skill publishes its own participant slots and endpoint
requirements, while a robot resource may expose any endpoints appropriate to
that embodiment: manipulation motion and grasping, a mobile base, a torso, or a
whole-body controller.

## Configured ownership

Componentized deployments build the in-memory profile from three explicit
owners:

- `configs/components/embodiments/<embodiment>.yaml` owns the simulation
  robot, sensor suite, and optional `skill_profile` resources, endpoints,
  command presets, and embodiment-specific services;
- task-local `task_program/integration.yaml` owns semantic defaults, action
  options, effect-monitor mappings, and task-specific services; and
- `configs/components/execution_policies/<policy>.yaml` owns motion, tracking,
  recovery, runner, and effect-assurance settings.

The runnable `task.<embodiment>.yaml` selects these components. Composition
checks their embodiment contracts and produces the exact
{class}`RobotSkillProfile` and {class}`SkillPolicyPreset` values described
below. The reusable physical `env.yaml` owns none of this Task Program metadata,
so changing environments does not copy the robot profile and changing a
compatible embodiment does not rewrite the source program.

## Contracts on the two sides

An atomic action owns a
{class}`~embodichain.lab.sim.atomic_actions.SkillBindingContract`:

- a {class}`~embodichain.lab.sim.atomic_actions.SkillResourceSlot` names each
  skill-local participant, such as `primary`, `source`, or `destination`;
- a {class}`~embodichain.lab.sim.atomic_actions.SkillEndpointRequirement`
  declares the all-of capabilities and typed semantic commands needed from that
  participant;
- {class}`~embodichain.lab.sim.atomic_actions.DisjointSlotEndpoints` declares
  endpoint views that must not share physical channels within one participant;
  coupled whole-body views may overlap when the skill does not declare this
  constraint; and
- {class}`~embodichain.lab.sim.atomic_actions.DisjointResourceSlots` requires
  multi-participant skills to select physically disjoint resources.

The robot side supplies {class}`RobotResource` values. A resource exposes named
{class}`ResourceEndpoint` values and may contain other resources through
`members`. Members form a directed acyclic graph and describe the physical
claim; endpoint capabilities are always explicit and are never inherited or
inferred from names. {class}`ControlPartEndpoint` is the built-in joint-backed
endpoint type, not the resource schema itself.

```text
skill contract                         robot profile

slot primary                          resource left_participant
+-- endpoint motion  <--------------> +-- endpoint motion -> left_arm
`-- endpoint grasp   <--------------> `-- endpoint grasp  -> left_hand
      capabilities + commands                 + members/physical claim
```

Binding the profile to an engine resolves each endpoint through a registered
{class}`ResourceEndpointAdapter` and validates physical claims, known
solver-backed kinematics capabilities, command types and dimensions, complete
defaults, policy presets, and installed skill contracts. The resulting
{class}`BoundRobotSkillProfile` exposes only installed, agent-visible skills
with at least one valid resource assignment.

Endpoint and resource declarations are snapshotted when owned by a resource,
profile, or resolved binding. Custom endpoint types whose payloads cannot be
deep-copied must override {meth}`ResourceEndpoint.snapshot` and return a new
value of the same exact type.

## Configure a manipulation participant

The following profile groups two physical leaves into one participant. The
`motion` and `grasp` endpoint names come from the built-in manipulation
contracts; they are local protocol names, not global robot-resource categories.

```python
import torch

from embodichain.lab.sim.atomic_actions import (
    BATCH_INVERSE_KINEMATICS_CAPABILITY,
    CARTESIAN_POSE_CAPABILITY,
    FORWARD_KINEMATICS_CAPABILITY,
    GRASP_CAPABILITY,
    ControlPartCommandProfile,
    ExecutionRunnerCfg,
    MotionPolicy,
    PickUpOptions,
)
from embodichain.lab.task_program.semantics import (
    ControlPartEndpoint,
    EffectAssurance,
    ResourceBinding,
    RobotResource,
    RobotSkillProfile,
    SkillPolicyPreset,
)

left_motion_capabilities = frozenset(
    {
        CARTESIAN_POSE_CAPABILITY,
        FORWARD_KINEMATICS_CAPABILITY,
        BATCH_INVERSE_KINEMATICS_CAPABILITY,
    }
)

profile = RobotSkillProfile(
    profile_id="example_robot",
    resources={
        # Physical leaves own disjoint robot joints.
        "left_arm_leaf": RobotResource(
            resource_id="left_arm_leaf",
            endpoints={"control": ControlPartEndpoint("left_arm")},
        ),
        "left_hand_leaf": RobotResource(
            resource_id="left_hand_leaf",
            endpoints={"control": ControlPartEndpoint("left_hand")},
        ),
        # A skill selects this participant as one indivisible resource.
        "left_participant": RobotResource(
            resource_id="left_participant",
            endpoints={
                "motion": ControlPartEndpoint(
                    "left_arm",
                    capabilities=left_motion_capabilities,
                ),
                "grasp": ControlPartEndpoint(
                    "left_hand",
                    capabilities=frozenset({GRASP_CAPABILITY}),
                ),
            },
            members=("left_arm_leaf", "left_hand_leaf"),
        ),
    },
    command_profiles={
        "left_hand": ControlPartCommandProfile.joint_positions(
            open=torch.tensor([0.04, 0.04]),
            grasp=torch.tensor([0.0, 0.0]),
        ),
    },
    defaults={
        "pick_up": ResourceBinding(
            resources={"primary": "left_participant"},
        ),
    },
    presets={
        "default": SkillPolicyPreset(
            preset_id="default",
            action_option_templates={"pick": PickUpOptions()},
            effect_assurance=EffectAssurance.PROJECTED,
            motion_policy=MotionPolicy(strategy="ik_interp"),
            runner_cfg=ExecutionRunnerCfg(command_timeout=2.0),
        ),
    },
    default_preset="default",
)
```

Set `SkillPolicyPreset.required_planner` only when a preset depends on one
planner backend, typically because it carries backend-specific typed planning
options. Profile binding checks that requirement against the engine's configured
backend and fails early on a mismatch. Leave it as `None` for portable presets.

A {class}`SkillPolicyPreset` owns independently snapshotted motion, tracking,
recovery, workflow-recovery, runner, action-option, and effect contracts. Task
Program integration selects a preset in this order: an integration-wide
`runtime_preset`, the profile's `skill_presets[atomic_skill_id]`, then
`default_preset`. At execution time, the environment adapter uses the selected
preset's transport timeouts, minimum cycle time, and completion-hold behavior.
Standard immutable registrations do not accept an unrelated task-side runner
override.

## Configure semantic action behavior with the preset

`SkillPolicyPreset.action_option_templates` is the required typed behavior
table for semantic calls that can select the preset. Each key is the exact
semantic call ID (`pick`, `place`, `hand_over`, or a registered call ID), and
each value is the target action's exact frozen `ActionOptions` dataclass.
Static linking rejects a missing entry or a value of the wrong exact type
before simulation starts.

The preset owns independent snapshots of every template. Semantic lowering may
replace only compiler-owned dynamic values—for example Pick's downstream target
poses—while reusable distances, directions, waypoint counts, and other behavior
remain configuration. A registered semantic lowerer builds the goal but cannot
return replacement options. Planner choice, sample count, tracking, recovery,
runner policy, and effect monitors remain in their dedicated preset fields.

## Select semantic grounding providers

Some semantic calls require embodiment knowledge that does not belong in the
agent-facing call or the atomic action. The built-in semantic HandOver is the
canonical example: the robot profile selects a named provider that supplies a
safe middle and default final object target for that embodiment. An explicit
semantic `HandOver.final_target` overrides the provider's final target.

```python
profile = RobotSkillProfile(
    profile_id="dual_arm_robot",
    resources=dual_arm_resources,
    command_profiles=hand_command_profiles,
    defaults=dual_arm_skill_defaults,
    presets={"default": default_preset},
    default_preset="default",
    grounding_providers={"hand_over": "center_workspace_handover"},
)

```

`grounding_providers` maps a **semantic call ID** to a provider ID. The selected
ID must match one provider declared by the immutable Task Program simulation
registration; missing or unknown providers fail during program analysis, before
observation, planning, or controller work. The provider is executable
integration code and therefore is owned by that registration rather than stored
inside the declarative profile.

Every `ControlPartEndpoint.control_part` must be a key in
`robot.control_parts`. A composite endpoint may reuse a member's control part,
but all joints controlled directly by the composite must already be covered by
its members. Two physical leaf resources may not claim the same joint; model a
shared physical part once and reference that leaf from multiple composites.

`command_profiles` are generic IDs selected by endpoint adapters; the built-in
control-part adapter defaults the ID to its `control_part`, and the engine
installs those profiles into the current action core automatically.
One-dimensional joint-position commands are broadcast across environments.
Their last dimension must equal the resolved endpoint's degree of freedom. Use
invocation-level command overrides for object- or environment-specific values.

## Safe preset and dynamic collision worlds

When the authoritative scene registry declares dynamic collision entities and
`safe` is reachable through the integration-wide, per-skill, or
profile-default preset selection, semantic integration validates that path
conservatively during binding. The `safe` preset must use `motion_gen`, and the
active motion generator must explicitly support dynamic collision worlds;
otherwise binding fails before provider observation, planning, or command
emission.

A linked call receives an effective immutable preset snapshot with
`DynamicCollisionMode.REQUIRED`; the source profile preset is not mutated.
Other presets, and scenes without dynamic collision entities, retain their
configured collision mode.

## Select effect assurance and monitors with the preset

A {class}`SkillPolicyPreset` must select one {class}`EffectAssurance`. There is
no default:

- {attr}`EffectAssurance.VERIFIED` advances semantic state from measured
  evidence. `effect_monitors` maps semantic call IDs to versioned
  {class}`EffectMonitorRef` values. Every curated Pick, Place, or HandOver call
  used by the preset requires a monitor.
- {attr}`EffectAssurance.PROJECTED` advances the action plan's expected symbolic
  effect after command completion and forbids monitor entries. It is intended
  for trajectory demonstrations, not physical task acceptance.

Monitor parameters are bounded declarative values; executable objects, tensors,
cyclic containers, and non-finite numbers are rejected. A manifest also rejects
entries whose semantic ID is absent from its call catalog, and Task Program
validates the exact monitor ID/revision before live grounding.

Task Program creates a fresh monitor for every verified grounded call. Pick
expects one attached destination relation, place one detached source relation,
and handover both source-detached and destination-attached relations in the
same observation. The monitor compares fresh backend evidence with owned
object-to-endpoint baselines; it never treats the planned `StateDelta` or
current `TaskState` as proof that the physical effect occurred. Invalid or
missing per-environment evidence remains unresolved. Consecutive-sample state
survives request-mask shrinkage within one attempt and resets when recovery
installs a new attempt.

```{note}
The monitor contract is backend-neutral. Simulation, hardware perception, or
controller feedback supplies typed pose-relation evidence. The Task Program
adapter that connects that evidence to `ExecutionRunner` is separate from the
profile and monitor configuration.
```

## Bind, discover, and resolve

Construct the atomic engine from the profile's lowered control-part commands,
install any custom actions, and then bind the profile in the semantic layer:

```python
from embodichain.lab.sim.atomic_actions import AtomicActionEngine

engine = AtomicActionEngine(
    motion_generator,
    control_profiles=profile.action_control_profiles(),
)
bound = profile.bind(engine)

# This is the embodiment-filtered semantic catalog, not every installed action.
assert "pick_up" in bound.skills

resolved = bound.resolve("pick_up")
assert resolved.resource_ids == {"primary": "left_participant"}
binding = resolved.action_binding
preset = bound.preset(skill_id="pick_up")
```

The atomic engine never imports or stores `RobotSkillProfile`. The returned
{class}`BoundRobotSkillProfile` is owned by the semantic integration and checks
the engine's catalog revision whenever it performs discovery or resolution.

{meth}`BoundRobotSkillProfile.resolve` returns a {class}`ResolvedSkillBinding`
containing the selected logical resources, their adapter-resolved endpoints,
their combined {class}`ResourceClaim`, and an engine-owned generic
{class}`~embodichain.lab.sim.atomic_actions.ActionBinding`. A
Task Program lowering uses that binding and the selected preset when constructing
an invocation; profile resolution does not plan or execute the action itself.

If exactly one assignment is valid, resolution selects it. If several remain,
the caller must provide enough skill-local selections or the profile must define
a complete per-skill default:

```python
left = bound.resolve("pick_up", selections={"primary": "left_participant"})
candidates = bound.candidates("pick_up")
```

Incomplete defaults are rejected when the profile is bound. Without an
unambiguous choice, resolution raises {class}`AmbiguousSkillBindingError` rather
than selecting a resource by declaration order. An unsupported selection raises
{class}`UnsupportedSkillError` with endpoint, capability, command, or claim
rejection details.

`engine.actions` remains the direct-core implementation registry.
`engine.skills` is the installed semantic catalog before embodiment filtering,
and `bound.skills` is the profile-supported catalog. Registering or replacing an
action invalidates the bound profile; bind it again before discovery or
resolution.

{attr}`BoundRobotSkillProfile.source_profile` identifies the exact immutable
profile used for the binding. The bound view also snapshots the engine's
monotonic Atomic Skill catalog revision. A later agent-visible action
registration or replacement makes discovery, preset selection, and resolution
fail until the profile and semantic integration are rebound; an equal public
descriptor does not make a different implementation owner safe to reuse.

## Extend the graph beyond manipulation

Resource and capability identifiers are open strings. A joint-driven mobile
robot can model a base and a whole-body controller without changing the profile
schema:

```python
base = RobotResource(
    resource_id="base",
    endpoints={
        "motion": ControlPartEndpoint(
            "base",
            capabilities=frozenset({"motion.planar_pose"}),
        )
    },
)
torso = RobotResource(
    resource_id="torso",
    endpoints={"motion": ControlPartEndpoint("torso")},
)
whole_body = RobotResource(
    resource_id="whole_body",
    endpoints={
        "motion": ControlPartEndpoint(
            "full_body",
            capabilities=frozenset({"motion.whole_body"}),
        )
    },
    members=("base", "torso", "left_arm_leaf", "right_arm_leaf"),
)
```

Here `base`, `torso`, and `full_body` must be real, non-empty robot control
parts, and the `full_body` joint set must be covered by the listed members. A
future locomotion or whole-body skill can require the corresponding endpoint
and capability in its own binding contract. Existing built-in actions do not
consume these example capabilities.

Non-joint controllers add one endpoint declaration type and one adapter. The
adapter returns {class}`EndpointResolution` with a typed immutable
{class}`~embodichain.lab.sim.atomic_actions.RuntimeEndpointTarget`, an optional
command-profile key, joint IDs when applicable, and adapter-defined claim
tokens. The generic graph, matching, command, default, and conflict code does
not change. For example, a twist controller can return a target addressed to a
`base_velocity` transport and `claim_tokens={"controller:base"}` with no joint
IDs. Exclusive endpoints must provide joint IDs or claim tokens; a read-only or
otherwise shareable virtual endpoint must opt into `exclusive=False`
explicitly.

Adapters are registered by exact endpoint type. The built-in
{class}`ControlPartEndpointAdapter` cannot be overridden; define a distinct
endpoint subtype and adapter when controller semantics differ. An adapter may
set `requires_command_profile=True` when a missing generic command-profile ID
must make profile binding fail immediately.

On the standard Task Program path, endpoint adapters and their ordered Gym
transports are declared by `SimulationTaskProgramRegistration`. Adapter
classes publish their endpoint type, runtime target types, transport IDs, and
versioned tracking/evidence routes; encoder classes publish their transport ID
and exact target/payload types. Registration rejects missing, unused,
duplicate, or conflicting declarations, and live profile binding checks the
resolved routes against the fingerprinted declarations. Stateful adapters,
transports, grounding providers, and safety factories must be frozen dataclasses
whose configuration is recursively immutable.

The standard factory currently accepts built-in closed-loop routes only for
`ControlPartEndpoint`. Custom endpoint adapters must declare empty tracking and
effect-evidence routes and therefore support timed/open-loop completion. Custom
mobile or whole-body closed-loop tracking needs a registration-owned provider
factory; it cannot be supplied later as a task-side callback.

A resolved action binding is keyed only by the skill-local
`(slot_id, endpoint_id)` pair. A reusable non-joint capability supplies a
matching {class}`~embodichain.lab.sim.atomic_actions.RuntimeCommandPayload`, a
shared atomic skill that emits
{class}`~embodichain.lab.sim.atomic_actions.RuntimeCommandFrame` values, and an
{class}`~embodichain.lab.sim.atomic_actions.EndpointCommandTransport` registered
with {class}`~embodichain.lab.sim.atomic_actions.EndpointCommandRouter`. The
core binding, session, runner, and router do not need controller-specific
changes. Once that shared capability exists, new tasks and robot variants reuse
it through profile and task configuration rather than task-specific motion
code.

```{important}
`ResourceClaim` combines leaf IDs, concrete joint IDs, and adapter claim tokens.
It and explicit disjoint constraints detect physical overlap for binding and
future scheduling work. They do not enable parallel action execution. The
runtime does not merge concurrent endpoint-command streams. Joint-backed plans
may retain a full-robot trajectory for feedback and offline compilation, but
runtime dispatch is scoped to the endpoints in each command frame.
```

See {doc}`../sim/atomic_actions/index` for the direct atomic-action core,
{doc}`index` for declarative semantic contracts,
{doc}`../task_program/index` for semantic task execution, and
{doc}`scene_registry` for canonical scene identity and snapshots.
