{"slug": "tracesage-see-inside-your-langgraph-agents", "title": "tracesage: See Inside Your LangGraph Agents", "summary": "A developer built tracesage, an open-source local-first observability tool for LangChain and LangGraph agents. It hooks into LangChain's callback stream to capture events, stores them locally, and renders an interactive graph and timeline UI in the browser in real time. The tool requires only two lines of code to integrate and runs entirely on the user's laptop with no external infrastructure.", "body_md": "*Open-source LangChain/LangGraph tracing — drop in two lines, watch your agents run live in your browser.*\n\nIf you've built anything non-trivial with **LangChain** or **LangGraph** — a multi-agent supervisor, a RAG pipeline, a tool-using ReAct loop — you know the feeling. It works on the happy path, then a real query comes in and… something goes wrong. But *what*?\n\nWhich agent actually ran? In what order?\n\nDid the model call the tool you expected, or hallucinate a different one?\n\nHow many tokens did that one request burn?\n\nWhere did the error come from — your tool, the model, or the orchestration?\n\nThe usual answer is a wall of `print()`\n\nstatements and `verbose=True`\n\nlogs you scroll through at 2 a.m. There are great hosted tracing platforms, but they mean signing up, shipping your prompts to a third party, and wiring up an SDK.\n\nI wanted something I could `pip install`\n\nand have running in ten seconds, entirely on my laptop. So I built **tracesage**.\n\n**tracesage** is a local-first observability tool for LangChain & LangGraph agents. It hooks into LangChain's callback stream, captures every chain / tool / LLM / retriever event, stores it locally (SQLite + gzipped blobs), and renders it as an **interactive graph + timeline UI** in your browser — in real time.\n\n🚀 **Two-line integration.** One callback added to your existing `invoke`\n\n/`ainvoke`\n\n.\n\n🧰 **Zero infrastructure.** No Docker, no Postgres, no external service. Just `pip install`\n\n.\n\n🔒 **Never crashes your app.** The callback handler is wrapped to never raise — tracing can fail, your agent keeps running.\n\n🗺️ **MCP-aware.** Tools loaded from MCP servers are attributed back to their server, so you can see which tools came from where.\n\n🧪 **Testable.** A `pytest`\n\nfixture lets you assert \"did my agent call `search`\n\n?\" in CI.\n\n📦 **MIT licensed**, runs in a single Python process.\n\nLinks:\n\n**Examples gallery (30 before/after apps):** in the repo under `examples/showcase/`\n\nBefore we write any code, see what we're aiming for:\n\n```\npip install \"tracesage[langchain]\"\ntracesage demo            # seeds a sample trace and opens the UI\n```\n\nYour browser opens to `http://localhost:7842/ui`\n\nand you're looking at a live agent topology.\n\nLet's build a tiny but real LangGraph agent and wire tracesage into it. You'll need Python 3.11+ and an LLM provider key (we'll use OpenAI; Anthropic or any LangChain model works identically — tracesage is provider-agnostic).\n\n```\npip install \"tracesage[langchain]\" langgraph langchain-openai\nexport OPENAI_API_KEY=sk-...\n```\n\n`before.py`\n\n— a standard LangGraph ReAct agent with two tools:\n\n``` python\nimport asyncio\nfrom langchain_core.tools import tool\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import create_react_agent\n\n@tool\ndef get_weather(city: str) -> str:\n    \"\"\"Return the current weather for a city.\"\"\"\n    return f\"It's 22°C and sunny in {city}.\"\n\n@tool\ndef to_fahrenheit(celsius: float) -> float:\n    \"\"\"Convert a temperature in Celsius to Fahrenheit.\"\"\"\n    return celsius * 9 / 5 + 32\n\nagent = create_react_agent(\n    ChatOpenAI(model=\"gpt-4o-mini\", temperature=0),\n    tools=[get_weather, to_fahrenheit],\n)\n\nasync def main() -> None:\n    result = await agent.ainvoke(\n        {\"messages\": [{\"role\": \"user\",\n                       \"content\": \"What's the weather in Paris, in Fahrenheit?\"}]}\n    )\n    print(result[\"messages\"][-1].content)\n\nasyncio.run(main())\n```\n\nRun it and you get an answer. But you have **no idea** how it got there — did it call `get_weather`\n\nthen `to_fahrenheit`\n\n? Did it loop? How many model calls?\n\n`after.py`\n\n— the *only* difference is creating a tracer and passing its handler:\n\n``` python\nimport asyncio\nfrom langchain_core.tools import tool\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import create_react_agent\n\nfrom tracesage import TraceSage          # 1️⃣ import\n\n@tool\ndef get_weather(city: str) -> str:\n    \"\"\"Return the current weather for a city.\"\"\"\n    return f\"It's 22°C and sunny in {city}.\"\n\n@tool\ndef to_fahrenheit(celsius: float) -> float:\n    \"\"\"Convert a temperature in Celsius to Fahrenheit.\"\"\"\n    return celsius * 9 / 5 + 32\n\nagent = create_react_agent(\n    ChatOpenAI(model=\"gpt-4o-mini\", temperature=0),\n    tools=[get_weather, to_fahrenheit],\n)\n\nasync def main() -> None:\n    tracer = await TraceSage.create()    # 2️⃣ start tracesage (UI on :7842)\n\n    result = await agent.ainvoke(\n        {\"messages\": [{\"role\": \"user\",\n                       \"content\": \"What's the weather in Paris, in Fahrenheit?\"}]},\n        config={\"callbacks\": [tracer.handler]},   # 3️⃣ the one line you add\n    )\n    print(result[\"messages\"][-1].content)\n\n    # Keep the process alive so you can explore the UI.\n    input(\"Trace ready at http://localhost:7842/ui — press Enter to exit.\")\n    await tracer.stop()\n\nasyncio.run(main())\n```\n\nThat's it. Run `python after.py`\n\n, open ** http://localhost:7842/ui**, and your run is there.\n\n`with`\n\nblock\nFor scripts and notebooks, there's a context manager that starts the UI *and* installs a global handler — so you don't even pass `callbacks=`\n\n:\n\n``` python\nimport tracesage\n\nwith tracesage.trace() as tl:                 # starts UI + global capture\n    result = agent.invoke({\"messages\": [...]})    # 🔍 tracesage: http://127.0.0.1:7842/ui/#run=...\n    input(\"Trace ready — open the printed link, then Enter to exit.\")\n```\n\nEvery new run prints a clickable deep link to that exact trace.\n\nHere's where tracesage earns its keep. Open a run and you get a **topology graph** of everything that happened.\n\nEvery node is one of six kinds, colour-coded in the legend (bottom-left):\n\n| Kind | What it is |\n|---|---|\n`agent` |\na function you registered as a node, that calls other things |\n`tool` |\na `@tool` side-effect function (DB, API, calculation) |\n`llm` |\na language-model call (what you count, cost, and cache) |\n`retriever` |\na `BaseRetriever` — the \"R\" in RAG |\n`chain` |\nplumbing: LCEL pipes, the LangGraph state machine, routing functions |\n`mcp` |\na synthesized node grouping the tools loaded from one MCP server |\n\n**Click any node** to open its inspector — call counts, durations, errors, and the tools it provides or uses:\n\nThe timeline on the right replays the run step-by-step; click a step to expand the full payload (prompts, tool inputs/outputs, token usage, and — on errors — the exception type and traceback).\n\nIf your agent loads tools from **MCP servers** (via `langchain-mcp-adapters`\n\n), you usually lose track of *where* each tool came from — they all look like generic LangChain tools at runtime. tracesage fixes that.\n\nInstall the extra and register your MCP client:\n\n```\npip install \"tracesage[mcp]\"\npython\nfrom langchain_mcp_adapters.client import MultiServerMCPClient\nfrom tracesage import TraceSage\nfrom tracesage.adapters.mcp import register_mcp_client\n\ntracer = await TraceSage.create()\n\nclient = MultiServerMCPClient({\n    \"weather\": {\"command\": \"python\", \"args\": [\"weather_server.py\"], \"transport\": \"stdio\"},\n    \"math\":    {\"command\": \"python\", \"args\": [\"math_server.py\"],    \"transport\": \"stdio\"},\n})\n\n# Loads every server's tools AND records tool → server provenance.\ntools = await register_mcp_client(tracer, client)\n# Your own @tool functions stay \"local\" (unattributed) automatically.\n```\n\nNow the UI shows a **\"Tools by source\"** panel and dedicated `mcp:`\n\nnodes — every tool is grouped by where it came from:\n\nClick an MCP server node and you see exactly what it provides, how often it was called, and which agents used it:\n\nA complete, runnable MCP example (two local stdio servers + hardcoded tools, **no API key needed**) lives in the repo at `examples/mcp/`\n\n:\n\n```\npip install \"tracesage[mcp]\"\npython examples/mcp/main.py     # then open http://localhost:7842/ui\n```\n\nTracing isn't just for eyeballing. tracesage ships a `pytest`\n\nfixture (`tracesage_capture`\n\n, auto-registered) so you can assert behaviour:\n\n``` python\ndef test_agent_uses_search(tracesage_capture):\n    agent.invoke(\"find me a hotel in Paris\")\n    tracesage_capture.assert_tool_called(\"get_weather\")\n    tracesage_capture.assert_no_errors()\n    assert tracesage_capture.total_tokens()[0] < 5000   # input-token budget\n```\n\nNo setup, no server — the fixture captures the run in-process and gives you assertions like `assert_tool_called`\n\n, `assert_no_errors`\n\n, and `total_tokens`\n\n.\n\ntracesage is built so you can wire it in *once* and control it per-environment:\n\n**Kill switch:** set `TRACESAGE_ENABLED=false`\n\n(or `enabled=False`\n\n) and `TraceSage`\n\nreturns an inert tracer — no server, no DB, a no-op handler, near-zero overhead. Same code ships to prod; tracing just turns off.\n\n**Capture without the UI:** `TRACESAGE_START_SERVER=false`\n\nrecords traces to disk in prod without binding the in-process UI; view them later with `tracesage serve`\n\n.\n\n**Safety rails:** bearer-token auth, root-level sampling (`sample_rate`\n\n), a per-run event cap, and a hard fail-stop if you bind a non-loopback address without an auth token.\n\n```\n# In prod: capture is off, zero overhead, no code change.\n#   TRACESAGE_ENABLED=false\n# Or capture quietly, no UI:\n#   TRACESAGE_START_SERVER=false   → then `tracesage serve` to look later\npip install \"tracesage[langchain]\"\ntracesage demo\n```\n\n**Docs & full quickstart:** [https://kjgpta.github.io/tracesage/](https://kjgpta.github.io/tracesage/)\n\n**Concepts (what each node kind means):** [https://kjgpta.github.io/tracesage/concepts/](https://kjgpta.github.io/tracesage/concepts/)\n\n**MCP attribution guide:** [https://kjgpta.github.io/tracesage/mcp/](https://kjgpta.github.io/tracesage/mcp/)\n\n**30 before/after example apps:** `examples/showcase/`\n\nin the repo\n\nIf you build agents with LangChain or LangGraph and you're tired of `print`\n\n-debugging your way through a run, give it a spin. Two lines, one browser tab, and your agent stops being a black box.\n\n*tracesage is MIT-licensed and open source.*", "url": "https://wpnews.pro/news/tracesage-see-inside-your-langgraph-agents", "canonical_source": "https://dev.to/kjgpta/tracesage-see-inside-your-langgraph-agents-55ek", "published_at": "2026-06-16 14:08:03+00:00", "updated_at": "2026-06-16 14:17:12.888788+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-agents"], "entities": ["LangChain", "LangGraph", "tracesage", "OpenAI", "SQLite", "MCP"], "alternates": {"html": "https://wpnews.pro/news/tracesage-see-inside-your-langgraph-agents", "markdown": "https://wpnews.pro/news/tracesage-see-inside-your-langgraph-agents.md", "text": "https://wpnews.pro/news/tracesage-see-inside-your-langgraph-agents.txt", "jsonld": "https://wpnews.pro/news/tracesage-see-inside-your-langgraph-agents.jsonld"}}