Framework guides
Agent frameworks
Send agents built on the Claude Agent SDK, the OpenAI Agents SDK, Google ADK, Mastra or an OpenInference-instrumented framework to Otis.
Otis records the work of an agent built on one of these frameworks as , one span for each operation. For the Claude Agent SDK, the Otis SDK records the spans. The OpenAI Agents SDK, Google's Agent Development Kit (ADK), Mastra and the instrumentations that follow the OpenInference conventions record their own OpenTelemetry spans, which Otis reads when they arrive over OTLP, the OpenTelemetry protocol. Those frameworks need no Otis SDK. OpenTelemetry is the reference for the endpoint and for every attribute Otis reads.
Agent runs from a framework
An is one execution of an agent, from the input it receives to its final output, with every AI call, tool call and subagent in between. An is one request to an AI model, with its response. A is a call the model asked for, to a function, an API or an MCP tool. Otis recognizes the agent, AI call and tool call spans that each of these frameworks produces, and groups each agent's spans into an agent run. It passes over the step, turn and chain spans that frameworks place between an agent and its tools.
Otis then uses each agent run in three places:
- Tasks. The agent runs in a session join the tasks that Otis detects there.
- The Otis MCP server. A coding agent reads an agent run as numbered steps: each user message, AI call, tool call and subagent, with its status, model, tokens and text. MCP server describes the tools.
- The Otis app. A task's page lists the spans of the agent runs it holds, tool calls included.
Agent runs covers what Otis records about each agent run, including its result.
Claude Agent SDK
Wrap the Claude Agent SDK module with otis.wrap(), and call query() from the module it returns:
import * as agentSdk from "@anthropic-ai/claude-agent-sdk";
import { otis } from "./otis";
const { query } = otis.wrap(agentSdk, { context: { userId, sessionId } });
for await (const message of query({ prompt, options })) {
// ...
}- Identity. Pass the user and session IDs in the wrap's
context, or set them around the call withotis.withContext(). Every span of the agent run carries them. - Agent runs. Each
query()call is one agent run. With a streaming-input prompt, each turn of the conversation is an agent run of its own. - What Otis records. Each AI call, each tool call with its input and result, each subagent's calls under the tool call that started it, and each point where the agent's context was compacted. The prompt, the answer, the usage and cost (subagents included) and the system prompt you pass are on the agent run's own span.
- Reasoning. Otis records no reasoning text and no reasoning-token count for the Claude Agent SDK.
Claude Agent SDK in the tracing guide lists every span and attribute.
Setup for OpenTelemetry frameworks
Three settings apply to every framework below, each of which sends its own OpenTelemetry spans:
- Export to Otis. Point an OTLP HTTP exporter at Otis, as Point an OTLP exporter at Otis ingest describes.
- Put a session ID and a user ID on every span. A is all the activity that shares one session ID. Otis builds tasks and agent runs from the spans of a session, and builds neither from a span without a session ID. Identifying sessions and users shows a span processor that sets both IDs.
- Turn on content capture where it is off. Without it, Otis records each agent run's structure, timing and usage, but not its messages or tool arguments.
OpenAI Agents SDK
The OpenAI Agents SDK sends its built-in traces to OpenAI. To send them to Otis, add OpenInference's instrumentation for the SDK: openinference-instrumentation-openai-agents in Python, or @arizeai/openinference-instrumentation-openai-agents in JavaScript. In Python:
from agents import Runner
from openinference.instrumentation import using_attributes
from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor
# `provider` exports to Otis, as in Point an OTLP exporter at Otis ingest.
OpenAIAgentsInstrumentor().instrument(tracer_provider=provider)
with using_attributes(session_id=current_session_id, user_id=current_user_id):
result = await Runner.run(agent, user_input)In JavaScript, create new OpenAIAgentsInstrumentation({ tracerProvider: provider }) and register it with manuallyInstrument(), passing it the @openai/agents module, as the package's README shows. Set session.id and user.id on every span with a span processor.
- Identity. In Python,
using_attributesputssession.idanduser.idon every span created inside it, including the agent, AI call and tool call spans. - Content. OpenInference records messages, tool arguments and tool results by default.
- Traces sent to OpenAI. By default the instrumentation replaces the SDK's own exporter, so traces stop reaching OpenAI. To keep sending them there as well, pass
exclusive_processor=Falsein Python orexclusiveProcessor: falsein JavaScript. - What Otis reads. Each AI call's messages, model and tokens, each tool call's arguments and result, and each agent's name.
Google ADK
Register a tracer provider that exports to Otis as your app's global provider before your agent runs (trace.set_tracer_provider in Python). Add the identity span processor from Identifying sessions and users to it.
- Identity. ADK records its session ID as a conversation ID, which Otis reads as a chat ID, and most of its spans carry no user ID. The span processor sets
session.idanduser.idon every span. - Content in ADK Python 2.13 and later. ADK writes the OpenTelemetry GenAI attributes and leaves content capture off. Set
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY. The valuetruesends content to log records, which Otis doesn't receive. - Content in the legacy telemetry. ADK Java and TypeScript, and ADK Python before 2.13 or with
ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN=1, write content under ADK's own attributes. ADK Java writes it only whenADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS=trueis set. ADK Python and TypeScript write it unless the variable isfalse. - Usage. ADK Python's legacy telemetry records each AI call on two spans. Otis counts the call once. Don't also install
opentelemetry-instrumentation-google-genaiwith this telemetry: its spans repeat the same AI calls, and Otis then counts each one twice.
Mastra
Mastra sends spans through @mastra/otel-exporter, or through @mastra/otel-bridge when your app already has an OpenTelemetry setup. Otis reads Mastra's user and session metadata from these two packages only. Configure the exporter with the Otis endpoint, and pass the IDs in each call's tracing metadata:
import { Mastra } from "@mastra/core";
import { Observability } from "@mastra/observability";
import { OtelExporter } from "@mastra/otel-exporter";
export const mastra = new Mastra({
agents: { support: supportAgent },
observability: new Observability({
configs: {
otel: {
serviceName: "my-app",
exporters: [
new OtelExporter({
provider: {
custom: {
endpoint: "https://ingest.runotis.com/v1/traces",
protocol: "http/json",
headers: { Authorization: `Bearer ${process.env.OTIS_API_KEY}` },
},
},
}),
],
},
},
}),
});
await mastra.getAgent("support").generate(message, {
tracingOptions: { metadata: { userId, sessionId } },
});- Identity. Every span of the agent run inherits
userIdandsessionId, and Otis reads them as the user and session IDs. Put the user's ID inuserId, which may differ from the memoryresourceyou pass to the agent. The memory thread arrives as the chat ID. To set the IDs once for every call, userequestContextKeysin the observability configuration. - Content. Mastra records messages, each step's text and tool calls by default.
- What Otis reads. Each step's text and tool calls, each tool call's arguments and result, and the agent run's input and reply.
OpenInference
OpenInference is a set of conventions for recording AI calls, tool calls and agents as OpenTelemetry spans, with instrumentations for many frameworks and AI clients, such as LangChain and LlamaIndex. The OpenAI Agents SDK route above is one of them. Otis reads the AI call, tool call and agent spans of every OpenInference instrumentation in the same way. In Python, set the session and user IDs with using_attributes.
If you also filter spans with beforeSend in the Otis SDK, don't apply isAISpan() to OpenInference spans. OpenInference's tool and agent spans don't match it, and without them Otis can't build their agent runs.
Checklist
- With the Claude Agent SDK,
querycomes from the moduleotis.wrap()returns, with the user and session IDs in its context - With any other framework, spans reach
https://ingest.runotis.com/v1/tracesover OTLP HTTP, with your Ingest Key in theAuthorizationheader - Every OpenTelemetry span carries
session.idanduser.id(with Mastra,userIdandsessionIdin tracing metadata) - Content capture is on, and set to span mode where the instrumentation asks for one
- A test request shows up as an agent run, with its tool calls, when a coding agent reads it over the Otis MCP server
Next steps
- Agent traces — every span the Claude Agent SDK wrapper records
- OpenTelemetry — the endpoint and every attribute Otis reads
- MCP server — reading an agent run as numbered steps
- Privacy — PII redaction and identifier hashing