Maniac Docs
Streaming & tracing

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_call open and close maniac.lm_call spans with usage and finish reason
  • Inner agent events open/close maniac.agent spans; agent_id === "parallel" maps to maniac.parallel
  • tool events become zero-duration maniac.tool_call spans parented via parent_span_id
  • cell, final, error, memory, retry, and step become span events on the enclosing span (not separate spans)
  • token and tool_call_arguments_delta are 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

OptionDefaultDescription
tracerProviderglobal providerBYO TracerProvider (Honeycomb, Datadog, Tempo, etc.)
serviceName"maniac-agents"Used when installing a provider via fromOtlp
recordTokensfalseAttach every token and tool_call_arguments_delta as span events
recordPromptsfalseAttach truncated inner-agent prompt preview (privacy-sensitive)
runIdauto UUIDForwarded 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.trace still 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.

On this page