Skip to content

Add Actions: event-driven timed agent behavior#3308

Closed
EwoutH wants to merge 2 commits intomesa:mainfrom
EwoutH:actions
Closed

Add Actions: event-driven timed agent behavior#3308
EwoutH wants to merge 2 commits intomesa:mainfrom
EwoutH:actions

Conversation

@EwoutH
Copy link
Copy Markdown
Member

@EwoutH EwoutH commented Feb 15, 2026

Summary

Adds a minimal Action system to mesa.experimental.actions, enabling agents to perform actions that take time, can be interrupted, and give proportional reward based on a reward curve.

Motive

Mesa 3.5 introduced event scheduling (schedule_event, run_for, etc.), but agents still lack a built-in concept of doing something over time. There's no way to know if an agent is busy, interrupt what it's doing, or get partial credit for incomplete work. This has been discussed extensively in #2526 (Tasks), #2529 (Continuous States), #2538 (Behavioral Framework), and #2858 (ActionSpace).

This PR introduces the minimal foundation that those proposals build on: timed actions with interruption and partial completion.

Implementation

Two classes in mesa/experimental/actions/__init__.py:

  • Action: Defines an action with name, duration, priority, reward curve, and on_effect callback. Tracks its own runtime state (progress, scheduled event).
  • ActionAgent: Agent subclass with current_action, start_action(), interrupt_for(), and cancel_action(). Integrates with model.schedule_event for completion timing.

Reward curves map progress [0,1] to effective completion [0,1]. Two built-ins: linear (default, proportional) and step (all-or-nothing). Any float → float callable works.

Key design decisions (mainly for simplicity, now):

  • Single on_effect(agent, completion) callback fires on both completion and interruption, scaled by the reward curve. No separate complete/interrupt reward logic.
  • interrupt_for() silently ignores if current action is non-interruptible.
  • start_action() raises if agent is already busy (explicit over implicit).
  • Agent.remove() cancels any scheduled action event.

Usage Examples

from mesa import Model
from mesa.experimental.actions import Action, ActionAgent, step


class Sheep(ActionAgent):
    def __init__(self, model):
        super().__init__(model)
        self.energy = 50.0
        self.alive = True

        # Start the first action
        self.start_action(self.forage())

    def forage(self):
        """Forage for food. Linear reward: partial grazing still gives some energy."""
        return Action(
            "forage", duration=5.0, priority=1,
            on_effect=self.gain_energy,
        )

    def flee(self):
        """Flee from predator. All-or-nothing: must complete to survive."""
        return Action(
            "flee", duration=2.0, priority=10,
            interruptible=False, reward_curve=step,
            on_effect=self.resolve_flee,
        )

    def gain_energy(self, agent, completion):
        agent.energy = min(100, agent.energy + 30 * completion)
        # Done foraging (or interrupted), start again
        if not agent.is_busy:
            agent.start_action(agent.forage())

    def resolve_flee(self, agent, completion):
        if completion < 1.0:
            agent.alive = False  # Didn't escape
        elif not agent.is_busy:
            agent.start_action(agent.forage())  # Safe, back to grazing

    def spot_predator(self):
        """Called externally (e.g. by a Wolf or the model). Triggers flee."""
        self.interrupt_for(self.flee())


# Run it
model = Model()
sheep = Sheep(model)
model.run_for(3)          # Sheep forages for 3 time units
sheep.spot_predator()     # Wolf! Forage interrupted (3/5 = 60% energy gained), flee starts
model.run_for(2)          # Flee completes, sheep goes back to foraging

Additional Notes

This is deliberately minimal. Future work (not in this PR):

  • Callable duration/priority for stochastic/state-dependent values
  • Preconditions and available_actions repertoire
  • evaluate() / select_action() behavioral loop
  • Action queue and resumability
  • Integration into the base Agent class (pending stabilization)

See the #3304 for the full roadmap.

@EwoutH EwoutH added feature Release notes label experimental Release notes label labels Feb 15, 2026
Introduce mesa.experimental.actions with Action and ActionAgent classes.

Actions have duration, priority, reward curves, and integrate with Mesa's event scheduling for precise timing. Agents can start, interrupt, and cancel actions, receiving proportional reward based on progress.
Cover action lifecycle (start, complete, interrupt, cancel), reward curves (linear, step, custom), non-interruptible actions, agent removal cleanup, and multi-interrupt integration sequences.
duration: float = 1.0,
priority: float = 0.0,
reward_curve: Callable[[float], float] = linear,
on_effect: Callable[[ActionAgent, float], None] | None = None,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave this out of the class and instead make Action an ABC and have users implement this themselves.

I am even wondering whether duration should be user specified rather than being a float parameter. Basically, I see an action as something that is started and at start_time it schedules its time of completion.

This time of completion is a number, but it might be sampled from a distribution.

Next, there should be a on_completed() abstract method, and an on_interrupted() abstract method.

I would leave priority out of it completely because I don't see the value yet of integrating ranking and other decision logic inside the Action primititive.

# Runtime state
self.progress: float = 0.0
self._started_at: float | None = None
self._event: Event | None = None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make it the responsibility of the action to schedule this event, potentially via a start_action() method. I think this is much cleaner than doing this from the agent side as you do in this PR.

self._event: Event | None = None

@property
def effective_completion(self) -> float:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strange name

@property
def effective_completion(self) -> float:
"""Reward earned at current progress, based on reward curve."""
return self.reward_curve(self.progress)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave this out completely. This does not generalize at all. For something like a grazing model, this makes sense. But in say a call center model, this makes no sense at all. I would replace it with an on_interupt and on_completed method, both of which the user has to specify.

return f"Action({self.name!r}, progress={self.progress:.0%})"


class ActionAgent(Agent):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure we need a new Agent subclass or we can just have a more developed subclasseable Action primitive.

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Mar 6, 2026

Thanks for everyone's input. I used it to create a second version, which succeeds this PR:

@EwoutH EwoutH closed this Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

experimental Release notes label feature Release notes label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants