Skip to Content
SDKFrameworks

Frameworks

Every way to instrument an agent, in Python and TypeScript — from a bare context manager to a fully patched framework runner. Pick the one that matches your stack; the install command pulls in the right adapter and the snippet is the minimal working integration.

Python

Python SDK (direct)

pip install morse-ai

Direct instrumentation of any Python function via a context manager — no framework required. Quote the extras in every pip install below ("morse-ai[...]"): zsh, the default shell on macOS, stops with no matches found on an unquoted bracket.

import morse_ai morse_ai.init() # reads MORSE_API_KEY from your environment # Stand-ins so this runs as written, with no model key. Replace them with your agent logic # and a check your code can really make. def do_work(): return "42" def is_valid(result): return result == "42" with morse_ai.run("my-agent") as r: result = do_work() # Declare an outcome only from something your code can check. ok = is_valid(result) r.set_outcome(success=ok, outcome="valid" if ok else "invalid")

A run with no model call is recorded but has no cost, and the Traces list can hide it as low-signal (click Show them to see it). For a first run that shows a model, tokens and a cost, use the Quickstart snippet. Leave set_outcome out when your code cannot tell whether the run worked: the run then stays unlabeled, which is not the same as failed.

LangChain

pip install "morse-ai[langchain]" langchain

Automatic chain/agent tracing via a callback handler — no code inside the chain changes. If langchain_core isn’t installed, MorseCallbackHandler is still importable but produces no output (silent no-op). Plain LangChain needs only the langchain extra; the langgraph extra is for LangGraph.

import morse_ai from morse_ai.adapters.langchain import MorseCallbackHandler morse_ai.init() # reads MORSE_API_KEY from your environment handler = MorseCallbackHandler(agent_name="my-agent") # Add the callback to any LangChain chain or agent chain = your_chain.with_config(callbacks=[handler]) chain.invoke({"input": "Hello, world!"})

LangChain callbacks are per call: a tool or model you call directly (my_tool.invoke(...)) outside the configured chain is recorded only if you pass the handler to that call too (my_tool.invoke(args, config={"callbacks": [handler]})).

LangGraph

pip install "morse-ai[langgraph]" langgraph

Per-node span capture plus automatic sub-agent topology from graph structure. When both LangGraph and LangChain are importable, auto-detect installs only the LangGraph adapter so a graph built on LangChain primitives doesn’t get duplicate spans.

init() installs the graph-aware adapter for you, so there is nothing to attach to the graph:

import morse_ai morse_ai.init() # reads MORSE_API_KEY from your environment; also auto-installs the LangGraph adapter result = your_graph.invoke({"messages": [...]})

Async graphs work the same way: await your_graph.ainvoke(...) (or astream) inside a FastAPI handler records one trace per request, with every node, LLM and tool span, without run(). Wrap a request in with morse_ai.run("my-graph"): only when you want to name it yourself.

You do not need MorseCallbackHandler here. If you already pass one in config={"callbacks": [...]}, each LLM, chain and tool call is still recorded once (the SDK de-duplicates by LangChain run id), and on langgraph 1.0.x as well as the latest release.

OpenAI

pip install "morse-ai[openai]" openai

In the Python SDK, wrap() traces both client.chat.completions.create and client.responses.create, for synchronous and asynchronous clients. The module-level install() also traces both APIs by patching their resource classes; Responses API patching is skipped if the installed OpenAI package does not expose those classes.

import morse_ai from morse_ai.adapters.openai import wrap import openai morse_ai.init() # reads MORSE_API_KEY from your environment # Wrap your OpenAI client — Chat Completions and Responses calls are traced client = wrap(openai.OpenAI()) with morse_ai.run("my-agent"): response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "Hello, world!"}], )
⚠️

Known limits of the OpenAI client integration.

  • Streaming is not captured yet. Calls with stream=True pass through untouched, but the model, token counts and cost are missing from the span.
  • Zero-config auto-detect does not instrument a raw OpenAI client. Setting only MORSE_API_KEY is not enough for openai.OpenAI(): call wrap() on the client (above) or install() from morse_ai.adapters.openai. (The OpenAI Agents SDK below is auto-detected.)

OpenAI Agents

pip install "morse-ai[openai_agents]" openai-agents

Runner is patched automatically once you call install() — every agent run and handoff is traced.

