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:
- Tool Use gives Claude the ability to interact with the external world, breaking through the limitations of training data
- Extended Thinking gives Claude deep reasoning capability, helping it avoid shallow analysis on complex 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:
- Complex data analysis: Think through the analytical framework first, then query data, then deeply interpret results
- Multi-step problem solving: Reason through the solution path, call tools to execute, verify results
- Strategic planning: Think through constraints, call tools to gather information, formulate the optimal plan
- Code generation and verification: Think through architectural design, generate code, run tests, analyze failures
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:
- thinking before tool calls: Helps Claude plan its tool usage strategy
- thinking after receiving results: Helps Claude deeply interpret and integrate data
- Sufficient thinking budget: Reasoning + tool-call scenarios typically require 5,000–15,000 tokens
- 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).