~12–16 hrs
Requires: Project 06
Project 08 of 9

AI Agent

Project 06's loop, made safe to leave running — budgets, stuck detection, recovery, and the judgment to know when not to use an agent at all

📖 Read the lesson ✍ Fill in understanding 🛠 Build the project 🚫 Break it 📊 Evaluate it

Learning Objectives

By the end of this project, you will be able to:

  • State the workflow vs. agent distinction — predefined code paths vs. the LLM directing its own process — and decide which a task needs.
  • Explain why an autonomous, multi-step loop is fundamentally riskier than P06's single-question copilot: the model now controls how many steps, which tools, and when to stop.
  • Implement loop/stuck detection, and explain why a max_steps cap alone misses an agent that oscillates with no progress.
  • Implement a budget over steps and tokens, and explain why an autonomous agent without one is a runaway-cost incident waiting to happen.
  • Implement tool-error recovery — catch a failing tool, feed the error back as an observation, let the model adapt — and why crashing wastes every prior step.
  • Evaluate an agent run quantitatively — completion, steps, tool calls, efficiency — instead of eyeballing "it seemed to work."
  • Apply "add complexity only when it demonstrably improves outcomes" — name tasks where a single call or a fixed workflow beats an agent.
  • Name the major failure modes of an autonomous agent (runaway cost, productive-looking oscillation, one tool error killing the run, agent-as-overkill) and which decision prevents each.

1. Motivation

Why This Exists

Project 06's copilot answers one question: it reads a few files and replies. But "refactor this module and make the tests pass," "research this topic and write a summary," or "triage this bug" are not one question — they are open-ended tasks where you "can't predict the required number of steps." That is exactly what an agent is for: the model plans, acts, observes, and decides for itself when it's done. But the moment you hand the model control over the loop, you hand it control over your bill, your correctness, and whether the program ever terminates.

The Core Problem

A bare autonomous loop has three ways to ruin your day the happy-path demo never shows: it gets stuck (calling the same tool with the same arguments forever); it runs away (80 steps and $4 of tokens on a 4-step task); and it dies on one bad tool call (a single tool raises and the whole multi-step run is lost). A reliable agent detects the stuck state, enforces a budget, and recovers from tool errors. The fourth problem is the meta one: engineers reach for an agent when a single function call would have been faster, cheaper, and more reliable.

What Breaks Without It

The single hardest thing in AI engineering is not making an agent that works on the happy path — it's making one that fails safely on every other path. "Users forgive bad suggestions, but not autonomous actions that cause damage." A copilot that suggests a wrong line costs you nothing; an agent that loops 200 times overnight costs real money, and one that acts without a guard is an incident.

Real-World Stakes

This is the product category everyone is racing to ship: autonomous coding agents, research agents, computer-use agents, customer-ops agents. Every production agent has the machinery you build here — step/token budgets, loop detection, retries with recovery, and an evaluation harness — because without it the agent is a liability, not a feature. "The most successful implementations use simple, composable patterns."

Startup Lens

"A coding agent that saves 2 hours per day at $100/hour is worth $500/month to a developer." But the value is gated entirely on reliability: an agent that occasionally burns $20 and produces nothing destroys the trust the $500/month depends on. The reliability layer isn't polish on the product — it is the product. The model is a commodity; the discipline around the loop is the moat.

2. Mental Model

Explain Like I'm 12

In Project 06 you gave a friend who can't see your computer some buttons to press ("show me a file," "search"), and they pressed buttons until they answered one question. Great for one question. Now give that friend a whole chore — "clean up my messy folder" — and leave the room. Three bad things can happen. One: they get stuck, opening the same drawer over and over, forever. Two: they never stop — they work for ten hours and you get a giant bill. Three: one drawer is jammed, and instead of skipping it they give up on the whole chore. A good helper notices "I keep opening this same drawer — let me stop," has a rule like "you get 20 minutes," and when a drawer jams says "okay, skip it, I'll do the rest." This project teaches your friend those three habits — and teaches you the wisdom to not send a friend to do a chore you could've done yourself in one move.

