Skip to main content

Production agent (TypeScript)

Availability

Open core · self-host + all Splyntra Cloud plans

The Quickstart gets a first trace flowing. This guide covers what changes when you actually deploy an agent: failing fast on bad config, exposing health probes, bounding every request with a timeout, and flushing telemetry on shutdown so no trace is lost. It follows the examples/production-agent service — a support-triage agent over HTTP built on @splyntra/sdk.

Initialize telemetry first

Construct the Splyntra tracer before any agent or LLM code runs — the instrumentors patch client prototypes at construction time, so ordering matters. Own the single instance in one module:

import { Splyntra } from "@splyntra/sdk";

let instance: Splyntra | null = null;

export function initTelemetry(cfg: Config): Splyntra {
if (instance) return instance;
instance = new Splyntra({
apiKey: cfg.splyntra.apiKey,
project: cfg.splyntra.project,
endpoint: cfg.splyntra.endpoint,
environment: cfg.env,
serviceName: "support-triage-agent",
instrument: [], // this service wraps its LLM call explicitly
redactByDefault: true, // strip secrets from spans before export
});
return instance;
}

export async function shutdownTelemetry(): Promise<void> {
if (!instance) return;
await instance.shutdown(); // flush buffered spans
instance = null;
}

This service wraps its business logic explicitly with wrapAgent / wrapTool / wrapLLM rather than relying on auto-instrumentation — that is version-independent across ESM/CJS and guarantees exactly one llm_call span. See Manual instrumentation.

Fail fast on configuration

Load and validate configuration once, before binding a port, so a misconfigured service never starts half-wired. In production, refuse the two most common leaks — the shared dev key and a plaintext remote endpoint:

if (cfg.env === "production") {
if (cfg.splyntra.apiKey === "splyntra_dev_key") {
throw new Error("Refusing to start: SPLYNTRA_API_KEY is the shared dev key in production");
}
if (cfg.splyntra.endpoint.startsWith("http://") && !cfg.splyntra.endpoint.includes("localhost")) {
throw new Error("Refusing to start: SPLYNTRA_ENDPOINT must be https:// in production");
}
}

Provider precedence

The classifier resolves its LLM provider by precedence: Gemini → OpenAI → simulated. Both real providers go through the OpenAI client — Gemini via its OpenAI-compatible endpoint — so a single instrumentation path captures token usage for both:

function resolveLlm() {
const gemini = process.env.GEMINI_API_KEY?.trim();
if (gemini) {
return {
provider: "gemini",
apiKey: gemini,
model: process.env.GEMINI_MODEL ?? "gemini-2.5-flash",
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
};
}
const openai = process.env.OPENAI_API_KEY?.trim();
if (openai) {
return { provider: "openai", apiKey: openai, model: "gpt-4o-mini", baseURL: null };
}
return { provider: "simulated", apiKey: null, model: "simulated-triage-v1", baseURL: null };
}

Leaving both keys unset falls back to a labeled simulated completion (wrapped with wrapLLM, returning a usage object) so traces and cost still flow without a provider account.

Bound every request

Wrap the agent call in a timeout backed by a real AbortController, so a hung model call is cancelled — not merely abandoned:

function withTimeout<T>(p: Promise<T>, ms: number, ac: AbortController, label: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
ac.abort();
reject(new TimeoutError(`${label} timed out after ${ms}ms`));
}, ms);
p.then(resolve, reject).finally(() => clearTimeout(timer));
});
}

Retry transient failures with exponential backoff and jitter, but never retry a timeout/abort or a 4xx client error:

const retriable = !(err instanceof TimeoutError) && !(status && status >= 400 && status < 500);

Health and readiness probes

Expose dependency-free liveness and readiness endpoints for your orchestrator. /readyz returns 503 once the process starts draining, so it stops receiving new traffic:

if (req.method === "GET" && req.url === "/healthz") return send(res, 200, { status: "ok" });
if (req.method === "GET" && req.url === "/readyz") {
return draining ? send(res, 503, { status: "draining" }) : send(res, 200, { status: "ready" });
}

Wire /healthz to the liveness probe and /readyz to the readiness probe.

Graceful shutdown that flushes telemetry

On SIGTERM, stop advertising ready, stop accepting connections, let in-flight requests finish, then flush spans before exiting — with a hard deadline so a hung request never blocks past the orchestrator's grace period:

async function shutdown(signal: string): Promise<void> {
draining = true;
const hardDeadline = setTimeout(() => process.exit(1), 25000);
hardDeadline.unref();

server.close();
while (inFlight > 0) await new Promise((r) => setTimeout(r, 50));
await shutdownTelemetry(); // flush buffered spans
clearTimeout(hardDeadline);
process.exit(0);
}
Take over the SDK's signal handlers

The SDK installs its own SIGTERM/SIGINT handlers that flush and immediately process.exit(0) — that would cut your drain short. Remove them and run your own shutdown, which flushes via shutdownTelemetry():

process.removeAllListeners("SIGTERM");
process.removeAllListeners("SIGINT");
process.on("SIGTERM", () => void shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));

What you get

Each wrapped function becomes a span: the wrapAgent triage agent, its wrapTool CRM lookup, and the wrapLLM model call with token usage and cost. Open Traces to see the tree; a step that throws is recorded with ERROR status and the exception attached.

Next steps