- TypeScript 83.8%
- Python 10.1%
- Java 3.6%
- Shell 1.9%
- JavaScript 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .agents/skills/check-models | ||
| .claude | ||
| .github/workflows | ||
| academy | ||
| ax | ||
| evals | ||
| memory | ||
| no-observability | ||
| phoenix | ||
| product-images | ||
| tests | ||
| .gitignore | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| README.md | ||
Project Rosetta Stone
The same AI agent, built with different frameworks, instrumented with different observability platforms.
If you're trying to add LLM observability to your own agent, this repo shows you exactly which files to touch — for whichever framework you use, and whether you ship to Arize Phoenix or Arize AX. Every framework is built three times:
no-observability/<framework>/— baseline, zero instrumentationphoenix/<framework>/— same agent, instrumented for Arize Phoenix Cloudax/<framework>/— same agent, instrumented for Arize AX
Read the no-obs version to see the bare agent. Diff the phoenix or ax version against it to see the exact instrumentation footprint. Run it locally to watch traces land.
Supported frameworks
| Framework | Python | TypeScript | Java |
|---|---|---|---|
| Agno | ✅ | — | — |
| Arconia | — | — | ✅ |
| AutoGen AgentChat | ✅ | — | — |
| AWS Strands | ✅ | — | — |
| BeeAI | ✅ | ✅ | — |
| Claude Agent SDK | ✅ | — | — |
| Claude Agent SDK (TypeScript) | — | ✅ | — |
| CrewAI | ✅ | — | — |
| DSPy | ✅ | — | — |
| Google ADK | ✅ | — | — |
| Haystack | ✅ | — | — |
| LangChain / LangGraph | ✅ | ✅ | — |
| LangChain4j | — | — | ✅ |
| LlamaIndex | ✅ | — | — |
| LlamaIndex Workflows | ✅ | — | — |
| Mastra | — | ✅ | — |
| Microsoft Agent Framework | ✅ | — | — |
| Microsoft Semantic Kernel | ✅ | — | — |
| OpenAI Agents SDK | ✅ | — | — |
| OpenAI Agents SDK (TypeScript) | — | ✅ | — |
| OpenAI Realtime API (Voice) | ✅ | — | — |
| OpenInference Annotation Tracing | — | — | ✅ |
| Pydantic AI | ✅ | — | — |
| Smolagents | ✅ | — | — |
| Spring AI | — | — | ✅ |
| Vercel AI SDK | — | ✅ | — |
| Vercel Eve | — | ✅ | — |
The agent — Wonder Toys
Every directory in the repo runs the same chat-to-purchase toy-store assistant, powered by Claude (Anthropic) for most tiers and OpenAI for the voice and agents-SDK tiers. It can:
- Search a 200-product inventory via semantic vector search (ChromaDB) with keyword fallback
- Browse products with rich markdown cards — images, prices, ratings, age ranges, descriptions
- Purchase products with shipping details (credit card assumed on file)
- Track order status by order ID or natural-language product search
- Cancel orders that haven't been delivered yet
The UI includes a home page with featured products and category chips, product detail pages, a shopping cart, and a streaming chat interface that renders product cards inline. The openai-voice tier adds a text/voice toggle that streams audio in and out via the OpenAI Realtime API.
Repo layout
rosetta/
├── no-observability/ No instrumentation (baseline)
│ ├── agno-py/ Agno (Python + Next.js)
│ ├── annotation-java/ OpenInference Annotation Tracing (Java + Next.js)
│ ├── arconia-java/ Arconia (Java + Next.js)
│ ├── autogen-py/ AutoGen AgentChat (Python + Next.js)
│ ├── aws-strands-py/ AWS Strands (Python + Next.js)
│ ├── beeai-py/ BeeAI (Python + Next.js)
│ ├── beeai-ts/ BeeAI framework (TypeScript)
│ ├── claude-agent-sdk-py/ Claude Agent SDK (Python + Next.js)
│ ├── claude-agent-sdk-ts/ Claude Agent SDK (TypeScript)
│ ├── crewai-py/ CrewAI (Python + Next.js)
│ ├── dspy-py/ DSPy (Python + Next.js)
│ ├── google-adk-py/ Google ADK (Python + Next.js)
│ ├── haystack-py/ Haystack (Python + Next.js)
│ ├── langchain-js/ LangChain.js / LangGraph (TypeScript)
│ ├── langchain-py/ LangChain / LangGraph (Python + Next.js)
│ ├── langchain4j-java/ LangChain4j (Java + Next.js)
│ ├── llamaindex-py/ LlamaIndex (Python + Next.js)
│ ├── llamaindex-workflows-py/ LlamaIndex Workflows (Python + Next.js)
│ ├── mastra/ Mastra framework (TypeScript)
│ ├── microsoft-agent-py/ Microsoft Agent Framework (Python + Next.js)
│ ├── openai-agents-py/ OpenAI Agents SDK (Python + Next.js)
│ ├── openai-agents-ts/ OpenAI Agents SDK (TypeScript)
│ ├── openai-voice/ OpenAI Realtime API + Chat Completions (Python + Next.js)
│ ├── pydantic-ai-py/ Pydantic AI (Python + Next.js)
│ ├── semantic-kernel-py/ Microsoft Semantic Kernel (Python + Next.js)
│ ├── smolagents-py/ Smolagents (Python + Next.js)
│ ├── spring-ai-java/ Spring AI (Java + Next.js)
│ ├── vercel-ai-sdk/ Vercel AI SDK (TypeScript)
│ └── eve/ Vercel Eve (Eve agent + Next.js proxy)
├── phoenix/ Arize Phoenix Cloud instrumentation (same set of frameworks)
├── ax/ Arize AX instrumentation (same set of frameworks)
├── evals/ Shared synthetic requests + eval harness (text + voice)
├── product-images/ 200 AI-generated product images (shared via symlinks)
└── chroma-data/ ChromaDB vector store (gitignored, auto-created)
Every tier × framework directory is a fully functional, self-contained Next.js app. The only differences between observability tiers are the instrumentation setup — agent logic, tools, UI, and data are identical.
Quick start
cd <tier>/<framework> # e.g. phoenix/mastra
cp env.example .env.local # fill in your API keys
npm install
npm run dev # starts ChromaDB + indexes products + runs the app
npm run dev handles everything:
- Creates a Python venv and installs ChromaDB (via uv)
- Starts ChromaDB if not already running
- Indexes all 200 products if the collection is missing
- Starts the dev server (Next.js for JS frameworks; Python backend + Next.js for Python frameworks)
All tiers share the same ChromaDB instance at the repo root, so you only index once.
To skip ChromaDB (search falls back to keyword matching): npm run dev:next.
Prerequisites
- Node.js 20+
- uv (for ChromaDB's Python venv)
- An Anthropic API key (all tiers except
openai-voice) - An OpenAI API key (
openai-voiceonly) - Observability credentials for the phoenix or ax tier you want to run (see further instructions below)
Environment variables
Every agent needs these in .env.local:
| Variable | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY |
All except openai-voice |
Claude API key |
OPENAI_API_KEY |
openai-voice only |
OpenAI key for Realtime + Chat Completions |
BACKEND_SECRET |
Python frameworks | Shared secret for Next.js ↔ Python auth (any string) |
BACKEND_URL |
Python frameworks | Python backend URL (default: http://localhost:8001) |
Phoenix credentials
| Variable | Description |
|---|---|
PHOENIX_COLLECTOR_ENDPOINT |
Phoenix Cloud endpoint (e.g. https://app.phoenix.arize.com/s/your-space) |
PHOENIX_API_KEY |
Phoenix API key from app.phoenix.arize.com |
PHOENIX_PROJECT_NAME |
Project name in Phoenix |
TypeScript frameworks require the full OTLP URL including
/v1/traces. Python frameworks expect just the base URL, asarize-phoenix-otelappends the path automatically.
AX credentials
| Variable | Description |
|---|---|
ARIZE_SPACE_ID |
AX space ID from app.arize.com |
ARIZE_API_KEY |
AX API key |
ARIZE_PROJECT_NAME |
Project name in AX |
See each directory's env.example for the full template.
Phoenix Academy
The Phoenix Academy exercises use Wonder Toys to teach tracing, evaluation, experiments, and release checks. The instrumentation lessons start with no-observability/langchain-py; the companion exercise scripts reuse the backend in phoenix/langchain-py.
Observability tiers
| Tier | What it shows |
|---|---|
| no-observability | Baseline — how the agent works with zero instrumentation overhead |
| phoenix | Arize Phoenix Cloud — open-source observability |
| ax | Arize AX — enterprise observability |
What changes between tiers — by framework
This is the heart of the repo. Below, for each framework, are the only files that differ between no-observability and the instrumented tiers. Everything else (agent logic, tools, UI, vector search, data) is identical.
If you're instrumenting your own app, find the framework you use, read what files change, and copy the pattern.
Mastra
src/mastra/index.ts— observability config in the Mastra constructornext.config.ts—serverExternalPackagesfor observability packagespackage.json— observability dependenciesenv.example— observability environment variables
LangChain.js
src/langchain/agent.ts— observability setup at the top of the file (before LangChain imports)next.config.ts—serverExternalPackagesfor observability packagespackage.json— observability dependenciesenv.example— observability environment variables
Vercel AI SDK
src/instrumentation.ts—registerOTelwith OTLP exporter (new file)src/root-aware-processor.ts— custom span processor that promotes the first AI SDK span to trace root and drops HTTP spans (new file)src/app/api/chat/route.ts— session ID injected into OTel context viacontext.with(setSession(...))src/components/Chat.tsx— session ID generated/rotated and sent asx-session-idrequest headernext.config.ts—serverExternalPackagesfor observability packagespackage.json— observability dependenciesenv.example— observability environment variables
Vercel Eve
Eve is a filesystem-first agent runtime with its own dev server and HTTP channel, so it follows the repo's separate-backend + Next.js-proxy pattern (like the Python tiers) rather than the in-process pattern of the Vercel AI SDK / Mastra tiers. The Eve agent lives in eve-agent/ (an Eve project: agent/agent.ts, agent/instructions.md, agent/tools/, agent/lib/), and the Next.js src/app/api/chat/route.ts proxies to the Eve dev server, translating Eve's NDJSON session stream into the Wonder Toys SSE shape. Observability lives entirely inside the Eve project:
eve-agent/agent/instrumentation.ts— auto-discovered by Eve (root-only slot), runs before agent code.registerOTel(via@vercel/otel) with an OTLP exporter (new file)eve-agent/agent/root-aware-processor.ts—RootAwareOpenInferenceProcessorkeeps OpenInference spans plus Eve'sai.eve.turnworkflow span and promotesai.eve.turnto the trace root, so each turn lands as a single un-orphaned root (new file)eve-agent/package.json— observability dependencies (@arizeai/openinference-vercel,@vercel/otel, OTel packages,lru-cache;@arizeai/openinference-semantic-conventionsfor Phoenix)env.example— observability environment variables
Unlike the Vercel AI SDK tier, userId is threaded into tools via Eve
clientContext(the runtime surfaces it to the model, which passes it into theuserIdtool arguments) rather than an OTel-context session ID. The model isanthropic/claude-sonnet-4.6via the Vercel AI Gateway.
Agno
backend/tracing.py— tracing initialization (new file, imported beforeagno). Uses the standardregister()+AgnoInstrumentor().instrument(tracer_provider=...)pattern. The OpenInference Agno instrumentation auto-emitssession.idanduser.idfrom the values passed toagent.arun(session_id=..., user_id=...)— nousing_session()wrap needed.backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— observability packages (arize-phoenix-otelorarize-otel+openinference-instrumentation-agno)env.example— observability environment variables
AutoGen AgentChat
backend/tracing.py— tracing initialization (new file, imported beforeautogen_agentchat). The instrumentation package isopeninference-instrumentation-autogen-agentchat(AgentChat layer) — notopeninference-instrumentation-autogen(low-level core).backend/agent.py— wrapsagent.run_stream()inusing_session(user_id)+using_user(user_id)because the instrumentor doesn't auto-emitsession.id/user.id. Atry/except ImportErrorfallback keeps no-observability working without anopeninference.instrumentationdependency.backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-autogen-agentchatenv.example— observability environment variables
AutoGen's
FunctionToolrequires plain-stringAnnotated[str, "what this is"]tool descriptions instead of the PydanticAnnotated[..., Field(description=...)]style other Python tiers use, sobackend/tools.pyis rewritten in the same way across all three AutoGen tiers.
AWS Strands
backend/tracing.py— tracing initialization (new file, imported beforestrands). Builds aTracerProviderwith two span processors in order:StrandsAgentsToOpenInferenceProcessor(mutates Strands' native gen_ai-conventioned spans in place into OpenInference shape), then the OTLP exporter (Phoenix or AX). Processor order matters — the OpenInference processor must run before the exporter sees the spans, otherwise gen_ai-only attributes ship.backend/main.py— importsbackend.tracingbefore other backend modules so the global TracerProvider is set before Strands' singleton tracer caches its reference.backend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-strands-agentsenv.example— observability environment variables
backend/agent.pyis shared across all three tiers. The StrandsAgentis built withtrace_attributes={"session.id": user_id, "user.id": user_id}so every span carries the IDs — the Strands OpenInference processor doesn't propagate baggage fromusing_session()to span attributes. The agent loop also wrapsagent.stream_async()inusing_session(user_id)as a belt-and-braces fallback.
BeeAI
backend/tracing.py— tracing initialization (new file, imported beforebeeai_framework). Usesregister()+BeeAIInstrumentor().instrument(tracer_provider=...). The instrumentor subscribes toEmitter.root()and converts BeeAI's internal events into OpenInference spans.session.idis auto-tagged viausing_session(user_id)aroundagent.run()inagent.py.backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-beeaienv.example— observability environment variables
Claude Agent SDK
backend/tracing.py— tracing initialization (new file, imported beforeclaude_agent_sdk). Usesregister()+ClaudeAgentSDKInstrumentor().instrument(tracer_provider=...).session.idis auto-tagged viausing_session(user_id)around theClaudeSDKClientcall inagent.py.backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-claude-agent-sdkenv.example— observability environment variables
backend/agent.pyandbackend/tools.pyare shared across all three tiers. The five Wonder Toys tools are served in-process viacreate_sdk_mcp_server(namespacedmcp__wonder_toys__*);allowed_toolsrestricts the agent to just those andpermission_mode="bypassPermissions"auto-approves them for headless serving. The Claude Agent SDK runs the Claude Code CLI as a subprocess, so theclaudebinary must be on PATH (npm install -g @anthropic-ai/claude-code) andANTHROPIC_API_KEYset. Since SDK MCP tools run in-process, tools read the user id from acurrent_user_idcontext var rather than through the model.
Claude Agent SDK (TypeScript)
src/ai/tracing.ts— new file in observability tiers. Phoenix callsregister({ projectName, url, apiKey, spanProcessors: [...] })from@arizeai/phoenix-otel; AX builds aNodeTracerProviderby hand — both wrapping the localOpenInferenceFilteredBatchSpanProcessor(src/ai/oi-filter-processor.ts, drops any span without anopeninference.span.kind) around anOTLPTraceExporter, and neither callsprovider.register(). It then runsnew ClaudeAgentSDKInstrumentation({ tracerProvider }).manuallyInstrument(sdk)and keeps the returned patched module — the SDK is native ESM whose exports are read-only, somanuallyInstrument()returns a patched copy rather than mutating in place, and the chat route must call that patchedquery()(exposed viagetInstrumentedQuery()). It also registers anAsyncLocalStorageContextManagerso implicit OTel context (context.with/context.active) works without a global provider. Tracing state is stashed onglobalThisso a single init survives Next.js dev HMR (a secondmanuallyInstrumentwould otherwise hit the instrumentor's process-global patch guard and hand back the un-patched module).src/ai/agent.ts— the instrumentor emits AGENT + TOOL spans only (the SDK makes its model calls inside the spawned Claude Code subprocess, invisible to an in-process instrumentor), sostreamAgentsynthesizes per-generationLLMspans from the assistant messages the SDK streams, with token counts read from each message'susage(and the finalresultmessage). It opens aCHAINparent span, takes the query iterator inside that span's context (so the instrumentor's AGENT span nests under it rather than becoming a separate root trace), and parents the LLM spans on the same context — yielding one coherent trace:CHAIN → { AGENT → TOOL…, LLM… }withsession.idset to the user id on the root. This whole block is guarded so it is inert if tracing isn't initialised.next.config.ts— adds@anthropic-ai/claude-agent-sdk+@arizeai/openinference-instrumentation-claude-agent-sdk+ the whole@opentelemetry/*stack (+@arizeai/phoenix-otelfor Phoenix) toserverExternalPackages, so the app code and the instrumentor share a single@opentelemetry/apiinstance and its one globalContextManager(a second bundled copy would make the CHAIN → AGENT nesting silently break).package.json— adds the OpenInference instrumentor + OTel SDK packages.env.example— observability environment variables.
src/ai/agent.ts,src/ai/context.ts,src/ai/tools/*, andsrc/app/api/chat/route.tsare otherwise identical across all three tiers. The five Wonder Toys tools are served in-process viacreateSdkMcpServer(namespacedmcp__wonder_toys__*);allowedToolsrestricts the agent to just those andpermissionMode: "bypassPermissions"+settingSources: []isolate the run. The TypeScript SDK has no persistentClaudeSDKClient— conversation memory is kept by retaining the SDKsession_idper user and passingoptions.resumeon the next turn (reset when the history shrinks on a browser refresh). Tools read the user id from anAsyncLocalStorage(the contextvar equivalent) rather than through the model. Tracing initialises lazily on the first chat request (no rootinstrumentation.ts) so the patchedquery()and the tracer live in the same module instance thatstreamAgentruns in.
CrewAI
backend/tracing.py— tracing initialization (new file, imported beforecrewai)backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-crewaienv.example— observability environment variables
DSPy
backend/tracing.py— tracing initialization (new file, imported beforedspy). Usesregister()plus bothDSPyInstrumentorandLiteLLMInstrumentor— DSPy is built on LiteLLM, so installing both gives complete coverage from agent down to each LLM call.backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-dspy+openinference-instrumentation-litellmenv.example— observability environment variables- The DSPy OpenInference instrumentor doesn't emit
session.idautomatically, soagent.pywraps the streaming call inwith using_session(user_id):across all three tiers (no-obs falls back to a no-op contextmanager whenopeninferenceisn't installed).
Google ADK
backend/tracing.py— tracing initialization (new file, imported beforegoogle.adk). Usesregister()+GoogleADKInstrumentor().instrument(tracer_provider=...). Auto-emitssession.idfrom the ADK Runner'ssession_id— nousing_session()wrap needed.backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-google-adkenv.example— observability environment variables
Haystack
backend/tracing.py— tracing initialization (new file, imported beforehaystack). Usesregister()+HaystackInstrumentor().instrument(tracer_provider=...).backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-haystackenv.example— observability environment variables
backend/agent.pyis shared across all three tiers and wrapsagent.run_asyncinusing_session(user_id)because the Haystack instrumentor doesn't emit it on its own. No-obs falls back tonullcontext()whenopeninference.instrumentationisn't installed.
LangChain Python
backend/tracing.py— tracing initialization (new file, imported before LangChain)backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-langchainenv.example— observability environment variables
LlamaIndex Python
backend/tracing.py— tracing initialization (new file, imported before LlamaIndex)backend/agent.py— manual root span + OTel context management for proper trace boundaries (seephoenix/llamaindex-py/README.mdfor the three workarounds)backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-llama-indexenv.example— observability environment variables
LlamaIndex Workflows
backend/tracing.py— tracing initialization (new file, imported before LlamaIndex). The sameLlamaIndexInstrumentorthat coversllamaindex-pyalso covers Workflow /@stepmachinery, so workflow steps surface as CHAIN spans (WonderToysWorkflow.prepare_chat_history,.handle_llm_input,.handle_tool_calls) withAnthropic.astream_chatas nested LLM spans andFunctionTool.acallas TOOL spans.backend/agent.py— manualagentroot span taggedopeninference.span.kind=AGENTwithinput.value,output.value,session.id,user.id. Same three LlamaIndex-tracing workarounds as thellamaindex-pytier.backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-llama-indexenv.example— observability environment variables
Microsoft Agent Framework
backend/tracing.py— tracing initialization (new file, imported beforeagent_framework). Manually constructs aTracerProviderwithResource.create({PROJECT_NAME: …})plusAgentFrameworkToOpenInferenceProcessorto reshape MAF's GenAI-convention spans into OpenInference. Theregister()shortcut doesn't route MAF spans to the configured project — seephoenix/microsoft-agent-py/backend/tracing.pyfor the working pattern.backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-agent-frameworkenv.example— observability environment variables
Microsoft Semantic Kernel
backend/tracing.py— tracing initialization (new file, imported beforesemantic_kernel). Usesopeninference-instrumentation-anthropicdirectly against the global TracerProvider set up byphoenix.otel.register/arize.otel.register. The Arize docs' suggested OpenLIT bridge is not used — OpenLIT has nosemantic_kernelinstrumentor, and its anthropic instrumentor wraps streaming responses in a class that breaks SK'sisinstance(response, AsyncStream)introspection.backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-anthropicenv.example— observability environment variables
backend/agent.pyis shared across all three tiers and wrapsChatCompletionAgent.invoke_streaminusing_session(user_id). SK emits its own native OTelagent/AutoFunctionInvocationLoop/execute_toolspans automatically, so the trace tree gets AGENT + CHAIN + TOOL + LLM kinds without manual wrapping. Note: SK's Anthropic connector parser rejectslist[T]tool args from Claude (FunctionExecutionException: expected to be parsed to list[str] but is not), sobackend/tools.pydeclares list args as comma-separated strings and splits them inside each tool.
OpenAI Agents SDK
backend/tracing.py— tracing initialization (new file, imported beforeagents). Uses the standardregister()+OpenAIAgentsInstrumentor().instrument(tracer_provider=...)pattern. Phoenix tier quirk:register()must be called withprotocol="http/protobuf"— the defaultgrpcprotocol mis-routes the configuredPHOENIX_COLLECTOR_ENDPOINT=http://localhost:6006/v1/tracesto the gRPC port 4317 and traces never land.backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-openai-agentsenv.example— observability environment variables
backend/agent.pyis shared across all three tiers and is the only Python tier whose LLM is not Anthropic Claude — it uses OpenAI's native Responses API viamodel="gpt-5.4-mini", because the OpenAI Agents SDK is OpenAI's own SDK and the LiteLLM-to-Anthropic adapter bypasses the SDK's native tracing hooks. The agent loop wrapsRunner.run_streamed()inusing_session(user_id)sosession.idlands on spans (the OpenInference instrumentor for openai-agents does not emit it automatically). Observability tiers callflush_traces()in the streaming generator'sfinallyblock — without it, spans buffer in the trace processor across FastAPI requests and never reach the OTel BatchSpanProcessor. The no-observability tier falls back to anullcontext()shim whenopeninference.instrumentationisn't installed.
OpenAI Agents SDK (TypeScript)
src/ai/tracing.ts— new file in observability tiers. Phoenix callsregister({ projectName, url, apiKey, spanProcessors: [...] })from@arizeai/phoenix-otel, passing a localOpenInferenceFilteredBatchSpanProcessor(a ~15-line subclass of OTel's standardBatchSpanProcessordefined insrc/ai/oi-filter-processor.tsthat drops any span without anopeninference.span.kindattribute) — this filter is load-bearing, because Next.js's auto-OTel would otherwise pipe its HTTP / fetch / page-render spans through the same global provider and pollute the Phoenix project. AX builds aNodeTracerProviderby hand with the same filter processor wrapping anOTLPTraceExporterpointed athttps://otlp.arize.com/v1/traceswithspace_id+api_keyheaders and theopeninference.project.nameresource attribute set — and deliberately does not callprovider.register(), sidestepping the Next.js-pollution problem at the registration layer too. The filter is defined locally rather than imported from@arizeai/openinference-vercelso the OpenAI Agents tiers don't carry a Vercel-specific dependency. Both then runnew OpenAIAgentsInstrumentation({ tracerProvider }).manuallyInstrument(agents)— the instrumentor doesn't monkey-patch the SDK; it implements the agents SDK's first-classTracingProcessorinterface and registers via the SDK's ownsetTraceProcessorsAPI.instrumentation.ts— Next.js auto-detects this file at the root and runsregister()once per server process at startup, before user-land modules load. It delegates toinitTracing()insrc/ai/tracing.ts.next.config.ts— adds@openai/agents+@openai/agents-core+@arizeai/openinference-instrumentation-openai-agents(+@arizeai/phoenix-otelfor the Phoenix tier) toserverExternalPackagesso Turbopack doesn't try to bundle them.package.json— adds the OpenInference instrumentor + OTel SDK packages; bumpszodto v4 (peer-dep requirement of@openai/agents).env.example— observability environment variables.src/app/api/chat/route.ts(AX tier only) — callsgetTracerProvider()?.forceFlush()after the stream completes so spans are pushed out of the OTel batch buffer before the route handler exits.
Both tiers use OpenAI's native Responses API (
model="gpt-5.4-mini"), matching the Pythonopenai-agents-pytier — the JS SDK could route through@ai-sdk/anthropicvia@openai/agents-extensions, but using the native path keeps the tracing surface honest. The AX tier intentionally does not callprovider.register()— making the provider global would otherwise let Next.js's built-in OTel auto-instrumentation pump its own HTTP infra spans into our project. The OpenInference instrumentor resolves its tracer directly off the provider we hand it, so the global-registration step isn't needed. Phoenix'sregister()does its own snapshot/restore of the global state, so the same caveat doesn't apply.
OpenAI Voice
The voice tier is built on the OpenAI Agents SDK with the realtime extras — RealtimeAgent + RealtimeRunner for voice, Agent + Runner for the text fallback. The same @function_tool-decorated functions in backend/tools.py serve both modes. OpenAIAgentsInstrumentor().instrument(...) patches both runtimes, so all tracing is automatic — no per-event handlers, no hand-rolled span tree.
backend/tracing.py— new file in observability tiers. AX callsarize.otel.register(...); Phoenix callsphoenix.otel.register(..., protocol="http/protobuf", batch=True). Both then callOpenAIAgentsInstrumentor().instrument(tracer_provider=provider). That's the entire tracing surface.backend/main.py— importsbackend.tracingat the top so the instrumentor patchesagents.realtimebefore the runtime imports itbackend/voice_agent.py— callsflush_traces()on session end so the per-turn spans reach the OTelBatchSpanProcessor(long-running servers don't auto-flush)backend/chat_agent.py— same —flush_traces()in the streaming generator'sfinallyblock per text-mode requestbackend/requirements.txt— addsarize-otel(ax) orarize-phoenix-otel(phoenix), plusopeninference-instrumentation-openai-agentsenv.example— addsARIZE_*(ax) orPHOENIX_*(phoenix)src/app/api/chat/route.ts— eval-bypass header check (x-eval-secret/x-eval-user-id) in the ax tier
The instrumentor emits the canonical OpenInference voice span tree per turn — AUDIO conversation.turn → USER user + LLM assistant → TOOL <tool_name> — with audio captured inline as data:audio/wav;base64,... URIs on input.audio.url / output.audio.url. Phoenix and AX both render the trace card audio player from those data URIs (Arize re-hosts to its multimodal bucket on ingest), so no external file hosting is needed.
The
ax/openai-voiceandphoenix/openai-voicetiers' Python code is byte-identical apart from theregister(...)call intracing.pyand the corresponding requirements + env vars. Phoenix needsprotocol="http/protobuf"— the gRPC default rewrites the port from 6006 to 4317 and traces never land.
Pydantic AI
backend/tracing.py— tracing initialization (new file, imported beforepydantic_ai). CallsAgent.instrument_all(InstrumentationSettings(tracer_provider=…))after registering the tracer provider — Pydantic AI doesn't emit OTel spans without this.OpenInferenceSpanProcessorreshapes the spans before export.backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-pydantic-aienv.example— observability environment variables
Smolagents
backend/tracing.py— tracing initialization (new file, imported beforesmolagents). Usesregister()+SmolagentsInstrumentor().instrument(tracer_provider=…). The smolagents OpenInference instrumentor doesn't auto-emitsession.id—agent.pywraps everyagent.run(...)inusing_session(user_id)so traces are grouped by user (no-obs falls back to anullcontext()shim, so the wrap is identical across all three tiers).backend/main.py— importsbackend.tracingbefore other backend modulesbackend/requirements.txt— addsarize-phoenix-otelorarize-otel+openinference-instrumentation-smolagentsenv.example— observability environment variables
LangChain4j
backend/src/main/java/com/wondertoys/Tracing.java— new file in observability tiers. Builds an OpenTelemetrySdkTracerProvider+ OTLP exporter (HTTP/protobuf for Phoenix, gRPC for AX) and registers anAiServiceRequestIssuedListenerthat bridges LangChain4j's request lifecycle into OpenInference spans.backend/build.gradle.kts— addsopeninference-instrumentation-langchain4j+ tier-appropriate OTLP exporterbackend/src/main/resources/application.yml— Phoenix or AX endpoint + project name propertiesenv.example— observability environment variables
Spring AI
backend/src/main/java/com/wondertoys/Tracing.java— new file in observability tiers. Builds an OpenTelemetry SDK + OTLP exporter and exposes a MicrometerObservationRegistrybean withSpringAIInstrumentorregistered on it. Spring AI's auto-config picks up the registry viaObjectProvider<ObservationRegistry>and emits OpenInference-shaped LLM spans automatically.backend/build.gradle.kts— addsopeninference-instrumentation-springAI+ OTel SDK + tier-appropriate OTLP exporterbackend/src/main/resources/application.yml— Phoenix or AX endpoint + project name propertiesenv.example— observability environment variables
Arconia
backend/src/main/resources/application.yml— adds anarconia.otel.*block (endpoint, headers, resource attributes). Arconia'sarconia-opentelemetry-spring-boot-starterauto-configures the OTel SDK + OTLP exporter from these properties, andarconia-openinference-ai-semantic-conventionsreshapes Spring AI's built-in micrometer observations into OpenInference span attributes — so there is noTracing.javaJava code in this tier at all.backend/build.gradle.kts— addsarconia-bom+arconia-opentelemetry-spring-boot-starter+arconia-openinference-ai-semantic-conventions(andopentelemetry-exporter-sender-okhttpfor the AX gRPC tier). Requires Spring Boot 4.0.x and Spring AI 2.0.0-M8.env.example— observability environment variables.PHOENIX_COLLECTOR_ENDPOINThere is the base URL (no/v1/traces) because Arconia auto-appends it — this differs from the other tiers.
OpenInference Annotation Tracing
This isn't really a framework — it's a library for instrumenting hand-built agents. The agent loop in this tier is a hand-rolled tool-use loop calling Claude directly via the official com.anthropic:anthropic-java SDK, with the annotations applied to the methods that participate in the loop.
backend/src/main/java/com/wondertoys/App.java— callsOpenInferenceAgentInstaller.install()beforeSpringApplication.run()so the ByteBuddy Java agent retransforms@Agent/@Chain/@LLM/@Toolannotated methods before Spring loads them.backend/src/main/java/com/wondertoys/Tracing.java— new file in observability tiers. Builds the OTel SDK, wraps it in anOITracer, and hands it toOpenInferenceAgent.register(...)so the ByteBuddy advice has a tracer to emit through.backend/build.gradle.kts— addscom.arize:openinference-instrumentation-annotation+ OTel SDK + tier-appropriate OTLP exporterbackend/src/main/resources/application.yml— Phoenix or AX endpoint + project name propertiesenv.example— observability environment variables
Framework reference
If you're picking which framework to read first, this table is a quick comparison of the agent runtime, LLM client, and streaming API used by each.
| Framework | Agent library | LLM client | Streaming API | Architecture |
|---|---|---|---|---|
| Agno | agno.agent.Agent + InMemoryDb |
agno.models.anthropic.Claude |
agent.arun(stream=True, stream_events=True) over RunContentEvent / ToolCallStartedEvent |
Python FastAPI backend + Next.js frontend |
| Arconia | Spring AI ChatClient + @Tool methods (Spring Boot 4) |
spring-ai-starter-model-anthropic |
chatClient.prompt().stream().chatResponse() returns Flux<ChatResponse> |
Spring Boot Java backend + Next.js frontend |
| AutoGen AgentChat | autogen_agentchat AssistantAgent |
autogen_ext.models.anthropic.AnthropicChatCompletionClient |
agent.run_stream() over ModelClientStreamingChunkEvent (model_client_stream=True) |
Python FastAPI backend + Next.js frontend |
| AWS Strands | strands.Agent with per-user instance + @tool-decorated functions |
strands.models.anthropic.AnthropicModel (direct Anthropic API, not Bedrock) |
agent.stream_async(prompt) over {"data": ...} text-delta events + {"current_tool_use": ...} tool events |
Python FastAPI backend + Next.js frontend |
| BeeAI | beeai_framework RequirementAgent + UnconstrainedMemory |
ChatModel.from_name("anthropic:claude-sonnet-4-6") (litellm) |
agent.run(...).observe(...) over RequirementAgentFinalAnswerEvent.delta |
Python FastAPI backend + Next.js frontend |
| BeeAI (TypeScript) | beeai-framework ReActAgent + UnconstrainedMemory |
AnthropicChatModel (wraps @ai-sdk/anthropic) |
agent.run().observe(emitter) — partialUpdate with update.key === "final_answer" |
Next.js monolith |
| Claude Agent SDK | claude_agent_sdk.ClaudeSDKClient + per-user session + @tool served via create_sdk_mcp_server |
Claude via the Claude Code CLI subprocess (model="claude-sonnet-4-6") |
client.receive_response() over AssistantMessage content blocks (TextBlock / ToolUseBlock) |
Python FastAPI backend + Next.js frontend |
| Claude Agent SDK (TypeScript) | @anthropic-ai/claude-agent-sdk query() + per-user options.resume session + tool(...) served via createSdkMcpServer |
Claude via the bundled Claude Code binary (model="claude-sonnet-4-6") |
for await (const message of query(...)) over SDKAssistantMessage.message.content blocks (text / tool_use) |
Next.js monolith |
| CrewAI | crewai Agent + Task + Crew |
crewai.LLM("anthropic/claude-sonnet-4-6") (litellm) |
crewai_event_bus LLMStreamChunkEvent |
Python FastAPI backend + Next.js frontend |
| DSPy | dspy.ReAct over a dspy.Signature + dspy.History |
dspy.LM("anthropic/claude-sonnet-4-6") (litellm) |
dspy.streamify + StreamListener(signature_field_name="answer") |
Python FastAPI backend + Next.js frontend |
| Google ADK | google.adk Agent + Runner + InMemorySessionService |
LiteLlm("anthropic/claude-sonnet-4-6") |
Runner.run_async(streaming_mode=SSE) over Event (event.partial) |
Python FastAPI backend + Next.js frontend |
| Haystack | haystack.components.agents.Agent |
AnthropicChatGenerator (anthropic-haystack) |
streaming_callback(StreamingChunk) bridged into an asyncio queue |
Python FastAPI backend + Next.js frontend |
| LangChain.js | @langchain/langgraph ReAct agent |
@langchain/anthropic |
streamEvents (v2) |
Next.js monolith |
| LangChain Python | langgraph ReAct agent |
langchain-anthropic |
astream_events (v2) |
Python FastAPI backend + Next.js frontend |
| LangChain4j | dev.langchain4j.service.AiServices (declarative AI services) |
AnthropicStreamingChatModel |
AiServices TokenStream callback |
Spring Boot Java backend + Next.js frontend |
| LlamaIndex Python | llama_index FunctionAgent |
llama-index-llms-anthropic |
stream_events |
Python FastAPI backend + Next.js frontend |
| LlamaIndex Workflows | Hand-rolled Workflow with @step methods + custom Event types |
llama-index-llms-anthropic (Anthropic.astream_chat_with_tools) |
handler.stream_events() over a workflow's StreamEvent events written by ctx.write_event_to_stream(...) |
Python FastAPI backend + Next.js frontend |
| Mastra | @mastra/core Agent |
@ai-sdk/anthropic (Vercel AI SDK) |
stream.fullStream |
Next.js monolith |
| Microsoft Agent Framework | agent_framework Agent + AgentSession |
agent_framework.anthropic.AnthropicClient |
agent.run(stream=True) over AgentResponseUpdate events |
Python FastAPI backend + Next.js frontend |
| Microsoft Semantic Kernel | semantic_kernel.agents ChatCompletionAgent + ChatHistoryAgentThread |
semantic_kernel.connectors.ai.anthropic.AnthropicChatCompletion |
agent.invoke_stream() over StreamingChatMessageContent chunks |
Python FastAPI backend + Next.js frontend |
| OpenAI Agents SDK | agents.Agent + SQLiteSession + @function_tool |
Native OpenAI Responses API (model="gpt-5.4-mini") — not Anthropic |
Runner.run_streamed().stream_events() filtered on raw_response_event + ResponseTextDeltaEvent |
Python FastAPI backend + Next.js frontend |
| OpenAI Agents SDK (TypeScript) | @openai/agents Agent + per-request AgentInputItem[] history + tool(...) with Zod schemas |
Native OpenAI Responses API (model="gpt-5.4-mini") — not Anthropic |
run(agent, history, { stream: true }) over stream.toStream() — raw_model_stream_event → output_text_delta for tokens, run_item_stream_event for tool boundaries |
Next.js monolith |
| OpenAI Voice | OpenAI Agents SDK with the realtime extras — RealtimeAgent + RealtimeRunner for voice, Agent + Runner for text fallback. Same 5 @function_tool wrappers serve both |
openai-agents (gpt-realtime voice, gpt-5.4-mini text) |
Voice: async for event in session over RealtimeAudio / RealtimeHistoryAdded / RealtimeRawModelEvent. Text: Runner.run_streamed().stream_events() |
Python FastAPI backend (HTTP /chat + WS /voice) + Next.js frontend |
| OpenInference Annotation Tracing | Hand-rolled tool-loop using the Anthropic Java SDK, with @Agent / @Chain / @LLM / @Tool annotations applied via ByteBuddy at startup |
com.anthropic:anthropic-java SDK |
Anthropic SDK messages.stream(...) MessageStreamEvent |
Spring Boot Java backend + Next.js frontend |
| Pydantic AI | pydantic_ai Agent |
"anthropic:claude-sonnet-4-6" model string |
agent.run_stream_events() over PartStart/PartDelta events |
Python FastAPI backend + Next.js frontend |
| Smolagents | smolagents.ToolCallingAgent |
LiteLLMModel("anthropic/claude-sonnet-4-6") |
agent.run(stream=True) over ChatMessageStreamDelta (stream_outputs=True) |
Python FastAPI backend + Next.js frontend |
| Spring AI | spring-ai-anthropic ChatClient + @Tool methods |
spring-ai-starter-model-anthropic |
chatClient.prompt().stream().chatResponse() returns Flux<ChatResponse> |
Spring Boot Java backend + Next.js frontend |
| Vercel AI SDK | Vercel AI SDK streamText |
@ai-sdk/anthropic |
result.fullStream |
Next.js monolith |
| Vercel Eve | Eve filesystem-first agent (defineAgent + defineTool), durable sessions over the built-in HTTP channel |
anthropic/claude-sonnet-4.6 via the Vercel AI Gateway |
Eve NDJSON session stream (message.appended deltas) → SSE in the Next.js proxy |
Eve dev server + Next.js proxy frontend |
Evaluations
Each observability tier includes an eval harness for testing agent quality. All frameworks share the same 25 synthetic requests and the same 6 evaluators.
Phoenix — programmatic
cd phoenix/<framework>
npm install
npm run synthetic-requests # generate 25 traces
npm run evals # run 6 evaluators, log results as span annotations
AX — UI-driven
cd ax/<framework>
npm install
npm run synthetic-requests # generate 25 traces
Then configure the same 6 evaluators in the Arize AX console using LLM-as-a-Judge and Code Evaluator task types. See evals/README.md for step-by-step setup with prompt templates and code — evaluators apply to all projects.
On AX you can also attach Agent-as-a-Judge evaluators: a Claude Code sandbox reads the full trace (including TOOL spans) instead of a mapped {input}/{output} template. Scoring instructions live in evals/aaaj/. After traces exist in any ax/<framework> project, invoke the rosetta-aaaj skill with PROJECT_DIR set to that directory (REST/ax cannot create harness evals). Evaluators are space-level; tasks are per project.
Voice harness (openai-voice tier only)
The openai-voice tier ships a synthetic voice runner too. Instead of text prompts hitting /api/chat, pre-generated MP3 prompts are streamed through the voice WebSocket — same path a real microphone uses, so every prompt produces a full AUDIO conversation.turn → USER + LLM → TOOL trace tree.
cd phoenix/openai-voice # or ax/openai-voice, or no-observability/openai-voice
npm install
npm run voice-requests # 8 voice prompts → 8 voice sessions
The MP3 prompts live in evals/voice-prompts/ — generated once via OpenAI TTS (evals/generate-voice-prompts.py) and committed so contributors don't need an OpenAI key just to run the harness.
The 6 evaluators
- Correctness — Does the response address the user's request? (LLM judge)
- Tool Selection — Were the right tools chosen? (LLM judge)
- Tool Response Handling — Did the agent use tool results properly? (LLM judge)
- Format Compliance — Does the response follow markdown formatting rules? (LLM judge)
- Image URL Correctness — Do all image URLs match
/product-images/toy-XXX.png? (code) - Tool Call Count — Appropriate number of tool calls? (code)
AX only — Agent-as-a-Judge examples (trajectory completion, tool-result grounding, purchase/cancel protocol). Attach with the rosetta-aaaj skill.
What you can learn
- Instrumentation footprint per framework — exactly which files you touch and what attributes you add, side-by-side across 22 frameworks
- Phoenix vs AX differences — what's identical (most things — both speak OpenInference), what's different (endpoint, registration call, occasional cookbook quirks)
- Auto-instrumentation vs manual — some frameworks have OpenInference auto-instrumentors that need almost zero code; some emit gen_ai-convention spans that need a translation processor; some emit nothing and need fully hand-rolled spans
- Production patterns — streaming architecture, vector search with fallbacks, in-memory order management, structured tool schemas, audio span attributes for the voice tier
Tech stack
| Component | Technology |
|---|---|
| Web framework | Next.js 16 (App Router) |
| Python backend | FastAPI + uvicorn (Python frameworks only) |
| Styling | Tailwind CSS |
| LLM | Anthropic Claude Sonnet (most tiers) / OpenAI gpt-realtime + gpt-5.4-mini (voice tier) |
| Vector search | ChromaDB + all-MiniLM-L6-v2 embeddings |
| Product images | AI-generated (gpt-image-1) |
Contributing
See CONTRIBUTING.md for how to add a new framework, run the end-to-end test harness, capture PR / demo screenshots, and other maintenance workflows.