Describe the bug
OtelTracingMiddleware.onModelCall and onActing use Context.current() (ThreadLocal) to resolve the parent span when building child spans. Inside a Reactor pipeline, the parent OTel Context is stored in the Reactor Context (via ContextPropagationOperator.runWithContext), not in ThreadLocal. As a result:
- Child spans (
chat <model>, execute_tool <name>) cannot find the invoke_agent parent span and become orphan root spans in the backend (e.g. Langfuse).
- Any
SpanProcessor.onStart that reads the parent Context (e.g. to inject business attributes) receives an empty context and cannot propagate biz metadata.
The correct approach is to use Flux.deferContextual and read the parent context via ContextPropagationOperator.getOpenTelemetryContextFromContextView(ctxView, Context.current()), then explicitly call spanBuilder.setParent(parentContext). This is exactly the pattern used in the now-deprecated TelemetryTracer.
To Reproduce
ReActAgent agent = ReActAgent.builder()
.name("assistant")
.model(model)
.middleware(new OtelTracingMiddleware())
.build();
Configure any OTLP-compatible backend (e.g. Langfuse). Invoke the agent. Observe in the trace UI:
- Only the
invoke_agent span appears at the top level.
chat <model> and execute_tool <name> spans appear as separate root spans (no parent), or are missing entirely.
Expected behavior
chat <model> and execute_tool <name> spans should be nested under invoke_agent as child spans, forming a complete trace tree.
Error messages
No exception. Silent behavior — orphan spans appear at the OTLP backend instead of a nested hierarchy.
Root cause
onModelCall (and onActing) use Flux.defer + Context.current():
// Current (broken)
return Flux.defer(() -> {
Span span = getTracer()
.spanBuilder("chat " + modelName)
// ...
.startSpan();
Context otelCtx = Context.current().with(span); // ThreadLocal — empty in Reactor pipeline
...
});
Should be:
// Fixed
return Flux.deferContextual(ctxView -> {
Context parentContext = ContextPropagationOperator
.getOpenTelemetryContextFromContextView(ctxView, Context.current());
Span span = getTracer()
.spanBuilder("chat " + modelName)
// ...
.setParent(parentContext)
.startSpan();
Context otelCtx = parentContext.with(span);
...
});
Note: onAgent already uses ContextPropagationOperator.runWithContext to propagate the span into Reactor Context, so the parent context is available — it just needs to be read correctly by the child hooks.
Environment
- AgentScope-Java Version: 2.0.0-RC4
- Java Version: 21
- OS: Windows 11
Additional context
The deprecated TelemetryTracer resolved this correctly via ContextPropagationOperator.getOpenTelemetryContextFromContextView. The same fix pattern should be applied to all three middleware hooks in OtelTracingMiddleware.
Describe the bug
OtelTracingMiddleware.onModelCallandonActinguseContext.current()(ThreadLocal) to resolve the parent span when building child spans. Inside a Reactor pipeline, the parent OTel Context is stored in the Reactor Context (viaContextPropagationOperator.runWithContext), not in ThreadLocal. As a result:chat <model>,execute_tool <name>) cannot find theinvoke_agentparent span and become orphan root spans in the backend (e.g. Langfuse).SpanProcessor.onStartthat reads the parentContext(e.g. to inject business attributes) receives an empty context and cannot propagate biz metadata.The correct approach is to use
Flux.deferContextualand read the parent context viaContextPropagationOperator.getOpenTelemetryContextFromContextView(ctxView, Context.current()), then explicitly callspanBuilder.setParent(parentContext). This is exactly the pattern used in the now-deprecatedTelemetryTracer.To Reproduce
Configure any OTLP-compatible backend (e.g. Langfuse). Invoke the agent. Observe in the trace UI:
invoke_agentspan appears at the top level.chat <model>andexecute_tool <name>spans appear as separate root spans (no parent), or are missing entirely.Expected behavior
chat <model>andexecute_tool <name>spans should be nested underinvoke_agentas child spans, forming a complete trace tree.Error messages
No exception. Silent behavior — orphan spans appear at the OTLP backend instead of a nested hierarchy.
Root cause
onModelCall(andonActing) useFlux.defer+Context.current():Should be:
Note:
onAgentalready usesContextPropagationOperator.runWithContextto propagate the span into Reactor Context, so the parent context is available — it just needs to be read correctly by the child hooks.Environment
Additional context
The deprecated
TelemetryTracerresolved this correctly viaContextPropagationOperator.getOpenTelemetryContextFromContextView. The same fix pattern should be applied to all three middleware hooks inOtelTracingMiddleware.