Skip to content

Support multiple and overlapping meta-agent memberships#3172

Merged
tpike3 merged 15 commits intomesa:mainfrom
falloficarus22:feature/multi-level-meta-agents
Jan 27, 2026
Merged

Support multiple and overlapping meta-agent memberships#3172
tpike3 merged 15 commits intomesa:mainfrom
falloficarus22:feature/multi-level-meta-agents

Conversation

@falloficarus22
Copy link
Copy Markdown
Contributor

@falloficarus22 falloficarus22 commented Jan 18, 2026

Summary

This PR refactors the experimental MetaAgent module to support multiple and overlapping memberships. Agents can now belong to more than one meta-agent (e.g., a "Worker" in both a "Company" and a "Team"), enabling more complex, real-world hierarchical simulations.

Motive

The previous implementation used a single scalar reference (agent.meta_agent = self), which restricted agents to a single-parent hierarchy. This made it impossible to model overlapping groups or modular organizational structures. Additionally, redundant model.register_agent calls within the MetaAgent lifecycle were causing KeyError crashes in multi-membership scenarios.

Implementation

  • Membership Set: Internal membership tracking has moved from agent.meta_agent to a set agent.meta_agents.
  • Backward Compatibility: A shim for agent.meta_agent is maintained to ensure existing model logic that expects a single value does not break.
  • Improved create_meta_agent: The helper function now only merges agents into an existing group if the requested class name matches. This allows the creation of independent, overlapping structures (e.g., a "Village" and a "Religion") that share the same constituent agents.
  • Single-Pass Removal Optimization: Refactored removal logic to handle both parent-to-child and child-to-parent relationship updates in a single $O(N)$ traversal, maximizing efficiency without requiring modifications to the core AgentSet class.
  • Registration Fix: Removed manual register_agent/deregister_agent calls from MetaAgent to align with Mesa's automatic agent registration lifecycle and prevent state-sync errors.
  • Testing: Added tests/experimental/test_multi_level_meta_agents.py to verify overlapping group creation, membership persistence, and removal logic.

Usage Examples

Creating Overlapping Groups:

from mesa.experimental.meta_agents.meta_agent import MetaAgent

# Agent 1 joins a Household and a Workplace simultaneously
household = MetaAgent(model, {agent1, agent2}, name="Household")
workplace = MetaAgent(model, {agent1, agent3}, name="Workplace")

# Agent 1 now tracks both
print(agent1.meta_agents) # {<MetaAgent: Household>, <MetaAgent: Workplace>}

Additional Notes

  • Modularity: This change is strictly localized to mesa.experimental.meta_agents. No changes were made to the core Agent, Model, or AgentSet classes, ensuring full backward compatibility and zero side effects for standard models.
  • Closes Support for Multi-Level and Overlapping Meta-Agents #3171

falloficarus22 and others added 4 commits January 18, 2026 14:21
Refactor MetaAgent to allow agents to belong to multiple meta-agent
groups simultaneously. Previously, agents were limited to a single
scalar 'meta_agent' reference, which prevented overlapping
hierarchies.

Update create_meta_agent to only merge agents into existing groups
if the class name matches, enabling independent overlapping groups
of different types. Maintain the 'agent.meta_agent' attribute for
backward compatibility with existing model logic.

Remove redundant model registration and deregistration calls in
MetaAgent methods. These calls were unnecessary as agents register
themselves on initialization, and were causing KeyErrors when agents
participated in multiple groups or transitioned between them.

Add a new test suite 'test_multi_level_meta_agents.py' to verify
overlapping memberships and proper group removal behavior.
Refactor MetaAgent to allow agents to belong to multiple meta-agent
groups simultaneously. Previously, agents were limited to a single
scalar 'meta_agent' reference, which prevented overlapping
hierarchies.

Update create_meta_agent to only merge agents into existing groups
if the class name matches, enabling independent overlapping groups
of different types. Maintain the 'agent.meta_agent' attribute for
backward compatibility with existing model logic.

