Skip to content

[bug] JS openai-agents: tool spans are never made active, so spans created inside a tool start a new trace #3925

Description

@selimaytac

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions