LangGraph
Record LangGraph, LangChain, and Deep Agents invocations with the kitaru-langgraph adapter package
KitaruGraphRunner records supported invoke() and ainvoke() calls as Kitaru v2 sessions. It wraps an already compiled LangGraph runnable without recompiling it, replacing its checkpointer, or changing the graph result. LangChain agents and Deep Agents use the same adapter because their public factories return LangGraph runnables.
The adapter ships as the installable kitaru-langgraph distribution with the kitaru_langgraph import package. Install it directly in the agent environment:
uv add kitaru-langgraphDeep Agents support is an optional extra; the langchain.agents.create_agent factory and direct graph wrapping work without it, and constructing a runner through deepagents.create_deep_agent requires it:
uv add "kitaru-langgraph[deepagents]"Model-provider packages are not bundled. An init_chat_model string like "openai:gpt-5-nano" needs the matching LangChain provider package, for example langchain-openai, installed by you.
Capability matrix
Check the construction path before requesting replay behavior. Unsupported operations fail before the graph runs.
Direct compiled graph wrapper
Yes
Yes
No
No
Public callbacks observed by the outer run
langchain.agents.create_agent factory
Yes
Yes
Yes, with one live model call
Yes, for supported static or history results
Main agent and observable descendants
deepagents.create_deep_agent factory
Yes
Yes
Yes, with one live model call
Yes, for supported static or history results
Main agent and explicit Kitaru-built local subagents
Opaque compiled or remote subagent
Included in the outer result
No separate capability
No
No
Reported as opaque when it has a stable public category
Use runner.capabilities to inspect the immutable view produced by the adapter. A direct wrapper reports only recording and whole-input replacement. Factory construction reports only capabilities attached to middleware that Kitaru actually injected.
Record a compiled graph
Import the runner from the installed package:
from typing import NotRequired, TypedDict
from langgraph.graph import END, START, StateGraph
from kitaru_langgraph import KitaruGraphRunner
class SupportState(TypedDict):
request: str
normalized_request: NotRequired[str]
def normalize(state: SupportState) -> dict[str, str]:
return {"normalized_request": " ".join(state["request"].split())}
builder = StateGraph(SupportState)
builder.add_node("normalize", normalize)
builder.add_edge(START, "normalize")
builder.add_edge("normalize", END)
runner = KitaruGraphRunner(builder.compile(), agent_id=agent_id)
result = runner.invoke({"request": " Reset my password "})Kitaru creates the session and its root node before the graph starts. The session records bounded copies of the effective input and final output or error, plus public chain, graph, model, and tool callbacks that LangGraph exposes during the call. Ordinary Python calls and provider SDK calls that emit no public callback do not get invented child nodes.
Each model-call node records the requested model from LangChain's ls_model_name callback metadata, the served model and provider from the chat model's response metadata, and token usage from the response message. When usage is present, Kitaru estimates the call's cost from the bundled genai-prices catalog. Chat models that report no usage metadata are recorded without tokens or cost.
LangGraph applies a middleware's trace policy before passing inputs to callbacks. Deep Agents 0.7.9 and later omit inputs for some built-in middleware hooks, so their Kitaru span nodes can contain inputs: {} even when the hook received state. The callback cannot distinguish an omitted payload from a genuinely empty one. Session inputs, model and tool call inputs, and replay data remain available, but payload_coverage can count these empty span inputs as present. If you configure a trace policy on your own middleware, the same limit applies; Kitaru does not override that policy because doing so would also change what other callbacks receive.
The wrapper returns the exact graph value or raises the exact graph exception. Caller config, callbacks, tags, metadata, configurable values, thread ID, store, and checkpointer behavior remain with LangGraph. If a Kitaru task supplies task inputs, those replace the whole graph input; a caller Command, including Command(resume=...), always takes precedence.
Run the complete provider-free example from the repository root:
The local command needs an existing KITARU_AGENT_ID or KITARU_AGENT_VERSION_ID and a configured Kitaru v2 connection. It does not need a model-provider key or replay setup. See the example README for the full setup.
Apply live model-request overrides
Model, prompt, system-prompt, and model-parameter overrides require construction through one of the two accepted public factories:
The factory path inserts Kitaru middleware before the agent is compiled. During a Kitaru replay, the middleware builds the effective model request from the replay override, then calls that live model exactly once. Changing the model, prompt, system prompt, or model parameters never reuses a stored model response and never reduces the model-call count to zero. A mapped model override requires the original factory_kwargs["model"] to be a string identifier because that exact string selects the replacement. If the factory receives an already constructed model object, use a direct replacement instead of a mapping. Prompt replacement targets the factory-built agent's message state; direct compiled graph wrappers reject prompt overrides instead of guessing a state field.
from_agent_factory() accepts the exact public langchain.agents.create_agent or deepagents.create_deep_agent factory objects. The factory in each LocalSubagentFactorySpec has the same exact-object restriction; wrappers and other compatible callables are rejected because Kitaru cannot prove which middleware they install. For Deep Agents, the spec lets Kitaru build named local subagents before the parent and report each one separately. Caller-supplied compiled, remote, or framework-created children remain opaque and do not gain model or tool substitution capabilities from the outer wrapper.
Substitute supported tool results
Factory construction also installs public tool middleware. During a Kitaru replay, a matching static result or valid recorded-history result becomes a framework-valid ToolMessage or Command with the current tool-call identity. That hit is the only adapter path that skips a live dependency: the live tool is called zero times.
Deep Agents places its built-in middleware before custom middleware, including Kitaru's. Built-in tool rejections therefore take precedence over Kitaru's static or history policy. For example, Deep Agents 0.7.17 rejects later parallel write, edit, or delete calls targeting the same file before Kitaru can substitute their results. Replaying recordings made with a different Deep Agents version can change these outcomes; keep framework versions consistent when comparing replay behavior.
Misses follow the replay policy without silent fallback:
failraises before a live tool call.error_resultreturns a tool error result without a live tool call.passthroughcalls the live tool exactly once.
Malformed, unsupported, or lossy recorded results fail closed with ToolPolicyError; they do not become misses or permit passthrough. A matched recorded failure also raises ToolPolicyError with the stored error text and aborts the graph unless application middleware catches it. Kitaru does not recreate the original exception class or LangGraph recovery behavior. The adapter rejects the llm tool policy. It does not substitute stored model responses.
Newly recorded supported tool results preserve nested tuples and lists as distinct types, including tuple pairs in Command.update and the default empty tuple in Command.goto. Stored ToolMessage values must have an explicit success or error status; a missing or invalid status is rejected rather than treated as success.
The tool-result envelope remains kitaru.langgraph.tool_result.v1. Valid older envelopes remain readable, and their stored lists remain lists. Older adapter versions reject the new nested tuple tag, so use an updated adapter to replay newly recorded tuple-bearing results. These changes cannot recover keys or tuple types already lost in historical records.
Interrupts and unsupported invocation modes
For a direct call outside a Kitaru worker, a public LangGraph interrupt result is returned unchanged and the session is recorded as completed with interruption metadata. Resume the same LangGraph thread with your existing checkpointer and Command(resume=...); the resume call becomes a second Kitaru session. Kitaru never reads or replaces private checkpointer state.
Worker-managed interrupt scheduling and resume are not supported. If a worker invocation returns an interrupt, the adapter records the partial session as interrupted and failed, then raises UnsupportedWorkerInterruptError so the task cannot appear complete.
The v2 adapter is non-streaming. stream(), astream(), astream_events(), astream_log(), batch(), abatch(), batch_as_completed(), and abatch_as_completed() raise UnsupportedInvocationError before session creation or graph execution. Use invoke() or ainvoke().
Recording safety and limits
CapturePolicy transforms copies sent to Kitaru. It never changes values passed to or returned by LangGraph. The built-in recursive key redactor matches common credential fields case-insensitively, including authorization headers, API keys, passwords, secrets, access and refresh tokens, and cookies. You can supply a final custom redactor for application fields.
Prompts, graph state, tool arguments, tool results, outputs, errors, and arbitrary free text can still contain sensitive data under names the key redactor cannot recognize. Apply a custom redactor where needed and use Kitaru access and retention controls for stored sessions.
The default per-invocation bounds are:
Child nodes
10,000
One UTF-8 JSON field
256 KiB
Buffered node data
16 MiB
Recording depth
20
Items per collection
1,000
Limit hits or serialization failures mark the recording as lossy, truncate or drop only the stored copy, and preserve the graph outcome. Lossy tool arguments or results are not eligible for history substitution.
Converting a non-string mapping key to a JSON string also marks the recording as lossy. This includes collisions such as the distinct keys 1 and "1"; the stored copy is best effort, and Kitaru will not use it for history substitution.
Recording failures
Session and root-node setup must succeed before the graph starts. After graph delegation begins, adapter-owned recording failures are contained in that invocation. The direct runner preserves the graph result or exception and attempts one private, structured local warning with the failed stage and exception class, without recorded payloads or exception text. A worker task can still fail if its linked result session cannot be completed.
Migrate from the v1 adapter
The v2 adapter is a smaller recording and replay boundary, not a port of the v1 execution system.
Record one graph invocation
Use KitaruGraphRunner.invoke() or ainvoke(); each invocation is one Kitaru session
Middleware-observed model and tool calls
Use from_agent_factory() with LangChain or Deep Agents
Graph-call versus calls checkpoint strategies
Removed; construction determines the declared capability set
Adapter streaming and live stream events
Deferred; streaming entry points fail before execution
Synthetic checkpoints and stored model-response substitution
Removed or deferred; model overrides always make one live model call
Native checkpoint reconstruction, time travel, and node-boundary replay
Deferred; LangGraph keeps its checkpointer and thread state
Worker-managed interrupt resume
Deferred; direct interrupts work, worker interrupts fail explicitly
ZenML pipeline, flow, stack, and sandbox helpers
Not part of the v2 LangGraph adapter
Separate adapter distribution
Available; install kitaru-langgraph and import from kitaru_langgraph
If your integration depends on v1 streaming, checkpoint strategies, synthetic checkpoints, or worker-managed resume, keep it on v1 until the required capability has an explicit v2 contract. Do not translate those options into the v2 runner.
Next steps
Run the provider-free recording example.
Compare other integration boundaries in the adapters overview.
Read the LangGraph overview and interrupt documentation.
Last updated
Was this helpful?