State = Dict
Lesson 5 of 9 — A Tour of Agents
The entire AI agent stack in 60 lines of Python.
Run the agent. "Add 10 and 5, then uppercase hello." It returns "15 and HELLO". Correct. But how many turns was that? Which tools fired? All you got back is a string.
The agent works. It loops, calls tools, remembers conversation. But the return value tells you nothing about what happened inside.
The problem
Right now the agent function returns a string — the assistant's final message. That's fine for a chatbot. But if you're building anything real — logging, debugging, retry logic, cost tracking — you need more.
You can't tell:
- How many loop iterations it took
- Which tools were called and in what order
- What each tool returned
The conversation array has all of this buried in it, but you'd have to dig through message objects to reconstruct it. There's a simpler way.
The fix: add a dict
Create a state dictionary before the loop. Every time something happens — a turn completes, a tool fires, a result comes back — record it.
Messages are the raw tape. State is the summary. One is for the LLM. The other is for you.
The code
state = {"turns": 0, "tool_calls": [], "results": []}
Three fields. Turns counts loop iterations. Tool calls tracks which functions ran. Results stores what they returned. Initialize it before the loop, update it inside:
async def agent(user_message, max_turns=5):
conversation.append({"role": "user", "content": user_message})
state = {"turns": 0, "tool_calls": [], "results": []}
for turn in range(max_turns):
state["turns"] += 1
msg = await ask_llm(conversation)
if not msg.get("tool_calls"):
conversation.append({"role": "assistant", "content": msg.get("content", "")})
state["answer"] = msg.get("content", "")
return state
conversation.append(msg)
for tc in msg["tool_calls"]:
name = tc["function"]["name"]
result = tools[name](**json.loads(tc["function"]["arguments"]))
state["tool_calls"].append(name)
state["results"].append(result)
conversation.append({"role": "tool", "tool_call_id": tc["id"], "content": str(result)})
return state
Instead of return msg.get("content", ""), return the whole dict. The answer is still in there — under state["answer"]. But now you also get everything else.
Watch it work
Same question: "Add 10 and 5, then uppercase hello."
Turn one: the agent calls add. Result: 15. The state dict records it.
Turn two: upper. Result: HELLO. Two turns. Two tools. The full picture — not just the answer, but the entire execution trace.
This is what LangChain calls AgentState and what LangGraph builds its entire graph around. It's a dictionary.
Try it
Run this code yourself at tinyagents.dev. Add your own fields to the state dict — token counts, timestamps, error flags — and watch the agent become observable.
Next up: Lesson 6 — Streaming. The agent returns a complete response. But what if you want to see tokens as they arrive?
This is Lesson 5 of A Tour of Agents — a free interactive course that builds an AI agent from scratch. No frameworks. No abstractions. Just the code.


Top comments (0)