npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@syrin/sdk

v1.2.0

Published

AI agent observability, remote configuration, and governance for TypeScript/Node.js

Readme

@syrin/sdk

npm Node 18+ License: MIT OpenTelemetry

Observability, remote config, and governance for AI agents — one import, one init() call, zero changes to your existing code.


What Syrin gives you

| Capability | What it means | |---|---| | Session timeline | Every LLM call, cost, latency, and custom event grouped by user and run | | Remote config | Change model, temperature, prompts live from the dashboard — no redeploy | | Governance | Stop or constrain agents at runtime from the backend | | Checkpoints | Save and restore conversation state for recovery flows | | Custom events | Emit structured log entries that appear on the session timeline | | OpenTelemetry | Standard gen_ai.* spans + syrin.* extensions, works with any OTLP backend | | Feedback | Rate sessions from your app; aggregate in the dashboard | | Multi-agent | Orchestrators, pipelines, parallel workers — topology rendered on the dashboard |


Install

npm install @syrin/sdk

Requires Node 18+. The only required runtime dependency is Node's built-in fetch; OpenTelemetry exporters are optional peer dependencies.


Two Lines. You're Live.

import { init } from "@syrin/sdk";

init({ apiKey: "syrin_..." });

That is the entire setup. Every openai.chat.completions.create() call in your process is now instrumented — costs tracked, latency measured, events batched to the dashboard.


Core Concepts

Sessions — Every Run Has a Home

Wrap each user request in withSession(). Everything inside — LLM calls, costs, custom events — appears together on the dashboard timeline under that session ID.

import { withSession } from "@syrin/sdk";

await withSession("u:alice:2026-04-19", async () => {
  const response = await openai.chat.completions.create({
    model: "gpt-4o",
    messages,
  });
});

Session IDs are yours to choose. Use deterministic IDs to group calls across requests:

| Pattern | Session ID | |---|---| | Random | ses_a1b2c3 (call generateSessionId()) | | User × day | u:alice:2026-04-19 | | User × hour | u:alice:2026-04-19T14 | | User × forever | u:alice | | Named batch | k:batch-etl:2026-04-19 |

// Check what session is active inside a withSession block
import { getSessionId } from "@syrin/sdk";

await withSession("u:alice:today", async () => {
  console.log(getSessionId()); // "u:alice:today"
});

One Config Key. Infinite Leverage.

Declare any parameter as remotely configurable. Push overrides from the dashboard and they take effect on the next call — no redeploy, no restart.

import { sdk } from "@syrin/sdk";
// or: const myAgent = sdk.agent("chat")

const response = await openai.chat.completions.create({
  model:       myAgent.cfg("llm.model",       "gpt-4o"),
  temperature: myAgent.cfg("llm.temperature", 0.7),
  max_tokens:  myAgent.cfg("llm.max_tokens",  1024),
  messages: [
    { role: "system", content: myAgent.cfg("prompt.system", "You are helpful.") },
    { role: "user",   content: userMessage },
  ],
});
  • Key format: "section.field" — sections appear as accordion groups in the dashboard
  • Default: used until you push an override from the backend
  • Priority: governance anchor → local configure() → remote push → default

Events That Actually Mean Something

Emit structured events that appear on the session timeline with timestamp and metadata.

sdk.emit("HANDOFF",        { from_agent: "orchestrator", to_agent: "researcher" });
sdk.emit("GUARDRAIL_INPUT",{ name: "pii_filter", passed: true });
sdk.emit("BUDGET_ESTIMATION", { estimated_cost_usd: 0.12, budget_usd: 1.0 });
sdk.emit("TOOL_SELECTED",  { tool: "web_search", query: "latest AI news" });

Built-in event types the dashboard understands:

| Event type | Dashboard treatment | |---|---| | GUARDRAIL_INPUT / GUARDRAIL_OUTPUT | Guardrail check icons on timeline | | CIRCUIT_BREAKER_OPEN / _CLOSE | Circuit breaker markers | | HANDOFF | Agent → agent arrows | | AGENT_FORK / AGENT_JOIN | Parallel branch markers | | BUDGET_ESTIMATION | Budget bar in session header | | CHECKPOINT | Checkpoint pins on timeline | | TOOL_SELECTED | Tool usage rows |

Any other string is stored as a raw event and shown on the timeline.


The Kill Switch. (Governance)

The backend can stop an agent mid-run when a governance rule fires — cost exceeded, loop detected, and so on. Opt in and catch GovernanceStopError:

import { init, GovernanceStopError } from "@syrin/sdk";

const sdk = await init({
  apiKey: "syrin_...",
  governance: { allowStop: true },   // opt in to destructive actions
});

