#23234·llama_index

Agent setup_agent permanently writes state_prompt into ChatMemoryBuffer history

Author: michaelkillgtaCreated Sep 25, 2026Updated Sep 28, 2026

Description

When a workflow agent runs with initial_state / state_prompt and the default ChatMemoryBuffer, setup_agent mutates the stored user ChatMessage in place. The formatted "Current state: … Current message: …" string becomes the permanent user turn in memory, so later LLM context, analytics, and resumed chats permanently contain workflow state (and lose a clean user utterance).

This is distinct from the within-run nesting fix (formatted_input_with_state / related issues): the corruption survives across the run into memory.get().

Environment

  • llama-index-core==0.14.25
  • Python 3.13

Minimal repro

python
import asyncio
from llama_index.core.agent.workflow import ReActAgent
from llama_index.core.llms.mock import MockLLM
from llama_index.core.memory import ChatMemoryBuffer

def ping(x: str) -> str:
    """Echo the input string."""
    return x

async def main() -> None:
    llm = MockLLM(max_tokens=32)
    memory = ChatMemoryBuffer.from_defaults(token_limit=5000)
    agent = ReActAgent(
        name="agent",
        llm=llm,
        tools=[ping],
        initial_state={"user_id": "u-42", "plan": "secret-plan"},
        streaming=False,
        timeout=60,
    )
    try:
        await agent.run(user_msg="What is 2+2?", memory=memory, max_iterations=1)
    except Exception:
        pass  # mutation happens in setup_agent before max-iterations

    content = memory.get()[0].content or ""
    print(repr(content))
    assert "secret-plan" in content  # permanently written into history

asyncio.run(main())

Expected

Memory keeps the user text "What is 2+2?". State injection should only affect the ephemeral LLM prompt.

Actual

Stored user message becomes:

Current state:
{'user_id': 'u-42', 'plan': 'secret-plan'}

Current message:
What is 2+2?

Root cause (pointer)

In BaseWorkflowAgent.setup_agent, block.text = self.state_prompt.format(...) mutates llm_input[-1] in place. ChatMemoryBuffer.get() returns the same ChatMessage objects held in the store, so the mutation persists. Using SQL Memory does not write the mutated objects back, but agents default to ChatMemoryBuffer.

Suggested fix direction

Copy-on-write before applying state_prompt (mutate a shallow copy of the message / text block for the LLM call only), or otherwise ensure memory never receives the state-prefixed text.