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.3Use @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:
| Option | Default | Behavior |
|---|---|---|
enabled | true | Creates the recorder and instruments session operations |
endpoint | Client backendEndpoint | Backend base URL; the shipper appends /observability/spans |
apiKey | Client API key | Sent in the X-API-Key header |
product | armoriq-sdk | Product attached to the ingest payload |
maxBufferSize | 1000 | Maximum traces held in the recorder; the oldest trace is dropped on overflow |
flushIntervalMs | 5000 | Periodic flush interval |
batchSize | 100 | Maximum completed trace batches per POST |
durable | false | In-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_KEYsupplies the API key when it is not passed toArmorIQClient.BACKEND_ENDPOINTsupplies the backend base URL whenbackendEndpointis not passed.USER_IDandAGENT_IDsupply client identities. Only UUID-shaped user IDs are attached to telemetry; agent IDs are free-form.ARMORIQ_ENVcontrols the SDK environment and resolved backend endpoint.backendEndpointonArmorIQClientorendpointinObservabilityConfigprovides 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:
startPlan()closes the previous plan trace, if one exists, and opens the next trace.check(), enforcement methods,report(), dispatch, and reanchor operations create nested spans.- The exported
recordGeneration()helper adds model usage to the same active plan trace. flushObservability()ends the active trace and flushes completed batches.close()ordispose()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.