try {
  const response = await openai.chat.completions.create({ ... });
} catch (e) {
  if (e instanceof GovernanceStopError) {
    console.warn("Agent stopped:", e.reason, "incident:", e.incidentId);
    return { error: "request_blocked", reason: e.reason };
  }
  throw e;
}

Multi-agent Apps — AgentHandle

For apps with multiple agents, use sdk.agent() to declare each agent's config fields once and scope calls precisely. Each agent appears as a separate group in the dashboard.

import { init } from "@syrin/sdk";
import OpenAI from "openai";

const sdk = await init({ apiKey: "syrin_..." });
const openai = new OpenAI();

// ── Declare agents and their configurable fields ──────────────────────────────
const researcher = sdk.agent("researcher");
researcher.field("llm.temperature", 0.3, { ge: 0.0, le: 2.0, label: "Temperature" });
researcher.field("llm.model", "gpt-4o", { label: "Model" });
researcher.field("prompt.system", "Research thoroughly.", { multiline: true });

const writer = sdk.agent("writer");
writer.field("llm.temperature", 0.7, { ge: 0.0, le: 2.0 });
writer.field("output.format", "markdown", { enum: ["markdown", "plain", "html"] });

// ── Session + agent scope in one call ─────────────────────────────────────────
await withSession("u:alice:today", async () => {
  const response = await openai.chat.completions.create({
    model:       researcher.cfg("llm.model", "gpt-4o"),
    temperature: researcher.cfg("llm.temperature", 0.3),
    messages: [
      { role: "system", content: researcher.cfg("prompt.system", "Research thoroughly.") },
      { role: "user", content: query },
    ],
  });
});

Multi-agent Topology

Tell the dashboard how your agents are connected:

const sdk = await init({
  apiKey: "syrin_...",
  agentId: "travel-orchestrator",
  topology: {
    type: "orchestrator",
    nodes: {
      "researcher-agent":        { role: "worker", execMode: "sequential" },
      "hotel-finder-agent":      { role: "worker", execMode: "parallel", group: "swarm-1" },
      "transport-agent":         { role: "worker", execMode: "parallel", group: "swarm-1" },
      "events-agent":            { role: "worker", execMode: "parallel", group: "swarm-1" },
      "route-optimizer-agent":   { role: "worker", execMode: "sequential" },
      "itinerary-creator-agent": { role: "worker", execMode: "sequential" },
    },
    edges: [
      { from: "researcher-agent",     to: "hotel-finder-agent",       group: "swarm-1" },
      { from: "researcher-agent",     to: "transport-agent",           group: "swarm-1" },
      { from: "researcher-agent",     to: "events-agent",              group: "swarm-1" },
      { from: "hotel-finder-agent",   to: "route-optimizer-agent" },
      { from: "transport-agent",      to: "route-optimizer-agent" },
      { from: "events-agent",         to: "route-optimizer-agent" },
      { from: "route-optimizer-agent","to": "itinerary-creator-agent" },
    ],
    entryPoint: "researcher-agent",
    terminalNodes: ["itinerary-creator-agent"],
  },
});

Supported topology types: single, orchestrator, pipeline, parallel, graph, conditional_graph, hybrid.

Or define it after init():

sdk.defineTopology({ type: "pipeline", nodes: { ... }, edges: [...] });

Multi-agent HTTP Router

Route POST /agent/:agentId/run and POST /agent/:agentId/chat to your agent functions automatically:

// Express
const router = sdk.createAgentRouter({
  "researcher-agent": async (task) => {
    // task.input, task.sessionId, task.agentId available
    return { result: await runResearch(task.input) };
  },
  "writer-agent": async (task) => {
    return { result: await runWriter(task.input) };
  },
});

app.use(router.express());

// Fastify
fastify.register(router.fastify());

Each handler runs inside the correct agent + session context — all LLM calls inside are automatically tagged.


The Full Play — Express Chat Server

import { init, GovernanceStopError, withSession } from "@syrin/sdk";
import OpenAI from "openai";
import express from "express";

// ── Init ──────────────────────────────────────────────────────────────────────
const sdk = await init({
  apiKey: process.env.SYRIN_API_KEY!,
  agentId: "chat-agent",
  governance: { allowStop: true },
});

const chat = sdk.agent("chat");
chat.field("llm.model",       "gpt-4o",                         { label: "Model" });
chat.field("llm.temperature", 0.7, { ge: 0.0, le: 2.0,          label: "Temperature" });
chat.field("llm.max_tokens",  1024, { ge: 1, le: 8192 });
chat.field("prompt.system",   "You are a helpful assistant.",   { multiline: true });

const openai = new OpenAI();
const app = express();
app.use(express.json());

