Migrate to Observability v2
Observability v2 is an additive path. Existing onTrace integrations can keep
running while transport, storage, and OpenTelemetry are migrated one layer at a
time. There is no stop-the-world rewrite.
The copyable snippets on this page are mirrored by the compile-only
public-snippets.ts
fixture and typechecked in the Node 18/20/22 test matrix.
Compatibility contract
Section titled “Compatibility contract”onTraceremains supported throughout the current 1.x line and is not marked deprecated in this release.- The seven-member
TraceEventunion, completion/event timing, UUIDspanId, UUIDparentIdtree, and best-effort-redacted legacy tool payloads remain unchanged. - A synchronous callback throw or asynchronous rejection never changes an Agent, Task, or Run result and does not become an unhandled rejection.
- New identity, status, start/event/end, links, diagnostics, stores, and OTel
mapping use
TraceRecordschema v2. They do not mutate the legacy event shapes.
Migration stages
Section titled “Migration stages”| Stage | Change | Rollback |
|---|---|---|
| 0 | Keep onTrace exactly as it is. | None needed. |
| 1 | Put the callback behind LegacyCallbackTraceSink. | Move the same callback back to onTrace. |
| 2 | Add BatchingTraceSink and a custom TraceExporter. | Keep the stage-1 bridge while disabling the new exporter. |
| 3 | Replace the custom exporter with TraceStoreExporter or @open-multi-agent/otel. | Switch exporters; the OMA run contract is unchanged. |
| 4 | Make the application own forceFlush / shutdown / store/provider close. | Keep explicit flush and lengthen timeouts; never make business success depend on telemetry. |
Stage 0: no immediate migration
Section titled “Stage 0: no immediate migration”import { OpenMultiAgent, type TraceEvent } from '@open-multi-agent/core'
function existingCallback(event: TraceEvent): void { legacyCollector.write(event)}
const oma = new OpenMultiAgent({ onTrace: existingCallback })This remains a valid 1.x configuration.
Stage 1: wrap the existing callback
Section titled “Stage 1: wrap the existing callback”import { OpenMultiAgent } from '@open-multi-agent/core'import { LegacyCallbackTraceSink } from '@open-multi-agent/core/observability'
const legacySink = new LegacyCallbackTraceSink(existingCallback)const oma = new OpenMultiAgent({ observability: { sinks: [legacySink] },})
try { await oma.runAgent(agent, prompt) await legacySink.forceFlush({ timeoutMs: 1_000 })} finally { await legacySink.shutdown({ timeoutMs: 1_000 })}Configure the bridge directly in observability.sinks. Do not also pass the
same callback through onTrace, or the application has deliberately configured
two deliveries. The bridge retains the legacy privacy surface; v2 sinks beside
it still receive the narrower default-safe records.
Stage 2: batch a custom exporter
Section titled “Stage 2: batch a custom exporter”import { BatchingTraceSink, type TraceExporter,} from '@open-multi-agent/core/observability'
const exporter: TraceExporter = { async export(records, signal) { const delivered = await sendBatch(records, { signal }) return { status: 'success', exported: delivered } },}
const sink = new BatchingTraceSink(exporter)const oma = new OpenMultiAgent({ observability: { sinks: [sink] } })emit() only admits a record to a bounded local queue. Export results describe
the delivered prefix: a short success is a permanent partial delivery; a
short retryable retries only the remaining suffix. Rejection, timeout, queue
overflow, and permanent failure are visible in getStats(), diagnostics, and
flush results but never alter business results.
Runnable version: batching-exporter.ts.
Stage 3A: choose a TraceStore
Section titled “Stage 3A: choose a TraceStore”import { BatchingTraceSink, InMemoryTraceStore, TraceStoreExporter,} from '@open-multi-agent/core/observability'
const store = new InMemoryTraceStore()const sink = new BatchingTraceSink(new TraceStoreExporter(store))Use InMemoryTraceStore for tests and local inspection. Use the Node-only
FileTraceStore subpath for durable local files, CLIs, and modest
single-process services. It is not a shared database and must not have two
processes writing the same path.
Runnable versions: in-memory-store.ts
and file-trace-store.ts.
Stage 3B: choose the OpenTelemetry adapter
Section titled “Stage 3B: choose the OpenTelemetry adapter”The first compatible install pair is:
npm install @open-multi-agent/core@^1.11.0 @open-multi-agent/otel@^0.1.0import { createOtelTraceSink } from '@open-multi-agent/otel'
const sink = createOtelTraceSink({ tracerProvider: applicationProvider })const oma = new OpenMultiAgent({ observability: { sinks: [sink] } })The application constructs the provider, processors, sampler, resource, and exporter. The adapter does not use the global provider. Provider shutdown is off by default; after draining the OMA sink, the application shuts down the provider it owns.
Runnable in-memory-provider version: otel-provider.ts.
Stage 4: own lifecycle explicitly
Section titled “Stage 4: own lifecycle explicitly”| Runtime | End-of-work sequence |
|---|---|
| Serverless/FaaS warm singleton | run → sink.forceFlush(short timeout); do not shut down the shared singleton per invocation. |
| Short-lived CLI | run → sink.forceFlush → store.flush (if file-backed) → sink.shutdown → store.close. |
| Long-lived server | stop accepting work and await in-flight requests, then sink.forceFlush → sink.shutdown → store.close → provider.shutdown for resources the application owns. |
OMA does not install signal handlers, call process.exit(), close a supplied
store, or shut down an application-owned provider. See the runnable
CLI,
SIGTERM server,
and FaaS
examples.
Privacy difference to account for
Section titled “Privacy difference to account for”Legacy onTrace intentionally keeps its historical redacted tool input/output
fields. V2 instrumentation does not collect prompt, completion, tool arguments,
tool results, or reasoning content by default. Numeric usage facts, including
reasoning-token counts, remain eligible. Trace privacy does not redact
CheckpointStore or shared-memory data; use RedactingStore separately where
persisted run state needs masking.
Cutover checklist
Section titled “Cutover checklist”- Run legacy and v2 destinations side by side only when duplicate delivery is intentional and separately keyed.
- Compare run counts by
runId; do not equate legacy UUID span IDs with v2 W3C-compatible trace/span IDs. - Alert on sink
dropped,failed, andlastErrorrather than turning them into Agent failures. - Exercise exporter rejection, hang, and partial delivery before cutover.
- Add lifecycle handling for every process mode before removing the stage-1 bridge.
- Keep rollback as a configuration change until the new backend has met its retention, privacy, and durability requirements.