Explain Like I'm a Software Engineer

  • An agent is P06's loop — call model → dispatch tools → feed results back → repeat — but for open-ended tasks. The loop, tools, parsing, and sandbox are provided (you built them in P06). The learning target is the control layer.
  • Stuck detection (detect_stuck(history)): keep a history of (tool, arguments) actions; if the last N are identical, stop now — don't wait for max_steps.
  • Budget (BudgetTracker): a hard ceiling on steps and tokens, tick()'d each step and checked with over_budget(). It's what makes "autonomous" not mean "unbounded."
  • Recovery: dispatch each tool inside try/except; on failure, feed f"Error: {e}" back as the observation. "Reasoning traces help the model handle exceptions" — it reads the error and adapts.
  • Evaluation (evaluate_run): a run produces a RunResult (answer, history, steps, tokens, stop_reason). Score it: did it complete? how many steps vs. optimal? at what cost?
  • Judgment: before any of this, ask whether the task needs an agent at all. "Find the user's email" is a function call. "Summarize this PDF" is one retrieval-augmented call (P04). The loop is for tasks whose step count you genuinely can't predict.

Real-World Analogy

Analogy

An agent is a self-driving delivery vehicle, where P06's copilot was cruise control. Cruise control does one bounded thing on demand and hands back control. A self-driving vehicle is given a destination and decides the whole way — enormously more useful and more dangerous. So you don't ship it without a fuel gauge that forces a stop (the budget), a "you've circled this block 5 times, pull over" detector (stuck detection), the sense to route around a closed road instead of parking forever (recovery), and a trip log you can audit (evaluation). And the wisest dispatcher knows that to move a box across the room, you carry it — you don't summon the vehicle (the single call).

How It Works (Diagram)

 TASK: "What model does this repo default to, and where is it set?"  (multi-step)
   │
   ▼  run_agent(budget = steps≤10, tokens≤20k)
   step 1: model ─► search_code("default_model")     ── budget.tick ── history=[search]
   step 2: model ─► read_file("config.py")            ── budget.tick ── history=[search, read]
           (tool raises? → caught → "Error: ..." fed back → model adapts)   ← RECOVERY
   step 3: model ─► (no tool call) "Defaults to groq/llama-3.3-70b; config.py:24." ─► RETURN answered

 vs. a BROKEN run on a confusing task:
   step 1..3: model ─► search_code("x") x3            ← detect_stuck → STOP (stuck)
   ... or 50 productive-looking steps with no end     ─► budget.over_budget() → STOP (budget)

 evaluate_run(result, expected={answer_contains:"groq", optimal_steps:3})
   ─► {completed: True, steps: 3, tool_calls: 2, efficiency: 1.0, stop_reason: "answered"}

3. Technical Explanation

Formal Definition

A workflow orchestrates LLMs and tools "through predefined code paths." An agent is a system where "LLMs dynamically direct their own processes and tool usage." An agent run is a sequence of steps; each step is one model call that yields a final answer or one-or-more tool calls. The run has a stop reason ∈ {answered, stuck, budget, max_steps}. A run is stuck when the last N actions are identical (tool, arguments). A run is over budget when cumulative steps or tokens cross a pre-set ceiling. Recovery converts a raising tool into an error observation fed back into the conversation.

How It Works Step by Step

  1. Decide if you even need a loop. Known, fixed shape (one lookup, one summary, classify-then-route)? Use a single call or a workflow. Reach for the agent only when "it's difficult or impossible to predict the required number of steps."
  2. Set a budget. Construct a BudgetTracker(max_steps, max_tokens) before the run — the contract for what the agent may spend.
  3. Run the loop. Each iteration: call the model, tick the budget with the step's tokens, parse_tool_calls (provided). No calls → answered. Otherwise append the assistant turn, then for each call: record the action, dispatch_tool it inside try/except (recovery), append the result/error as a tool message.
  4. Guard after each step. detect_stuck(history) → stop (stuck). budget.over_budget() → stop (budget). Loop exhausts its structural bound → max_steps.
  5. Evaluate the run. Pass the RunResult + an expected spec to evaluate_run: completion, steps, tool calls, efficiency. Now a prompt or model change can be scored, not guessed at.

Key Concepts

