Courses / Building AI Agents

Session 3 of 11

The Single-Agent Loop

Overview

Chapter 3 — The Single-Agent Loop

Course: Building AI Agents

Prerequisites: Chapter 1 (a model asks for a tool call, your code validates and runs it, and the result goes back in a message) and Chapter 2 (a standard protocol for connecting an application to tools). You can read and run a short Python script. The words stop reason, step cap, transient error, backoff and idempotent are all defined in the notes before they are used.

What the reader can do after this chapter:

  • Trace an agent loop step by step, naming for each step what the model returned, what your code did with the stop reason, and what went back into the conversation.
  • Decide, for a given run, which of four conditions ended it (natural finish, step cap, repeated call, hand-off to a person) and report that outcome as success or failure.
  • Compute a retry schedule with exponential backoff, and say which failures to retry and which to stop on.

Core question this chapter answers: What makes a loop of model calls and tool calls stop, and which parts of your own code guarantee that it stops, stops safely and does not repeat a side effect?

Connections:

  • Builds on: Chapters 1 and 2, which give a model tools it can ask for. This chapter puts the request, the execution and the result into a repeating cycle.
  • Leads into: Chapter 4, which takes the action log you produce in this chapter’s last task and asks what to keep, summarise or discard from it across turns and sessions.

Your answers are scored

This chapter is assessed. The quiz is marked against a key held separately from your materials. Questions 1–8 are multiple choice and are marked by comparing your letter with the key. Questions 9–10 ask for a short answer in an exact format, and each one is marked by a rule printed with the question, so you can check that your answer has the right shape before you submit it.

  • Nothing is judged by reading it for style. An answer either matches the rule or it does not.
  • All three project tasks are scored from an evidence file of key: value lines. Every line’s accepted value, or the rule that decides it, follows from what the task’s code prints, so you can check your own file before you submit it.

Notes

Building AI Agents — Chapter 3: The Single-Agent Loop

Before you start

Nothing to install for the reading. For the project you need Python 3 and nothing beyond its standard library. No task needs an account with a model provider, an API key or any payment, and no task calls a real model: the “model” in the project is a fixed list of canned responses.

Keep one question in mind while you read: if this loop got stuck, which line of my code would end it, and what would it report? By the end of the chapter you should be able to answer it for any agent you build, and point at the line.

Summary

An agent, in the sense this chapter uses, is a model called repeatedly inside a loop your code owns: the model returns either a final answer or a request for a tool, your code runs the tool and feeds the result back, and the cycle repeats. The model decides what to ask for next, but your code decides whether the cycle continues, so the loop’s exits are an engineering responsibility. A run can end because the model finished, because a step cap was hit, because the same call keeps repeating, or because a person was asked to take over, and each exit needs its own report. Failures split into transient ones that a retry with growing delays can fix and permanent ones where a retry only repeats the failure, and a retry is safe only when repeating the operation has no extra effect. This chapter teaches you to write the loop, give it these exits, and compute the retry schedule.

Key concepts

Term Definition
Agent loop A cycle in which your code calls the model, reads its response, runs any tool it requested, appends the result to the conversation and calls the model again, until a stop condition is met.
Step One pass of the loop in this chapter: one model response received and handled. A step that runs a tool also adds the tool’s result to the conversation.
Observation What the loop learns from a step, usually a tool result or an error. It is the evidence the model uses to choose its next step.
Stop reason A field on a model response that says why the model stopped generating, such as finishing, reaching its token limit or asking for a tool. The loop branches on it.
Final output A model response that ends the loop successfully. One provider’s runner treats a response as final when it is text of the wanted type and contains no tool calls.
Step cap A fixed maximum number of steps a run may take. It is a safety limit, and reaching it is a failure outcome, not a success.
Loop detection A check that ends a run when it repeats itself. This chapter’s version fingerprints each tool call (tool name plus arguments in a fixed order) and stops at the third identical fingerprint.
Runaway A run that keeps taking steps without making progress, spending tokens and tool calls until something external stops it.
Transient error A failure that may succeed if the same request is sent again later, such as rate limiting or a server being briefly unavailable.
Non-transient error A failure that sending the same request again will not fix, such as invalid credentials or a spending limit that has been reached.
Exponential backoff A retry schedule in which each wait is longer than the one before, here the base delay multiplied by a factor to the power of the number of retries already made, with a maximum.
Idempotent operation An operation for which repeating the call has the same effect on the system as making it once. Reading a ticket is idempotent by nature; creating a ticket is not unless you make it so.

