Observability

TypeScript SDK Observability

Enable plan traces, policy spans, tool activity, and model usage with @armoriq/sdk 0.6.3.

The TypeScript SDK records one trace per IAP plan and sends completed traces to ArmorIQ on a fail-open path. Telemetry errors do not change the policy result, although an explicit flush waits for shipping and retries.

Install the production package

You need an organization-scoped ArmorIQ API key before the SDK can ship telemetry.

npm install @armoriq/sdk@0.6.3

Use @armoriq/sdk in customer applications. Development branches use a separate package identity; do not copy development package names or local dependency paths into production applications.

Default setup

Observability is enabled when you create an ArmorIQSession. With no override, the session uses the client's API key and backend endpoint.

import { ArmorIQClient } from '@armoriq/sdk';

const client = new ArmorIQClient({
  apiKey: process.env.ARMORIQ_API_KEY!,
  userId: '55555555-5555-4555-8555-555555555555',
  agentId: 'support-agent',
});

const session = client.startSession({ mode: 'sdk' });

Defaults:

OptionDefaultBehavior
enabledtrueCreates the recorder and instruments session operations
endpointClient backendEndpointBackend base URL; the shipper appends /observability/spans
apiKeyClient API keySent in the X-API-Key header
productarmoriq-sdkProduct attached to the ingest payload
maxBufferSize1000Maximum traces held in the recorder; the oldest trace is dropped on overflow
flushIntervalMs5000Periodic flush interval
batchSize100Maximum completed trace batches per POST
durablefalseIn-memory buffering

durable: true is accepted by the current TypeScript API but disk-backed durability is not implemented. The recorder warns and continues with in-memory buffering.

Configure a session

SessionOptions.observability accepts an ObservabilityConfig. The current TypeScript type requires the core fields when you provide this block.

import {
  ArmorIQClient,
  type ObservabilityConfig,
} from '@armoriq/sdk';

const apiKey = process.env.ARMORIQ_API_KEY!;
const client = new ArmorIQClient({
  apiKey,
  userId: '55555555-5555-4555-8555-555555555555',
  agentId: 'support-agent',
});

const observability = {
  enabled: true,
  endpoint: process.env.BACKEND_ENDPOINT ?? 'https://api.armoriq.ai',
  apiKey,
  product: 'armoriq-sdk',
  maxBufferSize: 1000,
  flushIntervalMs: 5000,
  batchSize: 100,
  durable: false,
} satisfies ObservabilityConfig;

const session = client.startSession({
  mode: 'sdk',
  observability,
});

The endpoint must be a backend base URL such as https://api.armoriq.ai. Do not include /observability/spans; the shipper adds that path.

Environment variables

The TypeScript observability layer does not read dedicated ARMORIQ_OBSERVABILITY_* variables. It inherits normal client configuration:

  • ARMORIQ_API_KEY supplies the API key when it is not passed to ArmorIQClient.
  • BACKEND_ENDPOINT supplies the backend base URL when backendEndpoint is not passed.
  • USER_ID and AGENT_ID supply client identities. Only UUID-shaped user IDs are attached to telemetry; agent IDs are free-form.
  • ARMORIQ_ENV controls the SDK environment and resolved backend endpoint.
  • backendEndpoint on ArmorIQClient or endpoint in ObservabilityConfig provides an explicit backend URL.

Keep observability configuration in application code if you need to change buffer, batch, or flush settings.

Disable observability

Set enabled to false for the session. The session skips recorder construction, buffering, timers, and HTTP calls.

const session = client.startSession({
  mode: 'sdk',
  observability: {
    enabled: false,
    endpoint: 'https://api.armoriq.ai',
    apiKey: process.env.ARMORIQ_API_KEY!,
    product: 'armoriq-sdk',
  },
});

Trace lifecycle

The session owns one active plan trace:

  1. startPlan() closes the previous plan trace, if one exists, and opens the next trace.
  2. check(), enforcement methods, report(), dispatch, and reanchor operations create nested spans.
  3. The exported recordGeneration() helper adds model usage to the same active plan trace.
  4. flushObservability() ends the active trace and flushes completed batches.
  5. close() or dispose() ends the trace, stops the periodic timer, and performs a final flush.

