Skip to content

[Bug] concurrent Evaluator workers share the same Agent.short_term_memory across benchmark examples #273

Description

@AmirF194

Describe the Bug

When Evaluator.evaluate(...) runs with num_workers > 1, concurrent benchmark examples share one Agent.short_term_memory buffer, so messages from one example's execution leak into another example's LLM context.

Evaluator._create_new_agent_manager() (evoagentx/evaluators/evaluator.py) builds what looks like a fresh per-thread AgentManager:

new_manager = AgentManager(agents=self.agent_manager.agents, storage_handler=self.agent_manager.storage_handler)

agents=self.agent_manager.agents passes the existing list of Agent objects, so the "new" manager holds references to the SAME Agent instances, not copies. _async_execute_workflow_graph does the same thing for the asyncio path. Every Agent.short_term_memory write (_prepare_execution, _create_output_message in evoagentx/agents/agent.py) and read (get_action_inputs) then operates on the one shared 5-message deque with no lock, so two examples running in parallel interleave their messages into each other's context.

Agent.clear_short_term_memory() exists but is not called anywhere in the codebase, so nothing resets the buffer between examples either.

Operating System

Linux (Docker python:3.11-slim), reproduces independent of OS

Python Version

3.11

Steps to Reproduce

Confirmed against current main (fd6b9a63), pip install evoagentx[tools,rag]:

from evoagentx.agents.agent import Agent
from evoagentx.agents.agent_manager import AgentManager
from evoagentx.core.message import Message
import threading, time

base_agent = Agent(name="worker", description="shared worker", is_human=True)
mgr_main = AgentManager(agents=[base_agent])

# mirrors Evaluator._create_new_agent_manager(): AgentManager(agents=self.agent_manager.agents)
mgr_a = AgentManager(agents=mgr_main.agents)
mgr_b = AgentManager(agents=mgr_main.agents)
agent_a, agent_b = mgr_a.agents[0], mgr_b.agents[0]
print("same Agent object:", agent_a is agent_b)  # True, not an independent copy

results = {}
def run_example(example_id, agent):
    for i in range(5):
        agent.short_term_memory.add_messages([Message(content=f"EXAMPLE_{example_id}_msg{i}", agent="worker")])
        time.sleep(0.01)
    results[example_id] = [m.content for m in agent.short_term_memory.get(n=agent.n)]

ta = threading.Thread(target=run_example, args=("A", agent_a))
tb = threading.Thread(target=run_example, args=("B", agent_b))
ta.start(); tb.start(); ta.join(); tb.join()
print(results)

Expected: each example's final short_term_memory.get() contains only its own EXAMPLE_A_* / EXAMPLE_B_* messages.

Actual: both examples read back the same shared buffer, mixed:

Logs or Screenshots

same Agent object: True
{'A': ['EXAMPLE_B_msg2', 'EXAMPLE_A_msg3', 'EXAMPLE_B_msg3', 'EXAMPLE_A_msg4', 'EXAMPLE_B_msg4'],
 'B': ['EXAMPLE_B_msg2', 'EXAMPLE_A_msg3', 'EXAMPLE_B_msg3', 'EXAMPLE_A_msg4', 'EXAMPLE_B_msg4']}

Both A and B end up with an identical, mixed 5-message window instead of their own messages.

Additional Context

Relevant lines on current main: evoagentx/evaluators/evaluator.py _create_new_agent_manager and _async_execute_workflow_graph (the two places a "new" AgentManager is built from the shared agents list); evoagentx/agents/agent.py _prepare_execution and _create_output_message (the writers) and get_action_inputs (the reader).

I did not run this against a real LLM/benchmark end to end, only the Agent/AgentManager/ShortTermMemory layer that carries the context, but that is the exact path Evaluator uses for every parallel-worker evaluation, so the contamination reaches the real prompt sent to the LLM for each example.

I'm happy to send a PR (likely: give each thread/coroutine its own deep-copied Agent set instead of sharing them, or clear short_term_memory at the start of each example) if that direction sounds right to you.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions