OTelTracer
Export TraceEvents as OpenTelemetry spans with GenAI semantic conventions and optional OTLP HTTP setup.
OTelTracer
OTelTracer wraps the in-memory Tracer and translates every TraceEvent into OpenTelemetry spans following the GenAI semantic conventions. The agent loop is unchanged — it still calls tracer.emit() on the wrapped tracer; translation happens synchronously in an onEvent hook.
Import from @maniac-ai/agents/observability:
import { OTelTracer } from "@maniac-ai/agents/observability";
import { runAgent } from "@maniac-ai/agents";
const otel = new OTelTracer({ runId: "prod-request-abc" });
const result = await runAgent(spec, "Hello", { tracer: otel.tracer });
await otel.shutdown();@opentelemetry/api and @opentelemetry/sdk-trace-base are optional peer dependencies. For OTLP HTTP export, also install @opentelemetry/exporter-trace-otlp-http.
Span model
One root span per agent run:
maniac.agent.run <- root span
|- maniac.lm_call (root iter 0)
|- maniac.lm_call (root iter 1)
|- maniac.agent (id=slack)
| |- maniac.lm_call (inner)
| +- maniac.tool_call (send_message)
+- maniac.parallel
|- maniac.agent (id=docs)
+- maniac.agent (id=crm)lm_call_start/lm_callopen and closemaniac.lm_callspans with usage and finish reason- Inner
agentevents open/closemaniac.agentspans;agent_id === "parallel"maps tomaniac.parallel toolevents become zero-durationmaniac.tool_callspans parented viaparent_span_idcell,final,error,memory,retry, andstepbecome span events on the enclosing span (not separate spans)tokenandtool_call_arguments_deltaare dropped by default (high cardinality)
Span correlation reuses maniac span_id / parent_span_id via an internal Map so events emitted concurrently still attach to the correct OTel span.
Constructor options
| Option | Default | Description |
|---|---|---|
tracerProvider | global provider | BYO TracerProvider (Honeycomb, Datadog, Tempo, etc.) |
serviceName | "maniac-agents" | Used when installing a provider via fromOtlp |
recordTokens | false | Attach every token and tool_call_arguments_delta as span events |
recordPrompts | false | Attach truncated inner-agent prompt preview (privacy-sensitive) |
runId | auto UUID | Forwarded to the wrapped Tracer |
Telemetry errors are caught and stored in otel.errors — they never crash the agent loop.
OTLP factory
For a self-contained OTLP/HTTP pipeline:
import { OTelTracer } from "@maniac-ai/agents/observability";
import { runAgentStream } from "@maniac-ai/agents";
const otel = await OTelTracer.fromOtlp({
endpoint: "http://localhost:4318/v1/traces",
serviceName: "my-app",
recordTokens: false
});
for await (const env of runAgentStream(spec, "Hello", { tracer: otel.tracer })) {
// stream UI from env events — OTel export runs in parallel
}
await otel.shutdown();fromOtlp wires OTLPTraceExporter + BatchSpanProcessor + BasicTracerProvider. If the exporter peer dep is missing, the factory throws a helpful install message.
When you pass your own tracerProvider, shutdown() only closes outstanding spans — provider lifecycle remains your responsibility.
Semantic conventions
Exported attribute and span names live in @maniac-ai/agents/observability as semconv (GenAI request model, usage tokens, tool name, agent principal, step iteration, etc.). See the generated semconv reference.
Desktop opt-in
The Maniac desktop app enables tracing only when MANIAC_AGENT_RUNTIME_OTEL=1 is set (with optional ..._RECORD_TOKENS / ..._RECORD_PROMPTS flags). Packaged builds leave tracing off by default.
Dual consumers
Because OTelTracer.tracer is a normal Tracer, existing consumers keep working:
runAgentStream/AgentResult.tracestill receive the full in-memory event buffer- OTel backends receive translated spans for the same run
Share one OTelTracer instance across foreground and background runs by passing otel.tracer into every RunOptions.tracer and the BackgroundTaskDispatcher parent tracer merge path.