Explanations

The failure, before the explanation

Here are two illustrative incidents, of the kind this chapter is designed to prevent.

Incident 1: the loop that never ends. A team builds a support agent with a logs_search tool. A user asks about a failed deploy. The model calls logs_search with {"query": "deploy failed"} and gets back zero lines. It calls again with the same arguments, gets zero lines again, and does so until the team’s token budget alert fires. Nothing was broken in the model or the tool: the code was a while True loop that exits only when the model says it is finished, and the model never said so.

Incident 2: the loop that repeats a side effect. The same team adds create_ticket. One request to the ticket service times out, so the loop’s retry code sends it again. The first request had actually succeeded. The customer receives two tickets, and later two more when the model, shown a timeout message, asks for the tool again.

Neither incident is about the model being unintelligent. In the first, nothing in the code could end the loop except the model. In the second, repeating a request was treated as harmless when it was not. Each is a missing design decision, and the rest of this chapter supplies it.

The loop: plan, act, observe, repeat

Take one run, written out. The user asks: “Why did the nightly job fail?”

  1. The application sends the question and the tool list. The model responds asking for logs_search with the job’s name.
  2. Your code runs the search and sends back the result: a line saying the job ran out of disk.
  3. The model asks for disk_usage on that host. Your code runs it and sends back the figure.
  4. The model responds with a written explanation and no tool request. The loop ends.

That is the whole mechanism: the model proposes a step, your code carries it out, and what happened goes back in as the next input. One provider describes the shape directly: “The canonical shape is a while loop keyed on stop_reason”, repeating “while stop_reason is “tool_use”“ (Anthropic, How tool use works). A well-known description of agents puts it as: they “are typically just LLMs using tools based on environmental feedback in a loop” (Anthropic, Building effective agents). The same article says that at each step the agent needs “ground truth” from the environment, “such as tool call results or code execution”, to assess its progress.

A second way to see it: an on-call engineer working an incident. Form a guess, run one command, read the output, revise the guess, run the next command. The engineer stops when the cause is found, when they are told to hand over, or when they have tried everything they can think of. An agent loop is the same cycle, except that the “engineer” is a model and the stopping rules have to be written into code in advance, because nobody is watching each step.

Another provider’s runner describes its loop in the same terms: call the model; if the output is final output, return it; if it requests tool calls, “we run those tool calls, append the results, and re-run the loop” (OpenAI, Running agents). Its rule for final output is that the model “produces text output with the desired type, and there are no tool calls”. Field names and details differ between providers. The cycle does not.

Reading the stop reason inside the loop

Start with a failure. A loop receives a response that stops in the middle of writing a tool request, because the model hit the token limit set for the reply. A loop that only checks “was there a tool request?” may try to run a half-written call, or may treat the response as a finished answer.

The stop reason is what tells these cases apart. One provider documents these values and what to do with each (Anthropic, Handling stop reasons):

Stop reason What it means What the documentation says to do
end_turn The model finished its response naturally. Use the response.
tool_use The model is calling a tool. Run the tool and return the result.
max_tokens The response reached your max_tokens limit. Raise max_tokens or continue the response.
pause_turn A server-tool loop reached its iteration limit. Send the assistant content back to continue.
refusal The model declined to respond. Read the details and retry on a fallback model.
model_context_window_exceeded The response filled the model’s context window. Treat the response as truncated.

A second way to hold this: the stop reason is the loop’s status code. An HTTP client branches on 200, 429 and 500 rather than guessing from the body; an agent loop branches on end_turn, tool_use and max_tokens for the same reason. A loop with a branch for tool_use and a branch for “everything else is the answer” is an agent that returns half-written text as a result.