ConceptDefinitionWhy It Matters
Workflow vs. agentPredefined code paths vs. the LLM directing its own process and tool usageThe first design decision: an agent is more capable and more failure-prone
Multi-step autonomyThe model decides how many steps, which tools, and when it's doneEvery one of those decisions is a thing it can get wrong — the source of all the failure modes
Loop / stuck detectionDetecting the agent is repeating the same action with no new informationA max_steps cap stops a runaway eventually; stuck detection stops a no-progress agent immediately
Budget (steps + tokens)A hard ceiling, checked every step, that ends the run when crossedThe model controls the loop, so an unbounded autonomous agent is an open-ended bill
Tool-error recoveryCatch a failing tool, feed the error back as an observation, let the model try againOne bad tool call shouldn't discard every step before it
Agent evaluationScoring a run: completion, steps/tool-calls, efficiency, cost"It worked once in the demo" is not evaluation — you need numbers to detect a regression
"Simplest thing that works"Add agentic complexity only when it demonstrably beats a single call or a workflowThe most valuable agent skill is knowing when not to build one
Common Misconception

max_steps is a backstop, not the primary guard. It bounds the absolute worst case. The primary guards are detect_stuck (catches no-progress immediately) and the token budget (catches expensive-but-progressing runs). An agent that hits max_steps every time is one you haven't tuned. And recovery means feeding the error back, not swallowing it: a bare except: pass is worse than crashing — the model never learns the tool failed and may repeat it.

4. Guided Examples

The lab stack: litellm (chat + tools via config.py), the provided P06 tool layer (safe_resolve, parse_tool_calls, dispatch_tool, file tools), and a pure-Python loop. Examples mirror the guiding tests.

Example 1: Simplest Case — detect a stuck agent

from safety import Action, detect_stuck

# An agent making progress: three DIFFERENT actions → not stuck.
progress = [Action("search_code", {"query": "x"}),
            Action("read_file", {"path": "a.py"}),
            Action("read_file", {"path": "b.py"})]
detect_stuck(progress, window=3)   # -> False

# An agent spinning: the last three actions are IDENTICAL → stuck.
spinning = [Action("read_file", {"path": "a.py"}),
            Action("search_code", {"query": "foo"}),
            Action("search_code", {"query": "foo"}),
            Action("search_code", {"query": "foo"})]
detect_stuck(spinning, window=3)   # -> True
What to Observe

Stuck detection compares whole actions (tool and arguments), not just the tool name — an agent reading ten different files is working, not stuck. This catches the no-progress case immediately, long before a max_steps cap would.

Example 2: Real-World Case — enforce a budget

from safety import BudgetTracker

b = BudgetTracker(max_steps=5, max_tokens=10_000)
b.tick(2_000)          # step 1
b.tick(2_000)          # step 2
b.over_budget()        # -> None        (2 steps, 4k tokens — within budget)

b.tick(7_000)          # step 3, now 11k tokens
b.over_budget()        # -> "token budget exhausted (11000/10000)"   (a reason string — truthy)
What to Observe

The budget is checked, not assumed. over_budget() returns None while there's room and a reason string once a ceiling is crossed — the loop uses that truthiness to stop and records why. An autonomous loop without this is an open-ended invoice.

Example 3: When It Fails — one tool error must not kill the run

from types import SimpleNamespace
from safety import BudgetTracker
from agent import run_agent
import agent

# Force the (provided) dispatch to RAISE on the first call, as a flaky real tool would:
calls = {"n": 0}
def exploding_dispatch(name, args, repo_root):
    calls["n"] += 1
    if calls["n"] == 1:
        raise RuntimeError("network down")
    return "ok"
agent.dispatch_tool = exploding_dispatch   # monkeypatch for the demo

turns = iter([
    SimpleNamespace(content=None, tool_calls=[_tc("c1", "read_file", '{"path": "a.py"}')]),  # raises
    SimpleNamespace(content="Recovered and answered.", tool_calls=None),                      # adapts
])
result = run_agent([{"role": "user", "content": "go"}], ".", lambda m, t: next(turns),
                   BudgetTracker(max_steps=5, max_tokens=10_000))
