CCAR-F Claude Certified Architect — Foundations

Chapter 02 — The Agentic Loop

Domain 1: Agentic Architecture & Orchestration (27%) — task statement 1.1

This is the foundation of the largest domain. Get it exact: the loop's control flow is a small mechanical thing, and the exam tests it by presenting four plausible descriptions of it, only one of which is right.


1. The loop, precisely

An agentic system is a while loop around the Messages API. Each iteration:

  1. Send the full conversation to the model, along with the tool definitions.
  2. Inspect the response's stop_reason.
  3. If stop_reason is "tool_use" → the response contains one or more tool_use blocks. Execute them, append the assistant message and the corresponding tool_result blocks to the conversation, and loop.
  4. If stop_reason is "end_turn" → the model is finished. Exit the loop; the final text is the answer.
messages = [{"role": "user", "content": task}]

while True:
    response = client.messages.create(
        model="claude-opus-5",
        max_tokens=4096,
        system=SYSTEM_PROMPT,
        tools=TOOLS,
        messages=messages,
    )

    # 1. Always append the assistant turn, verbatim.
    messages.append({"role": "assistant", "content": response.content})

    # 2. The stop_reason is the loop condition. Nothing else is.
    if response.stop_reason != "tool_use":
        break

    # 3. Execute every tool_use block and return every result in ONE user turn.
    results = []
    for block in response.content:
        if block.type == "tool_use":
            results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": execute(block.name, block.input),
            })

    messages.append({"role": "user", "content": results})

🎯 The four facts the exam tests about this loop

  1. stop_reason == "tool_use" means continue. stop_reason == "end_turn" means stop. These two values are the entire termination logic.
  2. Tool results are appended to the conversation as a user-role message containing tool_result blocks, each keyed to its tool_use_id. The conversation grows monotonically; you don't replace or rewrite prior turns.
  3. All tool results from one assistant turn go back in a single user turn. If the model requested three tools, you return three tool_result blocks together, not three separate messages.
  4. The model decides what to call next, based on the results it just received. That's what makes it an agent rather than a workflow.

🚫 The four loop anti-patterns

These are named in the guide's objectives and appear as distractors. Each one is a plausible implementation that is wrong.

Anti-pattern Why it's wrong
Parsing natural-language signals for termination — e.g. stopping when the text contains "I'm done" or "task complete" The model's prose is not a control signal. stop_reason is the API's structured, reliable indicator. Text parsing is brittle and silently misfires.
Using an arbitrary iteration cap as the primary stop condition — e.g. for i in range(10) A cap is a legitimate safety backstop against runaway loops. It is not a termination condition. If the cap is what ends your loop in normal operation, the agent is being cut off mid-task.
Checking whether the assistant's text response is non-empty to decide completion The model can emit text and request tools in the same turn. Non-empty text does not mean it's finished.
Terminating as soon as any single tool returns a result The whole point of the loop is multi-step work: call, observe, decide, call again. One result rarely completes a task.

⚠️ The trap: an option offering "add a maximum iteration limit" sounds like responsible engineering, and as a backstop it is. Read carefully whether the option positions it as the primary termination mechanism. If it does, it's wrong.


2. Model-driven vs pre-configured control flow

The exam distinguishes two ways to structure multi-step work, and asks you to choose between them based on the task's predictability.

Model-driven (agentic) Pre-configured (decision tree / workflow)
Who decides the next step The model, from the results so far Your code, from a fixed graph
Fits Open-ended investigation; unknown path; variable depth Known, stable sequence; auditable steps
Cost/latency Higher and variable Lower and predictable
Failure mode Wanders, over-calls, or stops early Can't handle situations you didn't anticipate
Example "Investigate why this customer's order was double-charged" "Validate → look up → format → respond"

🧠 How the exam frames this: it doesn't ask "which is better." It gives you a task and expects you to notice whether the path is knowable in advance. Support triage where each of five categories has a fixed handling procedure → pre-configured. Root-cause investigation across an unknown number of related orders → model-driven.

The hybrid is usually right for real systems and often right on the exam: model-driven within a step, pre-configured between steps. The agent freely explores how to gather the facts; your code enforces that verification precedes refund. That's the bridge to Chapter 04.