The values and their names are one provider’s, and other providers name their reasons differently. Learn the idea, then read your provider’s list, and make sure the loop has an explicit branch for every value, including a default that fails loudly for a value it does not know.

Four ways a run ends

Picture four runs of the same agent.

  • Run A asks for two tools and then answers. It ended naturally.
  • Run B keeps asking for a different tool each time and reaches 25 steps, the number the team set. It ended on the step cap.
  • Run C asks for logs_search with the same arguments again and again. It ended on repeat detection.
  • Run D reaches a step where the next tool is deploy_rollback, a high-impact action from Chapter 1, and waits for a person. It ended as a hand-off.

The first is success. The other three are the loop’s own safeguards firing, and each should be reported as what it is. One article describes the pattern: the task “often terminates upon completion, but it’s also common to include stopping conditions (such as a maximum number of iterations) to maintain control”, and agents can “pause for human feedback at checkpoints or when encountering blockers” (Anthropic, Building effective agents).

Providers build the cap in. One SDK’s tool runner “loops until Claude returns a message without a tool use, or until it reaches max_iterations if you set it” (Anthropic, Tool runner). Another provider’s runner raises an exception if the turn limit is exceeded: “If we exceed the max_turns passed, we raise a MaxTurnsExceeded exception. Pass max_turns=None to disable this turn limit” (OpenAI, Running agents). Two things follow. A cap may be optional, so check whether your framework applies one by default. And a cap that is hit is an event your code has to handle, as an exception or a status, rather than a normal return.

Hitting the cap is a failure outcome. Run B stopped, but the task was not completed. If your code returns whatever text the model last produced, the user sees an unfinished answer that looks finished. Return a status that says “step cap reached”, what was done so far, and what was not.

A second way to see the cap: it is a circuit breaker in a house. It does not know why the current is too high, and it does not need to. It limits the damage whatever the cause. Choosing the number is a judgement about the task; an agent that normally finishes in four steps can have a cap of ten, and a cap of ten thousand is a cap in name only.

Repeat detection: noticing a loop that goes nowhere

A cap catches every kind of runaway, but late: Run C above uses all of its steps before it is stopped. A tighter check watches for the specific pattern in Incident 1, the same call repeated.

The idea is a fingerprint. For each tool request, build a string from the tool name and the arguments, with the arguments written in a fixed key order. Keep a count per fingerprint. When a fingerprint reaches a threshold, stop the run before executing that call. In this chapter the threshold is three: the first two identical calls run; the third ends the run with a “loop detected” status.

Two calls whose arguments mean the same thing must produce the same fingerprint. {"q": "a", "limit": 5} and {"limit": 5, "q": "a"} are the same call with the keys in a different order. In Python, json.dumps(args, sort_keys=True) writes both identically, so the fingerprint logs_search {"limit": 5, "q": "a"} matches. Without sorting the keys the model could evade the check by reordering them, without meaning to.

This is a design choice made in this chapter, not a feature a provider documents, and it is a coarse one. Exact repetition is easy to detect. A run that varies one word in the query each time is making no progress either, and the fingerprint will not see it; the cap remains the backstop. Some tasks also legitimately repeat a call, such as polling a job’s status until it finishes. For those, the threshold is higher, or the repeated result must be the same for the loop to count as stuck.

A second view: repeat detection is a smoke alarm, and the cap is the sprinkler. The alarm reacts to a particular sign early, the sprinkler to anything, later. You want both.

Retrying: what to repeat and what to stop on

Take two failures from the same run. At one step the tool service answers “too many requests”. At another the logs service answers that the account’s monthly spending limit has been reached.

The first is transient: waiting a little and asking again may well succeed. The second is not: one provider’s error documentation says a 429 caused by reaching a spend cap “has no retry-after header and keeps failing until access resumes” (Anthropic, Errors). Retrying it only burns steps and requests. A general guidance on retries makes the same distinction: “For non-transient errors, if you can determine the cause of the failure, it is more efficient to fail fast” (AWS, Retry with backoff pattern).

