Domain 1 Project — build a support agent, then make it a team
Claude Certified Architect - Foundations (CCAR-F) › Domain 1: Agentic Architecture & Orchestration
Domain 1 Project — build a support agent, then make it a team
One system, seven stages, 3 hours 0 minutes. You start with a single agent that can call two tools and finish with a coordinator delegating to subagents across resumable sessions. Every stage is one of Domain 1's seven task statements, so finishing the project is finishing the domain's practical half.
Domain 1 is 27% of the exam, the largest block, and nearly all of it reduces to one question: what is actually in control? You cannot answer that from reading. You answer it by writing the loop, watching it stop for the wrong reason, and fixing it.
The project at a glance
- 1 · Agentic Architecture & Orchestration (27%)
- 7, one per task statement
- Intermediate
- 180 minutes
- Python 3.10+, the anthropic SDK, an API key
- Exam guide Preparation Exercises 1 and 4
What you will be able to do
- Write an agentic loop that terminates on stop_reason and explain why the other three termination strategies are wrong
- Enforce a business rule in code and say when a prompt instruction is not enough
- Build a coordinator that delegates to subagents and explain what each subagent does and does not inherit
- Choose between a fixed pipeline and adaptive decomposition, and justify the choice from the task rather than from preference
- Decide between resuming a session, forking it, and starting fresh with an injected summary
Build it in this order
The order is not the guide's. It is the order the system needs: enforcement has to have a loop to enforce, hooks intercept calls the loop is already making, and a coordinator is only worth having once there is decomposition to hand out.
Seven stages
1.1 · The loop · 30 min
BUILD — Two tools — lookup_order and a calculator — and a loop that calls the Messages API, branches on stop_reason, executes every tool_use block, and appends BOTH the assistant message and a user message carrying the tool results. WHY — Everything after this sits inside this loop, and it is the most reliably examined thing in the domain. Branching on stop_reason rather than on content is the difference between an agent that works and one that works on your test cases. YOU SHOULD SEE — History growing by exactly two messages per tool iteration, each tool_result carrying the tool_use_id of the call it answers, and the loop exiting only on stop_reason or the safety cap. TRAP — Appending the tool results but not the assistant message, or the reverse. Either one leaves the model reasoning from the same information twice, and the symptom is an agent that repeats a call rather than one that errors.
Stage 1 in full
Every other stage gives you the target and the trap. This one gets the code, because each later stage is a modification of it and a wrong loop invalidates everything built on top.
The four steps
Send the whole conversation. Read stop_reason. If it is tool_use, run the tools, append the results, and go again. If it is end_turn, you are done. The Messages API is stateless — everything the model knows on iteration four is something your loop put in the request.
Beyond the two values the guide names
The exam guide keys tool_use and end_turn — the two a basic loop branches on, and the two you will be graded on. The live API returns four more, and a loop that assumes anything other than end_turn means tool_use will mishandle all of them:
| stop_reason | What it means |
|---|---|
| end_turn | Finished naturally. Stop. |
| tool_use | Wants one or more tools. Run them and continue. |
| max_tokens | Hit your max_tokens ceiling. The response is truncated. |
| stop_sequence | Hit a stop sequence you supplied. |
| pause_turn | Paused a long-running turn and can be resumed. |
| refusal | Declined on safety grounds, on an otherwise normal 200 response. |
refusal arrives on a successful HTTP response, so a loop that only checks status codes treats it as ordinary output — and it is the only value that populates stop_details, which is null for every other one. The safe default is to treat anything other than end_turn as "not finished, find out why".
For the exam, know the two. For production, handle the six.
The three anti-patterns
Parsing natural language. Checking whether Claude said "I'm done". Ambiguous by construction — it may have finished the first file and be about to open the second.
An iteration cap as the primary stopping mechanism. It either cuts off work that needed twelve iterations or burns seven on a task that finished in three. A cap is a safety net; it is not a design.
Treating assistant text as completion. Claude routinely returns explanatory text alongside a tool_use block in the same response — "Let me look up your order" and the lookup arrive together. Text presence says nothing about whether the agent is finished.
An agent sometimes terminates early when Claude returns text alongside a tool call. The loop decides it is finished by checking whether the first content block has type text. Users report incomplete answers on complex queries. What should change?
Reference implementation
Work the stage first. This is what a finished stage 1 looks like, and the base every later stage modifies.
import ast
import operator
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-opus-5"
MAX_ITERATIONS = 20
TOOLS = [
{
"name": "lookup_order",
"description": "Look up an order by id. Returns status, total and placed_at.",
"input_schema": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
},
{
"name": "calculator",
"description": "Evaluate an arithmetic expression and return the result.",
"input_schema": {
"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"],
},
},
]
# A whitelist evaluator, not eval(). A tool is an execution surface the model
# chooses to invoke, so its blast radius is a design decision — this one can add,
# subtract, multiply and divide numbers, and can do nothing else.
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub,
ast.Mult: operator.mul, ast.Div: operator.truediv}
def calculate(expression):
def walk(node):
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in _OPS:
return _OPS[type(node.op)](walk(node.left), walk(node.right))
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
return -walk(node.operand)
raise ValueError("unsupported expression")
return walk(ast.parse(expression, mode="eval").body)
def run_tool(name, tool_input):
if name == "lookup_order":
return "status=delivered total=129.99 placed_at=2026-08-02"
if name == "calculator":
try:
return str(calculate(tool_input["expression"]))
except ValueError as error:
return f"Error: {error}"
return "Unknown tool"
def run_agent(prompt):
messages = [{"role": "user", "content": prompt}]
for _ in range(MAX_ITERATIONS):
response = client.messages.create(
model=MODEL, max_tokens=16000, tools=TOOLS, messages=messages
)
# Anything other than end_turn is "not finished, find out why".
if response.stop_reason == "end_turn":
return next(b.text for b in response.content if b.type == "text")
if response.stop_reason == "refusal":
return f"Declined: {response.stop_details.category}"
if response.stop_reason != "tool_use":
return f"Stopped early: {response.stop_reason}"
# The assistant turn goes back verbatim — the tool_use blocks in it are
# what each tool_result below refers to.
messages.append({"role": "assistant", "content": response.content})
# Every result in ONE user message. Splitting them teaches the model to
# stop calling tools in parallel.
results = [
{
"type": "tool_result",
"tool_use_id": block.id,
"content": run_tool(block.name, block.input),
}
for block in response.content
if block.type == "tool_use"
]
messages.append({"role": "user", "content": results})
print("WARNING: safety cap reached — the loop should have ended on stop_reason")
return "Terminated by safety cap"
if __name__ == "__main__":
print(run_agent("Look up order A-4417 and tell me what a 20% refund would be."))Three details worth reading twice. The refusal branch reads stop_details, which is populated for that value alone. The loop is a for over the cap rather than a while True with a counter, so the cap is structurally a bound — exactly what the anti-pattern says it should be. And the calculator walks a whitelisted syntax tree rather than calling eval: a tool is an execution surface the model decides to invoke, and a lab that ships eval teaches the opposite of what Domain 2 is about.
Prove stage 1 before moving on
- Break the append on purpose: stop adding the tool_result message and re-run. The agent should call the same tool again — the repeated-call symptom seen from the inside.
- Log stop_reason every iteration. A two-tool prompt should print tool_use, tool_use, end_turn.
- Confirm the safety cap never trips on a normal query. If it does, the loop is not terminating on stop_reason and stage 1 is not finished.
Where the depth lives
This page is for building. The reading is elsewhere, and repeating it here would put the same claims in three places to drift apart:
- The Domain 1 roadmap — what the domain is about, the order to learn it in, and where it catches people
- Seven flashcard decks, 48 cards — one card per published objective
- 66 Domain 1 questions — judgement under exam conditions, inside the six production scenarios
Sources
- CCAR-F Exam Guide v1.0, section 6 — Domain 1's seven task statements, which are this project's seven stages.
- CCAR-F Exam Guide v1.0, section 8 — Preparation Exercise 1 ("Build a Multi-Tool Agent with Escalation Logic") and Exercise 4 ("Design and Debug a Multi-Agent Research Pipeline"). This project merges them: exercise 1 is the loop-and-enforcement half, exercise 4 the coordinator half.
- Anthropic Messages API reference — the stop_reason set, stop_details, the tool_result message shape, and the parallel-tool-use rule.