Skip to content

1.6 Captured Underlays

Julio Toboso edited this page Oct 26, 2026 · 1 revision

The Underlay is captured

An Action's Underlay is selected when its Tag is applied.

For Ari:

Elf(ari)
Paladin(ari)

Paladin captures the Attack visible after Elf.

If another Tag later adds yet another Attack, Paladin does not suddenly acquire a different Underlay.

Its relationship to Elf remains stable.

That lets us ask for different stages of Ari's composition:

assert Elf[ari].Attack() == \
    "With elven grace, Attack!"

assert ari.Attack() == \
    "With elven grace, Attack! For the oath!"

ari.Attack() asks for the current Overlay.

Elf[ari].Attack() asks for the composition at Elf's layer.

Those are different coordinates in the same Agent.


Calling through the Agent is different

There are two very different ways to reach another Action.

underlay()

means:

use the contribution captured underneath this Action.

agent.Attack()

means:

resolve the Action visible on the Agent now.

For example:

class Combatant(Tag):
    @Action
    def Combat(agent):
        return agent.Attack()

Combat does not remember which Attack existed when Combatant was applied.

Every time Combat runs, it asks Ari's current Overlay for Attack.

So:

underlay()       → captured contribution underneath me

agent.Attack()   → contribution visible on the Agent now

This distinction is essential.


⚙️ Rules of Action Underlay

1. @Underlay selects by contribution name

For an Action marked with @Underlay, TOP selects the contribution visible under the same name immediately before the new Action takes effect.

An Action named Attack asks for the preceding Attack.

The local parameter name does not affect selection:

@Action
@Underlay
def Attack(agent, previous):
    return previous()

and:

@Action
@Underlay
def Attack(agent, underlay):
    return underlay()

have the same Underlay semantics.

2. In this pattern, the Underlay is an Action

The second parameter receives the previously visible Action as a callable.

underlay()

executes it.

The new Action decides what to do with its result:

return "before " + underlay() + " after"

It may call its Underlay once, several times, conditionally, or not at all.

Cross-kind Underlays are treated separately in Pattern 5.

3. Capture follows the Agent's composition

The captured contribution is whatever was visible on that Agent at application time.

It does not have to originate from a Base of the current Tag.

Base/Shape Geometry determines the Tag's Form.

The Agent's Overlay determines the contribution captured as Underlay.

4. Capture is stable

Once an Action captures its Underlay, later layers do not alter that relationship.

A later Action may become visible above it, but the existing chain underneath remains the one that was captured.

5. Arguments follow the call

When an Action has inputs, calling its Underlay without explicit arguments forwards the current Action call's arguments.

@Action
@Underlay
def Attack(agent, underlay, target, power=1):
    return underlay() + f" with power {power}"

Here, underlay() receives the current target and power.

Calling the Underlay with explicit arguments uses those arguments instead.

6. An absent Action gives a null Underlay

If @Underlay is requested but no previous Action exists under that name, TOP supplies a callable null Underlay.

underlay()

does nothing and returns None.

The new Action decides whether that result is meaningful.

7. Without @Underlay, the Action replaces

An Action declared without @Underlay does not automatically receive the previous Action.

It becomes the new visible contribution under that name.

Replacing an Action supplied by an unrelated Tag is allowed but diagnosed, because another semantic layer may have depended on the behaviour being replaced.

Clone this wiki locally