print(result.stop_reason)   # 'answered'  — the raise became an observation, the model adapted
Why This "Fails"

The tool raised, but run_agent caught it, fed "Error: network down" back as the observation, and the model recovered on the next step. Without the try/except, that single exception would have discarded the whole run. This is the failure you reproduce in M6 by removing the recovery.

5. Reflection Before Building

Stop Here

Before writing any code, fill in UNDERSTANDING.md in your own words. No copy-pasting from the lesson. No AI assistance for writing — only for checking. If you can't explain it simply, go back and re-read.

Knowledge Check — Answer These in UNDERSTANDING.md

  1. In your own words, what is the difference between a workflow and an agent, and what specifically does an agent gain control of that makes it both more powerful and more dangerous?
  2. Project 06 already bounded its loop with max_steps. So why isn't that enough for Project 08 — what failure does detect_stuck catch that a step cap doesn't, and what failure does a token budget catch that a step cap doesn't?
  3. Walk the recovery path: a tool raises an exception mid-run. What exact steps does run_agent take so the run survives, and why is try/except: pass worse than letting it crash?
  4. Predict the three failure modes of removing, respectively: (a) the budget, (b) detect_stuck, (c) tool-error recovery. Which design decision prevents each?
  5. Give one concrete task you would solve with a single LLM call, one with a fixed workflow, and one that genuinely needs the agent loop. Justify each — why is the loop overkill for the first two?
  6. evaluate_run reports efficiency = optimal_steps / steps. Why is a bare "did it answer?" boolean insufficient for deciding whether a prompt change made your agent better or worse?
  7. The one thing you still don't fully understand about making an autonomous loop safe.

After filling in UNDERSTANDING.md, use the AI mentor pattern in docs/meta/learning-flow.md to get feedback. Record the feedback in UNDERSTANDING_FEEDBACK.md.

6. Project Assignment

See PROJECT.md and source/project.md for the full specification including file structure and extended requirements.

Core Requirement

Build the reliability layer that turns P06's tool loop into a trustworthy autonomous agent, in code/:

  • safety.pydetect_stuck (M1) and BudgetTracker (M2) are the learner core: the no-progress detector and the step/token ceiling.
  • agent.pyrun_agent (M3) is the learner core — the generalized loop with tool-error recovery and the stuck/budget guards. RunResult, parse_tool_calls, and the token estimator are provided.
  • evaluate.pyevaluate_run (M4) is the learner core: score a run on completion, steps, tool calls, and efficiency.
  • tools.py — the entire P06 tool layer. (Provided — you built this in Project 06.)
  • agent_app.py — the orchestrator: a real LiteLLM complete, a budget, run the agent on a multi-step task, print the trace + answer + eval. (Provided.)

Extended: explicit planning (decompose before acting); a reflection loop (evaluator-optimizer); structured final output via a forced emit_answer tool; a real max_usd cost budget; retry-with-backoff on transient tool errors.

Definition of Done

detect_stuck flags identical repeated actions and not different productive ones; BudgetTracker enforces steps and tokens and reports why it stopped; run_agent catches a raising tool and feeds the error back (recovery), appends the assistant turn before the tool results, and returns a distinct stop_reason; evaluate_run reports completion/steps/tool-calls/efficiency; the guiding tests pass; UNDERSTANDING.md done before any code; FAILURE_ANALYSIS.md has ≥3 experiments (incl. the "agent where a call would do" ablation); EVALUATION.md is concrete; STARCALLOS_REFLECTION.md names ≥1 concrete pattern.

Start building

Open code/README.md for setup, the milestone build order, and the file roles (which files are provided vs. learner-owned). Run python -m pytest to see the failing guiding tests, then implement the learner-owned functions in milestone order until they pass.

7. Project Milestones

Work through these in order. Each milestone should produce runnable code before moving on.

1

M1 — Stuck detection

safety.detect_stuck(history, window) — True iff the last window actions are identical. Validation: three different actions → False; three identical → True (offline test).

2

M2 — Budget

safety.BudgetTracker.tick() + .over_budget() — count steps/tokens, return a reason when a ceiling is crossed. Validation: within budget → None; over steps or tokens → a reason string (offline test).

3

M3 — Reliable loop

agent.run_agent(messages, repo_root, complete, budget) — call → dispatch (recover) → feed back → guard → repeat. Validation: tool-then-answer → answered; a raising dispatch → still answered (recovered); an always-same-call model → stuck; an always-tool model → budget/max_steps (offline tests).

4

M4 — Evaluate a run

evaluate.evaluate_run(result, expected) — completion, steps, tool calls, efficiency. Validation: an answered run with the expected substring → completed=True, efficiency≤1.0; a stuck run → completed=False (offline test).

5

M5 — Agent end-to-end

agent_app.py runs the agent on a real multi-step task against a repo. Validation: give it a task; it acts over several steps, answers, and prints the trace + the eval summary.

6

M6 — Break / Evaluate

Remove the budget, remove detect_stuck, remove recovery, and run the agent on a task a single call would solve. Validation: runaway cost / infinite oscillation / one error kills the run / agent is slower-and-costlier-for-nothing — each recorded in FAILURE_ANALYSIS.md.

8. Self-Evaluation

After building, honestly evaluate your implementation against these criteria. Record your answers in EVALUATION.md.

CriterionDoes your implementation...Pass?
Stuck detectionflag identical repeated actions (tool and args), and not flag many different productive steps?
Budget enforcedcheck a steps and tokens ceiling every step, and return why it stopped?
Recoverycatch a raising tool, feed the error back as an observation, and keep looping (not crash, not swallow)?
Distinct stop reasonsreturn answered / stuck / budget / max_steps — never collapse a failure into "done"?
Message orderappend the assistant turn before the tool results (carried from P06)?
Quantitative evalreport completion, steps, tool calls, and efficiency — numbers, not "it seemed to work"?
Right tool for the jobcan you name a task where you deliberately did not use the agent loop, and why?
Red Flags

Your implementation may have problems if:

  • Your loop has no budget and relies on max_steps alone — one expensive step pattern blows your bill.
  • detect_stuck compares only the tool name, so a productive multi-file read gets killed as "stuck."
  • A single tool exception ends the whole run (no recovery), or you except: pass and the model spins.
  • Your run returns a bare True/False — you can't tell a stuck run from a budget stop from a real answer.
  • You used the agent loop for a task that was one lookup or one summary, and it's slower and costlier with no benefit.

9. Common Mistakes

MistakeWhy It HappensConsequenceFix
No budget, only max_steps"P06 capped steps and that was fine"An agent taking few but huge steps blows the token billTrack tokens and steps; check over_budget() every step
detect_stuck compares tool name only"Same tool = stuck"Kills a productive agent reading many different filesCompare the whole action — tool and arguments
try/except: pass on tool errors"Swallow it so it doesn't crash"Model never sees the failure, repeats it → stuckFeed f"Error: {e}" back as the observation
One tool raise kills the runNo try/except around dispatchEvery prior step's tokens wasted on one bad callWrap dispatch; convert the exception into an observation
Collapsing stop reasons to a bool"Did it finish? yes/no"Can't distinguish answered from stuck from budgetReturn an explicit stop_reason; evaluate keys off it
Using an agent where a call would do"Agents are the cool pattern"Slower, costlier, less reliable than one call"Add complexity only when it demonstrably improves outcomes"
Evaluating by eyeballing one demo"It worked when I ran it"A prompt change silently regresses the agentevaluate_run over a fixed task set — numbers, per run
Inventing a token price for the cost budget"Roughly a few dollars per million?"Wrong cost numbers, wrong budget decisionsUse the provider's official per-MTok price or disable cost

10. Connections

Builds On

This is Project 06's loop, made reliable. The tool layer (sandbox, schemas, dispatch), the parsing, and the basic call→dispatch→feed-back cycle are all carried over provided — which is the whole point: you already learned them, so here you spend your effort on the control layer. The budget is Project 01's token-cost arithmetic, enforced live. The evaluation is Project 07's discipline (score, don't eyeball) applied to a run. The optional reflection loop is Project 05's reflection. The agent is where the whole course converges.

Enables

