Bug Description
AgentWorkflow.__init__ in llama-index-core/llama_index/core/agent/workflow/multi_agent_workflow.py stores agent configs by reference: self.agents = {cfg.name: cfg for cfg in agents}. No copy.deepcopy(cfg), no cfg.model_copy(deep=True), no per-agent copy of cfg.tools. Upstream, BaseWorkflowAgent.validate_tools builds a fresh outer list per agent but keeps each pre-existing BaseTool element by reference (validated_tools.append(tool)).
If a user passes the same FunctionTool instance into the tools= list of two AgentConfigs and hands those to one AgentWorkflow, the runtime tool-call lookup at multi_agent_workflow.py:252 (agent_tools = self.agents[agent_name].tools or []) reaches through the same shared object. Per-tool mutations performed by or on behalf of one agent (closure-captured fn state, tool.metadata.description, tool.partial_params, an MCP session handle, a cached retriever) are visible to every sibling agent that shares the tool.
The natural way to "share a knowledge-base tool across a researcher + writer pair" is:
kb = FunctionTool.from_defaults(fn=lookup_kb)
researcher = FunctionAgent(name="researcher", tools=[kb], llm=llm, ...)
writer = FunctionAgent(name="writer", tools=[kb], llm=llm, ...)
w = AgentWorkflow(agents=[researcher, writer], root_agent="researcher")
This pattern is documented as the multi-agent API entry point, but the framework neither isolates nor warns about the shared identity. Per-agent partial parameters (region="us-west" on agent A vs region="eu-central" on agent B over a shared HTTP tool) race; last writer wins for all agents.
Version
llama-index-core==0.14.22
Steps to Reproduce
No LLM, no network. The PoC constructs a real AgentWorkflow with two FunctionAgents sharing one FunctionTool and exhibits three independent mutation-leak channels.
from llama_index.core.agent.workflow import AgentWorkflow, FunctionAgent
from llama_index.core.llms.mock import MockLLM
from llama_index.core.tools import FunctionTool
counter = {"hits": 0}
def lookup(query: str) -> str:
counter["hits"] += 1
return f"result({query})#{counter['hits']}"
shared = FunctionTool.from_defaults(fn=lookup)
llm = MockLLM()
a = FunctionAgent(name="researcher", description="finds facts",
tools=[shared], llm=llm, can_handoff_to=["writer"])
b = FunctionAgent(name="writer", description="writes summaries",
tools=[shared], llm=llm, can_handoff_to=["researcher"])
w = AgentWorkflow(agents=[a, b], root_agent="researcher")
t_a = w.agents["researcher"].tools[0]
t_b = w.agents["writer"].tools[0]
print("identity preserved through graph:", t_a is t_b is shared)
# Mutation 1: closure state behind tool.fn
w.agents["researcher"].tools[0].call(query="alpha")
w.agents["writer"].tools[0].call(query="beta")
print("closure counter:", counter) # both incremented
# Mutation 2: tool.metadata.description
w.agents["researcher"].tools[0].metadata.description = "RESEARCHER-SCOPED"
print("writer sees description:", w.agents["writer"].tools[0].metadata.description)
# Mutation 3: tool.partial_params
w.agents["researcher"].tools[0].partial_params["region"] = "us-west"
print("writer sees partial_params:", w.agents["writer"].tools[0].partial_params)
# CONFIRMED BUG when identity is True and all three mutations leak
Relevant Logs/Tracebacks
identity preserved through graph: True
closure counter: {'hits': 2}
writer sees description: 'RESEARCHER-SCOPED'
writer sees partial_params: {'region': 'us-west'}
AST: AgentWorkflow.__init__ has no per-agent/per-tool deepcopy: BUG PATTERN PRESENT
AST: BaseWorkflowAgent.validate_tools preserves tool identity: BUG PATTERN PRESENT
Runtime: shared tool identity + 3 mutation channels leak: reproduced
CONFIRMED BUG: shared tool identity / mutation visible across agents
Class: C25 — shared-tool-or-vectordb-mutation-across-nodes
Bug Description
AgentWorkflow.__init__inllama-index-core/llama_index/core/agent/workflow/multi_agent_workflow.pystores agent configs by reference:self.agents = {cfg.name: cfg for cfg in agents}. Nocopy.deepcopy(cfg), nocfg.model_copy(deep=True), no per-agent copy ofcfg.tools. Upstream,BaseWorkflowAgent.validate_toolsbuilds a fresh outer list per agent but keeps each pre-existingBaseToolelement by reference (validated_tools.append(tool)).If a user passes the same
FunctionToolinstance into thetools=list of twoAgentConfigs and hands those to oneAgentWorkflow, the runtime tool-call lookup atmulti_agent_workflow.py:252(agent_tools = self.agents[agent_name].tools or []) reaches through the same shared object. Per-tool mutations performed by or on behalf of one agent (closure-capturedfnstate,tool.metadata.description,tool.partial_params, an MCP session handle, a cached retriever) are visible to every sibling agent that shares the tool.The natural way to "share a knowledge-base tool across a researcher + writer pair" is:
This pattern is documented as the multi-agent API entry point, but the framework neither isolates nor warns about the shared identity. Per-agent partial parameters (
region="us-west"on agent A vsregion="eu-central"on agent B over a shared HTTP tool) race; last writer wins for all agents.Version
llama-index-core==0.14.22
Steps to Reproduce
No LLM, no network. The PoC constructs a real
AgentWorkflowwith twoFunctionAgents sharing oneFunctionTooland exhibits three independent mutation-leak channels.Relevant Logs/Tracebacks
identity preserved through graph: True closure counter: {'hits': 2} writer sees description: 'RESEARCHER-SCOPED' writer sees partial_params: {'region': 'us-west'} AST: AgentWorkflow.__init__ has no per-agent/per-tool deepcopy: BUG PATTERN PRESENT AST: BaseWorkflowAgent.validate_tools preserves tool identity: BUG PATTERN PRESENT Runtime: shared tool identity + 3 mutation channels leak: reproduced CONFIRMED BUG: shared tool identity / mutation visible across agents Class: C25 — shared-tool-or-vectordb-mutation-across-nodes