import morse_ai from morse_ai.adapters.openai_agents import install from agents import Agent, Runner morse_ai.init() # reads MORSE_API_KEY from your environment install() # patches Runner automatically — no further changes needed agent = Agent(name="my-agent", instructions="You are a helpful assistant.", model="gpt-4o-mini") result = Runner.run_sync(agent, "Hello, world!")

Since SDK 0.7.0, an agent with no model= records the model the SDK actually ran and is priced, a run that raises is recorded as failed without a morse_ai.run() wrapper, and a tool that raises shows as a failed tool span even though the Agents SDK feeds the error back to the model.

OpenAI’s own tracer keeps running. The Agents SDK uploads its own trace of every run to OpenAI, independently of Morse. We deliberately leave that alone — it is your data path, your key, and switching off another vendor’s telemetry inside your process is not something an observability SDK should do behind your back.

If you don’t want the double capture, turn it off yourself:

export OPENAI_AGENTS_DISABLE_TRACING=1

Set it before the first agent run — the Agents SDK reads it once, lazily. Note the name: a plausible-looking OPENAI_TRACING_DISABLED is read by nothing and will leave tracing on.

Anthropic

pip install "morse-ai[anthropic]" anthropic

Wrap your client with wrap() — every messages.create call on the returned instance is traced, including cache/token usage. No monkey-patching of the Anthropic class; only the wrapped instance is instrumented, so an unwrapped client alongside it is unaffected.

import morse_ai from morse_ai.adapters.anthropic import wrap import anthropic morse_ai.init() # reads MORSE_API_KEY from your environment # Wrap your Anthropic client — every messages.create call is traced client = wrap(anthropic.Anthropic()) with morse_ai.run("my-agent"): response = client.messages.create( model="claude-haiku-4-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello, world!"}], )

Since SDK 0.7.0, streaming is traced as well: messages.stream(...) and messages.create(..., stream=True) record one llm span with model, tokens (including cache tokens) and cost when the stream ends. A call that raises records a failed llm span with the requested model. Older SDKs pass streaming calls through with the model, tokens and cost missing.

wrap() is the explicit, recommended path. A module-level install() also exists (used by zero-config auto-detect when you only set MORSE_API_KEY), but wrap() mirrors the TypeScript SDK’s wrapAnthropic() and has no global state.

Claude Agent SDK

pip install morse-ai claude-agent-sdk

claude_agent_sdk.query is patched automatically once you call install() — existing code works unchanged. The Claude Agent SDK runs the Claude Code CLI underneath, so it needs the same Anthropic credentials Claude Code does (for example ANTHROPIC_API_KEY).

query() is an async generator: calling it only creates the generator, and nothing runs (no model call, no trace) until you iterate it with async for inside an async function. Run that function with asyncio.run:

import asyncio import claude_agent_sdk import morse_ai from morse_ai.adapters.claude_agent_sdk import install morse_ai.init() # reads MORSE_API_KEY from your environment install() # patches claude_agent_sdk.query automatically async def main() -> None: async for message in claude_agent_sdk.query(prompt="Give three uses for a paperclip."): print(message) asyncio.run(main()) morse_ai.flush() # short scripts: send everything before the process exits

Call it as claude_agent_sdk.query(...), or do from claude_agent_sdk import query only after install() has run: an earlier import keeps the unpatched function and records nothing.

When you do not open a run yourself, the adapter opens one named claude-agent-sdk and records it as successful unless the CLI reports an error. To judge the result with your own check instead, wrap the loop in with morse_ai.run("my-agent") as r: and call r.set_outcome(...) from something your code can verify.

TypeScript

The TypeScript SDK covers the same ground as Python, plus the Vercel AI SDK. Install commands use the published npm name @morsehq-dev/sdk. The snippets use top-level await, which needs an ES module: save them as .mjs (or set "type": "module" in your package.json).

TypeScript SDK (direct)

npm install @morsehq-dev/sdk

Direct instrumentation of any async function via run()/spanAsync(). run() accepts an async callback and closes the trace when it settles. There is no runAsync export: use run() (or runTraced()).

Top-level await needs an ES module: save the file as .mjs (or set "type": "module" in your package.json), or wrap the code in an async function in a CommonJS file. Otherwise node stops with a SyntaxError on the first await.

