Getting Started with the Claude Agent SDK

by ai-intensify
0 comments
Getting Started with Cloud Agent SDK

Getting Started with Cloud Agent SDK

Introduction

Duct-taping scripts, tools and prompts together gets old quickly. The Claude Agent SDK turns Claude Code’s “plan → build → run” workflows into real, programmable agents — automating tasks, wiring up tools and shipping command-line applications without piles of glue code. For anyone who already likes using Claude in the terminal, the SDK offers the same experience with proper structure, state and extensibility. Built on the foundation of Claude Code, it lets developers integrate Claude with a local environment, give it access to tools, and orchestrate complex workflows spanning coding, research, note-taking and automation.

This tutorial installs the Claude Agent SDK and builds a small multi-tool CLI that chains steps from start to finish.

Installing the Claude Agent SDK

1. Prerequisites

Python 3.10 or higher, Node.js 18+ for the CLI, and a Claude API key or Anthropic account.

2. Install the Claude Code CLI

On Windows, install the Claude Code CLI from PowerShell:

irm https://claude.ai/install.ps1 | iex

Add the path to the system environment, restart PowerShell and test. On other platforms, npm works:

npm i -g @anthropic-ai/claude-code

After installation, typing claude signs in from the terminal.

3. Install the Claude Agent SDK (Python)

pip install claude-agent-sdk

A CLINotFoundError at this stage usually means the Claude CLI is not installed correctly or missing from PATH.

Building a multi-tool app with the Claude Agent SDK

The example project, TrendSmith, tracks live market trends across industries including startups, AI, finance and sustainability. It combines Claude Sonnet 4.5, web search, web fetch and local storage tools in a single agent system. All code goes into a file called trend_smith.py.

1. Imports and basic settings

The first block loads Python libraries, the Claude Agent SDK types, a small help menu, model names, and soft gray text styles for status lines.

import asyncio
import os
import re
import sys
import time
from datetime import datetime
from pathlib import Path

from claude_agent_sdk import (
    AssistantMessage,
    ClaudeAgentOptions,
    ClaudeSDKClient,
    ResultMessage,
    TextBlock,
    ToolResultBlock,
    ToolUseBlock,
)

HELP = """Commands:
/trend   Quick multi-source scan (auto-saves markdown)
/scan    Short one-page scan
/help /exit     Help / Quit
"""

MODEL = os.getenv("CLAUDE_MODEL", "sonnet")  # e.g. "sonnet-4.5"
GRAY = "33(90m"
RESET = "33(0m"

2. System prompt and report destination

Next come the “house rules” for answers — fast, concise, coherent sections — and a reports/ folder beside the script where saved briefs will land.

SYS = """You are TrendSmith, a fast, concise trend researcher.
- Finish quickly (~20 s).
- For /trend: ≤1 WebSearch + ≤2 WebFetch from distinct domains.
- For /scan: ≤1 WebFetch only.
Return for /trend:
 TL;DR (1 line)
 3-5 Signals (short bullets)
 Key Players, Risks, 30/90-day Watchlist
 Sources (markdown: **Title** -- URL)
Return for /scan: 5 bullets + TL;DR + Sources.
After finishing /trend, the client will auto-save your full brief.
"""

BASE = Path(__file__).parent
REPORTS = BASE / "reports"

3. Saving files securely

These helpers create safe file names, build folders when necessary, and fall back to the home directory so reports can always be saved.

def _ts():
    return datetime.now().strftime("%Y%m%d_%H%M")

def _sanitize(s: str):
    return re.sub(r"(^w-.)+", "_", s).strip("_") or "untitled"

def _ensure_dir(p: Path):
    try:
        p.mkdir(parents=True, exist_ok=True)
    except Exception:
        pass

