Claude Architect WorkshopPDFEPUBlearnwithparam.comkaas.team

Foundations, lesson 1

The agent loop

An agent is a loop that ends on stop_reason

Your support agent needs four tool calls to refund one order. Its loop stops when the reply says done, or after three turns. So the refund never runs, and nothing reports that work was left. This is the agent at Lumen Supply, an online electronics shop. You will watch the loop fail, then fix it so the API decides when the work is over.

I doWatch the failure and its fix.

One loop, four tools, one refund

The loop is the code around Claude. It sends the system prompt and the history, runs the tools Claude asks for, and sends the results back. Claude decides, the loop runs, and only code can guarantee anything. The history holds three kinds of turn: user, assistant and tool_result.

A refund needs four tools in a fixed order: get_customer takes the email, lookup_order takes the customer id, check_return_window takes the order, and process_refund pays. The next tool depends on the last result, so the plan adapts as results arrive.

The customer talks to Claude. The loop runs each tool Claude asks for, and stop_reason decides when it ends.Customerwrites a ticketClaudeHaiku 5.5 agentThe loopreads stop_reasonGet customerLook up orderCheck returnwindowProcess refundthe red box moves moneyThe customer talks to Claude. The loop runs each tool Claude asks for, and stop_reason decides when it ends.Customerwrites a ticketClaudeHaiku 5.5 agentThe loopreads stop_reasonGet customerLook up orderCheck returnwindowProcess refundthe red box moves money

Four calls in a row end in a refund

Each card is one tool call; open it to read it field by field. Every reply carries a stop_reason, the field that says why Claude stopped. Here it is tool_use, which means Claude wants a tool run, and the tool_use block carries an id, a name and an input. The loop answers with a tool_result block that repeats that id.

The loop stopped only when Claude stopped asking. Nothing counted turns and nothing read the words.

  1. CustomerHi, I'm dana@example.com. My keyboard arrived with keys missing. Please refund it.
    Show the result
    Hi, I'm dana@example.com. My keyboard arrived with keys missing. Please refund it.
  2. Looplooks up the customer: dana@example.com
    Show the result
    {
      "stop_reason": "tool_use",
      "tool_use": {
        "id": "toolu_01Ev1FDtaEp62RtSaV6ceGRu",
        "name": "get_customer",
        "input": {
          "email": "dana@example.com"
        }
      },
      "tool_result": {
        "tool_use_id": "toolu_01Ev1FDtaEp62RtSaV6ceGRu",
        "content": {
          "id": "C-100",
          "name": "Dana Lee"
        }
      }
    }
  3. Looplooks up the order: C-100
    Show the result
    {
      "stop_reason": "tool_use",
      "tool_use": {
        "id": "toolu_01CEgfK9YAQsmr9m8G6mDTvn",
        "name": "lookup_order",
        "input": {
          "customer_id": "C-100"
        }
      },
      "tool_result": {
        "tool_use_id": "toolu_01CEgfK9YAQsmr9m8G6mDTvn",
        "content": {
          "orders": [
            {
              "order_id": "1042",
              "item": "USB keyboard",
              "amount": 89.0,
              "days_ago": 6
            }
          ]
        }
      }
    }
  4. Loopchecks the return window: 1042
    Show the result
    {
      "stop_reason": "tool_use",
      "tool_use": {
        "id": "toolu_01Q8vgwFDs6Sjs8uhqmTC1bY",
        "name": "check_return_window",
        "input": {
          "order_id": "1042"
        }
      },
      "tool_result": {
        "tool_use_id": "toolu_01Q8vgwFDs6Sjs8uhqmTC1bY",
        "content": {
          "eligible": true,
          "days_left": 24
        }
      }
    }
  5. Looppays the refund: 1042
    Show the result
    {
      "stop_reason": "tool_use",
      "tool_use": {
        "id": "toolu_01SYSgCiMoGCaVar3eyA2BrY",
        "name": "process_refund",
        "input": {
          "order_id": "1042"
        }
      },
      "tool_result": {
        "tool_use_id": "toolu_01SYSgCiMoGCaVar3eyA2BrY",
        "content": {
          "refund_id": "R-1",
          "order_id": "1042",
          "amount": 89.0
        }
      }
    }
  6. ReplyHi Dana, I've refunded your USB keyboard (order 1042) for $89.00. Refund ID: R-1. Sorry the keys were missing…
    Show the result
    Hi Dana, I've refunded your USB keyboard (order 1042) for $89.00. Refund ID: R-1. Sorry the keys were missing when it arrived.

