TypeScript SDK

Node 18+, ESM-only. A Run class, AsyncLocalStorage-backed runScope for active-run propagation, transparent wrappers for five LLM providers plus framework helpers, and non-blocking background delivery. Run IDs are available immediately.

Install

npm install @agentping/sdk

Also pnpm add and yarn add.

Quickstart

import * as agentping from "@agentping/sdk";

agentping.init({ apiKey: process.env.AGENTPING_API_KEY });

const run = agentping.run("support-triage", { customerId: "acme-corp" });
run.event("log", { message: "classifying ticket" });
run.event("llm_call", {
  provider: "anthropic",
  model: "claude-sonnet-4-5",
  input_tokens: 1024,
  output_tokens: 312,
  latency_ms: 1430,
});
await run.finish({ status: "success", scores: { confidence: 0.93 } });

run.id is populated synchronously before any network call.

Auto-instrumentation

The SDK doesn't modify globals. It returns a Proxy-wrapped client that preserves the original return types. There are two ways to scope a wrapper to a run:

Explicit { run } option

import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";
import * as agentping from "@agentping/sdk";

const run = agentping.run("answer-bot");

const anthropic = agentping.instrumentAnthropic(new Anthropic(), { run });
const openai = agentping.instrumentOpenAI(new OpenAI(), { run });

const reply = await anthropic.messages.create({
  model: "claude-sonnet-4-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "hi" }],
});

Active-run scope via runScopeAsync

For web servers, the cleaner pattern is to wrap each request in runScopeAsync and call instrument*(client) without passing run. The wrapper resolves the active run from Node's AsyncLocalStorage so concurrent requests stay isolated:

import Anthropic from "@anthropic-ai/sdk";
import * as agentping from "@agentping/sdk";

// One-time at module load:
const anthropic = agentping.instrumentAnthropic(new Anthropic());

// Per-request, in a route handler:
app.post("/chat", async (req, res) => {
  const run = agentping.run("chat-route", { customerId: req.headers["x-customer"] });
  await agentping.runScopeAsync(run, async () => {
    const reply = await anthropic.messages.create({ ... });
    res.json(reply);
  });
  await run.finish({ status: "success" });
});

Supported providers

Each provider has its own instrument* wrapper (chat, streaming, embeddings where applicable):

  • instrumentAnthropic(client, opts?) - Claude Messages API + streaming
  • instrumentOpenAI(client, opts?) - chat.completions, responses, streaming
  • instrumentGemini(client, opts?) - generateContent, generateContentStream, embedContent
  • instrumentMistral(client, opts?) - chat.complete, chat.stream
  • instrumentCohere(client, opts?) - chat, chatStream, embed

Framework helpers

  • withAgentPing(opts?) / agentPingOnFinish(opts?) - Vercel AI SDK streamText / generateText (auto-detects provider + model from response.modelId)
  • new AgentPingHooks() - OpenAI Agents SDK (@openai/agents), pass to Runner.run({ hooks })
  • new AgentPingLangChainCallbackHandler() - LangChain.js callback, pass via { callbacks: [...] }

All framework helpers accept an optional run argument; when omitted they resolve the active run from runScopeAsync.

Wrappers are version-pinned. Outside the supported range, the wrapper falls back to a no-op and logs a single console.warn.

Heartbeats

agentping.heartbeat("daily-summary", {
  status: "ok",
  costUsd: 0.084,
  durationMs: 12_300,
  metadata: { rows: 421 },
});

Same table as full runs.

Streaming

Streaming responses are wrapped through a transparent AsyncIterable adapter. Chunks flow without delay; token counts accumulate; the llm_call event emits once on stream close.

const stream = anthropic.messages.stream({
  model: "claude-sonnet-4-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "summarize" }],
});

for await (const event of stream) {
  process.stdout.write(formatChunk(event));
}
// llm_call event fires here, after stream closes, with full totals.

If the stream errors mid-flight, a failed event is emitted with partial counts.

Context propagation

// Env var, picked up by agentping.init() automatically:
// AGENTPING_PARENT_RUN=run_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6

// Explicit parameter:
const run = agentping.run("price-fetcher", {
  parentRunId: "run_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6",
});

Explicit parameter wins.

Flushing

Telemetry flushes automatically on beforeExit. In a long-running server that never exits, await agentping.flush() at the end of each unit of work. agentping.status() returns queue depth and the last error for debugging.

Configuration

  • AGENTPING_API_KEY (required) team API key, apk_<region>_<32 hex>
  • AGENTPING_BASE_URL override the region-derived base URL (https://<region>.ingest.agentping.io)
  • AGENTPING_PARENT_RUN parent run ID

init() accepts the same values plus queueSize.

Use the team API key for the SDK. Ping tokens (ping_...) are for URLs only.

Source

github.com/agent-ping/agent-ping-typescript. Public surface in src/index.ts.