1 Threads โ remembering across turns
Imagine you're chatting with a friend who has a terrible memory โ every time you say something new, they forget everything that came before. Annoying, right? To fix it, you keep a little notebook of your whole conversation, and before each new sentence you read the whole notebook back to them so they're caught up. A thread is that notebook. The SDK keeps it for you, so the agent feels like it remembers โ even though the model itself forgets the moment it finishes answering.
First, recall: the model is stateless
Back in Session 1 we learned the big, surprising truth: an LLM is stateless. It has no memory between requests. Each time you call the model, it reads what you send, produces an answer, and then completely forgets the whole exchange. ChatGPT only feels like it remembers because the app secretly re-sends the entire conversation with every new message.
So if the model has no memory, who keeps the conversation? Something on the outside has to store the history and re-send it each turn. In the Agent SDK, that "something" is a thread.
What a thread actually is
A thread (sometimes called a session or conversation) is a persistent object that holds everything about one ongoing conversation across multiple turns. A turn is one round of "user says something โ agent responds." A thread accumulates turns.
Concretely, a thread holds:
| What's in a thread | Why it's there |
|---|---|
| The full message history | Every user message and every agent reply, in order โ the "notebook." |
| Tool calls & their results | So the agent remembers it already looked something up and what it found (recall the agent loop from Session 3). |
| Running state / scratch data | Things the agent is keeping track of mid-task (e.g. a partly-filled form, a running total). |
| An identifier | A thread_id so you can save it, reload it later, and keep different users' conversations separate. |
The model forgets; the thread remembers. Continuity in a chat is an illusion created by replaying the thread's stored history to the stateless model on every single turn. The thread is the memory.
How the SDK uses a thread, step by step
When you send a new message on an existing thread, the SDK does roughly this:
Notice step 5: the reply is written back into the thread. That's what makes the next turn aware of this one. Without a thread, you'd be back to the forgetful friend.
Watch how a thread carries context so the agent can resolve "it" and "that one":
- Turn 1. User:
"What's the capital of Japan?"
Agent:"Tokyo."โ thread now holds both lines. - Turn 2. User:
"How many people live there?"
The word "there" means nothing on its own. But because the SDK re-sends the whole thread, the model sees Turn 1 and knows "there" = Tokyo.
Agent:"About 14 million in the city proper." - Turn 3. User:
"And compared to Osaka?"
Again, "compared" only makes sense with the prior turns in view. The thread supplies them, so the agent gives a sensible comparison.
Start a brand-new thread and ask only Turn 3 โ the agent has no idea what you're comparing. That gap is exactly what the thread fills.
A thread can grow forever, but the model's context window (Session 1) is a fixed size. Once a thread's history is longer than the window, something has to give โ usually the oldest turns get trimmed or summarised before sending. So a thread is the full record; what reaches the model each turn is whatever fits. (Smarter ways to handle this are exactly what Session 9 on memory systems is about.)
Because the whole thread is re-sent every turn, long threads cost more per message (more input tokens โ recall token economics in Session 1) and can eventually overflow the context window. Don't keep one giant thread running forever for unrelated topics; start fresh threads when the conversation truly changes subject.
2 What is tracing?
Imagine your friend goes to the shop to buy you a snack, but comes back with the wrong thing. You ask "what happened?" and they say "...I dunno." Useless! Now imagine they came back with a receipt and a little diary: "I went to aisle 3, picked the blue box, paid $2, it took 5 minutes." Now you can see exactly where it went wrong. Tracing is giving your agent that diary โ a record of every single thing it did on the way to its answer.
What tracing captures
Tracing means automatically capturing a structured, time-ordered record of everything that happened during one agent run (one full attempt to handle a request, which may involve many model calls and tool calls โ the agent loop from Session 3). A good trace records:
- Every model call โ the exact prompt sent, the response, the model used.
- Every tool call โ which tool, what arguments went in, what came back.
- Inputs and outputs at each step, so you can read the data flowing through.
- Timing โ how long each step took (latency).
- Token usage & cost โ how many input/output tokens each model call burned.
- Errors โ anything that failed, with the error message.
Two key words: span and trace
These two words are the whole vocabulary of tracing, so let's nail them:
| Term | What it is | Everyday analogy |
|---|---|---|
| Span | A record of one single operation โ one model call, or one tool call. It has a start time, an end time, inputs, outputs, and a status. | One line item on the receipt. |
| Trace | The whole tree of spans for one agent run, from the user's request to the final answer. Spans can nest inside spans (a model call that triggers a tool call that triggers another model call). | The entire receipt + diary for one shopping trip. |
So a trace is made of many spans, often nested like a tree. Reading a trace top-to-bottom is like replaying the agent's whole thought process in slow motion.
A user asks an agent: "What's the weather in Paris and should I bring an
umbrella?" The trace (simplified) might look like this tree of spans:
TRACE: "weather in Paris" run (total 2.3s, 1,840 tokens)
โโ SPAN model_call #1 (0.6s) LLM decides to call a tool
โ input: user question + system prompt
โ output: tool_call get_weather(city="Paris")
โโ SPAN tool_call get_weather (0.9s)
โ input: {"city": "Paris"}
โ output: {"temp_c": 14, "rain": true}
โโ SPAN model_call #2 (0.8s) LLM writes the final answer
input: history + tool result
output: "It's 14ยฐC and raining in Paris โ yes, bring an umbrella!"
Every step is visible: what the model decided, what the tool returned, how long each took. If the answer were wrong, you could point at the exact span where it went off.
Why you can't debug agents with print statements alone
When you wrote normal programs, sprinkling print() statements was often
enough. Agents break that approach for several reasons:
- They're non-deterministic. The same input can produce different steps each run (recall temperature, Session 1). A bug might appear only 1 time in 20 โ a print you happened to add won't be there next time it strikes.
- They loop and branch. An agent might call a tool, reconsider, call another, loop back (Session 3's agent loop). Flat printed lines make it almost impossible to see which step led to which.
- The data is huge. Full prompts, tool outputs, and histories are enormous. Dumping them as raw prints turns your console into an unreadable wall of text.
- You need structure and timing. "Which step was slow? How many tokens did step 3 cost?" Prints don't measure that; spans do, automatically.
- It happens in production. You can't add prints to a bug a user hit yesterday. But if tracing was on, the trace is already saved and waiting for you.
Tracing is to agents what an X-ray is to a doctor. Without it you're guessing from the outside; with it you can see the skeleton of the whole run. In the Agent SDK, tracing is usually a built-in feature you turn on โ you don't have to hand-write all those spans yourself.
3 Debugging agent workflows
If your toy robot ends up in the wrong room, you don't just shrug โ you follow its footprints backwards: "it turned left here, then went straight there... ah, it turned the wrong way at the kitchen!" A trace is the robot's footprints. Debugging an agent is just following those footprints back to the exact step where it took a wrong turn.
The common ways agent runs go wrong
From Session 3 you already met agent failure modes. Traces are how you actually spot which one happened. The usual suspects:
| Failure | What it looks like | How the trace reveals it |
|---|---|---|
| Wrong tool | Agent used the calculator when it should have searched the web. | A tool-call span names the wrong tool. |
| Bad arguments | Right tool, but garbage inputs (e.g. searched "wether" instead of "weather", or passed the wrong city). | The tool-call span's input field shows the bad args. |
| Looping | Agent calls the same tool over and over, never finishing. | The trace shows the same span repeating many times. |
| Bad retrieval | In a RAG agent, it fetched the wrong documents, so the answer is built on the wrong facts. | The retrieval span's output shows irrelevant chunks. |
| Tool error swallowed | A tool failed, but the agent carried on as if it succeeded. | A span has an error status, yet the next model call ignores it. |
| Model misreads | Tool returned the right data, but the model summarised it wrong. | The final model-call span: good input, wrong output. |
A step-by-step debugging walkthrough
Here's the reliable routine for using a trace to find where a run went wrong. Always work from the symptom back toward the cause:
- Reproduce or locate the run. Find the trace for the exact failed request (by thread_id, user, or timestamp). With tracing on, it's already saved.
- Read the final answer span first. Confirm what actually went wrong โ wrong fact? Cut off? Refused? This tells you what kind of failure to hunt for.
- Walk backwards through the spans. Look at the step just before the final answer. Was the input to that step already wrong? If yes, the bug is earlier; keep going back. If the input was good but the output was bad, you've found the broken step.
- Inspect inputs vs outputs at the suspect span. This is the heart of it: at each span, ask "given this input, is this output reasonable?" The first span where a good input produced a bad output is your culprit.
- Check tool calls closely. Right tool? Right arguments? Did the tool return what you expected, or an error/empty result the agent then misused?
- Check for loops. Scan for repeated spans. If the agent kept retrying, look at why each attempt "failed" in its eyes.
- Form a fix and re-run. Adjust the prompt, tool description, or logic โ then run the same input again and compare the new trace to the old one.
An agent that books meetings replied: "I've scheduled your meeting for
3pm." But the user wanted 10am. You open the trace and walk backwards:
- Final span (model_call #2): output says "3pm". Its input contained
{"time": "15:00"}from the tool result. So the model didn't invent 3pm โ it faithfully reported what the tool gave back. Bug is earlier. - Tool span (book_meeting): input was
{"time": "15:00"}. But the user said 10am! So the wrong time was sent into the tool. Bug is even earlier. - First span (model_call #1): the model read "10am" and produced the tool
call with
"15:00". Found it. The model mis-converted "10am" โ likely confused by an ambiguous timezone instruction in the system prompt.
Fix: clarify the system prompt about timezones / 12-vs-24-hour format, re-run
the same request, and confirm the new trace shows {"time": "10:00"}.
Don't fix the symptom. Here it would be tempting to "fix" the final answer wording โ but the real bug was three spans upstream. Always trace back to the first step where good input became bad output; fixing anything later just hides the problem.
4 Observability practices
A car has three kinds of "tell me what's happening" gadgets. The warning light that blinks when something specific happens ("door open!") โ those are logs. The dashboard dials showing speed and fuel as numbers โ those are metrics. And imagine a magic flight recorder that saved your whole journey turn by turn โ that's a trace. Observability just means having all three so you always know how your agent is doing.
The three pillars: logs vs metrics vs traces
Observability is the practice of being able to understand what your system is doing from the outside, without guessing. It rests on three kinds of signal โ people often confuse them, so here's the clean version:
| Signal | What it is | Answers the question | Example |
|---|---|---|---|
| Logs | Individual timestamped event messages. | "What happened, and when?" | 2026-06-20 10:02 ERROR weather_tool timed out |
| Metrics | Numbers aggregated over time โ counts, averages, rates. | "How much / how often / how fast, overall?" | "Average latency = 2.1s; error rate = 3%." |
| Traces | The full step-by-step story of one request (Topic 2). | "What exactly happened in this one run?" | The Paris-weather span tree above. |
They work together: a metric tells you something is wrong ("error rate jumped"), logs tell you roughly what ("weather tool timing out"), and a trace lets you see one bad run in full detail to confirm the cause.
The numbers that matter for agents
For LLM agents specifically, three metrics dominate everything:
| Metric | What it measures | Why you care |
|---|---|---|
| Latency | How long a run (or a step) takes. | Slow agents feel broken; users leave. Agents are extra slow because they make multiple model + tool calls per answer. |
| Token cost | Input + output tokens per run, in money. | Every model call in the loop costs tokens (Session 1). A chatty agent can be shockingly expensive at scale. |
| Error rate | Fraction of runs that fail or give bad answers. | Your headline quality number. A rising error rate is the first sign something broke. |
You ship a new version of your agent's prompt on Tuesday. On Wednesday a dashboard shows average tokens per run jumped from 1,800 to 4,500 and latency doubled. The metrics raised the alarm. You open a few traces from Wednesday and discover the new prompt caused the agent to call the search tool 3ร per answer instead of once. Metrics found the problem; traces explained it. You roll the prompt back (prompt versioning, Session 1) and the numbers recover.
Tools you'll hear about
You don't build observability from scratch. There are dedicated platforms โ at a high level:
- SDK tracing dashboards โ many agent frameworks (including the Agent SDK) ship a built-in trace viewer. You turn tracing on, run your agent, and view the span trees in a web UI.
- LangSmith and similar LLM-observability tools โ specialised dashboards for inspecting traces, comparing prompt versions, tracking token cost and latency, and running evaluations over many runs.
- General observability stacks (the kind used for any software) can ingest logs and metrics too, but the LLM-specific tools understand prompts, tokens, and tool calls out of the box.
The specific tool matters far less than the practice. Whatever you use, you want the same three things: searchable traces of individual runs, dashboards of the key metrics, and alerts when a metric crosses a threshold.
What to monitor in production
Once your agent is live and real users depend on it, watch at minimum:
- Latency (and percentiles โ the slowest 5% of users, not just the average).
- Token cost per run and total daily spend (catch runaway loops before the bill does).
- Error rate and tool-failure rate (which tools fail most?).
- Loop / step-count โ how many model+tool steps an average run takes; sudden jumps mean trouble.
- User feedback signals โ thumbs up/down, retries, abandonment.
- A sample of full traces kept for spot-checking and for debugging reported bugs.
Traces contain real user inputs and outputs, which may include personal or sensitive data. In production, be careful what you store, who can view it, and for how long โ redact secrets and follow your data-handling rules. Observability is powerful, but it's still real user data.
5 Failure investigation: a case study
A detective doesn't guess who did it โ they follow the clues from the scene of the crime backwards until they find the real culprit. When an agent gives a wrong answer, you become the detective, and the trace is your pile of clues. Let's solve one mystery together, start to finish.
The case: a confidently wrong answer
You run a customer-support agent for an online store. It has a tool
lookup_order(order_id) that fetches order details, and it uses
RAG retrieval over the store's policy documents. A customer complains:
Customer asked: "Can I return order #A-4471? It arrived broken." The agent confidently replied: "Sorry, order #A-4471 is outside the 30-day return window and cannot be returned." But the order arrived three days ago โ it absolutely should be returnable. The agent gave a wrong answer, and an angry customer.
The investigation process
Here's the repeatable flow a good investigation follows โ the same skeleton every time:
Following the clues
We pull up the trace for that exact run and walk it backwards, applying the debugging routine from Topic 3:
| Span (latest โ earliest) | Input | Output | Verdict |
|---|---|---|---|
| model_call #2 (final answer) | order data + retrieved policy text saying "30-day window from order date" | "outside the 30-day window, cannot return" | Output matches its input โ not the root cause. Look earlier. |
| retrieval span (RAG) | query: "return policy" | fetched the old 2023 policy doc (30 days from order date) | ๐ฉ Wrong document โ the current policy is 30 days from delivery date. Suspicious. |
| tool_call lookup_order | {"order_id": "A-4471"} | {"ordered": "2026-04-10", "delivered": "2026-06-17"} | Tool worked fine. Note: ordered long ago, delivered 3 days ago. |
| model_call #1 | user question | plans: lookup order, then retrieve policy | Reasonable plan โ fine. |
Two things combined. First, the agent retrieved an outdated policy document (30 days from order date) instead of the current one (30 days from delivery date) โ a bad retrieval failure. Given that wrong policy, the final model call reasoned correctly from wrong facts: ordered 2026-04-10 is indeed >30 days ago, so it said "no return." The model wasn't broken; it was fed bad source material.
Fix: the root problem is in retrieval, so we fix retrieval โ remove/replace the stale 2023 policy doc in the knowledge base so only the current "30 days from delivery" policy can be retrieved. (We might also tighten the system prompt to always reason from delivery date.)
Verify: re-run the same question and read the new trace. Now the retrieval span returns the current policy, the final model call sees "30 days from delivery (2026-06-17)", and the answer becomes: "Yes โ your order was delivered 3 days ago, well within our 30-day return window. Here's how to start a return." The new trace confirms the fix at the exact span that was broken. Case closed.
From the outside, this looked like "the model hallucinated." But the trace proved the model reasoned correctly โ the real fault was a stale document in the knowledge base. Without the trace you might have wasted days "fixing" the model with prompt tweaks, while the true bug sat quietly in your data. That's the whole point of this session.
โ Putting it all together
This session gave you the two things that turn an agent from a fragile demo into something you can run for real. Here's the one-paragraph story that ties it together:
Because the model is stateless (Session 1), a thread stores the full conversation and re-sends it each turn so the agent appears to remember. To see what the agent did, you turn on tracing, which records a trace made of spans โ every model call and tool call with its inputs, outputs, timing, and tokens. When something goes wrong, you debug by walking the trace backwards to the first step where good input produced bad output (recall the failure modes from Session 3). Across many runs, observability โ logs, metrics, and traces, watching latency, token cost, and error rate โ tells you the health of the whole system. And when a real user hits a bug, a disciplined failure investigation using the saved trace takes you from "it gave a wrong answer" all the way to the true root cause โ which, as we saw, is often the data, not the model.
Quick self-check
The model is stateless, so how does an agent "remember" earlier turns?
It doesn't โ the thread does. The thread stores the full conversation history and the SDK re-sends it to the model every turn, creating the illusion of memory.
What's the difference between a span and a trace?
A span is the record of one operation (one model call or one tool call) with its input, output, timing, and status. A trace is the whole nested tree of spans for one complete agent run.
Why aren't print statements enough to debug an agent?
Agents are non-deterministic (bugs appear intermittently), they loop and branch, the data is huge, you need timing/token info, and bugs happen in production where you can't add prints after the fact. Tracing handles all of that automatically.
When debugging, where in the trace is the real bug usually located?
At the first span where a good input produced a bad output. Walk backwards from the final answer; the symptom is at the end, but the root cause is upstream. Never patch the symptom.
An agent gave a wrong answer that looked like a hallucination. The trace showed the final model call reasoned correctly. Where was the bug?
Earlier in the run โ in our case study it was bad retrieval (a stale policy document). The model reasoned correctly from wrong facts. The fix was in the data, not the model โ which only the trace could reveal.
Name the three key metrics to watch for an agent in production.
Latency (how slow), token cost (how expensive), and error rate (how often it fails). Metrics raise the alarm; traces explain the cause.
๐ References & Further Reading
Class material
- ๐ Original course notes / handout (source sheet) โ open the shared GenAI class material for this session.
- Class handout: "Threads & Tracing in Agent SDK".
Papers, docs & deep dives
- ๐ LangSmith documentation โ tracing & observability โ inspecting traces, comparing prompt versions, and tracking cost/latency.
- ๐ OpenAI Agents SDK โ Tracing โ built-in traces and spans for agent runs (Topic 2).
- ๐ OpenTelemetry documentation โ the open standard for spans, traces, logs, and metrics (Topic 4).