# Scene Actor Affordances — let scene entity affordances drive AUT body sensors

**Status:** PARTIAL — Stages 1+2 shipped 2026-04-30 (PR #213). Stages 3-5 are 1.1+ work (orchestrator prompt update, designer template hint, validation experiment).
**Scope:** ~110 LOC across `embodiment/sem.py`, `embodiment/tool_bridge.py`, `simulation/tools.py`, `simulation/orchestrator.py`, plus prompt updates. Stages 1+2 (target_effect field + OrchestratorActorTool) shipped under v1_refinement.md execution-order item #2 — absorbed the world-physics-engine job from the adversarial persona prompt so persona could be deprecated in PR #217 without breaking narrative→SEM coupling.
**Depends on:** Imagination trigger (shipped 0.7), entity ownership (shipped 0.7), AUT embodiment in sim (E0, shipped 0.6), Cradle (B4, shipped 0.8 — provides three-layer sensation model this extends).
**Diagnostic for:** [deferred/agent_backed_entities.md](deferred/agent_backed_entities.md). If this closes the gap, the bigger plan stays deferred.
**Gates:** None.

---

## Front-gate scope pressure (retroactive)

Added 2026-05-27 per CLAUDE.md Principle 3.

**Question:** does this need to be its own mechanism, or can it ride on existing infrastructure?

**Existing infrastructure surveyed:**

| Candidate | Why insufficient (or sufficient) |
|---|---|
| `DamageComponentTool` ([simulation/tools.py](../../src/maxim/simulation/tools.py)) | Already exists for orchestrator-driven damage but bypasses the affordance spec — provenance is "orchestrator wrote thermal=0.8," not "dragon's breathe_fire affordance fired." Loses the bio-attribution chain |
| `SetEntitySensorTool` | Same shortcoming as DamageComponentTool — direct sensor write, no affordance invocation |
| `AffordanceSchema.self_effect` ([embodiment/sem.py](../../src/maxim/embodiment/sem.py)) | **Already the right model** — handles self-targeted sensor deltas with range-clamping, sub-sensor parsing, sim_sensor logging. Stages 1+2 extracted its application code into `_apply_sensor_deltas` and reused it for `target_effect`. Riding on existing infrastructure |
| Adversarial persona prompt (deprecated by PR #217) | Did "world physics engine" role via LLM narration. Lost when persona deprecated — `OrchestratorActorTool` absorbs the dispatch surface so persona could be removed without breaking narrative→SEM coupling |
| Full agent-backing for scene entities ([deferred/agent_backed_entities.md](deferred/agent_backed_entities.md)) | Larger, deferred. This plan is the **diagnostic** for whether the bigger investment is needed |

**Verdict:** could-ride-on-existing. The `target_effect` field is a strict **extension** of the existing `self_effect` semantics (same dataclass, same write-back helper). `OrchestratorActorTool` adds one orchestrator-only Tool subclass; no new bus, bridge, registry.

**Specific reason:** the substrate already has the contract surface (`AffordanceSchema` + `_apply_sensor_deltas`) and the right ownership model (`EntityMap` self/scene split). Adding a target-side write-back rides on the self-side machinery; new code is one dataclass field + one helper-call duplication + one tool class.

## Vision

When the orchestrator narrates "the dragon breathes fire on you," that narration should produce real SEM mechanics on the AUT body — sensor changes → `evaluate_failures()` → PainBus → NAc — without requiring the dragon to be a full Maxim agent. The infrastructure to make this work is 90% already shipped via the imagination pipeline; the missing piece is the path from "scene entity has a `breathe_fire` affordance" to "calling that affordance writes to the AUT's thermal sensor."

This is a **diagnostic** for the larger agent-backed-entities question ([deferred/agent_backed_entities.md](deferred/agent_backed_entities.md)). If embodied-but-scripted hostile entities close the lived-experience gap ("sims feel one-way, narrator struggles with dragon embodiment"), then full agent-backing can wait until the Minecraft demo proves the runtime value. If they don't, we've ruled out the simpler fix and earned the bigger investment.

---

## Current state

**What imagination ships today** (verified in source):

- [`imagination/trigger.py:533-536`](../../src/maxim/imagination/trigger.py#L533-L536), `_ensure_entity_live()`: "registers it as a **scene entity** (NO callable affordance tools). The agent interacts with scene entities using its own body's tools, not the entity's affordances."
- [`imagination/trigger.py:769`](../../src/maxim/imagination/trigger.py#L769): `# 10. Register as scene entity in EntityMap (observe-only, no tools).`
- [`embodiment/sem.py`](../../src/maxim/embodiment/sem.py) — `AffordanceSchema` includes `self_effect: dict[str, float]` (sensor deltas applied to the executor's body on voluntary use).
- [`embodiment/tool_bridge.py:213-272`](../../src/maxim/embodiment/tool_bridge.py#L213-L272) — `self_effect` write-back logic — handles entity-level sensors and qualified sub-sensors (`arms.thermal`), range clamping, and `sim_sensor()` logging.

**What does not exist:**
- No `target_effect` field on `AffordanceSchema`. Affordances cannot declare "writes to whoever I'm acting on."
- No tool that lets the orchestrator invoke a scene entity's affordance with the AUT as target.
- The orchestrator's existing `DamageComponentTool` and `SetEntitySensorTool` write damage/sensor values directly, bypassing the entity's own affordance spec — so provenance is "orchestrator wrote thermal=0.8" rather than "dragon's breathe_fire affordance fired."

**The lived-experience symptom:** orchestrator narrates "the dragon breathes fire" but the AUT's thermal sensor doesn't change unless the orchestrator separately remembers to call `DamageComponentTool`. The dragon's `breathe_fire` spec generated by imagination is declarative-only.

---

## Design

### A. `target_effect` field on `AffordanceSchema`

```python
# embodiment/sem.py — AffordanceSchema additions
@dataclass(frozen=True, slots=True)
class AffordanceSchema:
    params: dict[str, type | tuple[type, Any]] = field(default_factory=dict)
    description: str = ""
    timeout: float = 30.0
    requires: dict[str, float] = field(default_factory=dict)
    self_effect: dict[str, float] = field(default_factory=dict)
    target_effect: dict[str, float] = field(default_factory=dict)  # NEW
```

YAML semantics:

```yaml
modulators:
  fire_breath:
    affordances:
      breathe_fire:
        description: "Exhale fire on target"
        params:
          target: str
        self_effect:
          stamina: -0.2          # dragon's own stamina drops
        target_effect:
          arms.thermal: 0.8      # target's arms get hot
          torso.thermal: 0.6
```

**Semantics:** when an affordance fires *with a `target` parameter*, `target_effect` deltas are applied to the target's body sensors. Same range-clamping, sub-sensor parsing, and `sim_sensor()` logging as `self_effect`. If no target is provided, `target_effect` is silently skipped (the affordance still works for self-targeted use cases like meditation).

**Refactor:** extract the existing `self_effect` application code from `tool_bridge.py:217-272` into a helper `_apply_sensor_deltas(body, deltas, sensor_log_path)`. Call it twice — once for `self_effect` on `self._embodiment.root`, once for `target_effect` on the resolved target body. This keeps the range-clamping, sub-sensor parsing, and `sim_sensor` logging in one place rather than duplicated.

### B. `OrchestratorActorTool` — invoke scene affordances against the AUT

A new orchestrator-only tool in [`simulation/tools.py`](../../src/maxim/simulation/tools.py) alongside `DamageComponentTool`:

```python
class OrchestratorActorTool(Tool):
    """Have a scene entity perform one of its affordances against a target.

    The orchestrator narrates 'the dragon breathes fire on you' AND calls
    actor(entity='dragon', affordance='breathe_fire', target='player').
    The dragon's affordance spec drives the SEM mechanics — sensor changes,
    failure-mode evaluation, PainBus, NAc — instead of the orchestrator
    writing raw damage values.
    """
    name = "actor"
    input_schema = {
        "entity": str,            # name in EntityMap (scene side)
        "affordance": str,        # affordance name on the entity
        "target": (str, "player"),  # default: AUT
        "params": (dict, {}),     # additional affordance kwargs
    }
```

Execution path:
1. Resolve `entity` in EntityMap (must be `register_scene` — not `register_self`).
2. Resolve `target`: `"player"` / `"aut"` / `"self"` → AUT body root; otherwise look up by entity name.
3. Look up the affordance schema on the entity's modulators.
4. Construct an ephemeral `ModulatorAffordanceTool` bound to the *entity*'s embodiment but with the *target* body for `target_effect` write-back.
5. Invoke `.execute(target=<target_name>, **params)`.
6. Affordance's `requires` gate fires naturally (dragon must have integrity > threshold to breathe fire).
7. `target_effect` writes to AUT body → AUT's existing `evaluate_failures()` runs → PainBus → NAc.
8. Return result with both the entity's state delta and the target's state delta for the orchestrator to narrate.

**Provenance preservation:** the resulting `ToolOutput.side_effects["embodiment_failures"]` carries `entity="dragon"` and `affordance="breathe_fire"`, so when NAc records the AUT's pain, the causal link is to "dragon's breathe_fire" — not "orchestrator wrote 0.8 to thermal." This matters for the substrate-encoded concept transfer path (already shipped) — the AUT learns "dragon → fire → thermal pain" with the right concept embeddings.

### C. Orchestrator prompt update

Add to the orchestrator system prompt: a brief section explaining when to use `actor` vs `damage_component` / `set_entity_sensor`. The rule is simple: if the scene entity has an affordance that mechanically describes what's happening, use `actor`; if you need to write a raw value not covered by any affordance (e.g., environmental temperature drift), use the existing tools.

The prompt update is the load-bearing-via-prompt piece — without it, local LLMs default to `damage_component` because it's familiar. Per `feedback_orch_prompt_strength.md`, this needs forceful framing.

---

## Stages

### Stage 1 — `target_effect` field + sensor delta helper (~40 LOC)

- Add `target_effect: dict[str, float]` to `AffordanceSchema` ([sem.py](../../src/maxim/embodiment/sem.py)).
- Update `embodiment/spec.py` SpecModulator parsing to read the field from YAML.
- Refactor [`tool_bridge.py:217-272`](../../src/maxim/embodiment/tool_bridge.py#L217-L272) `self_effect` logic into `_apply_sensor_deltas(body, deltas, log_path)`.
- Add `target_effect` application path that resolves target body from a `target` kwarg, calls the helper.
- No call site exists yet (Stage 2 wires it).

**Exit:** unit test confirms `target_effect` writes to the target body's sensors with correct clamping. Existing `self_effect` tests still pass (refactor regression).

### Stage 2 — `OrchestratorActorTool` (~50 LOC)

- New `OrchestratorActorTool` in `simulation/tools.py`.
- Registered in `SimToolRegistry` alongside `SendMessageTool`, `ObserveActionsTool`, `DamageComponentTool`.
- Resolves entity from EntityMap scene side; rejects with clear error if entity is on self side ("AUT cannot operate scene actors against itself").
- Resolves target: `"player"`/`"aut"`/`"self"` → AUT root.
- Constructs an ephemeral affordance executor bound to the entity but writing `target_effect` to the target body.

**Exit:** integration test — sim with imagined dragon, orchestrator calls `actor(entity="dragon", affordance="breathe_fire", target="player")`, AUT's thermal sensor changes, `evaluate_failures` fires, `PainSignal` lands on NAc with `entity="dragon"` provenance.

### Stage 3 — Orchestrator prompt update (~10 LOC + prompt text)

- Add the actor-vs-damage_component decision rule to the orchestrator system prompt.
- Forceful framing per `feedback_orch_prompt_strength.md`: when narrating an entity acting on the AUT, **MUST** call `actor` if the entity has the affordance.
- Add an example block showing the pattern.

**Exit:** Claude-Sonnet-driven sim with imagined dragon naturally produces `actor` calls without prompt-engineering each turn. Local 14B may need extra reinforcement — log success rate.

### Stage 4 — Designer template hint (~10 LOC)

- Update `imagination/designer.py` prompt to suggest `target_effect` declarations on aggressive affordances (`attack`, `charge`, `breathe_fire`, `claw`, `bite`).
- Validation in `_validate_design()` warns if an entity has obvious-attack affordance names without `target_effect`.

**Exit:** newly imagined hostile entities reliably ship with `target_effect` populated.

### Stage 5 — Validation + lived-experience check

- Run `maxim --sim "fight a dragon" --embodiment bodies/humanoid` end-to-end.
- Read JSONL log: confirm `actor` calls fire, AUT thermal sensor changes, pain signal published, NAc records causal link with dragon provenance.
- **Subjective check:** does the sim feel less narrated and more embodied? Capture the user's read.
- If yes: agent-backed entities plan stays deferred.
- If no: surface what's still missing — likely entity agency (dragon decides when to attack), which is what cognition tier addresses.

**Multi-agent attribution edge case:** when scene entity acts on a multi-agent setup (e.g., NPC in a co-op sim), which agent gets the pain attribution? Wave 1's [v1_refinement.md P4](v1_refinement.md) shipped per-agent attribution at the bio-pipeline layer; this stage should verify `OrchestratorActorTool` plumbs the resolved target's `agent_id` through `record_outcome` correctly.

**Exit:** decision recorded in `docs/experiments/12_scene_actor_affordances.md` (or similar): does this close the gap, or do we need agent-backing after all?

---

## Load-bearing invariants

1. **`target_effect` is silent if no target is provided.** Affordances without a target argument fall back to `self_effect`-only behavior. No exception, no warning at call time. This preserves backward compatibility with all existing affordances that have no `target_effect` field.

2. **AUT cannot call `OrchestratorActorTool`.** Registered to `SimToolRegistry`, never to AUT's tool registry. Verified by tool-name allowlist check at AUT executor construction. Prevents the player from operating the dragon's body.

3. **`OrchestratorActorTool` rejects self-side entities.** If `entity` resolves to a self-owned entity (the AUT's own body), tool returns an error. Prevents the orchestrator from puppeting the AUT's body via the scene-actor path. Self-body operations go through `damage_component` / `set_entity_sensor` (which already exist for orchestrator use).

4. **Affordance `requires` gating fires naturally.** If a dragon's `breathe_fire` requires `integrity > 0.3` and the dragon is at 0.2, the affordance returns a failure result with reason. The orchestrator should narrate the failure ("the dragon tries to breathe fire but only chokes"). This is the SEM-faithful failure path — same as today's affordance failure semantics for the AUT.

5. **Target resolution is explicit, not magic.** `target="player"` / `"aut"` / `"self"` resolve to AUT root. Anything else must be an entity name in EntityMap. Unresolvable target → tool failure. Prevents silent no-op writes.

6. **Provenance carries through to NAc.** The resulting pain signal's context dict includes `source_entity` and `source_affordance` so NAc's causal learning attributes correctly. `feedback_nac_similarity_directional.md` invariant still applies — denominator is `len(ctx1)`.

7. **Multi-agent target attribution.** When `target` resolves to a non-AUT agent body in a multi-agent sim, `record_outcome` MUST be called with that agent's `agent_id`, not a default. This composes with Wave 1 P4 — verify in Stage 5 testing.

---

## Risks and tradeoffs

**R1. Orchestrator LLM doesn't reliably call `actor`.** Same risk as today's `DamageComponentTool` underuse. Mitigation: forceful prompt framing + `actor` listed before `damage_component` in the tool list (priority order matters for local LLMs). Stage 3 verifies on Claude Sonnet first; local-LLM tuning is iterative.

**R2. Imagined entities don't ship `target_effect`.** Designer LLM may omit it. Stage 4 mitigation: prompt hint + validation warning. Fallback: at affordance-call time, if `target_effect` is empty but the affordance name suggests an attack (heuristic match on `attack`, `breathe`, `claw`, `bite`, `charge`), apply a small default thermal/pressure delta. Better: improve the designer prompt over time so the heuristic isn't load-bearing.

**R3. Non-attack affordances with targets may write unintended sensor deltas.** E.g., `give_item(target=player)` shouldn't damage the player. Mitigation: `target_effect` is opt-in per affordance — most affordances won't declare it. Designer prompt should reserve it for affordances that mechanically affect the target's body.

**R4. The diagnostic might succeed *just enough* to feel like progress without actually closing the agency gap.** A scripted-but-embodied dragon still has no learning, no novel behavior, no decision-making. The "sim feels less boring" check is subjective. Mitigation: Stage 5 is explicit about capturing the user's read. If it's "better but still narrated," that's data — escalate to cognition tier.

**R5. SEM-faithful provenance might not be load-bearing yet.** NAc learns from "thermal pain after dragon encounter" today via the substrate path's contextual binding regardless of whether the source attribute is `dragon` or `orchestrator_damage`. The provenance benefit is real but might not produce measurable learning differences until concept transfer is exercised across many encounters. Mitigation: don't oversell the provenance angle in the research framing — `target_effect` is primarily about closing the lived-experience gap.

---

## Why 1.1 not 1.0

The cradle (B4 in [v1_refinement.md](v1_refinement.md)) already validates the cross-session learning claim with a simpler scene model — single AUT, narrative-driven sensor writes via reflexes. This is enrichment for richer adversarial scenes, not gating for the cross-session story. Slotting it into the 1.1 track keeps 1.0 focused on the interface freeze.

---

## Cross-references

- [deferred/agent_backed_entities.md](deferred/agent_backed_entities.md) — the bigger plan this diagnoses against. Revives if Stage 5 fails to close the gap.
- [archive/cradle_sensorimotor_development.md](archive/cradle_sensorimotor_development.md) — shipped 2026-04-25 (PR #200, [exp 11](../experiments/11_cradle_sensorimotor_poc.md)). The `self_effect` field was added there; `target_effect` is the natural sister.
- [v1_refinement.md](v1_refinement.md) Section 8 (1.1 track) — index entry.
- [v1_refinement.md](v1_refinement.md) P4 — multi-agent attribution work shipped via PR #202; this plan composes with it for Stage 5's edge case.
- [minecraft_benchmark.md](minecraft_benchmark.md) — relevant if Minecraft mobs use the same scripted-actor pattern (zombies, creepers, skeletons).
- `feedback_orch_prompt_strength.md` — local LLMs need forceful prompt framing for new tools.
- `feedback_imagined_entity_affordance_encoding_gap.md` — related: imagined entities skip substrate encoding for affordances. Stage 4 of this plan touches the designer prompt; not the same fix but adjacent.