Remove redundant model registration and deregistration calls in
MetaAgent methods. These calls were unnecessary as agents register
themselves on initialization, and were causing KeyErrors when agents
participated in multiple groups or transitioned between them.

Add a new test suite 'test_multi_level_meta_agents.py' to verify
overlapping memberships and proper group removal behavior.
@quaquel quaquel requested a review from tpike3 January 18, 2026 15:41
@quaquel quaquel added experimental Release notes label enhancement Release notes label labels Jan 18, 2026
@EwoutH
Copy link
Copy Markdown
Member

EwoutH commented Jan 18, 2026

Cool, seems like an obvious use case.

I would let @tpike3 do the (initial) review, since he's the author of this module.

@edgeofchaos42
Copy link
Copy Markdown

edgeofchaos42 commented Jan 20, 2026

Thanks @falloficarus22, this will be a great addition and is critical to get to the goal of rich agent behavior at the overlapping memberships which can allow for activation of different behaviors based on context and group memberships.

Based on the new merges to fix the memory leak could you take a look at #3183 to resolve the conflicts, and if you have the time/desire take a look at the other issue I highlighted in the comments.

Quick note: this is @tpike3 I just was logged into another account.

@falloficarus22
Copy link
Copy Markdown
Contributor Author

Thanks @falloficarus22, this will be a great addition and is critical to get to the goal of rich agent behavior at the overlapping memberships which can allow for activation of different behaviors based on context and group memberships.

Based on the new merges to fix the memory leak could you take a look at #3183 to resolve the conflicts, and if you have the time/desire take a look at the other issue I highlighted in the comments.

Quick note: this is @tpike3 I just was logged into another account.

sure

@EwoutH
Copy link
Copy Markdown
Member

EwoutH commented Jan 20, 2026

@tpike3 @falloficarus22 for both of you, this idea might also be interesting: #2539 (comment)

@falloficarus22
Copy link
Copy Markdown
Contributor Author

falloficarus22 commented Jan 20, 2026

Hi @edgeofchaos42 / @tpike3 ,

I definitely agree with your point about the distinction between membership and existence. The current logic in remove_constituting_agents that calls deregister_agent is indeed a bug—it conflates "leaving a group" with "dying in the simulation," which prevents any kind of dynamic regrouping or multi-level interaction.

The distinction you draw is essential for making meta-agents more robust. Regarding your point about ensuring cleanup when an agent is removed from the model, I'd like to propose a specific direction for discussion:

Proposal: Bidirectional Cleanup Hooks Instead of horizontal orchestration, what if we leveraged hooks in the base lifecycle?

  1. Agent -> Meta-Agent: If we ensure agents have a way to track which meta-agents they belong to, we could add a hook to the core Agent.remove() method. When an agent is removed from the model, it would automatically notify its meta-agents to discard the reference.
  2. Meta-Agent -> Agent: Similarly, should MetaAgent have a formal remove() method that cleans up references on the constituent agents if the meta-agent itself (like an alliance) is dissolved?

Do you think centralizing this cleanup inside the remove() methods is the right path, or should meta-agents be managed more like spatial memberships (where the space/grouping is the source of truth)?

@falloficarus22
Copy link
Copy Markdown
Contributor Author

@tpike3 @falloficarus22 for both of you, this idea might also be interesting: #2539 (comment)

Hey, @EwoutH
This is a really elegant proposal. Making MetaAgent a subclass of AgentSet (rather than just wrapping one) feels much more native to how Mesa 3.0+ handles collections. Using multiple inheritance here basically "upgrades" a group of agents to be a first-class citizen in the model.

A few thoughts from an architectural perspective:

  1. Automatic Lifecycle Management: One huge advantage of inheriting from AgentSet is that it uses WeakKeyDictionary internal storage. This actually solves the "Existence vs. Membership" concern raised in other discussions—if an agent is removed from the model (the only strong reference), it will automatically disappear from the
    MetaAgent without us needing manual cleanup loops or hooks in model.deregister_agent.

  2. Domain-Specific Back-references: The membership_name idea (e.g., agent.household) is a big improvement over a generic agent.meta_agent attribute. It makes the model logic far more readable.

  3. Scaling for Multi-Level/Multi-Membership: For this to be truly robust, we should consider how it handles multi-level support.