import * as morse from "@morsehq-dev/sdk"; morse.init(); // reads MORSE_API_KEY from your environment await morse.run({ agentName: "my-agent" }, async (handle) => { const result = await doWork(); // your agent logic here // Declare an outcome only from something your code can check. const ok = isValid(result); handle.setOutcome(ok, ok ? "valid" : "invalid"); }); await morse.flush(); // short scripts: send everything before the process exits

As in Python, a run with no model call has no cost and can be hidden from the default Traces list as low-signal. For a first run that shows a model, tokens and a cost, use the Quickstart snippet.

OpenAI Agents

npm install @morsehq-dev/sdk @openai/agents

Runner is wrapped — agent, llm, tool, handoff and guardrail spans.

import * as morse from "@morsehq-dev/sdk"; import { Runner, Agent } from "@openai/agents"; import { wrapRunnerWithFullInstrumentation } from "@morsehq-dev/sdk/openai-agents"; morse.init({ apiKey: process.env.MORSE_API_KEY }); const { runner, dispose } = wrapRunnerWithFullInstrumentation(new Runner()); const agent = new Agent({ name: "my-agent", model: "gpt-4o" }); await runner.run(agent, "Hello, world!"); dispose();

LangGraph

npm install @morsehq-dev/sdk @langchain/langgraph

One callback handler — agent spans per chain, plus llm and tool spans.

import * as morse from "@morsehq-dev/sdk"; import { MorseCallbackHandler } from "@morsehq-dev/sdk/langgraph"; morse.init({ apiKey: process.env.MORSE_API_KEY }); // Wrap the invocation in run(): outside an open trace the handler has nowhere to record the // spans and logs "called with no active trace". const handler = new MorseCallbackHandler(); await morse.run({ agentName: "my-graph" }, async () => { const result = await graph.invoke(input, { callbacks: [handler] }); });

LangChain

npm install @morsehq-dev/sdk @langchain/core

Chains, LLMs, tools and retrievers outside a LangGraph graph.

import * as morse from "@morsehq-dev/sdk"; import { MorseCallbackHandler } from "@morsehq-dev/sdk/langchain"; morse.init({ apiKey: process.env.MORSE_API_KEY }); // Unlike the langgraph adapter this one opens no trace of its own — // wrap the invocation so the spans have a parent. const handler = new MorseCallbackHandler({ agentName: "my-agent" }); await morse.run({ agentName: "my-agent" }, async () => { await chain.invoke(input, { callbacks: [handler] }); });

Anthropic

npm install @morsehq-dev/sdk @anthropic-ai/sdk

Every messages.create call on the wrapped client is traced. Save the file as .mjs (top-level await needs an ES module) and set ANTHROPIC_API_KEY:

import * as morse from "@morsehq-dev/sdk"; import Anthropic from "@anthropic-ai/sdk"; import { wrapAnthropic } from "@morsehq-dev/sdk/anthropic"; morse.init(); // reads MORSE_API_KEY from your environment const client = wrapAnthropic(new Anthropic()); // Each messages.create call on the wrapped client emits an llm span inside the run. await morse.run({ agentName: "my-agent" }, async () => { await client.messages.create({ model: "claude-haiku-4-5", max_tokens: 200, messages: [{ role: "user", content: "Hello, world!" }], }); }); await morse.flush(); // short scripts: send everything before the process exits

Since SDK 0.7.0, messages.create({ stream: true }) and messages.stream(...) are traced too: the span gets model, tokens, cache tokens and cost when the stream ends.

OpenAI

npm install @morsehq-dev/sdk openai

wrapOpenAI wraps chat.completions.create on the client, the same way wrapAnthropic wraps messages.create. The TypeScript wrapper covers Chat Completions only; Python’s wrap() also covers responses.create.

import * as morse from "@morsehq-dev/sdk"; import OpenAI from "openai"; import { wrapOpenAI } from "@morsehq-dev/sdk/openai"; morse.init(); // reads MORSE_API_KEY from your environment const client = wrapOpenAI(new OpenAI()); await morse.run({ agentName: "my-agent" }, async () => { await client.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "Hello, world!" }], }); }); await morse.flush(); // short scripts: send everything before the process exits

Claude Agent SDK

npm install @morsehq-dev/sdk @anthropic-ai/claude-agent-sdk

Agent, llm, tool, subagent-spawn and hook spans. Wrap the package’s top-level query and call the returned function in its place — the result is still a Query, with interrupt() and the other control methods forwarded through.