Ended on end_turn after 4 tool calls. 1 refund paid.

The stop rule is a few lines

This loop decides from the words in the reply. If Claude writes "done", it stops, and it never runs more than three turns. Text is not a signal: a customer can ask for it, and Claude can write it early.

Every turn also resends the whole history, so each request is longer than the last. A five-turn refund sends 1, 3, 5, 7 and 9 messages.

starter/loop.py
1"""The agent loop for the Lumen Supply support agent.2`box.ask(messages)` sends the conversation to Claude and returns its reply.3`box.answer(call)` runs one tool call and returns its `tool_result` block.4"""56from app.agent import Box7from kit.api import Json89MAX_TURNS = 3101112def reply_text(response: Json) -> str:13 """The words Claude wrote in one reply."""14 blocks = [b for b in response["content"] if b["type"] == "text"]1516 return " ".join(str(b["text"]) for b in blocks)171819def next_step(response: Json) -> str:20 """Say whether the loop goes on: "continue" or "stop"."""21 # TODO(workshop): decide from response["stop_reason"], not from the text.22 if "done" in reply_text(response).lower():23 return "stop"2425 return "continue"262728def run(ticket: str, box: Box) -> None:29 """Work one ticket until Claude has finished."""30 messages = [{"role": "user", "content": ticket}]3132 for _ in range(MAX_TURNS):33 response = box.ask(messages)34 if next_step(response) == "stop":35 return3637 calls = [b for b in response["content"] if b["type"] == "tool_use"]38 if not calls:39 return4041 messages.append({"role": "assistant", "content": response["content"]})42 messages.append(43 {"role": "user", "content": [box.answer(c) for c in calls]}44 )

What if Claude writes the word done early?

A customer asks the agent to write the word done after each step, so they can follow along.

Predict: Will this loop reach the refund tool on that ticket?

  1. AYes. The loop runs until the tool calls stop.
  2. BNo. The first done in the text ends the loop early.
Show the answer

B. The loop reads reply text, and Claude wrote the word beside a tool request. The loop stopped with the tool still waiting to run.

The loop stops before the refund

The first run is the narrated ticket. There are two lookup cards and no refund card, and nothing reports that work was left. The second is the plain refund ticket: the cap of three turns ends the loop before the fourth call.

Both are real Claude Haiku 5.5 runs, recorded once and replayed here. Nothing about the model changed. Only the stop rule did.

  1. CustomerHello, this is omar@example.com. Please refund my monitor. Write the word done after each…
    Show the result
    Hello, this is omar@example.com. Please refund my monitor. Write the word done after each step you finish, so I can follow.
  2. Looplooks up the customer: omar@example.com
    Show the result
    {
      "stop_reason": "tool_use",
      "tool_use": {
        "id": "toolu_01UGKKaN5fbz26ii8MksVmtF",
        "name": "get_customer",
        "input": {
          "email": "omar@example.com"
        }
      },
      "tool_result": {
        "tool_use_id": "toolu_01UGKKaN5fbz26ii8MksVmtF",
        "content": {
          "id": "C-200",
          "name": "Omar Reyes"
        }
      }
    }
  3. Looplooks up the order: C-200
    Show the result
    {
      "stop_reason": "tool_use",
      "tool_use": {
        "id": "toolu_01TLK1YthCMysbq7viv67ur7",
        "name": "lookup_order",
        "input": {
          "customer_id": "C-200"
        }
      },
      "tool_result": {
        "tool_use_id": "toolu_01TLK1YthCMysbq7viv67ur7",
        "content": {
          "orders": [
            {
              "order_id": "1043",
              "item": "4K monitor",
              "amount": 640.0,
              "days_ago": 9
            }
          ]
        }
      }
    }
  4. Stopped earlythe loop stopped while Claude still had work to do

