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.
Describe the Bug
When
Evaluator.evaluate(...)runs withnum_workers > 1, concurrent benchmark examples share oneAgent.short_term_memorybuffer, 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-threadAgentManager:agents=self.agent_manager.agentspasses the existing list ofAgentobjects, so the "new" manager holds references to the SAMEAgentinstances, not copies._async_execute_workflow_graphdoes the same thing for the asyncio path. EveryAgent.short_term_memorywrite (_prepare_execution,_create_output_messagein 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]:Expected: each example's final
short_term_memory.get()contains only its ownEXAMPLE_A_*/EXAMPLE_B_*messages.Actual: both examples read back the same shared buffer, mixed:
Logs or Screenshots
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_managerand_async_execute_workflow_graph(the two places a "new"AgentManageris built from the sharedagentslist);evoagentx/agents/agent.py_prepare_executionand_create_output_message(the writers) andget_action_inputs(the reader).I did not run this against a real LLM/benchmark end to end, only the
Agent/AgentManager/ShortTermMemorylayer that carries the context, but that is the exact pathEvaluatoruses 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
Agentset instead of sharing them, or clearshort_term_memoryat the start of each example) if that direction sounds right to you.