Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,734 changes: 1,066 additions & 668 deletions claude_agent_sdk/01_The_chief_of_staff_agent.ipynb

Large diffs are not rendered by default.

1,083 changes: 636 additions & 447 deletions claude_agent_sdk/02_The_observability_agent.ipynb

Large diffs are not rendered by default.

16 changes: 10 additions & 6 deletions claude_agent_sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A tutorial series demonstrating how to build sophisticated general-purpose agentic systems using the [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-python), progressing from simple research agents to multi-agent orchestration with external system integration.

> **📌 Updated for SDK v0.1.6**: These notebooks use the latest Claude Agent SDK v0.1.6 features. Requires Claude Code 2.0.0+. See the [migration guide](https://docs.claude.com/en/api/agent-sdk/migrate-to-claude-agent-sdk) for details if you started developing with the Claude Code SDK.

## Getting Started

#### 1. Install uv, [node](https://nodejs.org/en/download/), and the Claude Code CLI (if you haven't already)
Expand Down Expand Up @@ -42,11 +44,12 @@ This tutorial series takes you on a journey from basic agent implementation to s
### What You'll Learn

Through this series, you'll be exposed to:
- **Core SDK fundamentals** with `query()` and the `ClaudeSDKClient` & `ClaudeAgentOptions` interfaces in the Python SDK
- **Core Claude Agent SDK fundamentals** with `query()` and the `ClaudeSDKClient` & `ClaudeAgentOptions` interfaces in the Python SDK
- **Tool usage patterns** from basic WebSearch to complex MCP server integration
- **Multi-agent orchestration** with specialized subagents and coordination
- **Enterprise features** by leveraging hooks for compliance tracking and audit trails
- **External system integration** via Model Context Protocol (MCP)
- **Custom tool creation** with in-process SDK MCP servers

Note: This tutorial assumes you have some level of familiarity with Claude Code. Ideally, if you have been using Claude Code to supercharge your coding tasks and would like to leverage its raw agentic power for tasks beyond Software Engineering, this tutorial will help you get started.

Expand All @@ -72,20 +75,21 @@ Build a comprehensive AI Chief of Staff for a startup CEO, showcasing advanced S
- **Output Styles:** Tailored communication for different audiences
- **Plan Mode:** Strategic planning without execution for complex tasks
- **Custom Slash Commands:** User-friendly shortcuts for common operations
- **Hooks:** Automated compliance tracking and audit trails
- **Subagent Orchestration:** Coordinating specialized agents for domain expertise
- **Hooks:** Automated compliance tracking and audit trails (PreToolUse & PostToolUse). Defined programmatically or via filesystem.
- **Subagent Orchestration:** Coordinating specialized agents for domain expertise. Defined programmatically or via filesystem.
- **Bash Tool Integration:** Python script execution for procedural knowledge and complex computations
- **System Prompt Patterns:** Append to presets and custom replacements.
- **Session Management:** Resume and fork conversations with session IDs for multi-day workflows

### [Notebook 02: The Observability Agent](02_The_observability_agent.ipynb)

Expand beyond local capabilities by connecting agents to external systems through the Model Context Protocol. Transform your agent from a passive observer into an active participant in DevOps workflows.

**Advanced Capabilities:**
**Integration:**
- **Git MCP Server:** 13+ tools for repository analysis and version control
- **GitHub MCP Server:** 100+ tools for complete GitHub platform integration
- **SDK MCP Servers:** Create custom in-process tools with `create_sdk_mcp_server()` and `@tool` decorator
- **Real-time Monitoring:** CI/CD pipeline analysis and failure detection
- **Intelligent Incident Response:** Automated root cause analysis
- **Production Workflow Automation:** From monitoring to actionable insights

## Complete Agent Implementations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/report-tracker.py123"
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/report-tracker.py"
}
]
}
Expand Down
33 changes: 25 additions & 8 deletions claude_agent_sdk/chief_of_staff_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,27 @@

from dotenv import load_dotenv

from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ClaudeSDKClient,
ResultMessage,
ToolUseBlock,
UserMessage,
)

load_dotenv()


