Framework support

Frameworks need no integration, because they call the provider clients the SDK patches. What the SDK adds is caller attribution that resolves past framework internals.

There is no LangChain integration to install.

Framework calls are recorded automatically

Every framework here calls a provider client. LangChain's ChatOpenAI constructs an openai client, CrewAI delegates to LiteLLM which delegates to openai, and LlamaIndex wraps the same libraries. Since the SDK patches those classes, framework calls are recorded with no extra setup.

This also means a framework released next week is recorded, as long as it uses a provider SDK underneath.

What frameworks affect

Not capture, but attribution.

Every event records the file and line that made the call, found by walking the stack to the first frame that is not Capsera and not a provider library. Inside a framework, that frame is in the framework:

caller_file = langchain_core/language_models/chat_models.py:412

The SDK keeps a list of framework paths to skip, so the walk continues to the function you wrote. These are recognised automatically when installed:

LangChain, LangGraph, CrewAI, LlamaIndex, AutoGen, Haystack, DSPy.

LiteLLM is also on the list. See LiteLLM and gateways.

If caller_file points into a library not on that list, capture is still correct and only the reported location is unhelpful. Report the library and it will be added.

Attribution must come from you

The framework does not know your agent names. Without a scope, framework calls are attributed to unknown.

The plain decorator works everywhere:

@capsera.agent("researcher", team="core")
def research(question):
    return chain.invoke({"question": question})

Three helpers also record which framework produced the work, so reports can separate graph nodes from crew agents from your own functions:

from capsera import langgraph_node, crewai_agent, crewai_task

@langgraph_node("planner")
def plan(state): ...

@crewai_agent("researcher")
def build_researcher(): ...

@crewai_task("summarize")
def summary_task(): ...

They accept the same optional team, task_type, customer_id, and cost_center as @agent(). Using @agent() everywhere instead works fine.

When there is no function to decorate

Sometimes the LLM call happens inside framework machinery, such as a chain assembled at module level and invoked from a handler. Two options.

Use tag() around the invocation:

with capsera.tag("classifier", task_type="triage"):
    result = chain.invoke(payload)

Or, for LangChain, use the callback handler, which attaches attribution for the duration of each LLM call:

from capsera import LangChainCapseraCallback

chain.invoke(
    payload,
    config={"callbacks": [LangChainCapseraCallback(agent="classifier", team="support")]},
)

The callback is useful when the invocation site is not yours to wrap, such as inside an agent executor or a retriever chain.

Per-framework pages

LangChain, LangGraph, CrewAI, LlamaIndex, LiteLLM and gateways, AutoGen, Haystack and DSPy.

Detection timing

The skip-list is built when init() runs, from the frameworks importable at that moment. A framework imported for the first time after init() is not added, so its internals can appear as caller_file.

This is rare, because frameworks are normally imported at module load. It can occur if you lazy-import a framework inside a request path.