If an instrumented method runs before startPlan(), the SDK opens an iap.plan trace lazily so telemetry is still recorded.

Record model usage

Use the session's recorder and active trace with the exported recordGeneration() helper. Pass usage reported by your model provider and compute cost with computeCostUsd().

import { computeCostUsd, recordGeneration } from '@armoriq/sdk';

const recorder = session.observabilityRecorder;
const trace = session.activePlanTraceContext;

if (recorder && trace) {
  recordGeneration(recorder, trace, {
    model: 'gpt-4o',
    inputTokens: 420,
    outputTokens: 96,
    cacheReadTokens: 0,
    cacheWriteTokens: 0,
    costUsd: computeCostUsd('gpt-4o', 420, 96),
    prompt: null,
    completion: null,
    finishReason: 'stop',
  });
}

Prompts and completions are optional. Pass null when you only need usage totals or when storing content is not appropriate.

Flush and shut down

flushObservability() is safe to call at a request or turn boundary. It completes the current plan trace and waits for queued batches to ship, but it does not stop the periodic timer.

Always call close() or dispose() at the natural end of the session:

try {
  // Run the agent turn.
  await session.flushObservability();
} finally {
  await session.close();
}

All observability teardown paths are fail-open. Shipping errors are logged and dropped rather than thrown into the caller.

Complete example

import {
  ArmorIQClient,
  computeCostUsd,
  recordGeneration,
  type ObservabilityConfig,
} from '@armoriq/sdk';

async function main() {
  const apiKey = process.env.ARMORIQ_API_KEY;
  if (!apiKey) throw new Error('Set ARMORIQ_API_KEY');

  const client = new ArmorIQClient({
    apiKey,
    userId: '55555555-5555-4555-8555-555555555555',
    agentId: 'crm-assistant',
  });

  const observability = {
    enabled: true,
    endpoint: process.env.BACKEND_ENDPOINT ?? 'https://api.armoriq.ai',
    apiKey,
    product: 'armoriq-sdk',
    maxBufferSize: 1000,
    flushIntervalMs: 5000,
    batchSize: 100,
    durable: false,
  } satisfies ObservabilityConfig;

  const session = client.startSession({
    mode: 'sdk',
    defaultMcpName: 'crm-mcp',
    llm: 'gpt-4o',
    observability,
  });

  const toolName = 'search_contacts';
  const toolInput = { query: 'Acme' };

  try {
    await session.startPlan(
      [{ name: toolName, args: toolInput }],
      'Find contacts at Acme',
    );

    const decision = await session.check(toolName, toolInput);
    if (!decision.allowed) {
      console.log(`Blocked: ${decision.reason ?? decision.action}`);
    } else {
      const startedAt = Date.now();
      const result = {
        matches: [{ id: 'contact-1', company: 'Acme' }],
      };

      await session.report(toolName, toolInput, result, {
        status: 'success',
        durationMs: Date.now() - startedAt,
      });

      const recorder = session.observabilityRecorder;
      const trace = session.activePlanTraceContext;
      if (recorder && trace) {
        recordGeneration(recorder, trace, {
          model: 'gpt-4o',
          inputTokens: 420,
          outputTokens: 96,
          cacheReadTokens: 0,
          cacheWriteTokens: 0,
          costUsd: computeCostUsd('gpt-4o', 420, 96),
          prompt: null,
          completion: null,
          finishReason: 'stop',
        });
      }
    }

    await session.flushObservability();
  } finally {
    await session.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Lower-level API

Advanced integrations can import ObservabilityRecorder, startTrace, openSpan, closeSpan, recordSpan, recordPolicyCall, recordGeneration, recordEvent, endTrace, flushObservability, computeCostUsd, and the related record and context types from @armoriq/sdk.

Prefer the session API unless you are building a product bridge. The session already maintains the one-plan-per-trace lifecycle and ensures generation spans share the correct trace.

Next: understand the shared schema or troubleshoot delivery.

On this page