// ── Routes ────────────────────────────────────────────────────────────────────
app.post("/chat", async (req, res) => {
  const { user_id = "anonymous", messages = [] } = req.body;
  const sessionId = `u:${user_id}:${new Date().toISOString().slice(0, 10)}`;

  try {
    let reply = "";
    await withSession(sessionId, async () => {
      const systemPrompt = chat.cfg("prompt.system", "You are a helpful assistant.");
      const response = await openai.chat.completions.create({
        model:       chat.cfg("llm.model",       "gpt-4o"),
        temperature: chat.cfg("llm.temperature", 0.7),
        max_tokens:  chat.cfg("llm.max_tokens",  1024),
        messages:    [{ role: "system", content: systemPrompt }, ...messages],
      });
      reply = response.choices[0].message.content ?? "";
      sdk.emit("CUSTOM_LOG", { message: "Chat completed", turns: messages.length });
    });

    res.json({ reply, session_id: sessionId });
  } catch (e) {
    if (e instanceof GovernanceStopError) {
      return res.status(503).json({ error: "blocked", reason: e.reason });
    }
    throw e;
  }
});

app.get("/health", (_req, res) => res.json({ ok: true }));

app.listen(3000, () => console.log("Listening on :3000"));

// Flush on shutdown
process.on("SIGTERM", async () => {
  await sdk.shutdown();
  process.exit(0);
});

Every Knob, Mapped. (init() options)

const sdk = await init({
  apiKey:          "syrin_...",       // Required — from dashboard Settings
  agentId:         "my-agent",        // Default agent ID for un-scoped calls
  backendUrl:      "https://...",     // Default: https://api.syrin.dev
  offline:         false,             // true = no network (local dev / CI)
  captureContent:  false,             // true = record prompt/response text (check PII policy)
  otelExporter:    "none",            // "none" | "console" | "otlp"
  otelEndpoint:    "http://...",      // OTLP endpoint (Jaeger, Tempo, Honeycomb, etc.)
  debug:           false,             // Verbose SDK logging
  governance: {
    allowStop:          false,        // Opt in to backend-driven agent stops
    allowInjectMessage: false,        // Opt in to backend-injected messages
  },
  idleFlushMs:     10_000,            // How long to wait before flushing a partial batch
  batchSize:       100,               // Max events per /ingest POST
  topology:        { ... },           // Optional: multi-agent topology declaration
});

Skip Telemetry for Specific Calls

Exclude a block from all Syrin instrumentation — useful for health probes and internal calls.

import { withSession } from "@syrin/sdk";

// Pass a special "skip" session ID — the interceptor ignores it
await withSession("__syrin_skip__", async () => {
  const probe = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [{ role: "user", content: "ping" }],
  });
});

React to Config Pushes

Register a callback that fires whenever the backend pushes a config update:

import { onConfigChange, onAlert } from "@syrin/sdk";

onConfigChange((sessionId, updates) => {
  console.log(`Config updated for session ${sessionId}:`, updates);
});

onAlert((action) => {
  if (action["level"] === "critical") {
    pagerduty.trigger(String(action["message"]));
  }
});

Feedback — Let Users Rate Runs

Rate a session from anywhere in your app:

// Single session rating
await sdk.sessions.rate("u:alice:2026-04-19", "positive");
await sdk.sessions.rate("u:alice:2026-04-19", "negative", { reason: "Wrong output" });

// Fluent builder
await sdk.sessions.withId("u:alice:2026-04-19").rate("positive", { voterId: "reviewer-1" });

// Batch rating
await sdk.sessions.rateBatch([
  { sessionId: "ses_001", rating: "positive" },
  { sessionId: "ses_002", rating: "negative", reason: "Hallucinated" },
]);

Errors: AlreadyRatedError (409), SessionNotFoundError (404), ValidationError (422).


Checkpoints — Roll Back to Any Point

// Save state before a risky operation
const checkpoint = await sdk.createCheckpoint(messages, { label: "before-tool-call" });

try {
  const toolResult = await callRiskyTool();
  messages.push({ role: "tool", content: toolResult });
} catch {
  // Restore to pre-tool state on failure
  const restored = await sdk.restoreCheckpoint(checkpoint.checkpointId);
  if (restored) messages = restored;
  console.warn("Tool failed — restored to checkpoint", checkpoint.checkpointId);
}

// List all checkpoints for a session
const cps = sdk.listCheckpoints("u:alice:today");

Runtime Tunability — @tunable Decorator

Mark class properties as remotely configurable. The dashboard can push new values at runtime without a redeploy.

import { tunable, TunableField } from "@syrin/sdk";

@tunable({ namespace: "document-processor" })
class DocumentProcessor {
  batchSize   = TunableField({ default: 10,  ge: 1,   le: 100 });
  temperature = TunableField({ default: 0.7, ge: 0.0, le: 2.0 });
  model       = TunableField({ default: "gpt-4o" });
}

const processor = new DocumentProcessor();
// processor.batchSize   → 10 (or whatever the dashboard has pushed)
// processor.temperature → 0.7

