Observability

Built-in distributed tracing with OpenTelemetry-compatible spans.

Quick Example

import { InMemoryRuntime, InMemoryExporter } from '@noetic/core';

const exporter = new InMemoryExporter();
const runtime = new InMemoryRuntime({ traceExporter: exporter });

const ctx = runtime.createContext();
await runtime.execute(myStep, input, ctx);

// Inspect collected spans
for (const span of exporter.spans) {
  console.log(`${span.name} [${span.duration}ms]`);
}

Noetic includes a built-in tracing system modeled on OpenTelemetry conventions. Every step execution, LLM call, and tool invocation creates a span with timing data, token counts, and custom attributes. You can export these spans to any observability backend.

Span Interface

A span represents a single unit of work in a trace tree.

interface Span {
  readonly traceId: string;
  readonly spanId: string;
  readonly parentSpanId: string | null;
  setAttribute(key: string, value: string | number | boolean): void;
  addEvent(name: string, attributes?: Record<string, string | number | boolean>): void;
  end(): void;
}
Property / MethodDescription
traceIdShared across all spans in the same execution trace.
spanIdUnique identifier for this span.
parentSpanIdLinks this span to its parent. null for root spans.
setAttribute(key, value)Attach metadata (model name, token counts, cost).
addEvent(name, attributes?)Record a point-in-time event within the span.
end()Mark the span as complete and record its end time.

Every Context has a span property. You can add custom attributes from within any step:

const myStep = step.run('annotated', async (input: string, ctx) => {
  ctx.span.setAttribute('input.length', input.length);
  ctx.span.addEvent('processing_started');

  const result = doWork(input);

  ctx.span.addEvent('processing_finished', { resultSize: result.length });
  return result;
});

SpanImpl

SpanImpl is the concrete implementation used by InMemoryRuntime. It adds fields useful for export and inspection:

PropertyTypeDescription
namestringThe span name (typically the step ID).
startTimenumberDate.now() when the span was created.
endTimenumber | undefinedSet when end() is called.
attributesMap<string, string | number | boolean>All attributes set via setAttribute.
eventsArray<{ name, timestamp, attributes? }>All events recorded via addEvent.
durationnumber (getter)endTime - startTime, or time since creation if still open.

MemoryTraceSpan

Memory layer operations produce their own trace records:

interface MemoryTraceSpan {
  layerId: string;
  hook: 'init' | 'recall' | 'store' | 'onSpawn' | 'onReturn' | 'onComplete' | 'dispose';
  durationMs: number;
  status: 'ok' | 'error' | 'timeout' | 'skipped';
  budget?: { allocated: number; used: number; yielded: number };
  itemCount?: number;
  error?: { message: string; stack?: string };
}
FieldDescription
layerIdWhich memory layer produced this trace.
hookThe lifecycle hook that executed.
durationMsHow long the hook took.
statusOutcome: ok, error, timeout, or skipped.
budgetToken budget allocation and usage (when applicable).
itemCountNumber of items produced by recall.
errorError details if status is error.

TraceExporter Interface

Exporters receive completed spans and send them to a backend:

interface TraceExporter {
  export(spans: Span[]): Promise<void>;
}

Built-in Exporters

NoopExporter

Discards all spans. This is the default when no exporter is configured.

import { NoopExporter } from '@noetic/core';

const runtime = new InMemoryRuntime({
  traceExporter: new NoopExporter(),
});

InMemoryExporter

Collects spans in an array for testing and debugging.

import { InMemoryExporter } from '@noetic/core';

const exporter = new InMemoryExporter();
// ... run agent ...

// Query spans
const llmSpans = exporter.getSpansByName('llm-call');
const children = exporter.getChildSpans(rootSpan.spanId);
const fullTrace = exporter.getTraceTree(rootSpan.traceId);

// Reset
exporter.clear();
MethodDescription
spansArray of all collected SpanImpl instances.
getSpansByName(name)Filter spans by name.
getChildSpans(parentSpanId)Get direct children of a span.
getTraceTree(traceId)Get all spans in a trace.
clear()Empty the spans array.

Custom Exporters

Implement TraceExporter to send spans to any backend:

import type { TraceExporter, Span } from '@noetic/core';

class OtlpExporter implements TraceExporter {
  async export(spans: Span[]): Promise<void> {
    await fetch('https://otel-collector.example.com/v1/traces', {
      method: 'POST',
      body: JSON.stringify(spans),
    });
  }
}

GenAI Semantic Attributes

Noetic defines constants following the OpenTelemetry GenAI semantic conventions. These are automatically set on LLM and tool spans:

GenAI Constants

ConstantValueDescription
GenAI.SYSTEM'gen_ai.system'LLM provider name.
GenAI.REQUEST_MODEL'gen_ai.request.model'Model identifier.
GenAI.USAGE_INPUT_TOKENS'gen_ai.usage.input_tokens'Input token count.
GenAI.USAGE_OUTPUT_TOKENS'gen_ai.usage.output_tokens'Output token count.
GenAI.COST'gen_ai.cost'Estimated cost.

ToolAttr Constants

ConstantValueDescription
ToolAttr.NAME'tool.name'Tool name.
ToolAttr.NEEDS_APPROVAL'tool.needs_approval'Whether the tool requires human approval.
import { GenAI, ToolAttr } from '@noetic/core';

// These are set automatically by the runtime, but you can use them
// in custom exporters or assertions:
const model = span.attributes.get(GenAI.REQUEST_MODEL);
const inputTokens = span.attributes.get(GenAI.USAGE_INPUT_TOKENS);
  • Context & Event Log -- every context carries a span.
  • Runtime -- createSpan and trace exporter configuration.
  • Memory -- memory layer operations produce MemoryTraceSpan records.

On this page