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
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_stepscap 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.
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."
"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 formax_steps. - Budget (
BudgetTracker): a hard ceiling on steps and tokens,tick()'d each step and checked withover_budget(). It's what makes "autonomous" not mean "unbounded." - Recovery: dispatch each tool inside
try/except; on failure, feedf"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 aRunResult(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
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
- 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."
- Set a budget. Construct a
BudgetTracker(max_steps, max_tokens)before the run — the contract for what the agent may spend. - Run the loop. Each iteration: call the model,
tickthe 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_toolit inside try/except (recovery), append the result/error as atoolmessage. - Guard after each step.
detect_stuck(history)→ stop (stuck).budget.over_budget()→ stop (budget). Loop exhausts its structural bound →max_steps. - Evaluate the run. Pass the
RunResult+ anexpectedspec toevaluate_run: completion, steps, tool calls, efficiency. Now a prompt or model change can be scored, not guessed at.
Key Concepts
| Concept | Definition | Why It Matters |
|---|---|---|
| Workflow vs. agent | Predefined code paths vs. the LLM directing its own process and tool usage | The first design decision: an agent is more capable and more failure-prone |
| Multi-step autonomy | The model decides how many steps, which tools, and when it's done | Every one of those decisions is a thing it can get wrong — the source of all the failure modes |
| Loop / stuck detection | Detecting the agent is repeating the same action with no new information | A 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 crossed | The model controls the loop, so an unbounded autonomous agent is an open-ended bill |
| Tool-error recovery | Catch a failing tool, feed the error back as an observation, let the model try again | One bad tool call shouldn't discard every step before it |
| Agent evaluation | Scoring 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 workflow | The most valuable agent skill is knowing when not to build one |
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
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)
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
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
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
- 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?
- Project 06 already bounded its loop with
max_steps. So why isn't that enough for Project 08 — what failure doesdetect_stuckcatch that a step cap doesn't, and what failure does a token budget catch that a step cap doesn't? - Walk the recovery path: a tool raises an exception mid-run. What exact steps does
run_agenttake so the run survives, and why istry/except: passworse than letting it crash? - Predict the three failure modes of removing, respectively: (a) the budget, (b)
detect_stuck, (c) tool-error recovery. Which design decision prevents each? - 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?
evaluate_runreportsefficiency = optimal_steps / steps. Why is a bare "did it answer?" boolean insufficient for deciding whether a prompt change made your agent better or worse?- 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.py—detect_stuck(M1) andBudgetTracker(M2) are the learner core: the no-progress detector and the step/token ceiling.agent.py—run_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.py—evaluate_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 LiteLLMcomplete, 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.
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.
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.
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).
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).
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).
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).
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.
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.
| Criterion | Does your implementation... | Pass? |
|---|---|---|
| Stuck detection | flag identical repeated actions (tool and args), and not flag many different productive steps? | ☐ |
| Budget enforced | check a steps and tokens ceiling every step, and return why it stopped? | ☐ |
| Recovery | catch a raising tool, feed the error back as an observation, and keep looping (not crash, not swallow)? | ☐ |
| Distinct stop reasons | return answered / stuck / budget / max_steps — never collapse a failure into "done"? | ☐ |
| Message order | append the assistant turn before the tool results (carried from P06)? | ☐ |
| Quantitative eval | report completion, steps, tool calls, and efficiency — numbers, not "it seemed to work"? | ☐ |
| Right tool for the job | can you name a task where you deliberately did not use the agent loop, and why? | ☐ |
Your implementation may have problems if:
- Your loop has no budget and relies on
max_stepsalone — one expensive step pattern blows your bill. detect_stuckcompares 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: passand 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
| Mistake | Why It Happens | Consequence | Fix |
|---|---|---|---|
No budget, only max_steps | "P06 capped steps and that was fine" | An agent taking few but huge steps blows the token bill | Track tokens and steps; check over_budget() every step |
detect_stuck compares tool name only | "Same tool = stuck" | Kills a productive agent reading many different files | Compare 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 → stuck | Feed f"Error: {e}" back as the observation |
| One tool raise kills the run | No try/except around dispatch | Every prior step's tokens wasted on one bad call | Wrap 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 budget | Return 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 agent | evaluate_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 decisions | Use 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 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— thetools/tool_callsshape andusagefor 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.