{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Module 2 Self-Driven Lab*\r\n",
    "\r\n",
    "# Agentic Loops & SDK Foundations\r\n",
    "\r\n",
    "**Objective:** master the stateless nature of history bookkeeping and the transition to model-driven orchestration.\r\n",
    "\r\n",
    "## Challenge Outline\r\n",
    "\r\n",
    "Build a complete notebook that demonstrates the following outcomes:\r\n",
    "\r\n",
    "- **History bookkeeping:** create a manual message array. Send a three-turn conversation where you manually append each user and assistant message. Verify that omitting one assistant turn causes the API to reject the request.\r\n",
    "- **Tool result mapping:** simulate a `tool_use` event. Manually append the `tool_use` block to history, followed by a `tool_result` block. Ensure the `tool_use_id` matches exactly.\r\n",
    "- **SDK transition:** refactor the same loop using the Claude Agent SDK. Observe how the SDK handles history, tool dispatch, and termination automatically.\r\n",
    "- **Loop safety:** implement an iteration cap of 5 turns as a defensive guardrail. Log a hard error if the cap is reached before `stop_reason: \"end_turn\"`.\r\n",
    "\r\n",
    "Your solution should include enough code, output, or written observations to prove each outcome worked. Keep the notebook focused on final behavior and evidence rather than a guided walkthrough.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Student Workspace\n",
    "\n",
    "Use the sections below to build your solution. Each section maps to one required outcome from the challenge outline.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Part 1: History bookkeeping\n",
    "\n",
    "create a manual message array. Send a three-turn conversation where you manually append each user and assistant message. Verify that omitting one assistant turn causes the API to reject the request.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Part 1: History bookkeeping\n",
    "# Add your implementation, outputs, or notes here.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Part 2: Tool result mapping\n",
    "\n",
    "simulate a `tool_use` event. Manually append the `tool_use` block to history, followed by a `tool_result` block. Ensure the `tool_use_id` matches exactly.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Part 2: Tool result mapping\n",
    "# Add your implementation, outputs, or notes here.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Part 3: SDK transition\n",
    "\n",
    "refactor the same loop using the Claude Agent SDK. Observe how the SDK handles history, tool dispatch, and termination automatically.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Part 3: SDK transition\n",
    "# Add your implementation, outputs, or notes here.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Part 4: Loop safety\n",
    "\n",
    "implement an iteration cap of 5 turns as a defensive guardrail. Log a hard error if the cap is reached before `stop_reason: \"end_turn\"`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Part 4: Loop safety\n",
    "# Add your implementation, outputs, or notes here.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Verification Notes\n",
    "\n",
    "Summarize the evidence that each part worked. Capture API signals, validation outcomes, errors, recovery behavior, cost observations, or comparisons required by this lab.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Verification notes\n",
    "# Record the evidence that proves each lab outcome worked.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Answer Key\n",
    "\n",
    "The cells below contain the completed reference implementation/content for this module. Use this section only after attempting the self-driven lab."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Module 2 of 12*\n",
    "\n",
    "# Agentic Loops & SDK Foundations\n",
    "\n",
    "This module covers the lower-level orchestration required for production Claude systems: the **Messages API** and how tool results re-enter conversation history, the **Agent SDK** for model-driven workflows, **session management** (when to resume, fork, or start fresh), and the agentic loop pattern that governs every multi-turn workflow.\n",
    "\n",
    "\n",
    "> **Tip.** **Prerequisites:** If you haven't installed VS Code, Jupyter, and the Anthropic SDK yet, complete [Module 1: Dev Environment & Foundations](module-1.html) before continuing.\n",
    "\n",
    "## 1. Setting Up the Messages API (Stateless Design)\n",
    "\n",
    "The Claude API is **stateless**, it does not persist history between calls. To build a conversation, you must maintain a local array of messages and send the full history back to the API with every request. The same pattern carries through tool use: when Claude requests a tool, you append the assistant's `tool_use` block, then append a `tool_result` block on the next turn so the model can reason about what the tool returned.\n",
    "\n",
    "**Implementation Task: Local Message History**\n",
    "\n",
    "Start by initializing your conversation locally and capturing the assistant's reply."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import anthropic\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "load_dotenv()  # reads ANTHROPIC_API_KEY from your .env file\n",
    "\n",
    "client = anthropic.Anthropic()\n",
    "\n",
    "# Start with your local message history\n",
    "messages = [\n",
    "    {\"role\": \"user\", \"content\": \"Analyze the target audience for an AI consulting firm.\"}\n",
    "]\n",
    "\n",
    "# Create the first request\n",
    "response = client.messages.create(\n",
    "    model=\"claude-opus-4-7\",\n",
    "    max_tokens=4096,\n",
    "    messages=messages\n",
    ")\n",
    "\n",
    "# Build the synthetic conversation locally by adding the assistant's response\n",
    "messages.append({\"role\": \"assistant\", \"content\": response.content})\n",
    "\n",
    "# Add a manual follow-up to the local history\n",
    "messages.append({\"role\": \"user\", \"content\": \"Now, generate three LinkedIn post ideas for this audience.\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Implementation Task: Feeding Tool Results Back Into History**\n",
    "\n",
    "When `stop_reason == \"tool_use\"`, the assistant's last message contains one or more `tool_use` content blocks. You execute each tool locally, then send the output back as a `tool_result` block on a *user* turn. Re-calling `messages.create` with the updated history lets the model read the tool output and decide the next action, this is the spine of every agentic loop."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Assume the prior `response` came back with stop_reason == \"tool_use\"\n",
    "# 1. Append the assistant turn verbatim, including its tool_use block(s).\n",
    "messages.append({\"role\": \"assistant\", \"content\": response.content})\n",
    "\n",
    "# 2. Locate the tool_use block and run the tool in your own code.\n",
    "tool_use = next(b for b in response.content if b.type == \"tool_use\")\n",
    "tool_output = run_local_tool(tool_use.name, tool_use.input)  # your dispatcher\n",
    "\n",
    "# 3. Append the tool_result on a USER turn, keyed by the tool_use_id.\n",
    "messages.append({\n",
    "    \"role\": \"user\",\n",
    "    \"content\": [{\n",
    "        \"type\": \"tool_result\",\n",
    "        \"tool_use_id\": tool_use.id,\n",
    "        \"content\": tool_output,\n",
    "    }],\n",
    "})\n",
    "\n",
    "# 4. Re-call the API so Claude can reason about the tool output and decide next steps.\n",
    "response = client.messages.create(\n",
    "    model=\"claude-opus-4-7\",\n",
    "    max_tokens=4096,\n",
    "    messages=messages,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Tip.** Architect Tip for the Exam\n",
    "Tool results are **user-role messages**, not a separate role. The `tool_use_id` on the result must match the `id` from the assistant's `tool_use` block, and you must include the assistant's full `content` verbatim in history before the result, or the next call will reject the conversation.\n",
    "\n",
    "## 2. Designing Agentic Workflows with the Agent SDK\n",
    "\n",
    "Before writing any code, decide which type of system you actually need. The exam draws a sharp line between two architectures:\n",
    "\n",
    "- **Workflows (pre-configured decision trees)**, You hard-code the control flow. The model fills in the leaves (classify, summarize, extract), but humans pick the branches. Predictable, cheap, easy to test, no autonomy.\n",
    "- **Agents (model-driven decision-making)**, You expose tools and a goal. The *model* decides which tool to call, when to stop, and how to recover. Higher cost and variance, but able to handle open-ended tasks where you can't enumerate the branches in advance.\n",
    "\n",
    "Pick the workflow when the steps are knowable in advance. Reach for an agent only when the model genuinely needs to choose its own path.\n",
    "\n",
    "**Implementation Task: Stand Up the Agent SDK**\n",
    "\n",
    "The [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-python) (`claude-agent-sdk`) is Anthropic's reference harness for the agentic loop. It owns history bookkeeping, tool dispatch, and termination so you can focus on the system prompt, allowed tools, and permission policy.\n",
    "\n",
    "```bash\n",
    "pip install claude-agent-sdk\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import asyncio\n",
    "from claude_agent_sdk import query, ClaudeAgentOptions\n",
    "\n",
    "# Configure the agent: persona, model, and the tool surface it may use.\n",
    "options = ClaudeAgentOptions(\n",
    "    model=\"claude-opus-4-7\",\n",
    "    system_prompt=(\n",
    "        \"You are an expert Marketing Strategist. Ensure your plans are \"\n",
    "        \"data-driven, and cite sources when you research.\"\n",
    "    ),\n",
    "    allowed_tools=[\"WebSearch\", \"WebFetch\"],\n",
    "    permission_mode=\"acceptEdits\",  # Auto-approve file-edit operations specifically\n",
    ")\n",
    "\n",
    "async def main():\n",
    "    # The SDK runs the agentic loop for you: tool dispatch, history, stop_reason.\n",
    "    async for message in query(\n",
    "        prompt=\"Generate three LinkedIn post ideas for AI consulting buyers.\",\n",
    "        options=options,\n",
    "    ):\n",
    "        print(message)\n",
    "\n",
    "asyncio.run(main())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Tip.** Architect Tip for the Exam\n",
    "The Agent SDK does not replace the Messages API, it sits on top of it. Knowing how to drop down to raw `messages.create` with your own `tool_use` / `tool_result` bookkeeping (Section 1) is what lets you debug an SDK-driven agent when it misbehaves.\n",
    "\n",
    "## 3. Session Management: Resume, Fork, and Fresh Starts\n",
    "\n",
    "Long-running agentic work rarely fits in a single session. Claude Code assigns every session an ID, and `claude --resume <session-id>` reloads that session's full conversation history, prior analysis, tool results, and decisions, so the agent picks up exactly where it left off.\n",
    "\n",
    "Sometimes you don't want to continue a session so much as **branch** it. Passing `--fork-session` alongside `--resume` (or `fork_session` in the SDK) creates a *copy* of the session under a new ID. The original stays untouched, which lets you explore several divergent approaches, say, two different refactoring strategies, from the same shared analysis baseline without the experiments contaminating each other.\n",
    "\n",
    "```bash\n",
    "# Continue a prior session in place\n",
    "claude --resume <session-id>\n",
    "\n",
    "# Branch: copy the session to a new ID, leaving the original untouched\n",
    "claude --resume <session-id> --fork-session\n",
    "```\n",
    "\n",
    "**The decision rule: resume vs. fresh start**\n",
    "\n",
    "- **Resume** when the prior context is mostly still valid, the codebase, data, and goals haven't materially changed since the session ended.\n",
    "- **Start a NEW session** when the prior tool results are stale (files rewritten, data refreshed, environment rebuilt). Inject a *structured summary* of the earlier session's conclusions into the fresh session instead of dragging along outdated raw context.\n",
    "- **Resuming after code changes?** Tell the agent exactly which files changed (\"since last session, `auth.py` and `routes.py` were rewritten, re-read those before continuing\") so it performs targeted re-analysis rather than trusting stale conclusions.\n",
    "\n",
    "> **Tip.** **Exam tip:** Resuming with stale tool results is worse than starting fresh with a summary — the model will confidently reason over files that no longer exist.\n",
    "\n",
    "## 4. Orchestration Pattern: The Agentic Loop\n",
    "\n",
    "For the exam, you must understand that the `stop_reason` field determines the loop's behavior:\n",
    "\n",
    "- **`\"tool_use\"`**, The loop continues. Execute the tool, append the `tool_result`, and re-call the API.\n",
    "- **`\"end_turn\"`**, The loop terminates. This is the only reliable signal that the task is complete.\n",
    "- **`\"max_tokens\"`** / **`\"pause_turn\"`**, Recoverable interruptions, continue the request rather than treating them as completion.\n",
    "\n",
    "**Anti-Patterns to Avoid**\n",
    "\n",
    "- **Parsing natural-language completion signals**, Do not check whether the assistant text contains \"I'm done\", \"task complete\", or similar phrases. Those strings are not part of the API contract, they will drift across model versions and prompts and silently break your loop.\n",
    "- **Using an iteration cap as the primary stopping mechanism**, Writing `for _ in range(10):` as the loop body and exiting whenever the counter runs out hides bugs (the agent looked complete but actually got cut off) and bills you for tool calls that never finish a task. Iteration caps belong in the loop as a *safety guardrail* against runaway agents, not as the signal that the work is done.\n",
    "\n",
    "> **Tip.** Architect Tip for the Exam\n",
    "The correct pattern: drive the loop off `stop_reason == \"end_turn\"`, keep an iteration cap as a defensive backstop, and log a hard error (rather than silently returning) if the cap is what stopped the run. That separation, structured signal for completion, counter for safety, is what the exam is testing."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}