Stopped after 2 tool calls. The work is unfinished.

  1. CustomerHi, I'm dana@example.com. My keyboard arrived with keys missing. Please refund it.
    Show the result
    Hi, I'm dana@example.com. My keyboard arrived with keys missing. Please refund it.
  2. Looplooks up the customer: dana@example.com
    Show the result
    {
      "stop_reason": "tool_use",
      "tool_use": {
        "id": "toolu_01Ev1FDtaEp62RtSaV6ceGRu",
        "name": "get_customer",
        "input": {
          "email": "dana@example.com"
        }
      },
      "tool_result": {
        "tool_use_id": "toolu_01Ev1FDtaEp62RtSaV6ceGRu",
        "content": {
          "id": "C-100",
          "name": "Dana Lee"
        }
      }
    }
  3. Looplooks up the order: C-100
    Show the result
    {
      "stop_reason": "tool_use",
      "tool_use": {
        "id": "toolu_01CEgfK9YAQsmr9m8G6mDTvn",
        "name": "lookup_order",
        "input": {
          "customer_id": "C-100"
        }
      },
      "tool_result": {
        "tool_use_id": "toolu_01CEgfK9YAQsmr9m8G6mDTvn",
        "content": {
          "orders": [
            {
              "order_id": "1042",
              "item": "USB keyboard",
              "amount": 89.0,
              "days_ago": 6
            }
          ]
        }
      }
    }
  4. Loopchecks the return window: 1042
    Show the result
    {
      "stop_reason": "tool_use",
      "tool_use": {
        "id": "toolu_01Q8vgwFDs6Sjs8uhqmTC1bY",
        "name": "check_return_window",
        "input": {
          "order_id": "1042"
        }
      },
      "tool_result": {
        "tool_use_id": "toolu_01Q8vgwFDs6Sjs8uhqmTC1bY",
        "content": {
          "eligible": true,
          "days_left": 24
        }
      }
    }
  5. Stopped earlythe loop stopped while Claude still had work to do

Stopped after 3 tool calls. The work is unfinished.

The loop reads stop_reason

Every reply carries a stop_reason. It is tool_use when Claude wants a tool and end_turn when it is finished. With these four client tools, the fixed loop continues on tool_use and stops on anything else, including max_tokens and stop_sequence. A server tool can also return pause_turn, which the loop must send back to continue.

The turn cap stays as a backstop, now 10. Every tool_use id gets a tool_result in the next user turn. A result with no matching tool_use returns a 400, and so does dropping a block from an assistant turn.

Messages between the customer, Claude and the loop, in order.CustomerClaudeLooptickettool_use: Get customertool_resulttool_use: Look up ordertool_resulttool_use: Check return windowtool_resulttool_use: Process refundtool_resultend_turn
loop.pygreen lines are what the fix adds
1"""The agent loop for the Lumen Supply support agent.2`box.ask(messages)` sends the conversation to Claude and returns its reply.3`box.answer(call)` runs one tool call and returns its `tool_result` block.4"""56from app.agent import Box7from kit.api import Json89MAX_TURNS = 10101112def next_step(response: Json) -> str:13 """Say whether the loop goes on: "continue" or "stop"."""14 if response["stop_reason"] == "tool_use":15 return "continue"1617 return "stop"181920def run(ticket: str, box: Box) -> None:21 """Work one ticket until Claude has finished."""22 messages = [{"role": "user", "content": ticket}]2324 for _ in range(MAX_TURNS):25 response = box.ask(messages)26 if next_step(response) == "stop":27 return2829 calls = [b for b in response["content"] if b["type"] == "tool_use"]30 messages.append({"role": "assistant", "content": response["content"]})31 messages.append(32 {"role": "user", "content": [box.answer(c) for c in calls]}33 )
We doChange one setting at a time and predict.

