CCAR-F Domain 1 — Agentic Architecture & Orchestration (27%)¶
Track: CCAR-F · Weight: 27%, the largest domain (see
registry.yaml) · Objectives: CCAR-F-1.1 to 1.7 · Time: ~9 hours including labs
This domain is about the machinery of agents: the loop that keeps an agent running, the coordinator that hands work to subagents, the hooks that enforce rules the model cannot be trusted to follow, and the sessions that let work pause, resume and branch. It is a primary domain in the support-agent, research-system and developer-productivity scenarios, so expect it to dominate whichever four you draw.
SDK facts on this page were checked against the Agent SDK documentation
and the claude-api reference on 2026-10-24. The SDK releases often; re-check option names before you rely
on them in code.
Objectives¶
| ID | Objective | Where to study |
|---|---|---|
| CCAR-F-1.1 | Design and build agentic loops for autonomous task execution | The agent loop, The tool-use loop |
| CCAR-F-1.2 | Orchestrate multi-agent systems with coordinator and subagent patterns | Multi-agent architecture, Workflow patterns |
| CCAR-F-1.3 | Configure subagent invocation, context passing and spawning | Agent SDK, Commands, skills and subagents |
| CCAR-F-1.4 | Build multi-step workflows with enforcement and handoff patterns | Human in the loop, Agent reliability |
| CCAR-F-1.5 | Use Agent SDK hooks to intercept tool calls and normalise data | Hooks |
| CCAR-F-1.6 | Choose task decomposition strategies for complex workflows | Task decomposition and chaining, Workflows vs agents |
| CCAR-F-1.7 | Manage session state, resumption and forking | Headless, CI and the Agent SDK, Memory and persistence |
Key concepts¶
- Agentic loop. Send a request; if the response stops because Claude wants a tool (
stop_reason == "tool_use"), run the tool, append the result and call again; stop when Claude finishes its turn (end_turn). The model, not your code, decides the next step. - Coordinator (orchestrator). An agent whose job is to plan, delegate to subagents, check what comes back and assemble the answer. It is the hub in a hub-and-spoke design.
- Subagent. A separate agent instance with its own context window, system prompt and tool set. It sees only what it is given and returns only its final message.
- Hook. Your code, run by the SDK at a lifecycle event (before a tool runs, after it returns, when a subagent stops). Hooks are deterministic; prompts are not.
- Session. The persisted conversation history of an SDK or Claude Code run. It can be continued (most recent), resumed (by ID or name) or forked (copied into a new, independent branch).
Detailed technical notes¶
1.1 The agentic loop¶
flowchart LR
A[Messages request<br/>tools + history] --> B{stop_reason}
B -- tool_use --> C[Run each requested tool<br/>validate + authorise]
C --> D[Append assistant turn<br/>+ all tool_result blocks<br/>in one user message]
D --> A
B -- end_turn --> E[Done: use final content]
B -- max_tokens / pause_turn / refusal --> F[Handle explicitly:<br/>continue, raise limit, or stop]
A minimal manual loop on the Messages API (Python, current request rules: no sampling parameters, no prefill):
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
def run_agent(messages: list, tools: list, execute_tool) -> list:
while True:
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=16000,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "tool_use":
results = []
for block in response.content:
if block.type == "tool_use":
results.append(execute_tool(block)) # returns a tool_result dict
messages.append({"role": "user", "content": results}) # all results, one message
continue
if response.stop_reason == "end_turn":
return messages
if response.stop_reason == "pause_turn": # a long server tool paused; send it back
continue
raise RuntimeError(f"Stopped early: {response.stop_reason}") # max_tokens, refusal, ...
Cost: requires API credits (live mode).
What the exam rewards:
| Do | Do not |
|---|---|
Branch on stop_reason |
Parse the model's prose for "I'm done" or "FINISHED" |
Append the whole assistant content (including tool_use blocks) and every tool_result |
Append only the text, or drop a failed tool's result |
Return tool failures as tool_result with is_error: true so Claude can adapt |
Raise and abandon the loop on the first tool error |
| Keep a turn or budget cap as a safety net | Use an iteration cap as the main way the loop ends |
| Treat "assistant text present" as normal (Claude often narrates before calling tools) | Treat any text block as a sign the task is complete |
In the Agent SDK you do not write this loop: query() runs it and yields messages. You still choose the
guard rails, for example max_turns and max_budget_usd in Python (maxTurns, maxBudgetUsd in
TypeScript). The final ResultMessage tells you how it ended: success, or an error subtype such as
error_max_turns or error_max_budget_usd.
Model-driven versus pre-scripted. In an agent, Claude picks the next tool from what it has learned so far. If you find yourself hard-coding "call A, then B, then C" for every input, you have a workflow; that is fine when the steps are fixed (see 1.6), but do not call it an agent and do not pay agent costs for it.
1.2 Coordinator and subagent patterns¶
flowchart TB
U[Request] --> C[Coordinator]
C -->|scoped brief| S1[Subagent: source finder]
C -->|scoped brief| S2[Subagent: document reader]
C -->|findings + sources| S3[Subagent: synthesiser]
S1 -->|structured findings| C
S2 -->|structured findings| C
S3 -->|draft + coverage notes| C
C -->|gaps found| S1
C --> R[Answer]
Design rules:
- Hub and spoke. Subagents report to the coordinator, not to each other. The coordinator sees every hand-off, so errors are handled in one place, traces are readable and nothing reaches the final answer without passing a check.
- Isolated context. A subagent starts fresh. It does not inherit the coordinator's conversation, earlier tool results or system prompt (unless it is a fork, below). Whatever it needs must be in its brief.
- Select, do not always fan out. A good coordinator reads the request and invokes only the subagents it needs. A one-line factual question should not trigger the full research pipeline.
- Partition to avoid overlap. Give each subagent a distinct slice (subtopic, source type, time window or repository area). Overlapping briefs waste tokens and produce duplicate, sometimes contradictory, findings.
- Watch for over-narrow decomposition. If the coordinator splits a broad question into three narrow sub-questions, the report will miss whatever fell between them. Ask the coordinator to state the coverage it expects and to check the result against it.
- Iterate on gaps. After synthesis, the coordinator compares the draft with the goal, sends targeted follow-up work to the right subagent and re-synthesises until coverage is good enough or the budget is spent.
Costs to remember: every subagent makes its own model calls, so multi-agent runs use many more tokens than a
single agent. The SDK can cap growth: nesting depth (CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH, default 3 layers;
1 disables nesting), concurrency (CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS, default 20) and total spend per
query (max_budget_usd), as of 2026-10-24.
1.3 Subagent invocation, context passing and spawning¶
In the Agent SDK you define subagents programmatically with the agents option, a map of names to
AgentDefinition objects. The required fields are description (when to use it; Claude reads this to decide)
and prompt (its system prompt). Optional fields include tools (an allow-list), disallowedTools, model,
skills, mcpServers and maxTurns. Claude invokes subagents through the Agent tool.
Naming change. Claude delegates through the
Agenttool, calledTaskin the v1.0 exam guide. Current SDK releases emit"Agent"intool_useblocks, while thesystem:inittool list still shows"Task"; code that detects subagent calls should match both names. If an exam item says "Task", read it as the same mechanism.
allowed_toolsapproves; it does not restrict. ListingAgentinallowed_tools(allowedTools) lets the coordinator delegate without a permission prompt. Tools you leave off that list are not removed; they fall back to normal permission handling. To restrict what an agent can use, setAgentDefinition.tools(a subagent's allow-list: omitted tools are absent from its session),disallowed_tools, or deny rules.
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
options = ClaudeAgentOptions(
allowed_tools=["Read", "Grep", "Glob", "Agent"], # auto-approved (not a restriction)
agents={
"dependency-tracer": AgentDefinition(
description="Traces where a function or module is used. Use for 'who calls X' questions.",
prompt="You trace call sites. Report file, line and caller for each use. Do not edit files.",
tools=["Read", "Grep", "Glob"], # read-only
),
},
)
async for message in query(prompt="Which services still call billing.legacy_tax()?", options=options):
...
Cost: requires API credits (live mode). Install with
pip install claude-agent-sdk.
Context-passing rules the exam probes:
| Rule | Why |
|---|---|
| Put everything the subagent needs in its brief: goal, scope, file paths, prior findings, output format | It cannot see the parent conversation |
| Pass prior agents' output in full (or as a structured digest) when a later agent depends on it | A synthesiser that never saw the reader's findings cannot cite them |
Separate content from metadata, for example {"claim": ..., "source_url": ..., "page": ...} |
Keeps attribution intact through hand-offs (see Domain 5, 5.6) |
| Emit several Agent tool calls in one coordinator response to run them in parallel | Calls spread across turns run one after another |
| Brief subagents with the goal and quality bar, not a step-by-step script | Lets them adapt to what they find |
Forks. A fork is a subagent that copies the conversation so far instead of starting fresh. Use one when the
branch genuinely needs the shared analysis, for example "try approach B from here". At session level, the
equivalent is fork_session (1.7). Do not fork just to save writing a brief: forks carry the whole history,
which costs tokens and imports any stale results.
1.4 Enforcement and hand-off patterns¶
The central idea: a prompt instruction is guidance with a non-zero failure rate; a programmatic gate is a guarantee. When an ordering or limit is a compliance requirement, enforce it in code.
| Requirement | Prompt-only approach (weak) | Programmatic approach (strong) |
|---|---|---|
| Verify identity before changing an account | "Always verify the customer first." | A PreToolUse hook denies account-changing tools until a verification tool has returned a verified ID in this session |
| Never exceed a credit limit | "Do not issue credits above the limit." | The hook (or the tool server) checks the amount and denies, returning a reason that tells Claude to escalate |
| Escalate certain request types | "Escalate legal threats." | Explicit criteria plus examples in the prompt and a deterministic check on the hand-off tool for categories that must always go to people |
Prerequisite gates are small state machines: the hook records that the prerequisite succeeded (keyed by session) and blocks the dependent tool until then. Keep the state outside the model.
Multi-concern requests. When a single message contains several issues ("my parcel is late, I was charged twice and I want to change my address"), split them into separate items, investigate each (in parallel where independent), then compose one reply. Track each item's status so none is dropped.
Structured hand-offs. When escalating to a person who will not read the transcript, send a compact, structured summary:
{
"case_id": "CS-20817",
"customer_ref": "ACCT-55102",
"issues": [
{"summary": "Duplicate charge on order 7781", "evidence": "two captures of 42.10 on 2026-10-19",
"action_taken": "none", "recommended_action": "refund one capture", "amount": 42.10}
],
"why_escalated": "amount needs supervisor approval under policy section 4",
"customer_expectation": "refund before 2026-10-31"
}
1.5 Agent SDK hooks¶
Hooks are callbacks you register in options.hooks, keyed by event name, each with an optional matcher
(for tool events, matched against the tool name; MCP tools look like mcp__<server>__<tool>).
| Event (Python and TypeScript) | Fires | Typical use |
|---|---|---|
PreToolUse |
Before a tool runs | Block (permissionDecision: "deny"), approve, or rewrite input (updatedInput) |
PostToolUse |
After a tool returns | Normalise or trim output (updatedToolOutput), add context (additionalContext), audit |
PostToolUseFailure |
A tool failed | Log, classify, add recovery guidance |
SubagentStart / SubagentStop |
Subagent lifecycle | Trace fan-out, aggregate results |
PreCompact |
Before compaction | Archive the full transcript |
UserPromptSubmit, Stop, Notification, PermissionRequest |
Various | Context injection, final checks, alerts |
Some events (for example SessionStart, SessionEnd) are TypeScript-only as SDK callbacks; in Python they are
available only as shell hooks in settings files (as of 2026-10-24).
A PreToolUse gate that blocks a policy breach and steers Claude to escalate:
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
CREDIT_LIMIT = 150.00 # synthetic policy value for Contoso Outfitters
async def enforce_credit_limit(input_data, tool_use_id, context):
amount = float(input_data["tool_input"].get("amount", 0))
if amount > CREDIT_LIMIT:
return {
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": (
f"Credits above {CREDIT_LIMIT:.2f} need a supervisor. "
"Call open_handoff with a structured summary instead."
),
}
}
return {} # allow
options = ClaudeAgentOptions(
hooks={"PreToolUse": [HookMatcher(matcher="mcp__billing__issue_credit", hooks=[enforce_credit_limit])]}
)
A PostToolUse hook that normalises timestamps from two backends (one returns epoch seconds, the other ISO 8601
with offsets) so the model always sees one format. Returning updatedToolOutput replaces what Claude sees;
additionalContext only appends.
Points to remember:
- When several
PreToolUsehooks disagree, the most restrictive wins (denybeatsdefer, which beatsask, which beatsallow). - A matcher of
"Write|Edit"does not see a Bash command that writes the same file. Enforce at the right layer (tool server, sandbox, permission rules) for anything that must hold whatever tool is used. - A
PreToolUsecallback that times out blocks the tool call; post-tool timeouts keep the result. - Hooks are code: test them, version them and log their decisions.
1.6 Task decomposition strategies¶
| Strategy | Use when | Example |
|---|---|---|
| Prompt chaining (fixed sequence) | The steps are known in advance and each benefits from focus | Review each changed file on its own, then run one cross-file pass for interactions |
| Dynamic decomposition (adaptive plan) | The work is open-ended and the next step depends on what you find | "Get this legacy service under test": map the structure, find the riskiest paths, plan, revise the plan as dependencies appear |
| Parallel sectioning | Independent parts can run at once | One subagent per microservice for a dependency inventory |
| Hybrid | A fixed outer pipeline with an adaptive step inside | Fixed intake → adaptive investigation → fixed report template |
Why split a large review into per-file passes plus an integration pass: one pass over 40 files dilutes attention, misses local defects and can contradict itself. Focused passes find local issues; a separate pass with summaries of every file looks only for cross-file problems (data flow, contract mismatches).
1.7 Sessions: resume, continue and fork¶
| Need | Claude Code CLI | Agent SDK (Python / TypeScript) |
|---|---|---|
| Carry on the most recent conversation in this directory | claude --continue (-c) |
continue_conversation=True / continue: true |
| Return to a specific session | claude --resume <id-or-name> (-r) |
resume=session_id / resume: sessionId |
| Give a session a memorable name | claude -n "auth-audit" or /rename |
rename_session() / renameSession() |
| Branch without changing the original | claude --resume <id> --fork-session |
resume=... plus fork_session=True / forkSession: true |
Capture the ID from the session_id on the result message. Forks get their own ID; the original stays as it
was. Sessions are stored locally (under ~/.claude/projects/), so resuming on another host needs a session
store adapter or a copied transcript file.
Resume or start fresh?
| Situation | Choose |
|---|---|
| The earlier context is still accurate and you want to build on it | Resume |
| Files changed since the session read them, but only a few | Resume and tell the agent exactly which files changed so it re-reads those |
| Much of the earlier tool output is now stale (big refactor, new branch, data refreshed) | Start a new session and inject a structured summary of the conclusions that still hold |
| You want to compare two approaches from the same analysis | Fork twice (or fork once and resume the original) |
Two cautions: sessions persist the conversation, not the filesystem (a fork that edits files changes the real files; use file checkpointing or git to branch code), and a resumed session trusts its old tool results unless you say otherwise.
Worked design scenario¶
Contoso Outfitters runs an Agent SDK support agent with four MCP tools: find_account, get_shipment,
issue_credit and open_handoff. Reviews show three problems: occasional credits on unverified accounts, a
loop that sometimes stops after Claude writes "Let me check that for you", and escalations that arrive without
enough context.
- Loop: the integration checked "is there text?" to decide completion. Replace with
stop_reasonbranching; keepmax_turns=25andmax_budget_usdonly as safety nets. - Verification: a
PreToolUsehook onissue_creditdenies the call unlessfind_accountreturnedverified: truefor this session; a second check denies amounts above the policy cap and tells Claude to useopen_handoff. - Normalisation: a
PostToolUsehook converts both backends' dates to ISO 8601 and drops the 30 shipment fields the agent never uses. - Hand-off:
open_handofftakes a strict schema (case, customer, issues with evidence, action taken, recommended action, customer expectation) so the human agent does not need the transcript. - Sessions: each customer conversation is a named session so a returning customer can be resumed; if the order data has since changed, the agent starts fresh with a case-facts summary.
Anti-patterns¶
- Ending the loop when the model's text "sounds finished", or when an iteration counter runs out.
- A coordinator that always runs every subagent, whatever the request.
- Subagents that talk to each other directly, bypassing the coordinator's checks.
- Briefs that say "continue the analysis" to a subagent that has never seen the analysis.
- Business rules that exist only as sentences in the system prompt.
- Resuming a week-old session after a large refactor and trusting its earlier reads.
Labs and further practice¶
- Lab 12: Agentic task execution
- Lab 13: Human-in-the-loop approvals
- Lab 08: Tool invocation
- Lab 10: Claude Code workflows
- Practical exercises 1, 2 and 3 in
practical-exercises.md
Self-check¶
- Which
stop_reasonkeeps the loop going, and which one ends it normally? - A synthesis subagent produces a report with no citations, although the reader subagent found sources. What is the most likely cause?
- Why is a
PreToolUsehook preferred over a system-prompt rule for a credit limit? - What is the difference between resuming and forking a session?
- When should you start a new session with a summary instead of resuming?
Answers
tool_usecontinues the loop (run the tools, append results, call again);end_turnends it. Other values (max_tokens,pause_turn,refusal) need explicit handling.- The coordinator did not pass the reader's findings (with their source metadata) into the synthesiser's brief. Subagents do not inherit context.
- The hook runs every time and cannot be talked out of the rule; a prompt instruction is followed most of the time but not always, and can be undermined by injected content.
- Resume continues the same session and adds to its history; fork copies the history into a new session with its own ID and leaves the original unchanged.
- When much of the earlier tool output is stale (files or data changed substantially), so the old reads would mislead the agent.