{"slug": "the-adapter-pattern-unified-tracing-across-ai-sdk-langchain-and-openai-agents", "title": "The Adapter Pattern: Unified Tracing Across AI SDK, LangChain, and OpenAI Agents", "summary": "A developer detailed an adapter pattern for unified tracing across AI SDK, LangChain.js, OpenAI Agents SDK, and direct model clients. The approach normalizes framework-specific lifecycles into a common span model, enabling shared observability tooling without coupling to any single framework. The design includes capability declarations and source attribution to aid debugging.", "body_md": "An adapter layer becomes strategically useful when several teams need one observability contract but cannot, or should not, standardize on one agent framework.\n\nAI SDK, LangChain.js, OpenAI Agents SDK, and direct model clients organize execution differently. One emphasizes generation and streaming, another exposes hierarchical callbacks, another has agent runs and handoffs, and a direct client exposes only provider requests unless the application adds its own spans.\n\nUnified tracing should preserve those differences while translating the common lifecycle into one model. Done well, teams can share execution-tree tooling, CI quality gates, privacy policy, and telemetry export without coupling every consumer to every framework.\n\nThe frameworks do not need a shared callback interface. They need a shared answer to a smaller set of questions:\n\nThe framework adapter converts its native lifecycle into those semantics. Consumers never call framework hooks directly.\n\n``` php\nAI SDK lifecycle ---------┐\nLangChain callbacks ------+--> framework adapters --> trace core\nOpenAI Agents tracing ----+\ndirect client wrappers ---┘\n                                      |\n                         +------------+------------+\n                         |            |            |\n                    execution UI   CI rules   telemetry sinks\n```\n\nThe exact public APIs change over time, so keep the mapping conceptual and verify it against the supported framework version.\n\n| Normalized concept | AI SDK-style integration | LangChain-style integration | OpenAI Agents-style integration | Direct client |\n|---|---|---|---|---|\n| Root run | Application request or generation | Chain, graph, or agent run | Agent trace or runner invocation | Manual application span |\n| Model span | Generation or stream lifecycle | LLM/chat-model callback | Model generation item/span | Provider request wrapper |\n| Tool span | Tool execution lifecycle | Tool callback/run | Function-tool execution | Application tool wrapper |\n| Retrieval span | Tool or explicit application span | Retriever callback/run | Tool or custom span | Application wrapper |\n| Handoff | Application-defined transition | Graph/chain transition | Native handoff lifecycle | Application span |\n| Parent context | App context plus source IDs | Parent run identifiers | Trace/span context |\n`AsyncLocalStorage` or explicit context |\n| Token usage | Final generation usage when exposed | Model callback metadata when exposed | Run/model usage when exposed | Provider response usage |\n\nThis matrix is a design guide, not a promise that every version exposes every cell. The adapter’s capability declaration is the authoritative record.\n\nThe common model should represent lifecycle and capability without importing framework types.\n\n```\ntype Capability =\n  | 'model_lifecycle'\n  | 'tool_lifecycle'\n  | 'retrieval_lifecycle'\n  | 'handoff_lifecycle'\n  | 'parent_relationships'\n  | 'streaming_completion'\n  | 'token_usage'\n  | 'cancellation';\n\ntype AdapterDescriptor = {\n  id: string;\n  adapterVersion: string;\n  framework: string;\n  supportedFrameworkRange: string;\n  capabilities: Partial<Record<Capability, boolean>>;\n};\n\ntype NormalizedAttribute = string | number | boolean;\n\ntype NormalizedSpan = {\n  schemaVersion: 1;\n  traceId: string;\n  spanId: string;\n  parentSpanId: string | null;\n  name: string;\n  kind: 'run' | 'model' | 'tool' | 'retrieval' | 'decision' | 'handoff';\n  startedAt: string;\n  endedAt?: string;\n  status?: 'ok' | 'error' | 'cancelled';\n  attributes: Record<string, NormalizedAttribute>;\n  source: {\n    adapterId: string;\n    adapterVersion: string;\n    sourceId: string;\n  };\n};\n```\n\nThe `source`\n\nblock is essential for support. When a span looks wrong, developers need to know which adapter and native event produced it.\n\nA base schema should contain portable fields, but frameworks may expose valuable extra data. Use namespaced extension attributes rather than adding a new top-level field for every integration.\n\n``` js\nconst span: NormalizedSpan = {\n  // portable fields omitted\n  attributes: {\n    'model.name': 'example-model',\n    'model.input_tokens': 840,\n    'model.output_tokens': 126,\n    'adapter.ai_sdk.finish_reason': 'stop',\n  },\n};\n```\n\nPortable consumers read `model.*`\n\n. Framework-specific diagnostics may read `adapter.ai_sdk.*`\n\n. Extension values must still follow the same privacy and size policy as core attributes.\n\nDocument extension keys and treat changes as adapter-version changes. Otherwise a shared schema slowly becomes an undocumented collection of framework internals.\n\nDuplicate instrumentation is one of the easiest ways to corrupt a trace. For example, a framework may already emit a model span while a provider-client wrapper emits another span for the same request.\n\n```\nagent run\n├─ generate_answer        framework span\n│  └─ provider_request    client wrapper span\n└─ generate_answer        accidental duplicate\n```\n\nNested spans may be intentional when they represent different layers. Duplicate peer spans are not.\n\nDefine precedence for each operation:\n\nDo not deduplicate by span name and timestamp alone. Concurrent model calls may legitimately share both.\n\nAutomatic framework detection sounds convenient but can activate multiple adapters in a monorepo or after a transitive dependency is added. Explicit registration makes ownership visible.\n\n```\ntype AdapterSession = {\n  descriptor: AdapterDescriptor;\n  stop(): Promise<void>;\n};\n\ntype AdapterFactory<Options> = {\n  descriptor: AdapterDescriptor;\n  start(options: Options, core: TraceCore): Promise<AdapterSession>;\n};\n\nclass AdapterRegistry {\n  private readonly factories = new Map<string, AdapterFactory<unknown>>();\n\n  register<Options>(factory: AdapterFactory<Options>): void {\n    if (this.factories.has(factory.descriptor.id)) {\n      throw new Error(`Adapter already registered: ${factory.descriptor.id}`);\n    }\n\n    this.factories.set(\n      factory.descriptor.id,\n      factory as AdapterFactory<unknown>,\n    );\n  }\n\n  get(id: string): AdapterFactory<unknown> {\n    const factory = this.factories.get(id);\n    if (!factory) throw new Error(`Adapter not registered: ${id}`);\n    return factory;\n  }\n}\n```\n\nApplication configuration can then select one or more adapters deliberately. The registry should reject conflicting ownership of the same capture surface unless the configuration explains the intended nesting.\n\nAn application may call a LangChain workflow from an AI SDK tool, or hand work to another service that uses a direct client. Adapter-local IDs are not enough.\n\nInside one Node.js process, the trace core can use `AsyncLocalStorage`\n\nto provide the active normalized context. An adapter reads that context when the framework does not supply a parent.\n\nAcross services or queues, propagate a standard trace carrier in request headers or message metadata. The receiving service validates the carrier and starts its framework run as a child or linked trace according to policy.\n\n```\ntype TraceCarrier = {\n  traceId: string;\n  parentSpanId: string;\n  sampled: boolean;\n};\n\ninterface ContextBridge {\n  inject(carrier: TraceCarrier): Record<string, string>;\n  extract(headers: Record<string, string | undefined>): TraceCarrier | null;\n}\n```\n\nDo not put prompts, user identifiers, or framework state in the carrier. It is correlation data, not a portable context dump.\n\nHandoffs deserve their own span kind because they change which agent owns the task. Treating a handoff as an ordinary tool call hides an important control-flow decision.\n\nA normalized handoff span can record:\n\nDo not record the complete transferred conversation by default. The trace needs the relationship, not a duplicate of the handoff payload.\n\nFrameworks without a native handoff concept can emit the same span from an application wrapper. That keeps cross-framework analysis consistent.\n\nUnified tracing fails when consumers interpret “unavailable” as zero. If an adapter cannot observe token usage, cancellation, or tool internals, emit a diagnostic and declare the capability `false`\n\n.\n\nQuality gates can then express policy:\n\nThe choice belongs to the consumer’s policy, not the adapter. The adapter’s responsibility is honest data.\n\nEvery adapter should run the same behavioral fixtures:\n\n| Fixture | Required invariant |\n|---|---|\n| Single model call | One root, one model child, one completion each |\n| Parallel tools | Siblings share the expected parent |\n| Tool retry | Attempts remain distinct and ordered |\n| Model stream | Completion or cancellation closes the span once |\n| Handoff | Source and destination traces are linked correctly |\n| Framework error | Controlled status and category are preserved |\n| Duplicate callback | Diagnostic emitted; no duplicate completion |\n| Abrupt shutdown | Open spans and dropped events are reported |\n\nNative callback fixtures test translation quickly. A small integration matrix should also run against the minimum and maximum supported framework versions.\n\nPublish adapter compatibility separately from the core release. Framework updates should not require an unrelated trace-core version bump.\n\nAn adapter has an ongoing compatibility cost. Give each integration:\n\nWithout ownership, adapters tend to keep compiling while silently losing events after framework lifecycle changes.\n\nAdopting unified tracing does not require replacing existing observability immediately.\n\nShadow mode should avoid exporting sensitive data twice. Compare normalized metadata, not raw payloads.\n\nOne small application with one stable framework may be better served by its built-in telemetry. An adapter platform is justified when several teams need shared rules, common privacy controls, migration flexibility, or one observability backend.\n\nThe pattern should remove repeated integration work. If it adds more maintenance than the framework differences it isolates, the boundary is premature.\n\nUnified tracing across AI SDK, LangChain, OpenAI Agents, and direct clients is not achieved by forcing every framework event into an identical callback shape. It comes from a stable semantic contract, honest capability reporting, explicit context propagation, and adapters that own framework-specific volatility.\n\nChoose one authoritative capture path, preserve handoffs and parentage, keep extensions namespaced, and operate each adapter with a conformance suite and compatibility policy. Teams remain free to choose the framework that fits their application while the organization gains one trustworthy language for execution, quality gates, and observability.", "url": "https://wpnews.pro/news/the-adapter-pattern-unified-tracing-across-ai-sdk-langchain-and-openai-agents", "canonical_source": "https://dev.to/raju_dandigam/the-adapter-pattern-unified-tracing-across-ai-sdk-langchain-and-openai-agents-4d3b", "published_at": "2026-08-13 00:05:01+00:00", "updated_at": "2026-08-13 00:15:38.769975+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "large-language-models"], "entities": ["AI SDK", "LangChain.js", "OpenAI Agents SDK"], "alternates": {"html": "https://wpnews.pro/news/the-adapter-pattern-unified-tracing-across-ai-sdk-langchain-and-openai-agents", "markdown": "https://wpnews.pro/news/the-adapter-pattern-unified-tracing-across-ai-sdk-langchain-and-openai-agents.md", "text": "https://wpnews.pro/news/the-adapter-pattern-unified-tracing-across-ai-sdk-langchain-and-openai-agents.txt", "jsonld": "https://wpnews.pro/news/the-adapter-pattern-unified-tracing-across-ai-sdk-langchain-and-openai-agents.jsonld"}}