If an agent belongs to two different Household meta-agents (e.g., a shared residence in a complex model), the current setattr logic would overwrite the reference.
Proposal: Could we allow membership_name to optionally point to a set? Or perhaps use a registry like agent.memberships["household"]?

Overall, I think the "is-a group" approach is the right direction. It eliminates a lot of boilerplate and makes the group-level analysis (like household.agg("wealth", sum)) extremely powerful.

I'd be curious to hear if anyone has concerns about MRO or serialization (pickle/getstate) when mixing Agent and AgentSet this closely?

@edgeofchaos42
Copy link
Copy Markdown

@falloficarus22 Based on the dev meeting yesterday, the plan is for a larger effort on meta-agents, so my proposal would we :
1 - Get this PR to the point of merge to address the immediate issues (remove agents from meta agent and multi-membership), which still might be non-trivial
2- Then use #2539 to come up with a long term better architecture

What do you think?

@falloficarus22
Copy link
Copy Markdown
Contributor Author

@falloficarus22 Based on the dev meeting yesterday, the plan is for a larger effort on meta-agents, so my proposal would we : 1 - Get this PR to the point of merge to address the immediate issues (remove agents from meta agent and multi-membership), which still might be non-trivial 2- Then use #2539 to come up with a long term better architecture

What do you think?

Agreed! I don't have the time to make the changes right away but I'll update the PR as early as tomorrow

agent.meta_agent = self

def remove_constituting_agents(self, remove_agents: set[Agent]):
def remove_constituting_agents(self, remove_agents: Iterable[Agent]):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry I think I missed something or misunderstood.

Why are we switching to a generic iterable vs a set? With set we can use set.difference_update to remove agents quickly. This would also prevent us from touching agent.py.

Let me know what i am missing. Thanks

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Even if we used set.difference_update() on the collection, we still have to run an O(N) loop over every agent being removed to handle the cleanup of the agent's internal state (agent.meta_agents.discard(self)). A bulk set operation only updates the parent's list; it doesn't handle the child's reference. Since the loop is mandatory for the side effects, the performance gain of difference_update is negligible.

Without the change to agent.py, every time a user wants to remove multiple agents, they have to write a logic loop:

for a in agents_to_remove:
    my_agentset.discard(a)

By putting difference_update into the core AgentSet class, we encapsulate that logic. If we ever find a faster way to perform bulk removals in the future (e.g., if AgentSet moves away from a WeakKeyDictionary), we only have to optimize it in one place (agent.py) rather than fixing manual loops scattered across the whole codebase.

How would you prefer it?

Copy link
Copy Markdown

@edgeofchaos42 edgeofchaos42 Jan 26, 2026

Choose a reason for hiding this comment

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

@falloficarus22 this is a good discussion and is helping understand the dynamics of meta-agents more deeply. I don't think we are quite synced up yet on our understanding.

I see the remove_constituting_agents as two transactions:

1 - meta-agent remove constituting_agents (parent remove child):
- thought is use pythons set.difference_update to remove these agents
2 - constituting_agents remove meta-agent (child remove parent):
- for loop through constituting_agents to remove meta-agent

How I understand your code is it is running two for loops:
1 - iterate through constituting_agents to remove from meta-agent (parent remove child)
2 - iterate through constituting_agents to remove meta-agent (child remove parent)

With this we can only optimize the first for loop since the references are centrally located in the meta-agent instance executing the transaction, we have to maintain the for loop for the second as each constituting_agent could have diverse set of meta-agent to which they belong.

Only to be thorough ---- What we are not doing is removing the constituting_agent or the meta-agent from the model, the user would have to specify different transaction(s) for that. Although at some point we may want to consider a boolean kwarg where the constituting_agent and /or meta-agent are entirely removed.

