2025-07-17

Building Agents? Stop Treating messages[] Like a Database

A woman presents in front of a screen displaying code, with a green diamond overlay.
By Danny Lake, Product Architect + Agentic AI SME, Orium
4 min read

When I first started building agents, I followed the examples. Like most developers, I handled state by appending everything to a single growing messages[] transcript: user prompts, model replies, tool-call requests, and tool results. It worked— at first.

But as my agent’s logic became more complex, cracks started to show. The model would repeat tool calls it had already run. Prompt templates ballooned with irrelevant context. I was writing functions to parse the messages[] transcript just to extract the right tool-call result when constructing prompts. It started to feel strange. There had to be a better way.

The deeper problem wasn’t the tools themselves, it was my assumption that the transcript was the state.

LangGraph gave me a new lens. Instead of treating the transcript as a catch-all for everything the agent might need to “remember,” I could model structured state explicitly: targeted inputs for each node and isolated fields for tool results, usage history, and other reusable data. Even certain assistant responses—like internal reasoning or chosen strategies—deserve their own state fields if they’ll be reused later. This state lives in the LangGraph thread object and persists across runs, letting me feed the model smaller, more-focused prompts with exactly the context it needs, no transcript archaeology required.

This post walks through that shift and how adopting structured state makes agents more robust, reliable, and ready for real use.

Pattern Snapshot

Most demos turn the transcript into an accidental database. Instead, lift heavy data into structured state and keep the transcript lightweight:

TEXT
12345678910111213
// Before
graphState = {
  messages[] //a mix of user prompts, assistant replies, tool call requests, toolcall results
}

//After
  graphState  = {
messages[], //chat flow only:  user prompts and assistant replies worth showing to the user
toolResults,
userPreferences,
 …other reusable data
  }

The messages[] transcript stays readable and short; the agent’s real “working memory” lives in dedicated fields contained in the graph’s state, where it’s structured, inspectable, and easy to reuse. The datatype of these dedicated fields is entirely up to you - for example a string, boolean, number, or an object - whichever best fits your needs.

Benefits of Shifting Data Out of messages[]

  • Lower token costs — lean prompts skip the full history of tool calls and chat.
  • Easier inspection & debugging — state lives in named fields you can log and view instead of being buried in a wall of tokens.
  • Predictable, controllable behaviour — explicit state makes each node’s inputs and outputs deterministic, so you direct the LLM with exactly the context it needs rather than hoping it finds the right slice in a giant transcript.
  • Higher accuracy — trimming unrelated messages and stale tool results reduces noise so the model focuses on the signal.
  • Simpler testing — each LangGraph node can be unit-tested with a small, explicit state payload instead of reconstructing a massive message log.

Case Example: Search Results Bloat

The anti-pattern

Most tutorials set up an LLM with tool calling enabled and then append both the tool-call request and its JSON result straight into the messages[] log. Now imagine a user asks, “What’s the forecast for this weekend?” — and then keeps chatting.

  1. Every new user message fires the search tool again.
  2. Each run appends another request and another JSON blob to messages[].
  3. When the user finally says, “So, should I pack an umbrella?”, the LLM must sift through the entire transcript, decide which of many search results is current, and use that data correctly.

That means longer prompts, more tokens, and fragile reasoning.

A better way

Lift the data into state:

TEXT
12
graphState["searchResults"] = results_json

…then inject only what each node needs:

TEXT
123
Here are the search results:
{{searchResults}}

Now each user message can reuse or refresh graphState.searchResults without bloating the transcript, and the LLM always sees the right data in a clean prompt.

LangGraph makes this possible, but it doesn’t force it. Making the leap is up to you. The same logic applies to reusable assistant insights: summaries, recommended actions, parsed conclusions. Store them in state, not in the messages[] array.

Implementation Tips

  • Treat LangGraph thread state as working memory — structured, inspectable, portable.
  • Create clear fields: searchResults, userProfile, pricingData, strategySummary, etc.
  • If an assistant message will guide later steps, lift it into state instead of leaving it in the transcript.
  • Keep messages[] clean: user text and final assistant replies only.
  • Construct prompt templates with just the relevant state needed to best support the LLM in its response.

Final Thought

Using messages[] for everything works— for a while. But as agents grow, this pattern becomes brittle, opaque, and expensive.

Treat the transcript as a transcript, not a brain. Structured state preserves what matters—tool results, assistant reasoning, decisions—bringing clarity, determinism, and real-world reliability.

Popular Articles