Index Ai Academy
Phase 10 · Agent harnesses · Scaling & shipping

Tracing, budgets and guardrails

See what an agent did, cap what it spends, and stop it when it loops.

About 35 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

In production an agent runs thousands of times a day with no one watching. When a user reports "it did something weird", you need to see exactly what happened. When a bug makes it loop, you need it to stop before it spends a fortune. Tracing, budgets and guardrails turn a clever demo into a service a team can run and trust.

What you'll be able to do

  • Trace an agent run as nested spans: the run, each model call and each tool call
  • Track tokens and cost per run, and stop at a budget
  • Detect an agent stuck repeating the same call
  • Plan the operational side of shipping a harness: logging, privacy, rollout and monitoring

What it covers

  1. Traces and spans
  2. Reading a trace
  3. Budgets
  4. Loop detection
  5. The same call, three times
  6. Shipping a harness
  7. Cost from usage
  8. A turn budget is the last resort

Traces and spans

After a plane has trouble, investigators read the flight recorder to see exactly what happened and when. A trace is that record for one agent run. It's made of spans: timed steps, each with a parent.

run "fix failing test"                 41.2 s   $0.183
├── model call 1                        3.1 s   in 4,210  out 312
├── tool read_file src/cart.py          0.01 s
├── model call 2                        2.8 s   in 5,060  out 158
├── tool bash "pytest -x"               6.4 s   exit 1
└── ...

Each span records its name, start and end time, its parent, and attributes: tokens, stop reason, tool name, exit code, error. Standards such as OpenTelemetry, with its conventions for generative AI, let you send these to common observability tools. Many LLM tracing products accept the same data.

What to record, and what not to:

  • Do: IDs, timings, token counts, costs, tool names, stop reasons, errors, the harness and model versions.
  • Careful: full prompts and tool outputs. They help debugging most, but may hold personal data, secrets or customer code. Redact, sample, restrict access, and set a retention period.
Worked example

Reading a trace

A trace is a tree of timed steps. Print one and the slow part stands out straight away.

spans = [
    ("agent turn", 0, 8200),
    ("  model call", 1, 1100),
    ("  tool: run_tests", 1, 6700),
    ("    install packages", 2, 5900),
    ("    pytest", 2, 780),
    ("  model call", 1, 400),
]

total = spans[0][2]
for name, depth, ms in spans:
    print(f"{name:<24}{ms:>6} ms {'#' * round(30 * ms / total)}")
It runs right here in your browser, with nothing to install.

Without a trace you might blame the model. Here most of the turn goes to installing packages, inside one tool, and that's what you'd cache. You can't fix what you can't see.

Budgets

A prepaid phone card can't run up a surprise bill. Every run needs limits that stop it cleanly in the same way:

budgetstops
turnsloops that never converge
tokens or dollarsexpensive runs, such as huge files read again and again
wall-clock timeruns stuck waiting on slow tools
tool calls of one kindfor example, at most 3 deploys

Work out cost from usage on each response. Cache reads cost much less than normal input, and cache writes a little more, so count them separately. Prices change; keep them in config, not code.

Try it now

At $4 and $20 per million tokens. Long conversations resend everything each turn, which is why cache reads matter so much, and why every run needs a budget.

Quick check
An agent's run costs 40 times more than usual. Which record helps you find out why fastest?
  1. The number of users that day
  2. A trace with tokens per model call and the tool calls between them
  3. The final answer
  4. The system prompt
Show the answer

A trace with tokens per model call and the tool calls between them

Per-span tokens show where the cost came from, such as one tool output read into context fifty times.

Loop detection

A common failure: the agent calls the same tool with the same input, gets the same error, and tries again. And again. It's a fly hitting the same window. A turn budget eventually stops it, but expensively. Better: notice repetition. Keep a fingerprint of each call (tool name plus its input as sorted JSON). If the same fingerprint appears, say, 3 times in the last 10 calls, step in:

  1. First, tell the model in the tool result: "You've made this exact call 3 times with the same result. Try a different approach."
  2. If it carries on, stop the run with a clear status.

In plain words: if the agent keeps doing the same thing and getting the same result, tell it. If that doesn't help, stop it.

Worked example

The same call, three times

Here's the fingerprint idea from this section. The call is the same if the tool and its input match, whatever order the input's keys came in.

import json
from collections import Counter

def fingerprint(tool, args):
    return tool + ":" + json.dumps(args, sort_keys=True)

def watch(calls, window=10, limit=3):
    recent = []
    for n, (tool, args) in enumerate(calls, 1):
        recent = (recent + [fingerprint(tool, args)])[-window:]
        if Counter(recent)[recent[-1]] >= limit:
            return f"step in at call {n}: {tool} repeated {limit} times"
    return "no loop"

