Production agent (TypeScript)
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);
}
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
- TypeScript SDK — full API reference.
- Guardrails — block risky calls inline with
guard. - Kubernetes & Helm — deploy the service.