Or register an existing object programmatically:

import { tune, getTune } from "@syrin/sdk";

tune({
  target: myConfig,
  namespace: "inference",
  fields: {
    temperature: { type: "number", default: 0.7, ge: 0.0, le: 2.0 },
    model:        { type: "string", default: "gpt-4o" },
  },
});

const current = getTune("inference"); // { temperature: 0.7, model: "gpt-4o" }

Environment Variables

All init() options can be set via environment variables:

| Variable | Equivalent init() option | |---|---| | SYRIN_API_KEY | apiKey | | SYRIN_BACKEND_URL | backendUrl | | SYRIN_AGENT_ID | agentId | | SYRIN_DEBUG | debug | | SYRIN_CAPTURE_CONTENT | captureContent | | SYRIN_OTEL_EXPORTER | otelExporter | | SYRIN_OTEL_ENDPOINT | otelEndpoint | | SYRIN_OFFLINE | offline | | SYRIN_IDLE_FLUSH_MS | idleFlushMs | | SYRIN_BATCH_SIZE | batchSize |


API Reference

Lifecycle

| Symbol | Description | |---|---| | init(options) | Initialize the SDK, returns SyrinSDKInstance | | shutdown() | Flush all pending events and tear down | | sdk.flush() | Flush pending events immediately | | sdk.refreshSchema() | Re-push agent schema to the backend | | sdk.configSnapshot() | Returns active config with API key masked |

Sessions & Scoping

| Symbol | Description | |---|---| | withSession(id, fn) | Scope all LLM calls in fn to session id | | getSessionId() | Returns the active session ID (inside withSession) | | sdk.agent(name) | Returns an AgentHandle with .field(), .cfg(), .run() |

Config

| Symbol | Description | |---|---| | sdk.agent(id).cfg(key, default) | Declare + read a remotely configurable value | | sdk.agent(id).field(key, default, opts) | Register a field in the dashboard config panel | | sdk.configure(overrides, sessionId?) | Push local config overrides programmatically | | sdk.activeConfig(sessionId?) | Returns the effective config for a session |

Events & Hooks

| Symbol | Description | |---|---| | sdk.emit(eventType, payload?, sessionId?) | Emit a custom lifecycle event | | sdk.checkpoint(label, metadata?, sessionId?) | Emit a CHECKPOINT event | | onConfigChange(fn) | Hook: called when backend pushes a config update | | onAlert(fn) | Hook: called on backend governance alerts | | sdk.onContextInjection(fn) | Hook: called when backend injects context | | sdk.getPendingInjections(sessionId?) | Pop and return pending context injections |

Governance

| Symbol | Description | |---|---| | GovernanceStopError | Thrown when backend sends a STOP action | | GovernanceStopError.reason | Human-readable reason string | | GovernanceStopError.incidentId | Dashboard incident ID | | GovernanceStopError.driftScore | Loop/drift score that triggered the stop |

Feedback

| Symbol | Description | |---|---| | sdk.sessions.rate(id, rating, opts?) | Rate a session "positive" or "negative" | | sdk.sessions.withId(id).rate(rating) | Fluent version of rate() | | sdk.sessions.rateBatch(items) | Rate multiple sessions concurrently | | sdk.sessions.start({ sessionId, successCriteria? }) | Start a session with criteria, returns feedback handle | | AlreadyRatedError | Thrown on 409 — session already rated | | SessionNotFoundError | Thrown on 404 | | ValidationError | Thrown on 422 |

Checkpoints

| Symbol | Description | |---|---| | sdk.createCheckpoint(messages, opts?) | Save conversation state | | sdk.restoreCheckpoint(checkpointId) | Restore saved conversation state | | sdk.listCheckpoints(sessionId?) | List all local checkpoints for a session |

Multi-agent

| Symbol | Description | |---|---| | sdk.defineTopology(topology) | Declare agent graph after init() | | sdk.createAgentRouter(fns) | Route /agent/:id/run|chat to handler functions | | sdk.createServer(opts) | Create an AgentServer for manual route mounting | | AgentServer.mountExpress(app) | Mount on Express | | AgentServer.fastifyPlugin() | Register as Fastify plugin |

Advanced (importable directly)

| Symbol | Description | |---|---| | tunable(opts) | Class decorator: marks fields as remotely tunable | | TunableField(opts) | Marks a class property as tunable (use inside @tunable class) | | tune(opts) | Programmatic registration of an existing object | | getTune(namespace) | Read current tunable values for a namespace | | globalRegistry | The global TunableRegistry singleton | | SyrinCore | Raw instrumentation engine for framework authors | | ConfigStore | Typed key-value config store with validation and history |


Docs

Getting Started

Sessions

Configuration

Observability

Control

Multi-Agent

Reference