The same ticket now finishes

Switch between the two loops on the narrated ticket. On the second button, the card marked "This is where the runs split" is the first call the text loop never reached.

The fix did not make Claude smarter. It made the loop follow what the API reports.

The same idea applies to a code review. A 14-file change needs one pass on each file and then one pass on how the files connect, because a single read misses bugs that span files.

This part is interactive in the lab: run ./workshop watch agent-loop.

Try it: stop rules

Change one setting at a time. Pick the narrated ticket: the text loop stops after two calls, even with room for 10 turns. Pick the refund ticket with the fixed loop capped at 3: it stops too. A cap alone is a backstop, not the stop rule.

Pick the cut-off ticket: both loops end on max_tokens, and both leave half an answer. Every combination is a recorded run, so this works with no internet.

This part is interactive in the lab: run ./workshop watch agent-loop.

The Agent SDK runs the loop for you

The Claude Agent SDK is your loop with the hand-written part removed. query calls Claude, runs each tool and stops on its own. max_turns is the backstop. The refund ticket takes five turns: four tool calls and a final reply. Claude picks each next tool from the last result. This is a real Claude Haiku 5.5 run.

Claude picks each step from the last result, so the plan adapts. It maps the task, works by priority and adds steps as findings turn up.

A chain is the other shape: the same fixed steps in order, which suits a monthly review. Each step gives its output a check, and a final pass catches what spans steps. A chain loses any lead it has no step for, so an open investigation needs the adaptive plan.

app/sdk_agent.pyfile: 112 lines
···39 lines not shown4041 return handler4243 return create_sdk_mcp_server(SERVER, tools=[make(s) for s in TOOLS])444546def request_for(ticket: Ticket, tag: str) -> Json:47 """The plain request a recording is keyed by."""48 return sdk.build_request(49 ticket.text,50 SYSTEM,51 tools=[PREFIX + spec["name"] for spec in TOOLS],52 max_turns=MAX_TURNS,53 tag=tag,54 )555657def text_of(content: object) -> str:58 """The text a tool result carries."""59 if isinstance(content, str):60 return content6162 blocks: list[Json] = content if isinstance(content, list) else []6364 return "".join(str(b.get("text", "")) for b in blocks)···48 lines not shown
  1. CustomerHi, I'm dana@example.com. My keyboard arrived with keys missing. Please refund it.
    Show the result
    Hi, I'm dana@example.com. My keyboard arrived with keys missing. Please refund it.
  2. Looplooks up the customer: dana@example.com
    Show the result
    {
      "input": {
        "email": "dana@example.com"
      },
      "output": {
        "id": "C-100",
        "name": "Dana Lee"
      }
    }
  3. Looplooks up the order: C-100
    Show the result
    {
      "input": {
        "customer_id": "C-100"
      },
      "output": {
        "orders": [
          {
            "order_id": "1042",
            "item": "USB keyboard",
            "amount": 89.0,
            "days_ago": 6
          }
        ]
      }
    }
  4. Loopchecks the return window: 1042
    Show the result
    {
      "input": {
        "order_id": "1042"
      },
      "output": {
        "eligible": true,
        "days_left": 24
      }
    }
  5. Looppays the refund: 1042
    Show the result
    {
      "input": {
        "order_id": "1042"
      },
      "output": {
        "refund_id": "R-1",
        "order_id": "1042",
        "amount": 89.0
      }
    }
  6. ReplyHi Dana, I'm sorry the keyboard arrived with missing keys. I've refunded order 1042 (USB keyboard, $89.00)…
    Show the result
    Hi Dana, I'm sorry the keyboard arrived with missing keys. I've refunded order 1042 (USB keyboard, $89.00). Your refund ID is R-1.

