Source code for mesa_llm.recording.record_model

"""Class decorator that instruments a Mesa `Model` subclass with a :class:`SimulationRecorder`.

Usage::

    from mesa_llm.recording.record_model import record_model

    @record_model
    class MyModel(Model):
        ...

The decorator will:
1. Instantiate a ``SimulationRecorder`` after the model's original ``__init__`` completes and assign it to ``self.recorder``.
2. Attach the same recorder to every agent that exposes a ``recorder`` attribute (e.g., subclasses of ``LLMAgent``).
3. Wrap the model's ``step`` method to automatically record ``step_start`` and ``step_end`` events and ensure late-added agents also receive the recorder.
4. Provide a convenience ``save_recording`` method on the model for persisting the captured simulation events.

Parameters
----------
**kwargs
    Any keyword arguments are forwarded directly to :class:`SimulationRecorder` when it is created.
    This allows callers to customize output directory, auto-save intervals, or disable certain event types::

        @record_model(output_dir="recordings", auto_save_interval=100)
        class MyModel(Model):
            ...
"""

import atexit
import logging
from collections.abc import Callable
from functools import wraps

from mesa.model import Model

from mesa_llm.recording.simulation_recorder import SimulationRecorder

logger = logging.getLogger(__name__)


def _attach_recorder_to_agents(model: Model, recorder: SimulationRecorder):
    """Utility that iterates over all agents and attaches the recorder."""
    for agent in list(model.agents):
        # Only set if the attribute exists to avoid leaking recorder to non-LLM agents
        if hasattr(agent, "recorder"):
            agent.recorder = recorder


[docs] def record_model( cls: type[Model] | None = None, **kwargs ) -> Callable[[type[Model]], type[Model]] | type[Model]: """ Class decorator that automatically instruments Mesa Model subclasses with SimulationRecorder functionality. Provides seamless integration without manual recorder setup. Features: - Automatically creates and attaches SimulationRecorder after model initialization - Attaches recorder to all LLMAgent instances in the model - Wraps model.step() to record step start/end events - Provides save_recording() convenience method - Registers automatic save on program exit Parameters: - **cls** (*type[Model] | None*) - The Model class to decorate. If None, returns a wrapper for use as a decorator with kwargs. - **kwargs** - Forwarded to SimulationRecorder constructor for customization """ if cls is None: # Decorator was called with optional kwargs -> return wrapper awaiting the class return lambda actual_cls: record_model(actual_cls, **kwargs) # type: ignore[misc] # All kwargs are passed directly to SimulationRecorder recorder_kwargs = kwargs original_init = cls.__init__ original_step = getattr(cls, "step", None) # ----------------------------- Wrap the model's __init__ method to create and attach the recorder ----------------------------- @wraps(original_init) def init_wrapper(self: "Model", *args, **kwargs): # type: ignore[override] original_init(self, *args, **kwargs) # type: ignore[arg-type] # Create and attach recorder self.recorder = SimulationRecorder(model=self, **recorder_kwargs) # type: ignore[attr-defined] _attach_recorder_to_agents(self, self.recorder) # Use a closure to capture a reference to `self` def _auto_save(): try: # Avoid creating multiple identical files if already saved manually if hasattr(self, "recorder") and self.recorder.events: self.save_recording() except Exception: # pragma: no cover - defensive logger.exception("SimulationRecorder auto-save failed") atexit.register(_auto_save) cls.__init__ = init_wrapper # type: ignore[assignment] if original_step is not None: # ----------------------------- Wrap the model's step method to record step start and end events ----------------------------- @wraps(original_step) def step_wrapper(self: "Model", *args, **kwargs): # type: ignore[override] # Record beginning of step if hasattr(self, "recorder"): self.recorder.record_model_event("step_start", {"step": self.steps}) # type: ignore[attr-defined] # Execute the original step logic result = original_step(self, *args, **kwargs) # type: ignore[misc] # Make sure any new agents created during the step also receive the recorder if hasattr(self, "recorder"): _attach_recorder_to_agents(self, self.recorder) # type: ignore[attr-defined] # Record end of step after agents have acted self.recorder.record_model_event("step_end", {"step": self.steps}) # type: ignore[attr-defined] return result cls.step = step_wrapper # type: ignore[assignment] def save_recording( self: "Model", filename: str | None = None, format: str = "json" ): if not hasattr(self, "recorder"): raise AttributeError( "Recorder not initialised - did you forget to decorate the model with @record_model?" ) return self.recorder.save(filename=filename, format=format) # type: ignore[attr-defined] cls.save_recording = save_recording # type: ignore[attr-defined] return cls