Skip to content
Skillv1.0.0

langchain-langgraph-agents

Build a correct LangGraph 1.0 ReAct agent with create_react_agent — typed tools, error propagation, recursion caps, and stop conditions that actually stop. Use when writing a first tool-calling agent,

by jeremylongshore(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from jeremylongshore/tons-of-skills-marketplace (plugins/saas-packs/langchain-py-pack/skills/langchain-langgraph-agents/SKILL.md). Install upstream with npx skills add jeremylongshore/tons-of-skills-marketplace --skill langchain-langgraph-agents. Copyright stays with the author (MIT).

LangChain LangGraph Agents (Python)

Overview

Two failure modes hit every team writing their first LangGraph 1.0 ReAct agent:

Loop-to-cap on vague prompts (P10). create_react_agent defaults to recursion_limit=25. A prompt like "help me with my account" never converges — the model calls a retrieval tool, gets irrelevant results, calls another tool, and repeats until GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition fires. Cost dashboards show the damage after the fact: $5-$15 per runaway loop on Sonnet with a 3-tool agent, assuming no tool is itself expensive.

Silent tool errors on legacy AgentExecutor (P09). The legacy executor defaults handle_parsing_errors=True and catches tool exceptions, feeding the error string back as the next observation. When the error serializes to empty (e.g., a ValueError("") or an HTTP 500 with no body), the loop continues with no signal. The agent says "I couldn't find the answer" — which was actually a silent crash three tool calls ago.

This skill walks through defining typed tools with @tool + Pydantic; building an agent with create_react_agent(model, tools, checkpointer=MemorySaver()); invoking with {"messages": [...]} and a thread-scoped config; setting recursion_limit per expected agent depth (5-10 interactive, 20-30 planner); adding middleware for a per-session token budget; and raise-by-default error propagation. Pin: langgraph >= 1.0, < 2.0, langchain-core >= 1.0, < 2.0. Pain-catalog anchors: P09, P10, P11, P32, P41, P42, P63.

Prerequisites

  • Python 3.10+
  • langgraph >= 1.0, < 2.0 and langchain-core >= 1.0, < 2.0
  • At least one provider package: pip install langchain-anthropic or langchain-openai
  • Completed skill: langchain-langgraph-basics (L25) — you already know StateGraph, MessagesState, and checkpointers
  • Provider API key: ANTHROPIC_API_KEY or OPENAI_API_KEY

Instructions

Step 1 — Define tools with typed schemas and short docstrings

from typing import Annotated
from pydantic import BaseModel, Field
from langchain_core.tools import tool

class LookupAccountArgs(BaseModel):
    account_id: str = Field(..., description="Account UUID. No email addresses.")

@tool("lookup_account", args_schema=LookupAccountArgs)
def lookup_account(account_id: str) -> dict:
    """Fetch an account record by UUID. Returns status, plan, and owner email."""
    if not account_id:
        raise ValueError("account_id is required")  # raised → agent sees real error
    return {"id": account_id, "status": "active", "plan": "pro", "owner": "a@b.co"}

Two rules that catch teams off-guard:

  1. Docstring is the tool description the provider sees. Keep it under 1024 chars (P11). Anthropic truncates at ~1024; OpenAI's effective cap is softer but still bites on tool descriptions over ~2KB. Long docstrings with examples should move into a system prompt, not the tool description.
  2. Raise real exceptions. Unlike the legacy AgentExecutor, LangGraph's create_react_agent does not silently swallow tool errors — the exception propagates and surfaces in your observability layer. See Step 6.

For async tools, use @tool on an async def — LangGraph invokes it via await. For structured return types, annotate the return with a Pydantic model.

See Tool Definition Patterns for the @tool vs tool() decision, async tools, and the args_schema vs auto-inferred trade-off.

Step 2 — Build the agent with create_react_agent

from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
from langchain_anthropic import ChatAnthropic

model = ChatAnthropic(
    model="claude-sonnet-4-6",
    temperature=0,
    timeout=30,
    max_retries=2,
)

agent = create_react_agent(
    model=model,
    tools=[lookup_account],
    checkpointer=MemorySaver(),  # required for stateful invocations
)

create_react_agent is the LangGraph 1.0 replacement for the removed initialize_agent factory (P41). Under the hood it builds a StateGraph with a model node and a ToolNode, plus a conditional edge that routes to END when the model emits no tool calls. The checkpointer persists state per-thread — required for multi-turn conversations and for resuming after interruption.

Step 3 — Invoke with a thread-scoped config

config = {"configurable": {"thread_id": "user-42"}}
result = agent.invoke(
    {"messages": [{"role": "user", "content": "look up account uuid-abc"}]},
    config=config,
)
print(result["messages"][-1].content)

Key contracts:

  • Input is {"messages": [...]} — a list of message dicts or LangChain HumanMessage / SystemMessage objects. You append to this list across turns.
  • thread_id scopes the checkpointer. Reusing it resumes the conversation.
  • Output is the full updated state. result["messages"] is the complete message list; the final assistant message is at index -1.

Step 4 — Set recursion_limit to your expected agent depth

create_react_agent defaults to recursion_limit=25. In LangGraph one "recursion step" is one node visit, and each tool round-trip is two visits (model node + tool node), so 25 means ~12 tool calls. For most workloads this is too generous and hides bugs:

Agent kind Suggested recursion_limit Rationale
Interactive chat with 1-3 tools 5-10 One tool call + one final answer is 3 visits. Cap low to expose loops.
Task-completion (e.g., booking flow) 10-15 3-5 tool calls + final answer.
Planner / research agent 20-30 Expect multiple retrieval + synthesis rounds.
Multi-agent supervisor 40+ Coordinator + worker rounds. Budget tokens separately.

Apply it on invocation, not at construction time:

result = agent.invoke(
    {"messages": [...]},
    config={"configurable": {"thread_id": "user-42"}, "recursion_limit": 10},
)

When the limit fires, LangGraph raises GraphRecursionError — catch it and surface a user-facing message; do not retry without a cost guard.

Step 5 — Add a per-session token budget via middleware

recursion_limit alone does not bound cost. A single tool call that returns a large document and triggers a long model response can cost more than 10 cheap tool calls. Cap tokens explicitly:

from langchain_core.callbacks import BaseCallbackHandler

class TokenBudget(BaseCallbackHandler):
    def __init__(self, max_tokens: int = 50_000):
        self.used = 0
        self.max = max_tokens

    def on_llm_end(self, response, **kwargs):
        usage = getattr(response, "llm_output", {}).get("token_usage", {}) or {}
        self.used += usage.get("total_tokens", 0)
        if self.used > self.max:
            raise RuntimeError(f"Token budget exceeded: {self.used}/{self.max}")

budget = TokenBudget(max_tokens=50_000)
result = agent.invoke(
    {"messages": [...]},
    config={
        "configurable": {"thread_id": "user-42"},
        "recursion_limit": 10,
        "callbacks": [budget],
    },
)

A per-session budget of 50K tokens on Sonnet is roughly $0.25 — a safe cap for interactive agents. For background planners raise to 200K-500K. See Loop Caps and Budgets for a repeated-tool-call early-stop node and a middleware pattern that terminates on the N-th identical call.

Step 6 — Propagate tool errors; do not silently swallow

LangGraph's default is to raise. Legacy AgentExecutor(handle_parsing_errors=True) swallowed everything. The new defaults are safer but different:

# Tool raises → the exception propagates out of agent.invoke()
try:
    result = agent.invoke({"messages": [{"role": "user", "content": "..."}]}, config=config)
except ValueError as e:
    # Your tool's own ValueError — log + user-facing message
    ...

When you want tolerant behavior (e.g., the tool is a flaky third-party API and you want the model to try a different approach), wrap the tool itself:

from langchain_core.tools import tool

@tool
def search_kb(query: str) -> str:
    """Search the internal knowledge base. Returns hits or a 'no results' string."""
    try:
        return _real_search(query)
    except HTTPError as e:
        return f"search_kb unavailable: {e.response.status_code}. Try a different query."

The key insight: the tool decides to degrade gracefully by returning a string the model can reason about. The agent never silently drops an error. See Error Propagation for a custom error-handler node that routes tool failures to a fallback tool.

Step 7 — Choose create_react_agent vs custom StateGraph vs legacy

Decision Use Why
Single agent, tool-calling loop create_react_agent Correct defaults, provider-native tool calling, smallest code surface
Multi-stage pipeline (plan → execute → review) Custom StateGraph You need named nodes, explicit conditional edges, typed state
Multi-agent supervisor create_supervisor + workers built with create_react_agent Built-in routing, per-worker checkpointing
New code in 2026+ Never use AgentExecutor or initialize_agent Removed / deprecated in 1.0 (P41); shape changes in intermediate_steps (P42)

For a single forced-tool single-shot (e.g., "always classify into one of these buckets"), skip agents entirely: use model.bind_tools([Schema], tool_choice={"type": "tool", "name": "Schema"}). But never loop a forced tool_choice (P63) — the model cannot emit stop_reason="end_turn" under forced tool_choice, so the agent never terminates.

Output

  • Agent built with create_react_agent(model, tools, checkpointer=MemorySaver())
  • Tools defined with @tool + Pydantic args_schema, docstrings under 1024 chars
  • Invocations pass {"configurable": {"thread_id": ...}, "recursion_limit": N}
  • TokenBudget callback enforces per-session cost ceiling
  • Tool errors raise and surface in observability; graceful-degrade patterns are explicit (return-a-string, not silent-swallow)
  • Decision table resolved: create_react_agent vs custom StateGraph vs supervisor vs legacy

Error Handling

Error Cause Fix
GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition Vague prompt never converges; default cap too high (P10) Lower recursion_limit to 5-10 interactive; add repeated-tool-call early-stop node
ImportError: cannot import name 'initialize_agent' from 'langchain.agents' Legacy 0.2 agent factory removed (P41) from langgraph.prebuilt import create_react_agent
AttributeError: 'ToolCall' object has no attribute 'tool' Old code accessing step.tool on new intermediate step shape (P42) Use step.tool_name (or step["name"] on dict form); check isinstance(step, ToolCall)
Agent says "couldn't find answer" but tool actually raised Legacy AgentExecutor handle_parsing_errors=True silently swallowed exception (P09) Migrate to create_react_agent; errors raise by default
Agent loops when tool_choice={"type": "tool", "name": "X"} is set Forced tool_choice blocks stop_reason="end_turn" (P63) Use tool_choice="auto" for agent loops; reserve forced choice for one-shot calls
Agent hallucinates a tool name like exec that is not in tools=[...] Older free-text ReAct parser accepts any string (P32) Use create_react_agent — it relies on provider-native tool calling; the allowlist is wire-enforced
RuntimeError: Token budget exceeded Your TokenBudget callback fired Working as intended; raise the cap or shorten the agent's scope
Tool description truncated, model calls with wrong args Docstring exceeded 1024-char cap (P11) Shorten docstring; move examples into system prompt

Examples

Migrating a legacy AgentExecutor agent

Before (LangChain 0.2):

from langchain.agents import initialize_agent, AgentType
agent = initialize_agent(
    tools, llm, agent=AgentType.OPENAI_FUNCTIONS,
    handle_parsing_errors=True, return_intermediate_steps=True,
)
result = agent.invoke({"input": "..."})
for action, observation in result["intermediate_steps"]:
    print(action.tool, observation)  # .tool attribute

After (LangGraph 1.0):

from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
agent = create_react_agent(llm, tools, checkpointer=MemorySaver())
result = agent.invoke(
    {"messages": [{"role": "user", "content": "..."}]},
    config={"configurable": {"thread_id": "t1"}, "recursion_limit": 10},
)
# intermediate steps are now ToolMessage entries in the messages list
for m in result["messages"]:
    if m.type == "tool":
        print(m.name, m.content)  # .name, not .tool

See AgentExecutor Migration for the full before/after including handle_parsing_errors, return_intermediate_steps, and max_iterations translations.

Interactive agent with a strict cost cap

A customer-support agent with two tools, 10-step recursion cap, and a 30K token budget. See Loop Caps and Budgets for the full example with a repeated-tool-call early-stop node.

Resources

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/jeremylongshore-tons-of-skills-marketplace-langchain-lan-d2efb0/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

jeremylongshore-tons-of-skills-marketplace-langchain-lan-d2efb0.ocm.jsonjson
{
  "ocm": "1",
  "id": "jeremylongshore-tons-of-skills-marketplace-langchain-lan-d2efb0",
  "kind": "skill",
  "name": "langchain-langgraph-agents",
  "description": "Build a correct LangGraph 1.0 ReAct agent with create_react_agent — typed tools, error propagation, recursion caps, and stop conditions that actually stop. Use when writing a first tool-calling agent, migrating from AgentExecutor or initialize_agent, or diagnosing an agent that loops on vague prompts. Trigger with \"langgraph agent\", \"create_react_agent\", \"langgraph tool calling\", \"AgentExecutor migration\", or \"agent loop cost\".",
  "publisher": "jeremylongshore",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "general"
    ],
    "tags": [
      "skill-md",
      "saas",
      "langchain",
      "langgraph",
      "python",
      "langchain-1-0",
      "agents",
      "tool-calling",
      "skills-sh"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Build a correct LangGraph 1.0 ReAct agent with create_react_agent — typed tools, error propagation, recursion caps, and stop conditions that actually stop. Use when writing a first tool-calling agent, migrating from AgentExecutor or initialize_agent, or diagnosing an agent that loops on vague prompts. Trigger with \"langgraph agent\", \"create_react_agent\", \"langgraph tool calling\", \"AgentExecutor migration\", or \"agent loop cost\"."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "skills.sh",
      "repository": "https://github.com/jeremylongshore/tons-of-skills-marketplace",
      "path": "plugins/saas-packs/langchain-py-pack/skills/langchain-langgraph-agents/SKILL.md",
      "ref": "HEAD",
      "url": "https://github.com/jeremylongshore/tons-of-skills-marketplace/blob/HEAD/plugins/saas-packs/langchain-py-pack/skills/langchain-langgraph-agents/SKILL.md",
      "key": "jeremylongshore/tons-of-skills-marketplace/plugins/saas-packs/langchain-py-pack/skills/langchain-langgraph-agents/SKILL.md"
    },
    "compatibility": "Designed for Claude Code",
    "allowed_tools": [
      "Read,",
      "Write,",
      "Edit,",
      "Bash(python:*)"
    ],
    "license": "MIT"
  },
  "instructions": "# LangChain LangGraph Agents (Python)\n\n## Overview\n\nTwo failure modes hit every team writing their first LangGraph 1.0 ReAct agent:\n\n**Loop-to-cap on vague prompts (P10).** `create_react_agent` defaults to\n`recursion_limit=25`. A prompt like \"help me with my account\" never converges —\nthe model calls a retrieval tool, gets irrelevant results, calls another tool,\nand repeats until `GraphRecursionError: Recursion limit of 25 reached without\nhitting a stop condition` fires. Cost dashboards show the damage *after* the\nfact: $5-$15 per runaway loop on Sonnet with a 3-tool agent, assuming no tool\nis",
  "cost": {
    "context_tokens": 3529
  }
}

Fetch it by URL: GET /api/v1/registry/jeremylongshore-tons-of-skills-marketplace-langchain-lan-d2efb0/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.