mirror of
https://github.com/anthropics/claude-cookbooks.git
synced 2025-10-06 01:00:28 +03:00
Fix context management for SDK 0.69.0 and improve documentation
This commit fixes the memory cookbook to work with anthropic SDK 0.69.0 and improves documentation around context clearing behavior. Changes: - Update context_management parameter usage for SDK 0.69.0 - Pass as direct parameter instead of extra_body - Update response handling to use getattr() for Pydantic objects - Fix context clearing configuration in notebook - Lower clear_at_least threshold from 3000 to 50 tokens - Memory tool operations have small results (~50-150 tokens) - Add documentation explaining why threshold is low - Add explanatory notes about context clearing behavior - Explain why token savings are small in the demo - Provide guidance for production configurations - Document that larger tool results would save more tokens The notebook now works correctly with SDK 0.69.0 and provides clear guidance for users configuring context management in their applications. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -59,7 +59,44 @@
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## 1. Introduction: Why Memory Matters {#introduction}\n\nThis cookbook demonstrates practical implementations of the context engineering patterns described in [Effective context engineering for AI agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents). That post covers why context is a finite resource, how attention budgets work, and strategies for building effective agents—the techniques you'll see in action here.\n\n### The Problem\n\nLarge language models have finite context windows (200k tokens for Claude 4). While this seems large, several challenges emerge:\n\n- **Context limits**: Long conversations or complex tasks can exceed available context\n- **Computational cost**: Processing large contexts is expensive - attention mechanisms scale quadratically\n- **Repeated patterns**: Similar tasks across conversations require re-explaining context every time\n- **Information loss**: When context fills up, earlier important information gets lost\n\n### The Solution\n\nClaude Sonnet 4.5 introduces two powerful capabilities:\n\n1. **Memory Tool** (`memory_20250818`): Enables cross-conversation learning\n - Claude can write down what it learns for future reference\n - File-based system under `/memories` directory\n - Client-side implementation gives you full control\n\n2. **Context Editing** (`clear_tool_uses_20250919`): Automatically manages context\n - Clears old tool results when context grows large\n - Keeps recent context while preserving memory\n - Configurable triggers and retention policies\n\n### The Benefit\n\nBuild AI agents that **get better at your specific tasks over time**:\n\n- **Session 1**: Claude solves a problem, writes down the pattern\n- **Session 2**: Claude applies the learned pattern immediately (faster!)\n- **Long sessions**: Context editing keeps conversations manageable\n\nThink of it as giving Claude a notebook to take notes and refer back to - just like humans do."
|
||||
"source": [
|
||||
"## 1. Introduction: Why Memory Matters {#introduction}\n",
|
||||
"\n",
|
||||
"This cookbook demonstrates practical implementations of the context engineering patterns described in [Effective context engineering for AI agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents). That post covers why context is a finite resource, how attention budgets work, and strategies for building effective agents—the techniques you'll see in action here.\n",
|
||||
"\n",
|
||||
"### The Problem\n",
|
||||
"\n",
|
||||
"Large language models have finite context windows (200k tokens for Claude 4). While this seems large, several challenges emerge:\n",
|
||||
"\n",
|
||||
"- **Context limits**: Long conversations or complex tasks can exceed available context\n",
|
||||
"- **Computational cost**: Processing large contexts is expensive - attention mechanisms scale quadratically\n",
|
||||
"- **Repeated patterns**: Similar tasks across conversations require re-explaining context every time\n",
|
||||
"- **Information loss**: When context fills up, earlier important information gets lost\n",
|
||||
"\n",
|
||||
"### The Solution\n",
|
||||
"\n",
|
||||
"Claude Sonnet 4.5 introduces two powerful capabilities:\n",
|
||||
"\n",
|
||||
"1. **Memory Tool** (`memory_20250818`): Enables cross-conversation learning\n",
|
||||
" - Claude can write down what it learns for future reference\n",
|
||||
" - File-based system under `/memories` directory\n",
|
||||
" - Client-side implementation gives you full control\n",
|
||||
"\n",
|
||||
"2. **Context Editing** (`clear_tool_uses_20250919`): Automatically manages context\n",
|
||||
" - Clears old tool results when context grows large\n",
|
||||
" - Keeps recent context while preserving memory\n",
|
||||
" - Configurable triggers and retention policies\n",
|
||||
"\n",
|
||||
"### The Benefit\n",
|
||||
"\n",
|
||||
"Build AI agents that **get better at your specific tasks over time**:\n",
|
||||
"\n",
|
||||
"- **Session 1**: Claude solves a problem, writes down the pattern\n",
|
||||
"- **Session 2**: Claude applies the learned pattern immediately (faster!)\n",
|
||||
"- **Long sessions**: Context editing keeps conversations manageable\n",
|
||||
"\n",
|
||||
"Think of it as giving Claude a notebook to take notes and refer back to - just like humans do."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -154,7 +191,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -259,7 +296,7 @@
|
||||
"============================================================\n",
|
||||
"\n",
|
||||
"🔄 Turn 1:\n",
|
||||
"💬 Claude: I'll review this code to identify the race condition issues. Let me start by checking my memory and then analyzing the problem.\n",
|
||||
"💬 Claude: I'll review this multi-threaded web scraper for you. Let me first check my memory and then analyze the code.\n",
|
||||
"\n",
|
||||
" 🔧 Memory tool: view /memories\n",
|
||||
" ✓ Result: Directory: /memories\n",
|
||||
@@ -272,93 +309,166 @@
|
||||
"🔄 Turn 3:\n",
|
||||
"💬 Claude: \n",
|
||||
"\n",
|
||||
"## Code Review: Race Condition in Multi-threaded Web Scraper\n",
|
||||
"## Code Review: Multi-threaded Web Scraper - Race Condition Issues\n",
|
||||
"\n",
|
||||
"### 🐛 **Critical Issues Found**\n",
|
||||
"Great catch on identifying this as a concurrency issue! I've found **multiple critical race conditions** that explain the inconsistent results.\n",
|
||||
"\n",
|
||||
"#### **1. Race Condition on Shared Lists (Lines 44-48)**\n",
|
||||
"### 🔴 **Critical Issues**\n",
|
||||
"\n",
|
||||
"**Problem:**\n",
|
||||
"#### **1. Race Condition in `self.results` (Primary Bug)**\n",
|
||||
"**Location:** Line in `scrape_urls()` method\n",
|
||||
"```python\n",
|
||||
"self.results = [] # Shared across threads\n",
|
||||
"self.failed_urls = [] # Shared across threads\n",
|
||||
"\n",
|
||||
"# In scrape_urls():\n",
|
||||
"self.failed_urls.append(result[\"url\"]) # NOT THREAD-SAFE\n",
|
||||
"self.results.append(result) # NOT THREAD-SAFE\n",
|
||||
"self.results.append(result) # RACE CONDITION\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**Why This Fails:**\n",
|
||||
"- Python's `list.append()` is **not atomic** for the overall operation\n",
|
||||
"- Multiple threads can simultaneously read and modify the list's internal state\n",
|
||||
"- This causes **lost updates** where some appends are overwritten\n",
|
||||
"- Results: Inconsistent counts, missing data\n",
|
||||
"**Problem:** \n",
|
||||
"- Python's `list.append()` is **NOT thread-safe** for concurrent modifications\n",
|
||||
"- Multiple threads simultaneously appending can cause:\n",
|
||||
" - **Lost updates:** One thread's append overwrites another's\n",
|
||||
" - **Inconsistent state:** Internal list structure can be corrupted\n",
|
||||
" - **Unpredictable counts:** Results randomly dropped\n",
|
||||
"\n",
|
||||
"**Race Condition Scenario:**\n",
|
||||
"1. Thread A reads `self.results` size: 5\n",
|
||||
"2. Thread B reads `self.results` size: 5 \n",
|
||||
"3. Thread A appends → size becomes 6\n",
|
||||
"4. Thread B appends → overwrites position, still size 6 (one result lost!)\n",
|
||||
"**Why it happens:**\n",
|
||||
"`list.append()` involves multiple operations:\n",
|
||||
"1. Read current list size\n",
|
||||
"2. Allocate space\n",
|
||||
"3. Insert item\n",
|
||||
"4. Update size\n",
|
||||
"\n",
|
||||
"Between these steps, another thread can interfere, causing data loss.\n",
|
||||
"\n",
|
||||
"#### **2. Race Condition in `self.failed_urls`**\n",
|
||||
"**Location:** Same method\n",
|
||||
"```python\n",
|
||||
"self.failed_urls.append(result[\"url\"]) # RACE CONDITION\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**Problem:** Identical issue - concurrent appends without synchronization.\n",
|
||||
"\n",
|
||||
"#### **3. Shared State Without Protection**\n",
|
||||
"**Location:** Class initialization\n",
|
||||
"```python\n",
|
||||
"self.results = [] # BUG: Shared mutable state accessed by multiple threads!\n",
|
||||
"self.failed_urls = [] # BUG: Another race condition!\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**Problem:** Instance variables shared across threads without any locking mechanism.\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"### ✅ **Solutions**\n",
|
||||
"\n",
|
||||
"#### **Option 1: Use Thread-Safe Queue (Recommended)**\n",
|
||||
"Here are three approaches to fix this, from simplest to most robust:\n",
|
||||
"\n",
|
||||
"#### **Solution 1: Use Thread-Safe Queue (Recommended)**\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"import queue\n",
|
||||
"import time\n",
|
||||
"from concurrent.futures import ThreadPoolExecutor, as_completed\n",
|
||||
"from typing import List, Dict, Any\n",
|
||||
"from queue import Queue\n",
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class WebScraper:\n",
|
||||
" \"\"\"Web scraper that fetches multiple URLs concurrently.\"\"\"\n",
|
||||
"\n",
|
||||
" def __init__(self, max_workers: int = 10):\n",
|
||||
" self.max_workers = max_workers\n",
|
||||
" # Use thread-safe queues instead of lists\n",
|
||||
" self.results = queue.Queue()\n",
|
||||
" self.failed_urls = queue.Queue()\n",
|
||||
" # Use thread-safe Queue instead of lists\n",
|
||||
" self.results_queue = Queue()\n",
|
||||
" self.failed_queue = Queue()\n",
|
||||
"\n",
|
||||
" def scrape_urls(self, urls: List[str]) -> List[Dict[str, any]]:\n",
|
||||
" with ThreadPoolExecutor(max_workers=self.max_workers) as executor:\n",
|
||||
" futures = [executor.submit(self.fetch_url, url) for url in urls]\n",
|
||||
" def fetch_url(self, url: str) -> Dict[str, Any]:\n",
|
||||
" \"\"\"Fetch a single URL and return the result.\"\"\"\n",
|
||||
" try:\n",
|
||||
" response = requests.get(url, timeout=5)\n",
|
||||
" response.raise_for_status()\n",
|
||||
" return {\n",
|
||||
" \"url\": url,\n",
|
||||
" \"status\": response.status_code,\n",
|
||||
" \"content_length\": len(response.content),\n",
|
||||
" }\n",
|
||||
" except requests.exceptions.RequestException as e:\n",
|
||||
" return {\"url\": url, \"error\": str(e)}\n",
|
||||
"\n",
|
||||
" for future in as_completed(futures):\n",
|
||||
" result = future.result()\n",
|
||||
" if \"error\" in result:\n",
|
||||
" self.failed_urls.put(result[\"url\"]) # Thread-safe\n",
|
||||
" else:\n",
|
||||
" self.results.put(result) # Thread-safe\n",
|
||||
"\n",
|
||||
" # Convert queue to list for return\n",
|
||||
" return list(self.results.queue)\n",
|
||||
"\n",
|
||||
" def get_stats(self) -> Dict[str, int]:\n",
|
||||
" total = self.results.qsize()\n",
|
||||
" failed = self.failed_urls.qsize()\n",
|
||||
" return {\n",
|
||||
" \"total_results\": total,\n",
|
||||
" \"failed_urls\": failed,\n",
|
||||
" \"success_rate\": total / (total + failed) if (total + failed) > 0 else 0,\n",
|
||||
" }\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"#### **Option 2: Use Threading Lock**\n",
|
||||
"```python\n",
|
||||
"import threading\n",
|
||||
"\n",
|
||||
"class WebScraper:\n",
|
||||
" def __init__(self, max_workers: int = 10):\n",
|
||||
" self.max_workers = max_workers\n",
|
||||
" self.results = []\n",
|
||||
" self.failed_urls = []\n",
|
||||
" self.lock = threading.Lock() # Add lock for synchronization\n",
|
||||
"\n",
|
||||
" def scrape_urls(self, urls: List[str]) -> List[Dict[str, any]]:\n",
|
||||
" def scrape_urls(self, urls: List[str]) -> List[Dict[str, Any]]:\n",
|
||||
" \"\"\"Scrape multiple URLs concurrently - FIXED with Queue.\"\"\"\n",
|
||||
" with ThreadPoolExecutor(max_workers=self.max_workers) as executor:\n",
|
||||
" futures = [executor.submit(self.fetch_url, url) for url in urls]\n",
|
||||
"\n",
|
||||
" for future in as_completed(futures):\n",
|
||||
" result = future.result()\n",
|
||||
" \n",
|
||||
" # Protect shared state with lock\n",
|
||||
" # Thread-safe operations\n",
|
||||
" if \"error\" in result:\n",
|
||||
" self.failed_queue.put(result[\"url\"])\n",
|
||||
" else:\n",
|
||||
" self.results_queue.put(result)\n",
|
||||
"\n",
|
||||
" # Convert queues to lists after all threads complete\n",
|
||||
" results = []\n",
|
||||
" while not self.results_queue.empty():\n",
|
||||
" results.append(self.results_queue.get())\n",
|
||||
" \n",
|
||||
" return results\n",
|
||||
"\n",
|
||||
" def get_stats(self) -> Dict[str, int]:\n",
|
||||
" \"\"\"Get scraping statistics.\"\"\"\n",
|
||||
" results_count = self.results_queue.qsize()\n",
|
||||
" failed_count = self.failed_queue.qsize()\n",
|
||||
" \n",
|
||||
" return {\n",
|
||||
" \"total_results\": results_count,\n",
|
||||
" \"failed_urls\": failed_count,\n",
|
||||
" \"success_rate\": (\n",
|
||||
" results_count / (results_count + failed_count)\n",
|
||||
" if (results_count + failed_count) > 0\n",
|
||||
" else 0\n",
|
||||
" ),\n",
|
||||
" }\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"#### **Solution 2: Use Threading Lock**\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"import threading\n",
|
||||
"from concurrent.futures import ThreadPoolExecutor, as_completed\n",
|
||||
"from typing import List, Dict, Any\n",
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class WebScraper:\n",
|
||||
" \"\"\"Web scraper that fetches multiple URLs concurrently.\"\"\"\n",
|
||||
"\n",
|
||||
" def __init__(self, max_workers: int = 10):\n",
|
||||
" self.max_workers = max_workers\n",
|
||||
" self.results = []\n",
|
||||
" self.failed_urls = []\n",
|
||||
" self.lock = threading.Lock() # Add lock for synchronization\n",
|
||||
"\n",
|
||||
" def fetch_url(self, url: str) -> Dict[str, Any]:\n",
|
||||
" \"\"\"Fetch a single URL and return the result.\"\"\"\n",
|
||||
" try:\n",
|
||||
" response = requests.get(url, timeout=5)\n",
|
||||
" response.raise_for_status()\n",
|
||||
" return {\n",
|
||||
" \"url\": url,\n",
|
||||
" \"status\": response.status_code,\n",
|
||||
" \"content_length\": len(response.content),\n",
|
||||
" }\n",
|
||||
" except requests.exceptions.RequestException as e:\n",
|
||||
" return {\"url\": url, \"error\": str(e)}\n",
|
||||
"\n",
|
||||
" def scrape_urls(self, urls: List[str]) -> List[Dict[str, Any]]:\n",
|
||||
" \"\"\"Scrape multiple URLs concurrently - FIXED with lock.\"\"\"\n",
|
||||
" with ThreadPoolExecutor(max_workers=self.max_workers) as executor:\n",
|
||||
" futures = [executor.submit(self.fetch_url, url) for url in urls]\n",
|
||||
"\n",
|
||||
" for future in as_completed(futures):\n",
|
||||
" result = future.result()\n",
|
||||
" \n",
|
||||
" # Protected by lock - only one thread at a time\n",
|
||||
" with self.lock:\n",
|
||||
" if \"error\" in result:\n",
|
||||
" self.failed_urls.append(result[\"url\"])\n",
|
||||
@@ -366,72 +476,77 @@
|
||||
" self.results.append(result)\n",
|
||||
"\n",
|
||||
" return self.results\n",
|
||||
"\n",
|
||||
" def get_stats(self) -> Dict[str, int]:\n",
|
||||
" \"\"\"Get scraping statistics.\"\"\"\n",
|
||||
" with self.lock: # Protect reads too\n",
|
||||
" return {\n",
|
||||
" \"total_results\": len(self.results),\n",
|
||||
" \"failed_urls\": len(self.failed_urls),\n",
|
||||
" \"success_rate\": (\n",
|
||||
" len(self.results) / (len(self.results) + len(self.failed_urls))\n",
|
||||
" if (len(self.results) + len(self.failed_urls)) > 0\n",
|
||||
" else 0\n",
|
||||
" ),\n",
|
||||
" }\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"#### **Option 3: Collect Results from Futures (Best for this use case)**\n",
|
||||
"#### **Solution 3: Let ThreadPoolExecutor Handle It (Best)**\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from concurrent.futures import ThreadPoolExecutor, as_completed\n",
|
||||
"from typing import List, Dict, Any\n",
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class WebScraper:\n",
|
||||
" \"\"\"Web scraper that fetches multiple URLs concurrently.\"\"\"\n",
|
||||
"\n",
|
||||
" def __init__(self, max_workers: int = 10):\n",
|
||||
" self.max_workers = max_workers\n",
|
||||
" # No shared state needed!\n",
|
||||
"\n",
|
||||
" def scrape_urls(self, urls: List[str]) -> List[Dict[str, any]]:\n",
|
||||
" def fetch_url(self, url: str) -> Dict[str, Any]:\n",
|
||||
" \"\"\"Fetch a single URL and return the result.\"\"\"\n",
|
||||
" try:\n",
|
||||
" response = requests.get(url, timeout=5)\n",
|
||||
" response.raise_for_status()\n",
|
||||
" return {\n",
|
||||
" \"url\": url,\n",
|
||||
" \"status\": response.status_code,\n",
|
||||
" \"content_length\": len(response.content),\n",
|
||||
" }\n",
|
||||
" except requests.exceptions.RequestException as e:\n",
|
||||
" return {\"url\": url, \"error\": str(e)}\n",
|
||||
"\n",
|
||||
" def scrape_urls(self, urls: List[str]) -> List[Dict[str, Any]]:\n",
|
||||
" \"\"\"\n",
|
||||
" Scrape multiple URLs concurrently - FIXED by avoiding shared state.\n",
|
||||
" Collect results after futures complete (no concurrent modifications).\n",
|
||||
" \"\"\"\n",
|
||||
" results = []\n",
|
||||
" failed_urls = []\n",
|
||||
" \n",
|
||||
" with ThreadPoolExecutor(max_workers=self.max_workers) as executor:\n",
|
||||
" futures = [executor.submit(self.fetch_url, url) for url in urls]\n",
|
||||
"\n",
|
||||
" # Collect results from futures - no race condition!\n",
|
||||
" # Single-threaded collection after async work completes\n",
|
||||
" for future in as_completed(futures):\n",
|
||||
" result = future.result()\n",
|
||||
" \n",
|
||||
" if \"error\" in result:\n",
|
||||
" failed_urls.append(result[\"url\"])\n",
|
||||
" else:\n",
|
||||
" results.append(result)\n",
|
||||
"\n",
|
||||
" self.results = results # Store after collection complete\n",
|
||||
" self.results = results # Store after all work is done\n",
|
||||
" self.failed_urls = failed_urls\n",
|
||||
" return results\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
" def get_stats(self) -> Dict[str, int]:\n",
|
||||
" \"\"\"Get scraping statistics.\"\"\"\n",
|
||||
" return {\n",
|
||||
" \"total_\n",
|
||||
"\n",
|
||||
"### 📊 **Why Option 3 is Best Here**\n",
|
||||
"\n",
|
||||
"1. **No synchronization overhead** - Single thread collects results\n",
|
||||
"2. **Simpler code** - No locks or queues needed\n",
|
||||
"3. **Natural fit** - The main thread iterates `as_completed()` sequentially anyway\n",
|
||||
"4. **No performance penalty** - Collection happens in calling thread, not worker threads\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"### 🔍 **Additional Issues**\n",
|
||||
"\n",
|
||||
"**Type hint error (Line 20):**\n",
|
||||
"```python\n",
|
||||
"def fetch_url(self, url: str) -> Dict[str, any]: # Should be 'Any' not 'any'\n",
|
||||
"```\n",
|
||||
"Should be:\n",
|
||||
"```python\n",
|
||||
"from typing import Any\n",
|
||||
"def fetch_url(self, url: str) -> Dict[str, Any]:\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"### 📝 **Summary**\n",
|
||||
"\n",
|
||||
"**Root Cause:** Unsynchronized concurrent access to `self.results` and `self.failed_urls` lists\n",
|
||||
"\n",
|
||||
"**Impact:** Lost updates, inconsistent result counts\n",
|
||||
"\n",
|
||||
"**Fix:** Use Option 3 (local variables during collection) - simplest and most efficient\n",
|
||||
"\n",
|
||||
" 🔧 Memory tool: str_replace /memories/review_progress.md\n",
|
||||
" ✓ Result: File /memories/review_progress.md has been edited successfully\n",
|
||||
"\n",
|
||||
"🔄 Turn 4:\n",
|
||||
"\n",
|
||||
"============================================================\n",
|
||||
"✅ Session 1 complete!\n",
|
||||
@@ -529,8 +644,8 @@
|
||||
" 🔧 Memory tool: view /memories/review_progress.md\n",
|
||||
" ✓ Result: 1: # Code Review: Multi-threaded Web Scraper\n",
|
||||
" 2: \n",
|
||||
" 3: ## Issue Type\n",
|
||||
" 4:...\n",
|
||||
" 3: ## Task\n",
|
||||
" 4: Revie...\n",
|
||||
"\n",
|
||||
"🔄 Turn 3:\n",
|
||||
" 🔧 Memory tool: str_replace /memories/review_progress.md\n",
|
||||
@@ -539,153 +654,253 @@
|
||||
"🔄 Turn 4:\n",
|
||||
"💬 Claude: \n",
|
||||
"\n",
|
||||
"Now let me review this async API client code:\n",
|
||||
"# Code Review: Async API Client\n",
|
||||
"\n",
|
||||
"## Code Review: Async API Client\n",
|
||||
"## 🔴 Critical Issues\n",
|
||||
"\n",
|
||||
"### ✅ **Correct Assessment of Issues**\n",
|
||||
"### 1. **Race Condition on Shared State (High Severity)**\n",
|
||||
"**Location:** `fetch_all()` method, lines with `self.responses.append()` and `self.error_count += 1`\n",
|
||||
"\n",
|
||||
"The comments in the code correctly identify the problems! Let me elaborate:\n",
|
||||
"**Problem:** \n",
|
||||
"Multiple coroutines concurrently modify shared instance variables without synchronization. While the comments correctly identify this as an issue, there's an important nuance: Python's asyncio is **single-threaded**, so the GIL isn't the protective factor here—the real issue is that coroutines can be suspended mid-operation during `await` points.\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"However, the actual race condition risk here is **somewhat overstated** because:\n",
|
||||
"- List `.append()` is atomic in CPython\n",
|
||||
"- The `+=` operation on integers is also atomic\n",
|
||||
"- Coroutines only switch at `await` points, and there are none between the operations\n",
|
||||
"\n",
|
||||
"### 🔴 **Critical Issues**\n",
|
||||
"**BUT** this is still problematic because:\n",
|
||||
"- It relies on CPython implementation details\n",
|
||||
"- It's not guaranteed by the language specification\n",
|
||||
"- The code is not portable to other Python implementations\n",
|
||||
"- Future refactoring could introduce `await` points that cause real races\n",
|
||||
"\n",
|
||||
"#### **1. Race Condition on Shared State (Lines 13-14, 44-48)**\n",
|
||||
"\n",
|
||||
"**Problem:**\n",
|
||||
"```python\n",
|
||||
"self.responses = [] # Shared across coroutines\n",
|
||||
"self.error_count = 0 # Race condition on increment\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"While Python's GIL prevents *some* threading issues, **asyncio doesn't have the same protection**. When you `await`, other coroutines can run, leading to interleaving:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"# What could happen:\n",
|
||||
"# Coroutine A: reads self.error_count (0)\n",
|
||||
"# Coroutine B: reads self.error_count (0)\n",
|
||||
"# Coroutine A: increments and writes (1)\n",
|
||||
"# Coroutine B: increments and writes (1) ← Should be 2!\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"Similarly, `self.responses.append()` can be interrupted mid-operation.\n",
|
||||
"\n",
|
||||
"#### **2. Incorrect Pattern (Lines 41-48)**\n",
|
||||
"\n",
|
||||
"The `as_completed` loop is inefficient and still buggy:\n",
|
||||
"```python\n",
|
||||
"for coro in asyncio.as_completed(tasks):\n",
|
||||
" result = await coro\n",
|
||||
" # Modifying shared state...\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"### 🟡 **Minor Issues**\n",
|
||||
"\n",
|
||||
"#### **3. Type Hint Error (Lines 25, 34)**\n",
|
||||
"```python\n",
|
||||
"Dict[str, any] # ❌ Wrong: 'any' is not defined\n",
|
||||
"```\n",
|
||||
"Should be:\n",
|
||||
"```python\n",
|
||||
"Dict[str, Any] # ✅ Correct (import from typing)\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"#### **4. Missing Error Handling**\n",
|
||||
"Errors are stored in results but never counted properly due to the race condition.\n",
|
||||
"\n",
|
||||
"#### **5. Reusability Issue**\n",
|
||||
"Calling `fetch_all()` multiple times will accumulate results incorrectly.\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"### ✅ **Recommended Fixes**\n",
|
||||
"\n",
|
||||
"#### **Option 1: Use Local Variables (Simplest)**\n",
|
||||
"**Fix:** Use proper async coordination or refactor to avoid shared state:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"async def fetch_all(self, endpoints: List[str]) -> List[Dict[str, Any]]:\n",
|
||||
" \"\"\"Fetch multiple endpoints concurrently.\"\"\"\n",
|
||||
" async with aiohttp.ClientSession() as session:\n",
|
||||
" tasks = [self.fetch_endpoint(session, endpoint) for endpoint in endpoints]\n",
|
||||
" results = await asyncio.gather(*tasks) # Collect all results\n",
|
||||
" \n",
|
||||
" # Now safely update instance variables\n",
|
||||
" self.responses = [r for r in results if \"error\" not in r]\n",
|
||||
" self.error_count = sum(1 for r in results if \"error\" in r)\n",
|
||||
" # Collect all results at once - no shared state modification\n",
|
||||
" results = await asyncio.gather(*tasks, return_exceptions=False)\n",
|
||||
" \n",
|
||||
" return results # Return ALL results (success + errors)\n",
|
||||
" # Process results in single-threaded manner after all are collected\n",
|
||||
" responses = []\n",
|
||||
" error_count = 0\n",
|
||||
" \n",
|
||||
" for result in results:\n",
|
||||
" if \"error\" in result:\n",
|
||||
" error_count += 1\n",
|
||||
" else:\n",
|
||||
" responses.append(result)\n",
|
||||
" \n",
|
||||
" # Update instance state only once\n",
|
||||
" self.responses = responses\n",
|
||||
" self.error_count = error_count\n",
|
||||
" \n",
|
||||
" return responses\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**Advantages:**\n",
|
||||
"- Simple and clean\n",
|
||||
"- No race conditions\n",
|
||||
"- Uses `asyncio.gather()` which is more efficient\n",
|
||||
"### 2. **Stateful Design Anti-Pattern (Medium-High Severity)**\n",
|
||||
"**Location:** Instance variables `self.responses` and `self.error_count`\n",
|
||||
"\n",
|
||||
"#### **Option 2: Use asyncio.Lock (If Shared State is Required)**\n",
|
||||
"**Problem:**\n",
|
||||
"The client stores results as instance variables, which means:\n",
|
||||
"- **Not reusable:** Calling `fetch_all()` multiple times accumulates results\n",
|
||||
"- **Not thread-safe:** If someone wraps this in a thread pool, real race conditions occur\n",
|
||||
"- **Confusing API:** Results are both returned AND stored in instance\n",
|
||||
"- **Memory leak potential:** Old responses never cleared\n",
|
||||
"\n",
|
||||
"**Fix:** Remove stateful design entirely:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"async def fetch_all(self, endpoints: List[str]) -> Dict[str, Any]:\n",
|
||||
" \"\"\"Fetch multiple endpoints concurrently.\"\"\"\n",
|
||||
" async with aiohttp.ClientSession() as session:\n",
|
||||
" tasks = [self.fetch_endpoint(session, endpoint) for endpoint in endpoints]\n",
|
||||
" results = await asyncio.gather(*tasks, return_exceptions=False)\n",
|
||||
" \n",
|
||||
" responses = []\n",
|
||||
" errors = []\n",
|
||||
" \n",
|
||||
" for result in results:\n",
|
||||
" if \"error\" in result:\n",
|
||||
" errors.append(result)\n",
|
||||
" else:\n",
|
||||
" responses.append(result)\n",
|
||||
" \n",
|
||||
" # Return everything as a structured result\n",
|
||||
" return {\n",
|
||||
" \"responses\": responses,\n",
|
||||
" \"errors\": errors,\n",
|
||||
" \"summary\": {\n",
|
||||
" \"total_responses\": len(responses),\n",
|
||||
" \"error_count\": len(errors),\n",
|
||||
" \"success_rate\": (\n",
|
||||
" len(responses) / len(results) if results else 0\n",
|
||||
" )\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"## ⚠️ Medium Issues\n",
|
||||
"\n",
|
||||
"### 3. **Error Handling Loses Information**\n",
|
||||
"**Location:** `fetch_endpoint()` exception handler\n",
|
||||
"\n",
|
||||
"**Problem:**\n",
|
||||
"All errors are caught and returned as dictionaries with just the error string. This loses:\n",
|
||||
"- Exception type information\n",
|
||||
"- Stack traces (useful for debugging)\n",
|
||||
"- HTTP status codes for failed requests\n",
|
||||
"\n",
|
||||
"**Fix:**\n",
|
||||
"```python\n",
|
||||
"async def fetch_endpoint(\n",
|
||||
" self, session: aiohttp.ClientSession, endpoint: str\n",
|
||||
") -> Dict[str, Any]:\n",
|
||||
" \"\"\"Fetch a single endpoint.\"\"\"\n",
|
||||
" url = f\"{self.base_url}/{endpoint}\"\n",
|
||||
" try:\n",
|
||||
" async with session.get(\n",
|
||||
" url, timeout=aiohttp.ClientTimeout(total=5)\n",
|
||||
" ) as response:\n",
|
||||
" data = await response.json()\n",
|
||||
" return {\n",
|
||||
" \"endpoint\": endpoint,\n",
|
||||
" \"status\": response.status,\n",
|
||||
" \"data\": data,\n",
|
||||
" \"success\": True,\n",
|
||||
" }\n",
|
||||
" except aiohttp.ClientError as e:\n",
|
||||
" return {\n",
|
||||
" \"endpoint\": endpoint,\n",
|
||||
" \"error\": str(e),\n",
|
||||
" \"error_type\": type(e).__name__,\n",
|
||||
" \"success\": False,\n",
|
||||
" }\n",
|
||||
" except Exception as e:\n",
|
||||
" # Log unexpected errors\n",
|
||||
" return {\n",
|
||||
" \"endpoint\": endpoint,\n",
|
||||
" \"error\": str(e),\n",
|
||||
" \"error_type\": type(e).__name__,\n",
|
||||
" \"success\": False,\n",
|
||||
" }\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"### 4. **Using `asyncio.as_completed()` Unnecessarily**\n",
|
||||
"**Location:** `fetch_all()` method\n",
|
||||
"\n",
|
||||
"**Problem:**\n",
|
||||
"The code uses `asyncio.as_completed()` but doesn't benefit from processing results as they arrive. It still waits for all results and returns them all at once. This adds complexity without benefit.\n",
|
||||
"\n",
|
||||
"**Fix:** Use `asyncio.gather()` which is simpler and more idiomatic for this use case.\n",
|
||||
"\n",
|
||||
"### 5. **Missing HTTP Status Code Validation**\n",
|
||||
"**Location:** `fetch_endpoint()` method\n",
|
||||
"\n",
|
||||
"**Problem:**\n",
|
||||
"A 404 or 500 status code will be treated as success if it returns valid JSON. The code only checks for exceptions, not HTTP error status codes.\n",
|
||||
"\n",
|
||||
"**Fix:**\n",
|
||||
"```python\n",
|
||||
"async with session.get(\n",
|
||||
" url, timeout=aiohttp.ClientTimeout(total=5)\n",
|
||||
") as response:\n",
|
||||
" if response.status >= 400:\n",
|
||||
" return {\n",
|
||||
" \"endpoint\": endpoint,\n",
|
||||
" \"status\": response.status,\n",
|
||||
" \"error\": f\"HTTP {response.status}\",\n",
|
||||
" \"success\": False,\n",
|
||||
" }\n",
|
||||
" data = await response.json()\n",
|
||||
" return {\n",
|
||||
" \"endpoint\": endpoint,\n",
|
||||
" \"status\": response.status,\n",
|
||||
" \"data\": data,\n",
|
||||
" \"success\": True,\n",
|
||||
" }\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"## 💡 Minor Issues & Suggestions\n",
|
||||
"\n",
|
||||
"### 6. **Session Should Be Reusable**\n",
|
||||
"**Location:** `fetch_all()` creates new session each time\n",
|
||||
"\n",
|
||||
"**Suggestion:**\n",
|
||||
"For better performance, consider managing the session at the class level or allowing session reuse:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"class AsyncAPIClient:\n",
|
||||
" def __init__(self, base_url: str):\n",
|
||||
" self.base_url = base_url\n",
|
||||
" self.responses = []\n",
|
||||
" self.error_count = 0\n",
|
||||
" self._lock = asyncio.Lock() # Add lock\n",
|
||||
" self._session: Optional[aiohttp.ClientSession] = None\n",
|
||||
" \n",
|
||||
" async def fetch_all(self, endpoints: List[str]) -> List[Dict[str, Any]]:\n",
|
||||
" async with aiohttp.ClientSession() as session:\n",
|
||||
" tasks = [self.fetch_endpoint(session, endpoint) for endpoint in endpoints]\n",
|
||||
" \n",
|
||||
" for coro in asyncio.as_completed(tasks):\n",
|
||||
" result = await coro\n",
|
||||
" \n",
|
||||
" async with self._lock: # Protect shared state\n",
|
||||
" if \"error\" in result:\n",
|
||||
" self.error_count += 1\n",
|
||||
" else:\n",
|
||||
" self.responses.append(result)\n",
|
||||
" \n",
|
||||
" return self.responses\n",
|
||||
" async def __aenter__(self):\n",
|
||||
" self._session = aiohttp.ClientSession()\n",
|
||||
" return self\n",
|
||||
" \n",
|
||||
" async def __aexit__(self, exc_type, exc_val, exc_tb):\n",
|
||||
" if self._session:\n",
|
||||
" await self._session.close()\n",
|
||||
" \n",
|
||||
" async def fetch_endpoint(self, endpoint: str) -> Dict[str, Any]:\n",
|
||||
" if not self._session:\n",
|
||||
" raise RuntimeError(\"Use client as async context manager\")\n",
|
||||
" # ... use self._session\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**Advantages:**\n",
|
||||
"- Explicitly safe\n",
|
||||
"- Good if multiple methods need to update shared state\n",
|
||||
"### 7. **Type Hints Could Be More Specific**\n",
|
||||
"The return type `Dict[str, Any]` is too generic. Consider using TypedDict:\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"```python\n",
|
||||
"from typing import TypedDict\n",
|
||||
"\n",
|
||||
"### 📊 **Comparison: Async vs Threading Races**\n",
|
||||
"class SuccessResponse(TypedDict):\n",
|
||||
" endpoint: str\n",
|
||||
" status: int\n",
|
||||
" data: Any\n",
|
||||
" success: bool\n",
|
||||
"\n",
|
||||
"| Aspect | Threading | Asyncio |\n",
|
||||
"|--------|-----------|---------|\n",
|
||||
"| **Protection** | GIL provides *some* protection | No GIL - explicit coordination needed |\n",
|
||||
"| **Interleaving** | Can happen anytime | Only happens at `await` points |\n",
|
||||
"| **Solution** | `threading.Lock()` | `asyncio.Lock()` or avoid shared state |\n",
|
||||
"class ErrorResponse(TypedDict):\n",
|
||||
" endpoint: str\n",
|
||||
" error: str\n",
|
||||
" error_type: str\n",
|
||||
" success: bool\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"### 8. **Missing Docstring Details**\n",
|
||||
"Add information about return value structure, exceptions raised, and parameters.\n",
|
||||
"\n",
|
||||
"### 🎯 **Final Recommendations**\n",
|
||||
"## 📋 Summary\n",
|
||||
"\n",
|
||||
"1. **Use Option 1** (local variables + `asyncio.gather()`) - it's cleaner and faster\n",
|
||||
"2. Fix the type hints: `any` → `Any`\n",
|
||||
"3. Consider returning ALL results (including errors) for better observability\n",
|
||||
"4. Reset `self.responses` and `self.error_count` at the start of `fetch_all()` if you want to reuse the client\n",
|
||||
"| Severity | Count | Issues |\n",
|
||||
"|----------|-------|---------|\n",
|
||||
"| 🔴 Critical | 2 | Shared state race conditions, stateful design anti-pattern |\n",
|
||||
"| ⚠️ Medium | 3 | Error handling, unnecessary complexity, missing status validation |\n",
|
||||
"| 💡 Minor | 3 | Session management, type hints, documentation |\n",
|
||||
"\n",
|
||||
" 🔧 Memory tool: str_replace /memories/review_progress.md\n",
|
||||
" ✓ Result: File /memories/review_progress.md has been edited successfully\n",
|
||||
"## Recommended Refactored Version\n",
|
||||
"\n",
|
||||
"🔄 Turn 5:\n",
|
||||
"💬 Claude: \n",
|
||||
"```python\n",
|
||||
"\"\"\"\n",
|
||||
"Async API client with proper async patterns.\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"import asyncio\n",
|
||||
"from typing import List, Dict, Any, TypedDict\n",
|
||||
"\n",
|
||||
"### 📝 **Summary**\n",
|
||||
"import aiohttp\n",
|
||||
"\n",
|
||||
"This code demonstrates a **common misconception**: that asyncio is automatically \"safe\" because it's not multi-threaded. While the GIL prevents *some* threading issues, asyncio coroutines can still interleave at `await` points, causing race conditions on shared mutable state.\n",
|
||||
"\n",
|
||||
"**Bottom line:** Use `asyncio.gather()` and local variables instead of modifying instance variables from multiple coroutines. It's simpler, safer, and faster! 🚀\n",
|
||||
"class ResponseResult(TypedDict):\n",
|
||||
" endpoint: str\n",
|
||||
" status: int\n",
|
||||
" data:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"============================================================\n",
|
||||
@@ -752,12 +967,14 @@
|
||||
"- Context fills up with tool results from previous reviews\n",
|
||||
"- But memory (learned patterns) must persist!\n",
|
||||
"\n",
|
||||
"Let's trigger **context editing** to see how Claude manages this automatically."
|
||||
"Let's trigger **context editing** to see how Claude manages this automatically.\n",
|
||||
"\n",
|
||||
"**Note on configuration:** We use `clear_at_least: 50` tokens because memory tool operations have small results (~50-150 tokens each). In production with larger tool results (like web search or code execution), you'd use higher values like 3000-5000 tokens."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -771,14 +988,20 @@
|
||||
"📝 Review 1: Data processor\n",
|
||||
" 🔧 Memory tool: str_replace /memories/review_progress.md\n",
|
||||
" ✓ Result: File /memories/review_progress.md has been edited successfully\n",
|
||||
" 📊 Input tokens: 5,977\n",
|
||||
" ℹ️ Context below threshold - no clearing triggered\n",
|
||||
" 📊 Input tokens: 6,243\n",
|
||||
" ✂️ Context editing triggered!\n",
|
||||
" • Cleared 1 tool uses\n",
|
||||
" • Saved 66 tokens\n",
|
||||
" • After clearing: 6,243 tokens\n",
|
||||
"\n",
|
||||
"📝 Review 2: SQL query builder\n",
|
||||
" 🔧 Memory tool: str_replace /memories/review_progress.md\n",
|
||||
" ✓ Result: File /memories/review_progress.md has been edited successfully\n",
|
||||
" 📊 Input tokens: 7,359\n",
|
||||
" ℹ️ Context below threshold - no clearing triggered\n",
|
||||
" 📊 Input tokens: 7,471\n",
|
||||
" ✂️ Context editing triggered!\n",
|
||||
" • Cleared 1 tool uses\n",
|
||||
" • Saved 66 tokens\n",
|
||||
" • After clearing: 7,471 tokens\n",
|
||||
"\n",
|
||||
"============================================================\n",
|
||||
"✅ Session 3 complete!\n",
|
||||
@@ -793,8 +1016,8 @@
|
||||
" {\n",
|
||||
" \"type\": \"clear_tool_uses_20250919\",\n",
|
||||
" \"trigger\": {\"type\": \"input_tokens\", \"value\": 5000}, # Lower threshold to trigger clearing sooner\n",
|
||||
" \"keep\": {\"type\": \"tool_uses\", \"value\": 2}, # Keep only the last 2 tool uses\n",
|
||||
" \"clear_at_least\": {\"type\": \"input_tokens\", \"value\": 3000}\n",
|
||||
" \"keep\": {\"type\": \"tool_uses\", \"value\": 1}, # Keep only the last tool use\n",
|
||||
" \"clear_at_least\": {\"type\": \"input_tokens\", \"value\": 50}\n",
|
||||
" }\n",
|
||||
" ]\n",
|
||||
"}\n",
|
||||
@@ -882,23 +1105,39 @@
|
||||
"**What just happened?**\n",
|
||||
"\n",
|
||||
"As context grew during multiple reviews:\n",
|
||||
"1. **Context clearing triggered automatically** when input tokens exceeded the threshold\n",
|
||||
"2. **Old tool results were removed** (data processor review details)\n",
|
||||
"1. **Context clearing triggered automatically** when input tokens exceeded 5,000\n",
|
||||
"2. **Old tool results were removed** - cleared 2 tool uses, saving ~66 tokens each time\n",
|
||||
"3. **Memory files remained intact** - Claude can still query learned patterns\n",
|
||||
"4. **Token usage decreased** - saved thousands of tokens while preserving knowledge\n",
|
||||
"4. **Token usage continued to grow** but at a slower rate due to clearing\n",
|
||||
"\n",
|
||||
"This demonstrates the key benefit:\n",
|
||||
"- **Short-term memory** (conversation context) → Cleared to save space\n",
|
||||
"- **Long-term memory** (stored patterns) → Persists across sessions\n",
|
||||
"- **Short-term memory** (conversation context with tool results) → Cleared to save space\n",
|
||||
"- **Long-term memory** (stored patterns in `/memories`) → Persists across sessions\n",
|
||||
"\n",
|
||||
"**Why such small token savings?** Memory tool operations return compact results (file paths, success messages). The `str_replace` operations only return \"File edited successfully\" plus metadata. In production use cases with larger tool results (web searches returning full articles, code execution with long outputs), context clearing would save thousands of tokens.\n",
|
||||
"\n",
|
||||
"Let's verify memory survived the clearing:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"📂 Memory files in demo_memory/:\n",
|
||||
"\n",
|
||||
"demo_memory/\n",
|
||||
" memories/\n",
|
||||
" ├── review_progress.md (257 bytes)\n",
|
||||
"\n",
|
||||
"✅ All learned patterns preserved despite context clearing!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Verify memory persists after context clearing\n",
|
||||
"import os\n",
|
||||
@@ -1198,7 +1437,20 @@
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## Next Steps\n\n### Resources\n\n- **API docs**: [Claude API reference](https://docs.claude.com/en/api/messages)\n- **Usage docs**: [Memory tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/memory-tool)\n- **GitHub Action**: [claude-code-action](https://github.com/anthropics/claude-code-action)\n- **Support**: [support.claude.com](https://support.claude.com)\n\n### Feedback\n\nMemory and context management are in **beta**. Share your feedback to help us improve!"
|
||||
"source": [
|
||||
"## Next Steps\n",
|
||||
"\n",
|
||||
"### Resources\n",
|
||||
"\n",
|
||||
"- **API docs**: [Claude API reference](https://docs.claude.com/en/api/messages)\n",
|
||||
"- **Usage docs**: [Memory tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/memory-tool)\n",
|
||||
"- **GitHub Action**: [claude-code-action](https://github.com/anthropics/claude-code-action)\n",
|
||||
"- **Support**: [support.claude.com](https://support.claude.com)\n",
|
||||
"\n",
|
||||
"### Feedback\n",
|
||||
"\n",
|
||||
"Memory and context management are in **beta**. Share your feedback to help us improve!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
@@ -1222,4 +1474,4 @@
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ Remember: Your memory persists across conversations. Use it wisely."""
|
||||
messages=self.messages,
|
||||
tools=[{"type": "memory_20250818", "name": "memory"}],
|
||||
betas=["context-management-2025-06-27"],
|
||||
extra_body={"context_management": CONTEXT_MANAGEMENT},
|
||||
context_management=CONTEXT_MANAGEMENT,
|
||||
)
|
||||
|
||||
print(" ✓")
|
||||
@@ -148,7 +148,7 @@ Remember: Your memory persists across conversations. Use it wisely."""
|
||||
|
||||
# Check for context management
|
||||
if hasattr(response, "context_management") and response.context_management:
|
||||
applied = response.context_management.get("applied_edits", [])
|
||||
applied = getattr(response.context_management, "applied_edits", [])
|
||||
if applied:
|
||||
context_edits_applied.extend(applied)
|
||||
|
||||
@@ -300,9 +300,9 @@ def run_session_3() -> None:
|
||||
if result["context_edits"]:
|
||||
print("\n🧹 Context Management Applied:")
|
||||
for edit in result["context_edits"]:
|
||||
print(f" - Type: {edit.get('type')}")
|
||||
print(f" - Cleared tool uses: {edit.get('cleared_tool_uses', 0)}")
|
||||
print(f" - Tokens saved: {edit.get('cleared_input_tokens', 0):,}")
|
||||
print(f" - Type: {getattr(edit, 'type', 'unknown')}")
|
||||
print(f" - Cleared tool uses: {getattr(edit, 'cleared_tool_uses', 0)}")
|
||||
print(f" - Tokens saved: {getattr(edit, 'cleared_input_tokens', 0):,}")
|
||||
|
||||
print("\n✅ Session 3 complete - Context editing kept conversation manageable!\n")
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ def run_conversation_turn(
|
||||
}
|
||||
|
||||
if context_management:
|
||||
request_params["extra_body"] = {"context_management": context_management}
|
||||
request_params["context_management"] = context_management
|
||||
|
||||
response = client.beta.messages.create(**request_params)
|
||||
|
||||
@@ -177,17 +177,25 @@ def print_context_management_info(response: Any) -> tuple[bool, int]:
|
||||
saved_tokens = 0
|
||||
|
||||
if hasattr(response, "context_management") and response.context_management:
|
||||
edits = response.context_management.get("applied_edits", [])
|
||||
edits = getattr(response.context_management, "applied_edits", [])
|
||||
if edits:
|
||||
context_cleared = True
|
||||
cleared_uses = edits[0].get('cleared_tool_uses', 0)
|
||||
saved_tokens = edits[0].get('cleared_input_tokens', 0)
|
||||
cleared_uses = getattr(edits[0], 'cleared_tool_uses', 0)
|
||||
saved_tokens = getattr(edits[0], 'cleared_input_tokens', 0)
|
||||
print(f" ✂️ Context editing triggered!")
|
||||
print(f" • Cleared {cleared_uses} tool uses")
|
||||
print(f" • Saved {saved_tokens:,} tokens")
|
||||
print(f" • After clearing: {response.usage.input_tokens:,} tokens")
|
||||
else:
|
||||
print(f" ℹ️ Context below threshold - no clearing triggered")
|
||||
# Check if we can see why it didn't trigger
|
||||
skipped_edits = getattr(response.context_management, "skipped_edits", [])
|
||||
if skipped_edits:
|
||||
print(f" ℹ️ Context clearing skipped:")
|
||||
for skip in skipped_edits:
|
||||
reason = getattr(skip, 'reason', 'unknown')
|
||||
print(f" • Reason: {reason}")
|
||||
else:
|
||||
print(f" ℹ️ Context below threshold - no clearing triggered")
|
||||
else:
|
||||
print(f" ℹ️ No context management applied")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user