def _safe_write(path: Path, text: str) -> Path:
    """Write text to path; if directory/permission fails, fall back to ~/TrendSmith/reports."""
    try:
        _ensure_dir(path.parent)
        path.write_text(text, encoding="utf-8")
        return path
    except Exception:
        home_reports = Path.home() / "TrendSmith"https://www.kdnuggets.com/"reports"
        _ensure_dir(home_reports)
        fb = home_reports / path.name
        fb.write_text(text, encoding="utf-8")
        return fb

def save_report(topic: str, text: str) -> Path:
    filename = f"{_sanitize(topic)}_{_ts()}.md"
    target = REPORTS / filename
    return _safe_write(target, text.strip() + "n")

4. Tracking each run

A small state object keeps what one request needs — streamed text, model, tool count, token usage and timing — then resets cleanly before the next request.

class State:
    def __init__(self):
        self.brief = ""
        self.model_raw = None
        self.usage = {}
        self.cost = None
        self.last_cmd = None
        self.last_topic = None
        self.tools = {}
        self.t0 = 0.0
        self.t1 = 0.0

    def reset(self):
        self.brief = ""
        self.model_raw = None
        self.usage = {}
        self.cost = None
        self.tools = {}
        self.t0 = time.perf_counter()
        self.t1 = 0.0

def friendly_model(name: str | None) -> str:
    if not name:
        return MODEL
    n = (name or "").lower()
    if "sonnet-4-5" in n or "sonnet_4_5" in n:
        return "Claude 4.5 Sonnet"
    if "sonnet" in n:
        return "Claude Sonnet"
    if "haiku" in n:
        return "Claude Haiku"
    if "opus" in n:
        return "Claude Opus"
    return name or "Unknown"

5. Run summary

A clean gray box prints model, tokens, tool usage and duration without blending into the streamed content.

def usage_footer(st: State, opts_model: str):
    st.t1 = st.t1 or time.perf_counter()
    dur = st.t1 - st.t0
    usage = st.usage or {}
    it = usage.get("input_tokens")
    ot = usage.get("output_tokens")
    total = usage.get("total_tokens")
    if total is None and (it is not None or ot is not None):
        total = (it or 0) + (ot or 0)
    tools_used = ", ".join(f"{k}×{v}" for k, v in st.tools.items()) or "--"
    model_label = friendly_model(st.model_raw or opts_model)

    box = (
        "┌─ Run Summary ─────────────────────────────────────────────",
        f"│ Model: {model_label}",
        f"│ Tokens: {total if total is not None else '?'}"
        + (f" (in={it if it is not None else '?'} | out={ot if ot is not None else '?'})"
            if (it is not None or ot is not None) else ""),
        f"│ Tools: {tools_used}",
        f"│ Duration: {dur:.1f}s",
        "└───────────────────────────────────────────────────────────",
    )
    print(GRAY + "n".join(box) + RESET, file=sys.stderr)

6. Main loop

The main loop starts the app, reads a command, queries the model, streams the answer, saves /trend reports and prints summaries.

