Skip to content

LangChain / LangGraph

Overview

LangChainConnector (src/chatomon/connectors/langchain.py) builds an atom into a LangGraph CompiledStateGraph with langchain.agents.create_agent and installs chatomon's checks as middleware. It needs the langchain extra (pip install 'chatomon[langchain]').

Building the Agent

Atom.build() returns the compiled graph; every extra keyword argument of the connector goes to create_agent (tools, checkpointer, middleware, ...). chatomon sets the prompt and name from the atom, so system_prompt, response_format and name are refused with ChatomonConfigError. Your own middleware runs inside chatomon's layer.

Hooks

Hook What chatomon does there
before_agent Runs the input chain, redacts a blocked message by id, jumps to the end when an orbital replies
wrap_model_call Adds the guidance, runs the output chain and the regenerations; rebuilds a turn resumed by another process or agent instance
wrap_tool_call Closes the turn as aborted when a tool raises
after_agent Finishes the turn, or closes it as aborted when it has no final reply, and clears chatomon's private state

Every hook has a sync and an async variant, so both invoke and ainvoke are supported.

Conversations

The checkpointer keeps the conversation and its thread_id becomes the conversation id of every report; a blocked message is replaced by the placeholder in the latest checkpoint.

from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage, HumanMessage
from langgraph.checkpoint.memory import InMemorySaver

from chatomon import Atom, TurnReport
from chatomon.connectors.langchain import LangChainConnector

reports: list[TurnReport] = []
model = GenericFakeChatModel(messages=iter([AIMessage(content="Hi!"), AIMessage(content="Bye!")]))
agent = Atom(
    name="speedypack",
    instructions="Be brief.",
    connector=LangChainConnector(model, checkpointer=InMemorySaver()),
    on_turn=reports.append,
).build()
config = {"configurable": {"thread_id": "customer-42"}}
agent.invoke({"messages": [HumanMessage(content="hello")]}, config)
agent.invoke({"messages": [HumanMessage(content="thanks")]}, config)
assert [report.conversation_id for report in reports] == ["customer-42", "customer-42"]

Earlier checkpoints keep the original message

A LangGraph checkpointer keeps every earlier snapshot of the thread, and the snapshot written before chatomon runs still contains the original user message. The nucleus never sees it again — replaying a thread runs the input orbitals again — but it remains in storage: pruning and retention of checkpoints are the application's responsibility, as for any other personal data.

Streaming

With input orbitals only, tokens stream normally. With output orbitals the model is tagged nostream and checked inside the model call, so the graph's output streams (stream / astream with messages, updates or values) emit only the approved reply.

astream_events and callbacks are not a safe user-facing channel

astream_events and callback handlers observe every internal model call, so when output orbitals exist they also see drafts that the output orbitals later reject. Use them for tracing, not to feed a user interface.

Tracing

LangChain does not propagate the OpenTelemetry context through its hooks. LangChainConnector accepts a parent_span callable that returns the span chatomon's spans should nest under; without it they attach to the caller's current span.

Failures and Human in the Loop

Failures of the model call and of tools close the turn as aborted (see Failures). A turn interrupted by interrupt() stays open and is closed when the graph resumes; if it is resumed by another process or agent instance, chatomon rebuilds the turn and runs the input orbitals again before the model is called, so the reply is always checked. A thread paused by a graph built without chatomon and resumed on the atom's agent is rebuilt in the same way, and the rebuilt turn lasts until the graph ends, through tool calls and further model calls. If the model call that rebuilds it is interrupted in turn, that turn is closed as aborted and built again when the graph resumes: the exchange leaves two reports. A turn left behind by a failure elsewhere is closed as aborted when the next message arrives on the same thread.

A model reply with no text and no tool call is not checked and leaves the turn open. If the graph ends there, the turn is closed as aborted and the state keeps the empty message; if a middleware of yours sends the graph back to the model, the reply that follows is checked in the same turn.


Last Updated: October 2026 Status: Implemented