calls = [("read", {"path": "a.py", "limit": 10}), ("edit", {"path": "a.py"}),
         ("read", {"limit": 10, "path": "a.py"}), ("run", {"cmd": "test"}),
         ("read", {"path": "a.py", "limit": 10})]
print(watch(calls))
print(watch(calls[:4]))
In the lesson you can change this and run it.

The third read has its keys in a different order and still counts, because the JSON is sorted first. This catches a stuck agent after a few calls, not after your turn budget and a large bill.

Shipping a harness

A restaurant tries a new dish on a few tables before it goes on the menu. Ship agents with the same care:

  • Version everything: system prompt, tool definitions, model and harness code. Record versions on every trace, so you can tell which change caused what.
  • Run the eval suite before each release (previous lesson).
  • Roll out gradually: a small share of traffic first, and compare success, cost and complaints.
  • Monitor: success rate, cost per task, turns per task, error and refusal rates, and loop stops. Alert on sudden changes.
  • Plan for failure: what does the user see when the API is down, a budget is hit, or a run is stopped? Clear statuses beat silent failures.
Quick check
The loop guard sees read_file with the same input three times in its window. What should the harness do first?
  1. Raise the turn budget
  2. Stop the run at once without a word
  3. Tell the model in the tool result that it's repeating itself; stop the run if it carries on
  4. Ignore it
Show the answer

Tell the model in the tool result that it's repeating itself; stop the run if it carries on

A nudge often breaks the loop cheaply. A firm stop comes next if it doesn't.

Worked example

Cost from usage

Count each kind of input token at its own rate. These prices are made up for the example.

PRICES = {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25}  # $ per million tokens

def cost(usage):
    return (usage["input_tokens"] * PRICES["input"]
            + usage["output_tokens"] * PRICES["output"]
            + usage.get("cache_read_input_tokens", 0) * PRICES["cache_read"]
            + usage.get("cache_creation_input_tokens", 0) * PRICES["cache_write"]) / 1_000_000

first = {"input_tokens": 300, "output_tokens": 400, "cache_creation_input_tokens": 20_000}
later = {"input_tokens": 300, "output_tokens": 400, "cache_read_input_tokens": 20_000}
print(f"first call  ${cost(first):.4f}")
print(f"later calls ${cost(later):.4f}")
In the lesson you can change this and run it.

Once the prefix is cached, later calls cost far less. That's why a stable prefix matters so much for agents.

Quick check
Prices are dollars per million tokens. What does this print?
prices = {"input": 3.0, "output": 15.0}
usage = {"input_tokens": 2000, "output_tokens": 1000}
cost = (usage["input_tokens"] * prices["input"] + usage["output_tokens"] * prices["output"]) / 1_000_000
print(round(cost, 3))
  1. 0.021
  2. 0.0021
  3. 21.0
  4. 0.018
Show the answer

0.021

2,000 × $3 plus 1,000 × $15 is $21 per million tokens of each, so $0.021 for this call.

A turn budget is the last resort

A building has smoke alarms and sprinklers as well as fire doors. A budget will stop a stuck agent eventually, but "eventually" might be $20 and 100 turns later. Loop detection catches it after three identical calls, and a cost limit stops expensive runs whatever they're doing. Use all of them, in layers.

Logs are full of secrets

Agent logs are unusually sensitive. Full prompts contain whatever the agent read: customer records, private code, or keys it found in a config file. So decide on redaction and a retention limit before you start logging, not after. Store the sensitive parts separately, with stricter access, and delete them on a schedule. Metadata such as token counts, tool names and timings can usually be kept for longer.

Prices belong in config

Prices change, and new models arrive. If they're hard-coded across your harness, your cost reports silently drift from reality. Keep one table in config, and record the model id on each span, so that you can recalculate old runs if you need to.

Treat prompts as code

A one-line change to a system prompt can change an agent's behavior more than a thousand lines of code. Yet people often edit prompts directly in production, with no review and no test. So keep prompts in version control. Run your eval suite on every change, and roll out in stages, just as you would with code. Include the prompt version on every trace, so that when behavior shifts, you can tell which change caused it.

When a shipped agent starts misbehaving, ask first what changed: the prompt, the tools, the model, or the data it reads. Those four cover almost every regression you'll see, and your traces should be able to answer it.

Practice what you just read

  • 3 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

What is a span in a trace?

  1. One timed step, such as a model call or a tool call, with a parent and attributes
  2. The whole agent run's final answer, saved along with how long it took
  3. A type of token, billed at its own rate and counted apart from the others
  4. A log line with no timing, recording that a step happened and nothing else

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson