axonpush
Python SDKIntegrations

LangChain

A callback handler that traces chain, LLM and tool lifecycle events from LangChain and LangGraph.

pip install "axonpush[langchain]"

Tested against langchain-core>=0.1,<0.4, including the 0.2 kwargs-only callback reshape.

Attach the handler

from axonpush import AxonPush
from axonpush.integrations.langchain import AxonPushCallbackHandler

client = AxonPush()
handler = AxonPushCallbackHandler(client, channel_id, agent_id="research-agent")

result = chain.invoke(
  {"input": "What are AI agents?"},
  config={"callbacks": [handler]},
)

handler.flush(timeout=2.0)

client and channel_id are positional. If you would rather not pick the class yourself, get_langchain_handler(client, channel_id, **kwargs) returns the async handler for an AsyncAxonPush and the sync one otherwise.

Match the handler to the client. AsyncAxonPushCallbackHandler needs an AsyncAxonPush; the sync handler needs an AxonPush. A mismatch does not raise, events are dropped with a warning on the axonpush logger.

Constructor

AxonPushCallbackHandler(
    client,                       # AxonPush - positional
    channel_id,                   # str UUID - positional
    *,
    agent_id="langchain",
    trace_id=None,
    metadata=None,                # merged into every event's metadata
    mode=None,                    # "background" (default) | "sync"
    queue_size=1000,
    shutdown_timeout=2.0,
)

AsyncAxonPushCallbackHandler is the same, except it takes max_pending=1000 in place of queue_size / shutdown_timeout.

trace_id pins every event this handler emits to one trace; omit it and the handler adopts the trace active on the current context, creating one if needed.

What gets published

IdentifierEvent typePayload
chain.startagent.startchain_type, inputs
chain.endagent.endoutputs
chain.erroragent.errorerror, error_type
llm.startagent.startmodel, prompt_count
llm.endagent.endgenerations
llm.tokenagent.llm.tokentoken
tool.<name>.startagent.tool_call.starttool_name, input (first 2000 chars)
tool.endagent.tool_call.endoutput
tool.erroragent.errorerror, error_type

Every event carries framework: "langchain" in metadata.

Trace shape

LangChain’s run_id becomes the event’s span_id, and parent_run_id becomes parent_event_id, so nested chains and sub-chains land as a connected tree rather than as siblings. Both ids are also mirrored into metadata as langchain_run_id and langchain_parent_run_id.

LangGraph

LangGraph compiles nodes into anonymous Runnables, so serialized arrives empty and the node identity is in the callback kwargs. The handler resolves a name from kwargs["name"], then metadata["langgraph_node"], then serialized["name"], then the last segment of serialized["id"], falling back to "Runnable".

It also promotes the framework’s own context into event metadata, with no configuration:

  • langgraph_node, langgraph_step, langgraph_triggers
  • thread_id
  • run_type
  • tags

so you can group and filter a graph run by node in the dashboard.

Before v0.0.13 every LangGraph step landed as chain_type: "unknown". If you have dashboards filtering on that literal, retire them.

Model names

llm.start reports the configured model id, not the LangChain wrapper class. It reads invocation_params["model"] or ["model_name"] first (set at call-time by every modern Chat* integration), then serialized["kwargs"]["model"], and only then falls back to the class name. So you get gpt-4o-mini, not ChatOpenAI.

Publishing modes

mode picks how events leave the process.

ModeBackendWhen
"background" (default)Bounded in-process queue drained by a workerAlmost always. Callbacks stay O(microseconds).
"sync"Direct HTTP call on the callback threadTests and debugging. Not supported on the async handler, which drops events in this mode.

The background queue holds 1000 events by default and drops the oldest when full, with a rate-limited warning on the axonpush.publisher logger.

Flush before you exit

Background publishing means an event can still be queued when your process, or your serverless container, stops.

handler.flush(timeout=2.0)   # block until drained, or give up
handler.close()              # drain and stop the worker

On the async handler these are await handler.aflush(...) and await handler.aclose().