Chapter 22

Programmatic Tool Calling: Direct Tool Calls Inside Code Execution Containers to Reduce Round-Trips

Chapter 22: Tool Use + Extended Thinking: The Ultimate Combination of Reasoning and Action

22.1 Why Combine These Two Capabilities?

Tool Use and Extended Thinking each solve different problems:

But truly complex tasks often require both: first thinking deeply about which tools to call and how to combine their results, then executing the tool calls, then performing deep analysis of the results.

This combination is especially powerful in these scenarios:

22.2 API Configuration: Enabling Both Capabilities

Configuration for using Tool Use and Extended Thinking simultaneously:

import anthropic
import json

client = anthropic.Anthropic()

analysis_tools = [
    {
        "name": "query_database",
        "description": "Query a database to retrieve statistical data",
        "input_schema": {
            "type": "object",
            "properties": {
                "sql": {
                    "type": "string",
                    "description": "SQL query statement"
                },
                "database": {
                    "type": "string",
                    "enum": ["sales", "users", "products"]
                }
            },
            "required": ["sql", "database"]
        }
    },
    {
        "name": "run_python_code",
        "description": "Execute Python code for data analysis and visualization",
        "input_schema": {
            "type": "object",
            "properties": {
                "code": {
                    "type": "string",
                    "description": "Python code to execute"
                }
            },
            "required": ["code"]
        }
    }
]

# Enable both Extended Thinking and Tool Use
response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=16000,
    thinking={
        "type": "enabled",
        "budget_tokens": 8000
    },
    tools=analysis_tools,
    messages=[{
        "role": "user",
        "content": """Analyze our sales data from the past three months, identify the 
        fastest-growing product categories, and provide inventory optimization 
        recommendations for next quarter. Please think deeply before giving data-backed conclusions."""
    }]
)

for block in response.content:
    print(f"Block type: {block.type}")

22.3 Distribution of thinking Blocks Across the Tool Call Cycle

In the Tool Use + Extended Thinking combination, thinking blocks can appear at multiple points:

Round 1 (initial reasoning):
  thinking: "Let me first think through the analytical framework..."
  tool_use: query_database(...)
  
Round 2 (result analysis reasoning):
  tool_result: {...query results...}
  thinking: "Seeing this data, I need to analyze further..."
  tool_use: run_python_code(...)
  
Round 3 (final reasoning and synthesis):
  tool_result: {...computation results...}
  thinking: "Synthesizing all the data, I can draw the following conclusions..."
  text: "Based on deep analysis, here are my recommendations..."
def analyze_thinking_distribution(all_messages: list) -> dict:
    """Analyze thinking block distribution across the tool call cycle"""
    
    distribution = {
        "pre_tool_thinking": [],
        "post_result_thinking": [],
        "final_thinking": []
    }
    
    for msg in all_messages:
        if msg["role"] != "assistant":
            continue
        
        content = msg["content"]
        if not isinstance(content, list):
            continue
        
        has_tool_use = any(
            (b.type == "tool_use" if hasattr(b, 'type') else b.get("type") == "tool_use")
            for b in content
        )
        has_text = any(
            (b.type == "text" if hasattr(b, 'type') else b.get("type") == "text")
            for b in content
        )
        
        for block in content:
            block_type = block.type if hasattr(block, 'type') else block.get("type")
            if block_type == "thinking":
                thinking_text = block.thinking if hasattr(block, 'thinking') else block.get("thinking", "")
                
                if has_tool_use and not has_text:
                    distribution["pre_tool_thinking"].append(len(thinking_text))
                elif has_text and not has_tool_use:
                    distribution["final_thinking"].append(len(thinking_text))
                else:
                    distribution["post_result_thinking"].append(len(thinking_text))
    
    return {
        k: {
            "count": len(v),
            "avg_chars": sum(v) / len(v) if v else 0,
            "total_chars": sum(v)
        }
        for k, v in distribution.items()
    }

22.4 Complete Implementation: Data Analysis Agent

import anthropic
import json
from typing import Any