For transient errors, retry with growing waits rather than at once. One provider’s SDK “automatically retries transient failures (such as connection errors, rate limits, and 5xx server errors) with exponential backoff, twice by default, honoring the retry-after header when present” (Anthropic, Errors). The AWS pattern defines the idea: “Exponential backoff is a technique where operations are retried by increasing wait times for a specified number of retry attempts”, and warns that “frequent retries can overload network bandwidth and cause contention” (AWS, Retry with backoff pattern).

The schedule is arithmetic. With a base delay of 1 unit, a factor of 2 and a maximum of 6 units, the delay before retry number n (counting from 0) is the smaller of 6 and 1 × 2ⁿ:

Retry 1 × 2ⁿ Delay used
0 1 1
1 2 2
2 4 4
3 8 6 (capped)
4 16 6 (capped)

A second example, with base 2, factor 3 and maximum 20, gives 2, 6, 18, 20, 20. A real loop waits for these delays; the project computes them instead, so that its results are exact. A real system may also add a random amount to each delay so that many clients do not retry in step. That is a common refinement, and the project leaves it out so that the numbers can be marked.

Retries are not free in an agent loop. A retry inside one tool call is invisible to the model. A failure returned to the model, as in Chapter 1 with is_error, makes the model decide whether to try again, and each decision is a step against your cap. Keep the two layers separate: let your code retry transient failures quietly, within a retry limit, and return what is left to the model as an instructive error.

Retry-safe means idempotent

Return to Incident 2. The retry was correct in principle: the timeout is a transient error. What went wrong is that the operation being retried creates something. The AWS guidance states the condition: “Operations should be idempotent when you use the retry with backoff pattern. Otherwise, partial updates might corrupt the system state” (AWS, Retry with backoff pattern), where idempotent means repeated calls “have the same effect on the system state” as a single call.

Compare two tools. tickets_get(ticket_id) is idempotent by nature: reading it five times changes nothing. tickets_create(title) is not: five calls make five tickets. One way to make the second safe, and the way the project uses, is to give each logical request a key chosen by your code, and have the tool keep the first result under that key and return it for any later call with the same key. The key must come from your code and stay the same across the retries of one request, not from the model on each attempt. This is a design technique; the AWS page states the requirement and leaves the mechanism to you.

A second view: a lift button. Pressing it five times does not call five lifts, because pressing again while a lift is already on its way has no further effect. A “call a taxi” button that dispatched a new taxi on each press would be a bad button to wire to an automatic retry.

Three situations you will meet, and what they mean for your design

Before the table, three situations:

  1. A QA engineer’s agent test sometimes hangs for a long time. The log shows the agent calling the same search tool with the same query dozens of times.
  2. A backend engineer sees a customer with three identical tickets. The ticket service logs show one timeout, followed by two retries and then the model asking again.
  3. A DevOps engineer sets the step cap to 8. A run reaches step 8 in the middle of a task and the chat shows a half-finished explanation as if it were complete.
What you see What the design allowed What to do instead
The same call repeated dozens of times (situation 1) Nothing but the model could end the loop Add a step cap and repeat detection; end with a “loop detected” status
A customer with duplicate tickets (situation 2) A non-idempotent create was retried Give each request a stable key and make the create return the first result for that key
A half-finished answer shown as final (situation 3) The cap ended the run but was reported as a normal answer Return a distinct status and report what was and was not done

Flashcards

Say the answer aloud before revealing it. Speaking it is what exposes the gaps that reading past a written answer hides.

What are the steps of one pass through an agent loop?

Your code calls the model; reads its stop reason; if the model asked for a tool, runs the tool and adds the result to the conversation; and calls the model again. When the model gives a final answer or a safeguard fires, the loop ends.

Why must the loop branch on the stop reason rather than only check for a tool request?

Because a response can stop for several reasons. A response cut off by the token limit, for example, is not a finished answer and may hold a half-written tool request. The stop reason says which case you have.

What are four ways an agent run can end?