3. Tools in the loop: definitions and tool_choice

Tool definitions travel with every request. A definition is a name, a description, and an input_schema.

TOOLS = [
    {
        "name": "lookup_order",
        "description": (
            "Retrieve the full record for a single order, including line items, "
            "shipment status, and payment state. Requires an exact order ID in the "
            "format ORD-XXXXXXXX. Use this when the customer references a specific "
            "order. Does NOT search by customer name, email, or date range — use "
            "get_customer first to obtain the customer's order IDs."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "order_id": {
                    "type": "string",
                    "description": "Exact order ID, format ORD-XXXXXXXX.",
                }
            },
            "required": ["order_id"],
        },
    }
]

That description is written the way the exam wants: what it returns, what input format it needs, when to use it, and an explicit boundary against the neighbouring tool. Chapter 05 is entirely about this.

📎 tool_choice — three values, know them cold

Value Behaviour Use when
{"type": "auto"} Model decides whether to use a tool at all, and which. Default. Normal agentic operation
{"type": "any"} Model must call one of the provided tools, but chooses which You need a tool call every turn and any of them is acceptable
{"type": "tool", "name": "extract_invoice"} Model must call that specific tool Structured output — you're using the tool as a schema, not as an action

🎯 The forced-tool form is the standard mechanism for reliable structured output (Chapter 10). If an item asks how to guarantee schema-conformant JSON, the answer involves a tool definition plus tool_choice forcing it.

⚠️ "any" and the forced form both remove the model's ability to answer without calling a tool. That's the point — but it also means the model can't tell you "there's nothing to extract." Handle that with nullable schema fields, not by relaxing tool_choice.


4. What the model actually needs each turn

Because the conversation is resent every iteration, the loop has a context-growth problem built into it. Two facts the exam tests:

  1. Tool results accumulate disproportionately. A tool returning 40 fields when 5 are relevant multiplies the noise every call. Trim tool outputs to the fields the agent needs before appending them. (Full treatment in Chapter 11.)
  2. You must pass the complete conversation history for the model to reason correctly about what it has already tried. Dropping earlier turns to save space causes repeated tool calls and lost commitments.

These pull in opposite directions, and resolving that tension is Domain 5's whole subject. The reflex: keep the full turn structure, shrink the payloads within it, and pin durable facts outside the summarizable region.


5. Worked example — reading a loop item

A support agent built with the Agent SDK sometimes stops responding to the customer after retrieving order details, without answering the original question. The implementation ends its loop when the API response contains a non-empty text block. What is the most likely cause?

A. The max_tokens value is too low for the full response. B. The termination condition is checking assistant text rather than stop_reason. C. The tools' input_schema definitions are missing required fields. D. The conversation history is exceeding the context window.

Answer and reasoning

B.

The stem hands you the implementation detail: the loop terminates on non-empty text. Claude routinely emits explanatory text alongside a tool_use block — "Let me look up that order" plus the call. A loop that stops on text will exit right there, mid-task, which is exactly the reported symptom (stops after retrieving details, question unanswered).

  • Amax_tokens truncation produces stop_reason: "max_tokens" and a cut-off response, not a clean early stop with a coherent partial answer.
  • C — Schema problems produce tool-call errors or validation failures, not premature termination.
  • D — Context overflow produces an error, not silent early exit. Also, "the conversation is too long" is the more context distractor family (Chapter 01 §3) — always check whether the stem actually supports it. Here it doesn't.

Reflex applied: the stem's evidence names the layer. The layer named is the termination condition, so the fix is the termination condition.


6. Decision reflexes (reproduce from memory)

  • The loop condition is stop_reason: "tool_use" → continue, "end_turn" → stop. Nothing else.
  • Append the assistant turn verbatim; return all tool_result blocks for one turn in one user message, keyed by tool_use_id.
  • Text presence, iteration count, and "got a result" are never termination conditions. An iteration cap is a backstop only.
  • Model-driven when the path is unknowable; pre-configured when it's fixed; hybrid when correctness ordering matters.
  • tool_choice: auto (default) / any (must call something) / {"type":"tool","name":...} (must call that one → structured output).
  • Trim tool result payloads, never the turn structure.