class ThinkingToolAgent:
    """Agent using both Extended Thinking and Tool Use"""
    
    def __init__(
        self,
        model: str = "claude-opus-4-5",
        thinking_budget: int = 8000,
        max_tokens: int = 20000
    ):
        self.client = anthropic.Anthropic()
        self.model = model
        self.thinking_budget = thinking_budget
        self.max_tokens = max_tokens
        self.tools = []
        self.tool_functions = {}
        self.conversation_history = []
        self.thinking_log = []
    
    def add_tool(self, tool_definition: dict, func):
        self.tools.append(tool_definition)
        self.tool_functions[tool_definition["name"]] = func
        return self
    
    def _execute_tool(self, name: str, inputs: dict) -> Any:
        if name not in self.tool_functions:
            raise ValueError(f"Tool not registered: {name}")
        return self.tool_functions[name](**inputs)
    
    def _build_tool_result(self, tool_use_id: str, result: Any, error: bool = False) -> dict:
        content = result if isinstance(result, str) else json.dumps(result, default=str)
        block = {"type": "tool_result", "tool_use_id": tool_use_id, "content": content}
        if error:
            block["is_error"] = True
        return block
    
    def run(self, user_message: str, system: str = "") -> str:
        """Execute the full reasoning + tool call loop"""
        
        self.conversation_history = [{"role": "user", "content": user_message}]
        
        create_kwargs = {
            "model": self.model,
            "max_tokens": self.max_tokens,
            "thinking": {"type": "enabled", "budget_tokens": self.thinking_budget},
            "tools": self.tools,
            "messages": self.conversation_history
        }
        if system:
            create_kwargs["system"] = system
        
        round_num = 0
        
        while round_num < 10:
            round_num += 1
            print(f"\n=== Round {round_num} ===")
            
            response = self.client.messages.create(**create_kwargs)
            
            for block in response.content:
                if block.type == "thinking":
                    self.thinking_log.append({
                        "round": round_num,
                        "chars": len(block.thinking),
                        "preview": block.thinking[:300]
                    })
                    print(f"[Thinking] {len(block.thinking)} chars")
                elif block.type == "tool_use":
                    print(f"[Tool] {block.name}: {json.dumps(block.input)[:100]}")
                elif block.type == "text":
                    print(f"[Text] {block.text[:100]}...")
            
            if response.stop_reason == "end_turn":
                return ' '.join(b.text for b in response.content if b.type == "text")
            
            if response.stop_reason == "tool_use":
                tool_results = []
                
                for block in response.content:
                    if block.type != "tool_use":
                        continue
                    try:
                        result = self._execute_tool(block.name, block.input)
                        tool_results.append(self._build_tool_result(block.id, result))
                        print(f"[Result] {block.name}: Success")
                    except Exception as e:
                        tool_results.append(self._build_tool_result(block.id, str(e), error=True))
                        print(f"[Result] {block.name}: Failed - {e}")
                
                # Keep complete content including thinking blocks
                self.conversation_history.append({
                    "role": "assistant", "content": response.content
                })
                self.conversation_history.append({
                    "role": "user", "content": tool_results
                })
                create_kwargs["messages"] = self.conversation_history
            else:
                break
        
        return "Maximum iterations reached"
    
    def get_thinking_summary(self) -> str:
        if not self.thinking_log:
            return "No thinking records"
        total_chars = sum(t["chars"] for t in self.thinking_log)
        summary = f"{len(self.thinking_log)} thinking blocks, {total_chars} total chars\n"
        for entry in self.thinking_log:
            summary += f"\nRound {entry['round']} ({entry['chars']} chars):\n  {entry['preview']}\n"
        return summary

22.5 Real-World Case: Complex Investment Analysis

def build_investment_analysis_agent():
    agent = ThinkingToolAgent(thinking_budget=12000, max_tokens=24000)
    
    def get_stock_data(symbol: str, period: str = "1y") -> dict:
        return {
            "symbol": symbol, "period": period,
            "current_price": 175.0, "52w_high": 185.0, "52w_low": 130.0,
            "data": [
                {"date": "2026-01-01", "close": 150.0},
                {"date": "2026-04-01", "close": 175.0}
            ]
        }
    
    def get_financial_statements(symbol: str, statement_type: str) -> dict:
        return {
            "symbol": symbol, "type": statement_type,
            "revenue": 5000000000, "net_income": 800000000,
            "eps": 4.5, "pe_ratio": 38.9, "debt_to_equity": 0.45
        }
    
    def get_analyst_ratings(symbol: str) -> dict:
        return {
            "symbol": symbol, "consensus": "Buy",
            "target_price": 200.0, "num_analysts": 28,
            "buy": 18, "hold": 8, "sell": 2
        }
    
    def calculate_valuation(current_price: float, eps: float,
                             growth_rate: float, discount_rate: float) -> dict:
        pe = round(current_price / eps, 2) if eps > 0 else 0
        dcf = sum(
            eps * (1 + growth_rate)**i / (1 + discount_rate)**i
            for i in range(1, 6)
        )
        return {
            "pe_ratio": pe,
            "dcf_value": round(dcf, 2),
            "upside_pct": round((dcf - current_price) / current_price * 100, 1)
        }
    
    for definition, func in [
        ({"name": "get_stock_data",
          "description": "Get historical price and market data for a stock",
          "input_schema": {"type": "object", "properties": {
              "symbol": {"type": "string"},
              "period": {"type": "string", "enum": ["1m","3m","6m","1y","3y"]}
          }, "required": ["symbol"]}}, get_stock_data),
        ({"name": "get_financial_statements",
          "description": "Get company financial statement data",
          "input_schema": {"type": "object", "properties": {
              "symbol": {"type": "string"},
              "statement_type": {"type": "string", "enum": ["income","balance_sheet","cash_flow"]}
          }, "required": ["symbol","statement_type"]}}, get_financial_statements),
        ({"name": "get_analyst_ratings",
          "description": "Get Wall Street analyst ratings and price targets",
          "input_schema": {"type": "object", "properties": {
              "symbol": {"type": "string"}
          }, "required": ["symbol"]}}, get_analyst_ratings),
        ({"name": "calculate_valuation",
          "description": "Calculate valuation metrics including P/E and DCF model",
          "input_schema": {"type": "object", "properties": {
              "current_price": {"type": "number"},
              "eps": {"type": "number"},
              "growth_rate": {"type": "number"},
              "discount_rate": {"type": "number"}
          }, "required": ["current_price","eps","growth_rate","discount_rate"]}}, calculate_valuation),
    ]:
        agent.add_tool(definition, func)
    
    return agent