async def main():
    """Setup → REPL → parse → query/stream → auto-save → summary."""
    st = State()
    _ensure_dir(REPORTS)

    opts = ClaudeAgentOptions(
        model=MODEL,
        system_prompt=SYS,
        allowed_tools=("WebFetch", "WebSearch"),
    )

    print("📈 TrendSmith nn" + HELP)

    async with ClaudeSDKClient(options=opts) as client:
        while True:
            # Read input
            try:
                user = input("nYou: ").strip()
            except (EOFError, KeyboardInterrupt):
                print("nBye!")
                break

            if not user:
                continue
            low = user.lower()

            # Basic commands
            if low in {"/exit", "exit", "quit"}:
                print("Bye!")
                break
            if low in {"/help", "help"}:
                print(HELP)
                continue

            # Parse into a prompt
            if low.startswith("/trend "):
                topic = user.split(" ", 1)(1).strip().strip('"')
                if not topic:
                    print('e.g. /trend "AI chip startups"')
                    continue
                st.last_cmd, st.last_topic = "trend", topic
                prompt = f"Run a fast trend scan for '{topic}' following the output spec."
            elif low.startswith("/scan "):
                q = user.split(" ", 1)(1).strip()
                if not q:
                    print('e.g. /scan "AI hardware news"')
                    continue
                st.last_cmd, st.last_topic = "scan", q
                prompt = f"Quick scan for '{q}' in under 10s (≤1 WebFetch). Return 5 bullets + TL;DR + sources."
            else:
                st.last_cmd, st.last_topic = "free", None
                prompt = user

            # Execute request and stream results
            st.reset()
            print(f"{GRAY}▶ Working...{RESET}")
            try:
                await client.query(prompt)
            except Exception as e:
                print(f"{GRAY}❌ Query error: {e}{RESET}")
                continue

            try:
                async for m in client.receive_response():
                    if isinstance(m, AssistantMessage):
                        st.model_raw = st.model_raw or m.model
                        for b in m.content:
                            if isinstance(b, TextBlock):
                                st.brief += b.text or ""
                                print(b.text or "", end="")
                            elif isinstance(b, ToolUseBlock):
                                name = b.name or "Tool"
                                st.tools(name) = st.tools.get(name, 0) + 1
                                print(f"{GRAY}n🛠 Tool: {name}{RESET}")
                            elif isinstance(b, ToolResultBlock):
                                pass  # quiet tool payloads
                    elif isinstance(m, ResultMessage):
                        st.usage = m.usage or {}
                        st.cost = m.total_cost_usd
            except Exception as e:
                print(f"{GRAY}n⚠ Stream error: {e}{RESET}")

            # Auto-save trend briefs and show the summary
            if st.last_cmd == "trend" and st.brief.strip():
                try:
                    saved_path = save_report(st.last_topic or "trend", st.brief)
                    print(f"n{GRAY}✅ Auto-saved → {saved_path}{RESET}")
                except Exception as e:
                    print(f"{GRAY}⚠ Save error: {e}{RESET}")

            st.t1 = time.perf_counter()
            usage_footer(st, opts.model)

if __name__ == "__main__":
    asyncio.run(main())

Testing the TrendSmith app

Running the Python file starts the CLI. The commands: /trend "topic" runs a brief multi-source scan and saves it automatically to reports/*.md; /scan "topic" produces a one-page quick scan (at most one WebFetch) and prints it without saving; /help shows the commands; /exit quits.

Getting Started with Cloud Agent SDK
Getting Started with Cloud Agent SDK

Testing the /trend option on AI chip startups:

/trend "AI chip startups"

The app used several search and web-scraping tools to gather information from different websites.

Getting Started with Cloud Agent SDK
Getting Started with Cloud Agent SDK

It streamed the answer, saved the report to a Markdown file and generated a usage summary — at a cost of $0.136 for the run.

Getting Started with Cloud Agent SDK
Getting Started with Cloud Agent SDK

A preview of the saved Markdown report on AI chip startups:

Getting Started with Cloud Agent SDK
Getting Started with Cloud Agent SDK

The scan option generates a concise summary using a simple web search and fetch:

Getting Started with Cloud Agent SDK
Getting Started with Cloud Agent SDK

Final thoughts

The app runs smoothly, and for anyone already on a Claude Code plan the SDK is a natural way to turn daily terminal workflows into a reliable, repeatable agentic CLI. Good uses include automating common development tasks (debugging, testing, deployment), scripting analysis or ops routines, and packaging flows into reusable, shareable tools. The SDK suits professionals who want stability, reproducibility and low glue-code overhead — and Claude Code itself can help write agentic applications with the SDK.

Limitations and what to watch

The SDK requires a paid Claude API key or subscription, and costs scale with tool calls and tokens — the $0.136 sample run shows the order of magnitude, but heavy multi-tool research runs cost more. The SDK and CLI evolve quickly, so commands and type names may shift between versions; the official documentation is the reference. For a contrasting approach that runs entirely offline on open models, see the fully local multi-agent orchestration tutorial, and for the wider tooling landscape, 10 GitHub repositories to master vibe coding.

Related Articles