def get_activity_text(msg) -> str | None:
"""Extract activity text from a message"""
try:
if "Assistant" in msg.__class__.__name__:
if hasattr(msg, "content") and msg.content:
first_content = msg.content[0] if isinstance(msg.content, list) else msg.content
if hasattr(first_content, "name"):
return f"🤖 Using: {first_content.name}()"
if isinstance(msg, AssistantMessage):
first_content = msg.content[0] if isinstance(msg.content, list) else msg.content
if isinstance(first_content, ToolUseBlock):
return f"🤖 Using: {first_content.name}()"
return "🤖 Thinking..."
elif "User" in msg.__class__.__name__:
elif isinstance(msg, UserMessage):
return "✓ Tool completed"
except (AttributeError, IndexError):
pass
Expand All @@ -44,6 +50,8 @@ async def send_query(
permission_mode: Literal["default", "plan", "acceptEdits"] = "default",
output_style: str | None = None,
activity_handler: Callable[[Any], None | Any] = print_activity,
resume: str | None = None,
fork_session: bool = False,
) -> tuple[str | None, list]:
"""
Send a query to the Chief of Staff agent with all features integrated.
Expand All @@ -54,6 +62,8 @@ async def send_query(
continue_conversation: Continue the previous conversation if True
permission_mode: "default" (execute), "plan" (think only), or "acceptEdits"
output_style: Override output style (e.g., "executive", "technical", "board-report")
resume: Session ID to resume (Feature 8: Session Management)
fork_session: If True with resume, creates branch; if False, continues same session

Returns:
Tuple of (result, messages) - result is the final text, messages is the full conversation
Expand Down Expand Up @@ -88,12 +98,19 @@ async def send_query(
"Bash",
"WebSearch",
],
"disallowed_tools": ["WebFetch"],
"continue_conversation": continue_conversation,
"system_prompt": system_prompt,
"permission_mode": permission_mode,
"cwd": os.path.dirname(os.path.abspath(__file__)),
"setting_sources": ["project", "local"],
}

# Add session management parameters if provided (Feature 8)
if resume:
options_dict["resume"] = resume
options_dict["fork_session"] = fork_session

# add output style if specified
if output_style:
options_dict["settings"] = json.dumps({"outputStyle": output_style})
Expand All @@ -113,7 +130,7 @@ async def send_query(
else:
activity_handler(msg)

if hasattr(msg, "result"):
if isinstance(msg, ResultMessage):
result = msg.result
except Exception as e:
print(f"❌ Query error: {e}")
Expand Down
22 changes: 14 additions & 8 deletions claude_agent_sdk/observability_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,27 @@

from dotenv import load_dotenv

from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ClaudeSDKClient,
ResultMessage,
ToolUseBlock,
UserMessage,
)

load_dotenv()


def get_activity_text(msg) -> str | None:
"""Extract activity text from a message"""
try:
if "Assistant" in msg.__class__.__name__:
if hasattr(msg, "content") and msg.content:
first_content = msg.content[0] if isinstance(msg.content, list) else msg.content
if hasattr(first_content, "name"):
return f"🤖 Using: {first_content.name}()"
if isinstance(msg, AssistantMessage):
first_content = msg.content[0] if isinstance(msg.content, list) else msg.content
if isinstance(first_content, ToolUseBlock):
return f"🤖 Using: {first_content.name}()"
return "🤖 Thinking..."
elif "User" in msg.__class__.__name__:
elif isinstance(msg, UserMessage):
return "✓ Tool completed"
except (AttributeError, IndexError):
pass
Expand Down Expand Up @@ -102,7 +108,7 @@ async def send_query(
else:
activity_handler(msg)

if hasattr(msg, "result"):
if isinstance(msg, ResultMessage):
result = msg.result
except Exception as e:
print(f"❌ Query error: {e}")
Expand Down
2 changes: 1 addition & 1 deletion claude_agent_sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"claude-agent-sdk>=0.0.20",
"claude-agent-sdk>=0.1.6",
"ipykernel>=6.29.5",
"mcp-server-git>=2025.1.14",
"python-dotenv>=1.1.1",
Expand Down
23 changes: 14 additions & 9 deletions claude_agent_sdk/research_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,27 @@

from dotenv import load_dotenv

from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ClaudeSDKClient,
ResultMessage,
ToolUseBlock,
UserMessage,
)

load_dotenv()


def get_activity_text(msg) -> str | None:
"""Extract activity text from a message"""
try:
if "Assistant" in msg.__class__.__name__:
# Check if content exists and has items
if hasattr(msg, "content") and msg.content:
first_content = msg.content[0] if isinstance(msg.content, list) else msg.content
if hasattr(first_content, "name"):
return f"🤖 Using: {first_content.name}()"
if isinstance(msg, AssistantMessage):
first_content = msg.content[0] if isinstance(msg.content, list) else msg.content
if isinstance(first_content, ToolUseBlock):
return f"🤖 Using: {first_content.name}()"
return "🤖 Thinking..."
elif "User" in msg.__class__.__name__:
elif isinstance(msg, UserMessage):
return "✓ Tool completed"
except (AttributeError, IndexError):
pass
Expand Down Expand Up @@ -78,7 +83,7 @@ async def send_query(
else:
activity_handler(msg)

if hasattr(msg, "result"):
if isinstance(msg, ResultMessage):
result = msg.result
except Exception as e:
print(f"❌ Query error: {e}")
Expand Down
65 changes: 38 additions & 27 deletions claude_agent_sdk/utils/agent_visualizer.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
from claude_agent_sdk import (
AssistantMessage,
ResultMessage,
SystemMessage,
TextBlock,
UserMessage,
ToolUseBlock,
)


def print_activity(msg):
if "Assistant" in msg.__class__.__name__:
print(
f"🤖 {'Using: ' + msg.content[0].name + '()' if hasattr(msg.content[0], 'name') else 'Thinking...'}"
)
elif "User" in msg.__class__.__name__:
if isinstance(msg, AssistantMessage):
# Check if using a tool or just thinking
if msg.content:
for block in msg.content:
if isinstance(block, ToolUseBlock):
print(f"🤖 Using: {block.name}()")
return
print("🤖 Thinking...")
elif isinstance(msg, UserMessage):
print("✓ Tool completed")


Expand All @@ -14,21 +28,20 @@ def print_final_result(messages):

# Find the last assistant message with actual content
for msg in reversed(messages):
if msg.__class__.__name__ == "AssistantMessage" and msg.content:
if isinstance(msg, AssistantMessage) and msg.content:
# Check if it has text content (not just tool use)
for block in msg.content:
if hasattr(block, "text"):
if isinstance(block, TextBlock):
print(f"\n📝 Final Result:\n{block.text}")
break
break

# Print cost if available
if hasattr(result_msg, "total_cost_usd"):
print(f"\n📊 Cost: ${result_msg.total_cost_usd:.2f}")

# Print duration if available
if hasattr(result_msg, "duration_ms"):
print(f"⏱️ Duration: {result_msg.duration_ms / 1000:.2f}s")
# Print cost and duration if result message
if isinstance(result_msg, ResultMessage):
if result_msg.total_cost_usd:
print(f"\n📊 Cost: ${result_msg.total_cost_usd:.2f}")
if result_msg.duration_ms:
print(f"⏱️ Duration: {result_msg.duration_ms / 1000:.2f}s")


def visualize_conversation(messages):
Expand All @@ -38,29 +51,27 @@ def visualize_conversation(messages):
print("=" * 60 + "\n")

for i, msg in enumerate(messages):
msg_type = msg.__class__.__name__

if msg_type == "SystemMessage":
if isinstance(msg, SystemMessage):
print("⚙️ System Initialized")
if hasattr(msg, "data") and "session_id" in msg.data:
print(f" Session: {msg.data['session_id'][:8]}...")
print()

elif msg_type == "AssistantMessage":
elif isinstance(msg, AssistantMessage):
print("🤖 Assistant:")
if msg.content:
for block in msg.content:
if hasattr(block, "text"):
if isinstance(block, TextBlock):
# Text response
text = block.text[:500] + "..." if len(block.text) > 500 else block.text
print(f" 💬 {text}")
elif hasattr(block, "name"):
elif isinstance(block, ToolUseBlock):
# Tool use
tool_name = block.name
print(f" 🔧 Using tool: {tool_name}")

# Show key parameters for certain tools
if hasattr(block, "input") and block.input:
if block.input:
if tool_name == "WebSearch" and "query" in block.input:
print(f' Query: "{block.input["query"]}"')
elif tool_name == "TodoWrite" and "todos" in block.input:
Expand All @@ -72,7 +83,7 @@ def visualize_conversation(messages):
)
print()

elif msg_type == "UserMessage":
elif isinstance(msg, UserMessage):
if msg.content and isinstance(msg.content, list):
for result in msg.content:
if isinstance(result, dict) and result.get("type") == "tool_result":
Expand All @@ -89,15 +100,15 @@ def visualize_conversation(messages):
print(f" 📥 {summary}")
print()

elif msg_type == "ResultMessage":
elif isinstance(msg, ResultMessage):
print("✅ Conversation Complete")
if hasattr(msg, "num_turns"):
if msg.num_turns:
print(f" Turns: {msg.num_turns}")
if hasattr(msg, "total_cost_usd"):
if msg.total_cost_usd:
print(f" Cost: ${msg.total_cost_usd:.2f}")
if hasattr(msg, "duration_ms"):
if msg.duration_ms:
print(f" Duration: {msg.duration_ms / 1000:.2f}s")
if hasattr(msg, "usage"):
if msg.usage:
usage = msg.usage
total_tokens = usage.get("input_tokens", 0) + usage.get("output_tokens", 0)
print(f" Tokens: {total_tokens:,}")
Expand Down
Loading
Loading