Again, i could be misunderstanding so please let me know your point of view.

Two other notes, regardless of next step.

1 - We want to try and stay clear of updating agent.py. This is for the larger goal of building out mesa modules (@EwoutH ) This will help us answer the question of how to make agent.py more extensible to allow for modular inputs.
2 - We would not want to call the function in agent.py difference_update as users could confuse it with the python native set function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The technical trade-off for remove_constituting_agents comes down to pass-count and performance:

  1. One Pass vs. Two: Your proposed 'two transaction' model (bulk removal + side-effect loop) requires two separate passes over the data. Since we have a mandatory requirement to update internal child states (agent.meta_agents.discard(self)), a single-pass loop that handles both operations in one traversal is computationally more efficient.
  2. The AgentSet Constraint: AgentSet is not a native Python set. It only supports difference_update if we modify the core agent.py. If the architectural goal is to keep agent.py minimal for modularity, then a local O(N) loop in meta_agent.py is the technically superior solution as it achieves the same result in one pass without requiring core library updates.
  3. API Clarity: Avoiding the addition of a difference_update method to AgentSet solves both the naming confusion and the modularity concern.

I will consolidate the logic into a single internal loop in meta_agent.py and revert the changes to agent.py. This maximizes local performance while respecting the boundary between experimental and core code.

Copy link
Copy Markdown
Member

@tpike3 tpike3 Jan 27, 2026

Choose a reason for hiding this comment

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

@falloficarus22 fair point -- one loop with each action parent remove child and child remove parent computationally looks more efficient than two transactions. However, python being written on C, when you have C optimized functions that you can employ, it can become counterintuitive (and this varies based on the situation). Running a simple test of 1,000,10,000, 100,000, 1,000,000 and 5,000,000 agents with different subagent discards of 1%, 10%, 25%, 50%, 75% and 90%. In these runs using difference_update is generally faster but not in all cases particularly around 100,000 agents and even then in some runs of the single loop maybe faster.

At this point, however, I think it is better to merge and then we can focus on the optimizations later. Thank you for all the work and conversation on this.

scenario comparison

@falloficarus22 falloficarus22 force-pushed the feature/multi-level-meta-agents branch from 8139feb to c7b1261 Compare January 27, 2026 03:39
Copy link
Copy Markdown
Member

@tpike3 tpike3 left a comment

Choose a reason for hiding this comment

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

LGTM

@tpike3 tpike3 merged commit d6dbea4 into mesa:main Jan 27, 2026
14 checks passed
@EwoutH
Copy link
Copy Markdown
Member

EwoutH commented Jan 27, 2026

Thanks for this effort! Few questions/requests:

  1. @falloficarus22 could you update the PR description to represent the final state of this PR?
  2. Are there any big (conceptual) design decisions made?
  3. Does this break backward compatibility in any way?
  4. Are there (major) follow-up issues?

@falloficarus22
Copy link
Copy Markdown
Contributor Author

Thanks for this effort! Few questions/requests:

  1. @falloficarus22 could you update the PR description to represent the final state of this PR?
  2. Are there any big (conceptual) design decisions made?
  3. Does this break backward compatibility in any way?
  4. Are there (major) follow-up issues?
  • Updated the PR description.
  • In this PR we shifted from a single-parent hierarchy to a set-based model, allowing an agent to belong to multiple meta-agents simultaneously.
  • Changes are isolated in the experimental module without touching agent.py as suggested by @tpike3. Maintained a shim for the old agent.meta_agent attribute, so existing experimental code continues to function as expected.

Possible follow up issues:

  1. Cycle Detection: Need to ensure recursive hierarchies don't create infinite loops during step() calls.
  2. Dynamic Joining: Adding user-defined selectors to control exactly which group an agent joins when multiple candidates of the same type are available.
  3. Optimizations of course!

@falloficarus22 falloficarus22 deleted the feature/multi-level-meta-agents branch January 27, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Release notes label experimental Release notes label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Multi-Level and Overlapping Meta-Agents

5 participants