The model finishes naturally; a step cap is reached; repeat detection stops it after the same call recurs; or the run pauses for a person, for example to approve a high-impact action. Only the first is a success.

A run hits its step cap. What should your code report?

A distinct failure status such as “step cap reached”, with what was done and what was not. It should not return the model’s last text as if it were a finished answer.

How is a call fingerprint built, and why sort the arguments?

From the tool name and the arguments written in a fixed key order, such as json.dumps(args, sort_keys=True). Sorting makes two calls with the same arguments in a different key order produce the same fingerprint.

Why keep a step cap if you already have repeat detection?

Repeat detection sees only exact repetition. A run that changes one word each time makes no progress and slips past it; the cap limits the damage whatever the cause.

What is the difference between a transient and a non-transient error, with one example of each?

A transient error may succeed if sent again later, such as a rate-limit response. A non-transient one will not be fixed by a repeat, such as a 429 caused by reaching a spend cap, which keeps failing until access resumes.

With base 1, factor 2 and maximum 6, what are the first five retry delays?

1, 2, 4, 6, 6. The third retry would be 8 but the maximum caps it at 6, and so does every later one.

What does idempotent mean, and why does it matter for retries?

Repeating the call has the same effect on the system as making it once. A retry may repeat a request that had in fact succeeded, so a retried operation that is not idempotent, such as creating a ticket, can create duplicates.

How does a stable request key make a create operation retry-safe?

Your code picks a key once per logical request and sends it with every attempt. The tool stores the first result under the key and returns that same result for any later call with the key, so repeats create nothing new.

Model answer

The question: “Our agent ran for two hundred steps and created five duplicate tickets. Walk me through how the loop should have ended, and what stops this from happening again.” This is the question a lead engineer asks after an incident, and an interviewer for a role building agents asks to see whether you own the loop. A strong answer hits five beats, in this order:

  1. Loop — the model proposes a step, my code runs it and feeds the result back, and my code, not the model, decides whether the cycle continues.
  2. Stop reason — each response is handled by its stop reason, with an explicit branch for every value and a failing default.
  3. Cap — a step cap ends every run eventually, and reaching it is reported as a failure with what was done.
  4. Repeat — repeat detection fingerprints each call and stops the run early when the same call recurs.
  5. Retry safety — transient errors are retried with growing delays, permanent ones are not, and anything retried must be idempotent.

“The loop is mine: the model asks for a step, my code runs it and returns the result, and my code decides whether to go round again. In this run nothing in that code could end it except the model. Each response should have been handled by its stop reason, with a branch for finishing, for a tool request and for a truncated reply, and a default that fails on a value I don’t know. A step cap would have ended the run long before two hundred steps, and I’d report hitting it as an incomplete run, not as an answer. A fingerprint check on tool name and arguments would have stopped a repeated call at the third occurrence. The duplicate tickets are a separate cause: a retry repeated a create that had already worked. I’d retry only transient errors, with growing delays and a retry limit, and I’d make the create idempotent with a key my code sets once per request, so a repeat returns the first ticket instead of making another.”

Missing a beat is a failure state. Without loop, the answer blames the model for a cycle the code owns. Without stop reason, truncated replies pass as answers. Without cap, nothing bounds the damage. Without repeat, the run burns its whole budget before the cap fires. Without retry safety, the fix for failures creates the next incident.

Why this matters

  • Cost control. Each step spends tokens and may spend tool calls. A cap and repeat detection put an upper bound on a run, which is the number a budget conversation needs.
  • Honest reporting. A status that distinguishes finished, capped, repeated and handed-off runs lets a dashboard show how often an agent really completes its work.
  • Safe automation. Retries are a normal part of unreliable networks. Making operations idempotent is what lets you retry without fear of repeating a side effect.
  • Talking to customers. When a customer asks “what stops it running away?”, the answer is in your loop: the cap, the repeat check and the approval step, and you can show each one.

After this chapter

The project for this chapter is Build a Loop That Knows When to Stop (project.md). You will write an agent loop that runs a scripted fake model, add repeat detection, and add a computed retry schedule and a retry-safe create operation. All results are deterministic.