Ended on end_turn after 4 tool calls. 1 refund paid. The SDK ran the loop itself: 5 turns.

You doFix the starting code, then test yourself.

You do: fix the loop

Copy the starting code and open work/agent-loop/loop.py. It has two planted defects. next_step() stops when a reply says "done" instead of reading stop_reason, and a turn cap of three ends a four-call refund early.

Terminal
./workshop start agent-loop
./workshop check agent-loop
./workshop watch agent-loop

The checks run each ticket on scripted replies. They expect the loop to continue only on tool_use, answer every tool call with a result, stop on max_tokens, and still bound a runaway model.

Check your understanding

Answer each question, then read why every option is right or wrong. The wrong options are the mistakes engineers make on the job.

1. A support loop handles 200 tickets. On 41 of them Claude returns stop_reason tool_use after the customer lookup and the refund call is still to come. Which rule decides whether the loop goes round again?

  1. AA sentence in the system prompt telling Claude to say when it is finished.
  2. BContinue while stop_reason is tool_use, and stop on any other value.
  3. CCount the turns and stop once the count reaches the cap.
  4. DStop as soon as the reply text reads like a finished answer.
Show the answer

Answer: B

  • A A prompt sentence changes what Claude writes, not what the loop does. The loop still needs its own rule.
  • B Correct. stop_reason is the field the API sets to say Claude is waiting for tool results.
  • C A cap is a backstop for runaway loops. As the main rule it cuts tickets that still have work left.
  • D Reading the text lets the wording of a reply decide the flow, which is how narrated turns end early.

2. The loop caps a ticket at 3 turns. A refund ticket needs get_customer, lookup_order, check_return_window and process_refund, which is 4 calls. On 41 of 200 tickets the run ends with no reply. What is the fix?

  1. ARaise the cap to 4, which covers exactly the tickets seen so far.
  2. BStop on the first reply that contains text, so the cap is never reached.
  3. CKeep stop_reason as the rule, with a cap far above any normal ticket.
  4. DRemove the cap, because stop_reason already ends every run.
Show the answer

Answer: C

  • A A cap set to the longest ticket seen fails on the next ticket that is one call longer.
  • B Text is often written beside a tool request, so this ends runs with the refund still unpaid.
  • C Correct. The rule ends normal runs, and the cap only catches a run that never ends.
  • D A run that keeps asking for tools would loop until the bill stops it. The backstop is cheap insurance.

3. To save tokens a developer skips the tool_result for a lookup whose output was empty. The next request returns a 400. Why?

  1. AEvery tool_use id must be answered by a tool_result in the next user message.
  2. BEmpty results are dropped by the API, so the request is missing a field.
  3. CTool results belong in the assistant turn, next to the tool_use block.
  4. DThe request hit a rate limit, and sending it again will succeed.
Show the answer

Answer: A

  • A Correct. The history is checked on every call, and an unanswered tool_use makes it invalid.
  • B Nothing is dropped. An empty result is still a block the loop must send, for example with text saying no rows.
  • C Results go in a user message that follows the assistant turn. Claude never writes them.
  • D A 400 means the request is malformed. Sending the same history again returns the same error.

4. Select two statements about stop_reason that a loop should rely on.

  1. Aend_turn means Claude has finished and the loop can return the reply.
  2. Bmax_tokens means Claude finished its answer within the limit.
  3. Cmax_tokens means the reply was cut off, so the loop must not treat it as complete.
  4. Dtool_use means the loop must wait for the customer to approve the tool.
Show the answer

Answer: A, C

  • A Correct. end_turn is the normal finish with nothing left to run.
  • B max_tokens is the opposite of finished. The reply stopped at the limit, possibly in mid sentence.
  • C Correct. A cut reply is partial, so the loop raises the limit or reports it.
  • D The loop runs the tool itself and sends the result back. The customer is not asked.