Project 09 (Personal Learning OS) orchestrates memory (P05), retrieval (P03/P04), this agent (P08), and evaluation (P07) into one system. The reliability layer you build here is what makes it safe for P09 to let an agent act over a user's personal data: a bounded, stuck-aware, recoverable agent with an auditable run log. Everything P09 routes to an agent inherits these guards.

Production Patterns

Real agents are this loop plus production engineering: hard budgets on steps/tokens/cost/wall-clock; loop and stuck detection; retries with backoff and graceful recovery; structured, validated final output; a full run trace for observability; and an evaluation harness gating every change. Anthropic's own guidance is to bias toward the simplest pattern that works — single call, then workflow, then agent — and to "invest in the agent-computer interface." The named patterns (prompt chaining, routing, orchestrator-workers, evaluator-optimizer) are the composable pieces; the autonomous loop you build here is the most general and the one to reach for last.

StarcallOS Relevance

StarcallOS Connection

StarcallOS doing anything autonomous on the user's behalf — multi-step research, organizing files, executing a chore across several tools — is this loop. The reliability layer is the difference between a feature and an incident: a budget so it never burns the user's money; stuck detection so it bails out of a no-progress spiral; recovery so one flaky integration doesn't abort a long task; evaluation so a change can be measured, not hoped at; and the judgment to route a simple request to a single call instead of spinning up a loop. P08 is the smallest complete instance of "StarcallOS can be trusted to act."

Sources

See source/resources.md for the complete annotated source list.

Tier 1 — Official Documentation

  • sources/articles/building-effective-agents.md — workflows vs. agents, "the most successful implementations use simple, composable patterns," "add complexity only when it demonstrably improves outcomes," and the named patterns. The source for whether and when to build an agent.
  • sources/official-docs/anthropic-tool-use.md — the tool-use protocol and the agentic loop; tool_choice; why the loop must be bounded (carried from P06, here generalized to a budget).
  • sources/official-docs/anthropic-pricing.md — per-MTok pricing for a real cost budget (don't invent prices).
  • sources/official-docs/litellm-completion.md — the tools/tool_calls shape and usage for live token accounting.

Tier 2 — Foundational Papers

  • sources/papers/react-paper.md — Yao et al. 2022: "reasoning traces help the model handle exceptions" (the basis for recovery) and why a reasoning step makes the loop terminate.
  • sources/papers/mt-bench.md — Zheng et al. 2023: evaluating with numbers, applied here to runs (completion + efficiency).
  • sources/papers/generative-agents.md — Park et al. 2023: reflection — synthesizing a higher-level judgment from raw steps — for the optional reflection loop.

Tier 3 — Engineering Guides

  • None specific to this lesson beyond the Anthropic engineering guidance in Tier 1 ("Building Effective Agents" is itself the primary engineering source for agent reliability patterns).

Tier 4 — Educational Sources

  • None specific to this lesson — the ReAct paper and the Anthropic agent guidance are the primary teaching sources; Project 06 is the conceptual prerequisite for the loop itself.

Optional — Going Deeper

Read these after the reliable loop works. Your agent runs a single linear ReAct trajectory and stops; these four are the principled versions of the extensions you reach for next — self-improvement and alternative control architectures. Treat them as design options, not requirements, and keep "use the simplest thing that works" as the default.

  • sources/papers/reflexion.md (optional — depth) — Shinn et al. 2023: verbal self-feedback stored across attempts — the agent writes a lesson from a failed run and conditions the next on it, no weight updates. The principled version of "retry, but smarter."
  • sources/papers/self-refine.md (optional — depth) — Madaan et al. 2023: generate → self-critique → revise within a single run — the evaluator-optimizer pattern applied to one answer (bridge from Project 07's judge).
  • sources/papers/tree-of-thoughts.md (optional — depth) — Yao et al. 2023: search and backtracking over multiple reasoning paths instead of one linear trajectory — what to reach for when a single ReAct chain gets stuck.
  • sources/papers/rewoo.md (optional — depth) — Xu et al. 2023: plan-execute — plan all tool calls up front, then execute, decoupling reasoning from observation. The architectural contrast to ReAct's per-step interleaving: fewer model calls, different failure modes.