agent = build_investment_analysis_agent()
result = agent.run(
    user_message="""Conduct a deep investment analysis of NVDA (NVIDIA) including:
    1. Recent price trends and technical analysis
    2. Fundamental data (revenue, profit, valuation)
    3. Analyst consensus
    4. Intrinsic value via DCF model (assume 20% growth for 5 years, 10% discount rate)
    5. Investment recommendation with supporting reasoning
    
    Think deeply through each step to ensure conclusions are data-backed.""",
    system="You are a professional stock analyst skilled at deep multi-dimensional investment analysis. Think through your analytical framework before starting."
)
print(result)
print(agent.get_thinking_summary())

22.6 Optimization: Dynamic thinking Budget Allocation

Different phases of reasoning require different depths of thinking:

class AdaptiveThinkingAgent(ThinkingToolAgent):
    """Agent with adaptive thinking budget"""
    
    THINKING_PROFILES = {
        "quick": 2000,
        "standard": 5000,
        "deep": 10000,
        "maximum": 20000
    }
    
    def run_with_adaptive_budget(self, user_message: str,
                                  complexity_hint: str = "auto") -> str:
        """Automatically adjust thinking budget based on task complexity"""
        
        if complexity_hint == "auto":
            score = 0
            score += min(len(user_message) // 100, 3)
            score += 2 if any(c.isdigit() for c in user_message) else 0
            score += 2 if any(w in user_message for w in ["compare","analyze","evaluate"]) else 0
            score += 2 if any(w in user_message for w in ["then","next","step","workflow"]) else 0
            
            if score <= 2:
                profile = "quick"
            elif score <= 4:
                profile = "standard"
            elif score <= 6:
                profile = "deep"
            else:
                profile = "maximum"
        else:
            profile = complexity_hint
        
        self.thinking_budget = self.THINKING_PROFILES.get(profile, 5000)
        print(f"Using thinking profile: {profile} ({self.thinking_budget} tokens)")
        return self.run(user_message)

22.7 Special Pattern: Plan Then Execute

For complex multi-step tasks, first have Claude plan the full path in a thinking-only round:

def run_plan_then_execute(agent: ThinkingToolAgent, task: str) -> str:
    """First have Claude create a complete plan, then execute step by step"""
    
    # Round 1: Planning only (no tools)
    planning_response = agent.client.messages.create(
        model=agent.model,
        max_tokens=agent.max_tokens,
        thinking={"type": "enabled", "budget_tokens": agent.thinking_budget},
        # No tools: force Claude to plan first
        messages=[{
            "role": "user",
            "content": f"""First create a complete execution plan (do not execute yet):

Task: {task}

Output your plan in this format:
## Analysis Framework
(How you plan to approach this problem)

## Execution Steps
1. (Step 1)
2. (Step 2)
...

## Expected Output Structure
(What sections the final report should contain)"""
        }]
    )
    
    plan_text = ' '.join(b.text for b in planning_response.content if b.type == "text")
    print("=== Execution Plan ===")
    print(plan_text)
    
    # Round 2: Execute according to plan (with tools)
    return agent.run(
        user_message=f"""Execute the task following this plan:

{plan_text}

Original task: {task}""",
        system="Follow the plan strictly, use tools to gather necessary data, and deliver a complete final report."
    )

Summary

The Tool Use + Extended Thinking combination represents the most advanced usage pattern currently available in the Claude API. Core principles:

  1. thinking before tool calls: Helps Claude plan its tool usage strategy
  2. thinking after receiving results: Helps Claude deeply interpret and integrate data
  3. Sufficient thinking budget: Reasoning + tool-call scenarios typically require 5,000โ€“15,000 tokens
  4. Preserve complete history: Message history containing thinking blocks must be passed in full to maintain reasoning continuity

The following chapters explore more advanced tool usage patterns: dynamic tool discovery (Tool Search) and Claude's self-planning capability (Advisor Tool).

Rate this chapter
4.5  / 5  (10 ratings)

๐Ÿ’ฌ Comments