Describe the bug
With @arizeai/openinference-instrumentation-openai-agents (0.3.2, also main), the span the processor creates for a function tool is never made active, so spans created while the tool runs (HTTP calls, database or RPC clients, any other instrumentation) are not its children. Inside the tool's execute, trace.getActiveSpan() is undefined, and a span started there begins a new trace.
OpenInferenceTracingProcessor.onSpanStart builds a parent context and calls this.oiTracer.startSpan(...) (js/packages/openinference-instrumentation-openai-agents/src/processor.ts, around line 1225), keeping the span in a private otelSpans map; nothing calls context.with or attaches the context. The Python processor does attach it (_processor.py, line 180: self._tokens[span.span_id] = attach(set_span_in_context(otel_span)), detached in on_span_end), so in Python such child spans nest under the tool span.
To Reproduce
A scripted model (no API key, no network) calls one tool, which starts a span:
npm init -y && npm pkg set type=module
npm install @openai/agents@0.18.0 @arizeai/openinference-instrumentation-openai-agents@0.3.2 @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/context-async-hooks zod
// repro.mjs
import { context, trace } from "@opentelemetry/api";
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import * as agents from "@openai/agents";
import { OpenAIAgentsInstrumentation } from "@arizeai/openinference-instrumentation-openai-agents";
import { z } from "zod";
const exporter = new InMemorySpanExporter();
const provider = new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] });
context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable());
trace.setGlobalTracerProvider(provider);
new OpenAIAgentsInstrumentation({ tracerProvider: provider }).manuallyInstrument(agents);
const { Agent, run, tool, Usage } = agents;
const tracer = trace.getTracer("app");
const payVendor = tool({
name: "pay_vendor",
description: "Pays the vendor",
parameters: z.object({}),
execute: async () => {
console.log("active OTel span inside the tool:", trace.getActiveSpan()?.spanContext().spanId ?? "none");
// Stands in for any instrumented work inside the tool (an HTTP call, an RPC request, ...).
tracer.startSpan("work inside the tool").end();
return "paid";
},
});
// A scripted model: first a call of the tool, then a final answer. No API key, no network.
let turn = 0;
const model = {
async getResponse() {
turn++;
const output = turn === 1
? [{ type: "function_call", callId: "call_1", name: "pay_vendor", arguments: "{}", status: "completed" }]
: [{ type: "message", role: "assistant", status: "completed", content: [{ type: "output_text", text: "Paid." }] }];
return { usage: new Usage(), output, responseId: `r${turn}` };
},
async *getStreamedResponse() { throw new Error("not used"); },
};
const agent = new Agent({ name: "treasury", instructions: "Pay vendors.", model, tools: [payVendor] });
const result = await run(agent, "Pay the vendor");
console.log("final output:", result.finalOutput);
await new Promise((resolve) => setTimeout(resolve, 200));
const spans = exporter.getFinishedSpans();
const names = new Map(spans.map((s) => [s.spanContext().spanId, s.name]));
for (const s of spans) {
const parent = s.parentSpanContext?.spanId;
console.log(`${s.name} (${s.spanContext().spanId}) <- ${parent ? (names.get(parent) ?? parent) : "root"} trace ${s.spanContext().traceId.slice(0, 8)}`);
}
$ node repro.mjs
active OTel span inside the tool: none
final output: Paid.
work inside the tool (1a33a9698248242a) <- root trace 5840fd80
pay_vendor (239197358d2bd98d) <- turn trace dec58212
turn (d1419acd68252a03) <- treasury trace dec58212
turn (09f92f0e0fef168e) <- treasury trace dec58212
treasury (0f78c2a7b892b5e4) <- Agent workflow trace dec58212
Agent workflow (1956b82645889bd0) <- Agent workflow trace dec58212
Agent workflow (8dd2a8fc5a394250) <- root trace dec58212
Expected behavior
work inside the tool is a child of pay_vendor, in the same trace, as with the Python instrumentation.
Desktop (please complete the following information):
- OS: macOS
- Version: Node.js v24.21.0,
@openai/agents 0.18.0, @arizeai/openinference-instrumentation-openai-agents 0.3.2
Additional context
If one of these directions is acceptable, I'm happy to open a pull request for it.
Describe the bug
With
@arizeai/openinference-instrumentation-openai-agents(0.3.2, alsomain), the span the processor creates for a function tool is never made active, so spans created while the tool runs (HTTP calls, database or RPC clients, any other instrumentation) are not its children. Inside the tool'sexecute,trace.getActiveSpan()isundefined, and a span started there begins a new trace.OpenInferenceTracingProcessor.onSpanStartbuilds a parent context and callsthis.oiTracer.startSpan(...)(js/packages/openinference-instrumentation-openai-agents/src/processor.ts, around line 1225), keeping the span in a privateotelSpansmap; nothing callscontext.withor attaches the context. The Python processor does attach it (_processor.py, line 180:self._tokens[span.span_id] = attach(set_span_in_context(otel_span)), detached inon_span_end), so in Python such child spans nest under the tool span.To Reproduce
A scripted model (no API key, no network) calls one tool, which starts a span:
Expected behavior
work inside the toolis a child ofpay_vendor, in the same trace, as with the Python instrumentation.Desktop (please complete the following information):
@openai/agents0.18.0,@arizeai/openinference-instrumentation-openai-agents0.3.2Additional context
withFunctionSpanin@openai/agents-core), andgetCurrentSpan()from@openai/agentsreturns that span inside the tool (in the repro above it reportsspanData.typefunction,namepay_vendor), but the processor offers no public way to get the OTel span or context for it.If one of these directions is acceptable, I'm happy to open a pull request for it.