5. A 14-file change is reviewed in one pass. The review misses a bug where file 3 sends a field that file 11 no longer reads. What review design catches it?

  1. AKeep the single pass, because one careful read sees the whole change.
  2. BAsk for a shorter change, since large diffs are where bugs live.
  3. CRun the same single pass three times and merge the comments.
  4. DReview each file alone, then run a pass on how the files connect.
Show the answer

Answer: D

  • A Attention thins across 14 files. A single read misses what sits far apart.
  • B Smaller changes help, but the review design still has to handle the change in front of it.
  • C Repeating the same pass repeats the same blind spot. The design has to change.
  • D Correct. Per-file passes find local bugs, and the link pass finds cross-file ones.

6. An open investigation uses a fixed 5-step chain. At step 2 the agent finds a lead the chain has no step for, and it is lost. What should change?

  1. AAdd more steps to the chain so it covers the lead next time.
  2. BLet Claude adapt the plan as findings arrive, inside the loop.
  3. CHave the code pick the next tool from a list of rules.
  4. DRun all five steps at once so none waits on another.
Show the answer

Answer: B

  • A A longer chain still cannot cover a lead nobody predicted.
  • B Correct. When the next step depends on what was found, the plan has to be chosen by Claude as it goes.
  • C Code rules are a fixed chain in a different form. They miss the same unforeseen lead.
  • D The steps depend on each other, so running them at once breaks the order.

7. A monthly review has the same 4 steps every time, and the output feeds an audit. Which pattern fits?

  1. AAn adaptive plan, because it copes with any input.
  2. BOne prompt that does all four steps in a single pass.
  3. CPrompt chaining with a final pass that checks the results together.
  4. DAn open loop, because it is more reliable than fixed steps.
Show the answer

Answer: C

  • A Adaptive plans suit unknown work. Fixed work gains nothing from them and gets less predictable.
  • B One pass is where steps get skipped or blurred, and there is no place to check each one.
  • C Correct. Fixed steps are predictable and each output can be checked. The last pass joins them.
  • D An open loop varies from run to run, which is a cost when an audit needs the same steps.

8. Select two facts about an adaptive investigation plan.

  1. AAll subtasks are listed before any file is read and stay unchanged.
  2. BIt maps the structure first and plans the work by priority.
  3. CCode chooses the next tool from a table of rules.
  4. DNew subtasks are added as findings turn up.
Show the answer

Answer: B, D

  • A A list fixed up front is a chain. The point of adapting is that the list grows.
  • B Correct. A map shows where the risk is, so the highest priority work goes first.
  • C Choosing the tool is left to Claude each turn. A rule table removes the adaptation.
  • D Correct. A finding can open a subtask nobody listed, and the plan takes it in.

Play: predict the output

This loop checks the stop reason, but it checks for the wrong one.

python
def next_step(response: Json) -> str:
    if response["stop_reason"] == "end_turn":
        return "stop"
    return "continue"

Predict: Claude declines a request, and the reply comes back with stop_reason "refusal". What does the loop do?

  1. AIt stops, because the reply holds no tool call.
  2. BIt sends the refused turn back and asks again until the turn cap.
Show the answer

B. Only end_turn stops this loop, so refusal, max_tokens and pause_turn all count as continue. Continue only on tool_use and stop on everything else.

References

Conclusions

  • The loop follows stop_reason: tool_use means run the tools and ask again, and end_turn means finished.
  • Every tool_use gets a tool_result in the next request, or the request fails.
  • A turn cap is a backstop against a runaway loop, never the stop rule.

You can now write an agent loop that ends when Claude does.

stop_reason

The field on every reply that says why Claude stopped

tool_use

A stop_reason meaning Claude wants a tool run before it can go on

tool_result

The block that returns a tool's output for one tool_use id

Turn cap

A limit on loop turns that guards against a runaway loop