Chapter 4 starts from the ticket log you produce in Task 3. It asks what a long-running agent should keep from a record of past actions like that, what it should summarise and what it should drop, and where keeping the wrong thing turns into a correctness bug.

Project

Hands-on Project — Build a Loop That Knows When to Stop

Objective

Write an agent loop that drives a scripted fake model, give it the exits a production loop needs (a step cap, repeat detection, a handled stop reason for every case), and add a computed retry schedule and a retry-safe create operation, so that you can show for each run which condition ended it and why a retry did not repeat a side effect.

What this project needs: Python 3 and nothing beyond its standard library (json). No task needs an account with a model provider, an API key or any payment, and no task calls a real model or waits for real time: the “model” is a fixed list of responses and retry delays are computed, not slept. All three tasks are required.

Your evidence file. Keep one plain-text file, chapter3-evidence.txt. Each task tells you which lines to add. Every line has the form key: value, one per line, with the key in lower case exactly as printed. Write what your code printed, not what you expected it to print.

Task 1 — Write the loop

A fake model is a list of responses. Each response is a dictionary with a stop_reason. A response with "stop_reason": "tool_use" also has a tool name and an args dictionary. Use this setup exactly:

def call(tool, **args):
    return {"stop_reason": "tool_use", "tool": tool, "args": args}

END = {"stop_reason": "end_turn", "text": "done"}

TOOLS = {
    "get_ticket": lambda id: "ticket " + id,
    "logs_search": lambda **kw: "2 lines",
}

SCRIPTS = {
    "a": [call("get_ticket", id="T-1"), call("logs_search", q="timeout"), END],
    "b": [call("get_ticket", id="T-%d" % i) for i in range(1, 7)] + [END],
    "c": [call("get_ticket", id="T-1"), {"stop_reason": "max_tokens"}],
    "d": [call("get_ticket", id="T-1"), {"stop_reason": "refusal"}],
    "e": [{"stop_reason": "pause_turn"}],
}

Write run_agent(script, tools, max_steps). It returns (status, steps, executed, transcript) and follows these rules exactly:

  1. transcript is a list of strings that starts with one entry for the user’s task ("user: task").
  2. Go through the script in order. Before taking each response, if steps already equals max_steps, stop the loop. Otherwise take the response, add 1 to steps, and append one transcript entry for it ("assistant: " followed by its stop reason).
  3. If the stop reason is tool_use, run the named tool from tools with the response’s args, add 1 to executed, append one transcript entry ("tool: " followed by the result as text), and go to the next response.
  4. If the stop reason is end_turn, return status done. If it is max_tokens, return truncated. If it is refusal, return refused. For any other stop reason, return unhandled.
  5. If the loop stopped because steps reached max_steps, return status step_cap.

Run run_agent on each of the five scripts with max_steps set to 4 for script b and 10 for all the others. Print each run’s status, steps, executed count and the length of its transcript.

Add to your evidence file, one line per script, with values separated by a comma and no spaces:

t1_a: <status>,<steps>,<executed>
t1_b: <status>,<steps>,<executed>
t1_c: <status>,<steps>,<executed>
t1_d: <status>,<steps>,<executed>
t1_e: <status>,<steps>,<executed>
t1_a_transcript_len: <the number of entries in script a's transcript>

Stop at these five scripts: no real model, no streaming, no concurrency.

Output: a script with one function, run_agent, and six evidence lines.

Task 2 — Add repeat detection

Change run_agent to take a fourth argument, detect, which defaults to False. When it is True, build a fingerprint for each tool_use response before running the tool: the tool name, one space, then json.dumps(args, sort_keys=True). Keep a count of each fingerprint. When a fingerprint’s count reaches 3, return status loop_detected at once, without running that call. The steps value includes the response that triggered the stop, and executed does not count it. With detect set to False the function behaves exactly as in Task 1.

Use these two scripts, with max_steps set to 10 and detect set to True:

SCRIPTS["f"] = [call("logs_search", q="a"), call("logs_search", q="b"),
                call("logs_search", q="a"), call("logs_search", q="a", limit=5),
                call("logs_search", q="a"), END]
SCRIPTS["g"] = [call("logs_search", q="a", limit=5), call("logs_search", limit=5, q="a"),
                call("logs_search", q="a", limit=5), END]

Also run script a with detect set to True and max_steps set to 10, and print the fingerprint of the first response in script g.

Add to your evidence file:

t2_f: <status>,<steps>,<executed>
t2_g: <status>,<steps>,<executed>
t2_a_detect: <status>,<steps>,<executed>
t2_fingerprint_g1: <the fingerprint text, exactly>

Before you submit, check that script g’s three calls were treated as one fingerprint. If your count shows two different fingerprints, your arguments are not being written in a fixed key order.

Output: the changed run_agent and four evidence lines.

Task 3 — Compute retries and make a create retry-safe

Part A: the schedule. Write backoff_delays(base, factor, max_delay, retries), which returns a list of retries delays. The delay before retry number n, counting from 0, is the smaller of max_delay and base * factor ** n. Delays are whole numbers of “delay units”; nothing waits.

Part B: the retry wrapper. Write call_with_retries(outcomes, max_retries, base, factor, max_delay). outcomes is a list of text values, one per attempt in order: "ok" is success; "429", "500", "503" and "529" are transient failures; "400", "401", "403" and "404" are non-transient failures. It returns (status, attempts, total_delay) by these rules:

  1. Attempt 1 happens at once. Add 1 to attempts for every outcome consumed.
  2. "ok" returns status ok.
  3. A non-transient outcome returns status failed_non_transient at once.
  4. A transient outcome, when max_retries retries have already been used, returns status gave_up. Otherwise the next retry is used: add the next delay from backoff_delays(base, factor, max_delay, max_retries) to total_delay, and consume the next outcome.

Run these five cases and print each result:

backoff_delays(1, 2, 6, 5)
call_with_retries(["429", "503", "ok"], 3, 1, 2, 10)
call_with_retries(["503", "503", "503", "503", "ok"], 3, 1, 2, 10)
call_with_retries(["401", "ok"], 3, 1, 2, 10)
call_with_retries(["500", "500", "ok"], 5, 1, 3, 5)

Part C: a retry-safe create. Write create_ticket(store, key, title), where store is a dictionary you create once. If key is not None and is already in store, return the ticket id stored under it and create nothing. Otherwise create a ticket: its id is "TK-" followed by the number of tickets created so far, counting from 0, and it is returned. If key is not None, also store the id under key. Keep a separate count of tickets created. Call it five times with these keys, in order: "k1", "k1", "k2", "k1", None. Append one line to a list called ticket_log for each call, in the form call <n>: <key> -> <id>, where <n> counts from 1, <key> is the key as text (None for no key) and <id> is the id the call returned. Then count how many tickets would exist if the key were ignored and every call created a ticket.

Add to your evidence file:

t3_delays: <the delays from the first case, comma-separated, no spaces>
t3_h: <status>,<attempts>,<total_delay>      (case 2)
t3_i: <status>,<attempts>,<total_delay>      (case 3)
t3_j: <status>,<attempts>,<total_delay>      (case 4)
t3_k: <status>,<attempts>,<total_delay>      (case 5)
t3_created_with_keys: <tickets created by the five calls with keys honoured>
t3_created_without_keys: <tickets that would exist if the key were ignored>
t3_log_len: <the number of lines in ticket_log>
t3_log_first: <the first line of ticket_log, exactly>
t3_log_last: <the last line of ticket_log, exactly>

Output: three functions, the ticket_log list and ten evidence lines.

What carries forward

Keep the ticket_log list from Task 3, and run_agent with it. The next chapter opens with that log: it asks what a longer-running agent should keep from a record of past actions like it, what it should summarise and what it should drop, and where keeping the wrong thing becomes a bug.

TechEazy Consulting training material — not affiliated with or endorsed by OpenAI, Anthropic or any other company named in this course.