import * as morse from "@morsehq-dev/sdk"; import { query } from "@anthropic-ai/claude-agent-sdk"; import { wrapClaudeAgentQuery } from "@morsehq-dev/sdk/anthropic-agent-sdk"; morse.init({ apiKey: process.env.MORSE_API_KEY }); const tracedQuery = wrapClaudeAgentQuery(query); for await (const message of tracedQuery({ prompt: "Hello, world!" })) { // your code unchanged — spans flow to Morse as a side-effect }

The TypeScript and Python packages have different APIs. In TypeScript you wrap the top-level query({ prompt, options }) with wrapClaudeAgentQuery; there is no ClaudeSDKClient class to construct. In Python you call install() once and then iterate claude_agent_sdk.query(prompt=...) in an async function. Use each language’s own snippet — the two are not interchangeable.

Vercel AI SDK

npm install @morsehq-dev/sdk ai

One agent span per streamText / generateText / generateObject call.

import * as morse from "@morsehq-dev/sdk"; import { createTracedVercelAI } from "@morsehq-dev/sdk/vercel-ai"; morse.init({ apiKey: process.env.MORSE_API_KEY }); // "ai" ships pure ESM with non-configurable exports, so there is nothing to // monkey-patch — call these traced replacements instead of ai's own. const { streamText, generateText, generateObject } = await createTracedVercelAI();

Keep your AI SDK calls: use its own telemetry

morseTelemetry() is in @morsehq-dev/sdk 0.7.0 and later. On an older version, send the AI SDK’s spans with no Morse SDK at all: see the Vercel AI SDK block under Other frameworks via OpenTelemetry.

Prefer to keep importing generateText and streamText from ai? Send the AI SDK’s own OpenTelemetry spans to Morse instead. In ai 7 the spans come from an integration in @ai-sdk/otel, so install it and pass the class in:

npm install @morsehq-dev/sdk ai @ai-sdk/otel
import { generateText } from "ai"; import { LegacyOpenTelemetry } from "@ai-sdk/otel"; import * as morse from "@morsehq-dev/sdk"; import { morseTelemetry } from "@morsehq-dev/sdk/vercel-ai"; morse.init({ apiKey: process.env.MORSE_API_KEY }); await generateText({ model, prompt, telemetry: morseTelemetry({ integration: LegacyOpenTelemetry, // or OpenTelemetry for GenAI span names functionId: "support-agent", // names the agent threadId, // one thread per conversation userId, }), });
  • The spans go through the SDK’s own exporter, sampling and redaction. Your own OpenTelemetry setup is not touched and does not see them.
  • Call morse.init() first. Without it, morseTelemetry() turns telemetry off for that call and prints one warning. It never throws.
  • Inside morse.run(), the AI SDK spans join that trace.
  • Use this or createTracedVercelAI() for a given call, not both: both would record it.
  • On ai 6 or older, pass morseTracer() as experimental_telemetry.tracer.

Other frameworks via OpenTelemetry

Frameworks without a Morse SDK adapter reach Morse over OTLP, through the framework’s own OpenTelemetry output or a community instrumentor. Every row below was checked against a real capture from the real library, not a mock.

Two tiers:

  • Verdict-ready: the framework’s spans carry everything Morse needs to price, thread and judge a run once you set an outcome: typed spans, model, tokens and cost, input and output, and a thread id. No framework declares whether a run succeeded, so you add morse.outcome.success yourself. See Where to set the outcome when a framework owns the spans.
  • Traces: the trace reaches Morse, but spend or a key field is missing or unreliable. You can read the run; Morse cannot yet price it or group it into a thread correctly.
FrameworkVersion testedPathTierKnown gaps
CrewAI1.15.27OTLP via OpenInferenceVerdict-readyNone found in the capture.
CrewAI1.15.27OTLP via OpenLLMetryTracesNo tool span, so a tool call is visible only inside the next model call’s input. No session id, so no thread.
smolagents1.26.0OTLP via OpenInferenceVerdict-readyThe agent span reports the sum of its model calls. Morse records it and never adds it to the total.
LlamaIndex0.14.25OTLP via OpenInferenceVerdict-readyNo agent span type (agent and workflow spans show as custom). Embedding calls report no tokens, so embedding spend is not counted.
LlamaIndex0.14.25OTLP via OpenLLMetryTracesNo agent, tool or retriever spans (all show as custom). The session id needs Traceloop SDK association properties. Embeddings are not priced.
DSPy3.4.1OTLP via OpenInferenceVerdict-readyNo agent span (ReAct shows as custom). finish counts as a tool call. Each model call produces two model spans; Morse counts only the one that reports usage.
Agno3.1.3OTLP via OpenInferenceVerdict-readyAgent configuration and tool schemas are kept as plain metadata.
Haystack 2.x2.31OTLP via OpenInferenceVerdict-readyNo tool span (tool calls show as custom). No agent span type.
Haystack 3.x3.3.0OTLP via OpenInferenceTracesThe instrumentor only emits pipeline and agent spans: no model, tokens or cost. Use the native route below.
Haystack 3.x3.3.0OTLP native (opentelemetry-haystack 1.0.0)TracesNo session id. The model call’s input is stored as raw text, not structured messages. A generator called directly by a pipeline gets no model span on this route; use OpenInference for that.
Strands Agents1.60.0OTLP native GenAIVerdict-readyNo session id: set morse.thread_id yourself.
Google ADK2.11.0OTLP native GenAIVerdict-readyPrompt and response are captured only when OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true.
pydantic-ai2.55.0OTLP native GenAIVerdict-readyThe run-level span reports the sum of its model calls. Morse records it and never adds it to the total.
Mastra (TypeScript)1.76.0OTLP native GenAI (@mastra/otel-exporter)Verdict-readyModel-call spans carry no input messages. The prompt is on the parent step span.
Vercel AI SDK (TS)6.0.314OTLP native ai.* (experimental_telemetry)Verdict-readyThe tool span keeps the name ai.toolCall; the tool’s name is in its details. Thread and user come only from metadata you set. Embedding calls are counted but not priced.
Vercel AI SDK (TS)7.0.150OTLP @ai-sdk/otel LegacyOpenTelemetryVerdict-readySame as 6.x. Thread and user come only from runtimeContext you include (7.x has no metadata).
Vercel AI SDK (TS)7.0.150OTLP @ai-sdk/otel OpenTelemetry (GenAI)Verdict-readyCached input tokens are not read yet, so a cached prompt is priced as uncached. Thread and user come only from runtimeContext you include.
AutoGen0.7.5OTLP native GenAITracesAutoGen emits no model-call span: no model, tokens, cost or prompt, and no thread id. Pair it with openinference-instrumentation-openai for spend.

“Version tested” is the framework version in the capture, not a supported range. Versions in between are expected to work but are not verified.

Errors

Error status is mapped from the OpenTelemetry span status on every route: a span that ends with status ERROR shows as failed in Morse.

Verified on real captures where a tool raises: CrewAI (OpenInference) and pydantic-ai mark the tool span failed with the exception message, and the trace is failed. LlamaIndex’s FunctionAgent catches tool exceptions and hands them back to the model as text, so on that route a failing tool does not show as an error.

Set up a framework

Shared setup. The same settings apply to every framework. Use the signal-specific endpoint variable, as described on the OTLP page:

export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://api.morsehq.dev/api/v1/otlp/v1/traces" export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer%20$MORSE_API_KEY" export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="http/protobuf" export OTEL_RESOURCE_ATTRIBUTES="service.name=support-agent,service.version=2.4.1,morse.agent_version=2.4.1,deployment.environment.name=production"
  • service.name becomes the agent name. service.version and morse.agent_version set the agent version with no code change.
  • deployment.environment.name is recorded on the trace as what your process reported. It does not change the trace’s environment: that always comes from the API key you send.
  • A Python process needs a tracer provider that exports over OTLP/HTTP. The standard setup reads the variables above:
from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter provider = TracerProvider() provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) trace.set_tracer_provider(provider)
  • A Node.js process needs the same. NodeSDK reads the variables above, including OTEL_RESOURCE_ATTRIBUTES:
npm install @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-proto
// instrumentation.mjs import { NodeSDK } from "@opentelemetry/sdk-node"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter(), // reads OTEL_EXPORTER_OTLP_TRACES_* from the environment }); sdk.start();

Load it before your app imports the framework, for example node --import ./instrumentation.mjs app.mjs.

Already sending these spans to another backend? Add Morse as a second exporter instead: see OTel alongside another backend.

Then open your framework below.

CrewAI

OpenInference (Verdict-ready):

pip install crewai openinference-instrumentation-crewai openinference-instrumentation-openai
from openinference.instrumentation.crewai import CrewAIInstrumentor from openinference.instrumentation.openai import OpenAIInstrumentor CrewAIInstrumentor().instrument() OpenAIInstrumentor().instrument()

Wrap the run in OpenInference’s using_session("<session id>") to get a thread id.

OpenLLMetry (Traces; no tool span, no session id):

pip install crewai opentelemetry-instrumentation-crewai opentelemetry-instrumentation-openai

smolagents

pip install smolagents openinference-instrumentation-smolagents
from openinference.instrumentation.smolagents import SmolagentsInstrumentor SmolagentsInstrumentor().instrument()

Wrap the run in using_session("<session id>") to get a thread id.

LlamaIndex

OpenInference:

pip install llama-index-core openinference-instrumentation-llama-index
from openinference.instrumentation.llama_index import LlamaIndexInstrumentor LlamaIndexInstrumentor().instrument()

OpenLLMetry (the OpenAI instrumentor supplies the model-call spans):

pip install llama-index-core opentelemetry-instrumentation-llamaindex opentelemetry-instrumentation-openai

For a thread id on this route, set the session through Traceloop SDK association properties.

DSPy

pip install dspy openinference-instrumentation-dspy openinference-instrumentation-litellm
from openinference.instrumentation.dspy import DSPyInstrumentor from openinference.instrumentation.litellm import LiteLLMInstrumentor DSPyInstrumentor().instrument() LiteLLMInstrumentor().instrument()

Agno

pip install agno openinference-instrumentation-agno
from openinference.instrumentation.agno import AgnoInstrumentor AgnoInstrumentor().instrument()

Wrap the run in using_session("<session id>") to get a thread id.

Haystack

Haystack 2.x, OpenInference:

pip install "haystack-ai==2.31.*" openinference-instrumentation-haystack
from openinference.instrumentation.haystack import HaystackInstrumentor HaystackInstrumentor().instrument()

Haystack 3.x, native (OpenInference emits pipeline and agent spans only on 3.x):

pip install haystack-ai opentelemetry-haystack

Enable Haystack’s OpenTelemetry tracer with content tracing on, as described by the opentelemetry-haystack package. It sends no session id, so set morse.thread_id yourself, per conversation on a wrapping span (pattern).

Strands Agents

pip install "strands-agents[otel]"

No instrumentor. Strands emits GenAI spans through the global tracer provider, so the shared setup above is enough. It sends no session id, so set morse.thread_id per conversation on a span that wraps the agent call. The pattern is in Where to set the outcome; the same span carries the outcome.

Do not set morse.thread_id in OTEL_RESOURCE_ATTRIBUTES for a service that handles many conversations. A resource attribute applies to every trace the process sends, so every conversation lands in one thread. It is only right for a process that runs a single conversation.

Google ADK

pip install google-adk export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true

No instrumentor. ADK emits GenAI spans through the global tracer provider. Without the content switch above, the run has model and tokens but no prompt or response.

pydantic-ai

pip install "pydantic-ai-slim[openai]"
from pydantic_ai import Agent Agent.instrument_all() # or pass instrument=True to one Agent

Mastra (TypeScript)

npm install @mastra/otel-exporter

Point the @mastra/otel-exporter OTLP/HTTP settings at the endpoint and header from the shared setup above.

Vercel AI SDK (TypeScript)

No Morse SDK needed. Run the shared Node setup above, then turn the AI SDK’s telemetry on.

ai 7 emits spans through an integration from @ai-sdk/otel. Register it once, and carry the thread and user as runtime context:

npm install @ai-sdk/otel
import { generateText, registerTelemetry } from "ai"; import { LegacyOpenTelemetry } from "@ai-sdk/otel"; registerTelemetry(new LegacyOpenTelemetry()); // or new OpenTelemetry({ runtimeContext: true }) await generateText({ model, prompt, telemetry: { isEnabled: true, functionId: "support-agent", // names the agent includeRuntimeContext: { thread_id: true, user_id: true }, }, runtimeContext: { thread_id: conversationId, user_id: userId }, });

ai 6 and older emit the spans themselves:

await generateText({ model, prompt, experimental_telemetry: { isEnabled: true, functionId: "support-agent", metadata: { thread_id: conversationId, user_id: userId }, }, });

Leave recordInputs and recordOutputs at their default (on), or the trace has no prompt or response.

AutoGen

pip install autogen-agentchat openinference-instrumentation-openai

AutoGen’s own tracing gives agent and tool spans. Add the OpenAI instrumentor so the model calls, and their spend, appear:

from openinference.instrumentation.openai import OpenAIInstrumentor OpenAIInstrumentor().instrument()

Where to set the outcome when a framework owns the spans

The framework creates the spans, so there is no span of yours to put morse.outcome.success on. Make one: wrap the framework call in your own span, and set the outcome on it once you know whether the run worked. Spans the framework starts inside your span join the same trace, and Morse reads the outcome from any span of the trace.

Python, with CrewAI as the example:

from opentelemetry import trace tracer = trace.get_tracer("support-crew") with tracer.start_as_current_span("support-crew") as span: span.set_attribute("morse.thread_id", conversation_id) # one thread per conversation try: result = crew.kickoff(inputs={"ticket": ticket_text}) except Exception: span.set_attribute("morse.outcome.success", False) raise span.set_attribute("morse.outcome.success", ticket_was_resolved(result))

TypeScript, with a Mastra agent as the example:

import { trace } from "@opentelemetry/api"; const tracer = trace.getTracer("support-agent"); const result = await tracer.startActiveSpan("support-agent", async (span) => { span.setAttribute("morse.thread_id", conversationId); // one thread per conversation try { const output = await agent.generate(prompt); span.setAttribute("morse.outcome.success", ticketWasResolved(output)); return output; } catch (err) { span.setAttribute("morse.outcome.success", false); throw err; } finally { span.end(); } });
  • ticket_was_resolved() and ticketWasResolved() stand for your own check. Only you know what success means for the run.
  • Your span becomes the trace’s root, so its name can become the agent name. Set morse.agent on it to name the agent explicitly. See Naming the agent.
  • Check in Traces that your span and the framework’s spans are one trace. A framework that does not start its spans in the active OpenTelemetry context puts them in a separate trace, and the outcome lands on the wrong one. Then set the outcome on a span of the framework’s own trace, or label the run later with the Record outcomes API.

Before you go live

  • Message content. Some libraries do not capture prompts and responses unless you ask. Set OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true for Google ADK. OpenLLMetry captures content by default (TRACELOOP_TRACE_CONTENT defaults to on). OpenInference captures content by default too; the OPENINFERENCE_HIDE_* switches (for example OPENINFERENCE_HIDE_INPUTS) turn it off. Whatever arrives is stored encrypted.
  • Run only one instrumentor per LLM client, or calls are counted twice. If a framework instrumentor and a provider instrumentor (for example OpenAI) both wrap the same model call, Morse sees two spans and prices both. Morse does not de-duplicate them.
  • Already sending spans to another backend? Add Morse as a second exporter, through an OpenTelemetry Collector or in your app, so the app is instrumented once. See OTel alongside another backend.
  • Check it worked. Run the agent once, then open Traces. The run should appear with a model, token counts and a cost on its model spans, and with the outcome you set. If the trace is there but spend is empty, find your framework in the table above: it is a Traces-tier gap, not a setup error. If the outcome is missing, the run is left out of success rate and cost per confirmed outcome: see Where to set the outcome. If nothing arrived, see Nothing showed up?.

Retriever documents (beta)

The LangChain and LangGraph adapters (Python) and the LangChain adapter (TypeScript) record every retriever call as a retriever span. In the span’s output, documents lists up to 10 returned chunks:

{ "docs_count": 12, "documents": [ { "rank": 1, "id": "kb-42", "content": "Seat changes are free up to…", "score": 0.91, "source": "kb/seats.md" } ] }
  • rank is the order the retriever returned the chunk, starting at 1.
  • id is the document id from the retriever (id or metadata.id); when there is none, Morse’s SDK derives a stable id from the content.
  • content is truncated to 2 KB. A truncated chunk also carries "truncated": true.
  • score is read from metadata.score or metadata.relevance_score, otherwise null.

Chunk content is stored with the rest of the span payload: redacted, then encrypted at rest. This field is @beta and additive — docs_count, top_score, sources and content_preview are unchanged. Morse uses it to score retrieval quality on a sample of traces.

  • Overview — auto-detect, adapter priority, and the silent-failure guarantee.
  • Installation — create an API key and the minimal init() call.
  • set_context() — attach cost-attribution dimensions to a trace.
Last updated on