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

voice-runtime

v2.1.0

Published

Local-first, provider-neutral runtime for realtime voice agents, powered by TVIC.

Readme

voice-runtime

npm CI License

voice-runtime is the public npm entry point for TVIC, a provider-neutral runtime for realtime voice agents with bring-your-own telephony, speech, model, and synthesis providers.

New to the package? Start with the beginner's guide. Use the voice-agent guide for application design, the TVIC provider overview for supported adapters, and transports for browser and phone connections. Sarvam API details are in its official TTS documentation.

Status

The 2.1.0 release follows 2.0.0 and adds optional audio taps and transient per-session provider usage. Version 2.0.0 changed the public durable-store contracts. If you maintain custom durable-store or idempotency-store adapters, read the 2.0.0 migration guide before upgrading.

[email protected] is TVIC's public Node.js entry point. It supports Node.js 22, 24, and 26, ships both ESM and CommonJS entry points, and is server-side only. The main CI compatibility matrix exercises all three supported lines. Check the installed package version before using APIs documented for a newer source checkout.

Browser audio connects to a Node server through the browser-audio transport adapter. Browser code does not import this package directly.

The stable managed API assembles the STT -> LLM -> TTS pipeline from a prompt and provider configuration. The host application still owns its HTTP server, webhook authentication, browser-session authentication, and domain tools.

For a system overview, read How it works. For testing and deployment, read the testing guide and deployment guide.

Install

npm install voice-runtime

The package has no provider SDK lock-in. You may use the built-in adapters, pass your own provider implementations, or mix both approaches.

Optional AI agent guidance

To give Claude Code, Codex, or another compatible coding agent project-scoped TVIC guidance, run this explicitly from your project directory:

npx voice-runtime skills install

The command asks for y before writing .claude/skills/tvic/SKILL.md for Claude Code and .agents/skills/tvic/SKILL.md for Codex and OpenRouter-compatible agents. It does not run automatically during npm install, write to a home directory, or overwrite an existing skill without --force. Use npx voice-runtime skills install --yes in automation. See the AI coding-agent guide and skills reference.

Prompt-first agent

The prompt-first API assembles the STT -> LLM -> TTS pipeline. It still needs an authenticated transport CallHandle; npm install does not create a public audio endpoint or a phone call for your application.

import { createVoiceAgent } from "voice-runtime";

const agent = createVoiceAgent({
  prompt: "You schedule appointments for a doctor Be concise and confirm the time.",
  providers: {
    telephony: { provider: "web-client-audio" },
    stt: { provider: "deepgram", apiKey: process.env.DEEPGRAM_API_KEY },
    llm: {
      provider: "groq",
      apiKey: process.env.GROQ_API_KEY,
      model: "openai/gpt-oss-20b",
    },
    tts: {
      provider: "cartesia",
      apiKey: process.env.CARTESIA_API_KEY,
      voiceId: process.env.CARTESIA_VOICE_ID,
    },
  },
});

const session = await agent.start({
  callHandle,
  channel: "web_audio",
});

for await (const event of session.run) {
  if (event.kind === "transcript_delta") console.log("caller:", event.text);
  if (event.kind === "error") console.error(event.error);
}

const result = await session.run;
console.log(result.turnsHandled);
await agent.stop();

agent.start() returns { sessionId, run }. The run value is both an async event stream and an awaitable final result. The separate envelope is required by JavaScript Promise assimilation rules; returning the Promise-like run directly from an async start() method would make await agent.start() lose the session handle. Use agent.run(options) when you only need the final result.

TVIC does not receive media from a prompt alone. A verified transport adapter must produce the callHandle passed to start().

For a built-in Web Client Audio or Twilio adapter, use the factory form so the adapter receives the session ID created by TVIC:

import { createTwilioMediaStreamsProvider, createVoiceAgent } from "voice-runtime";

const telephony = createTwilioMediaStreamsProvider();

const session = await agent.start({
  call: verifiedInboundCall,
  channel: "phone",
  callHandle: ({ sessionId, call }) => telephony.acceptWebSocket(socket, call.id, sessionId),
});

The factory runs after the runtime session exists and receives { sessionId, call, channel }. The supplied call is required in this form, and its ID must match the returned handle's callId. Replace the provider and channel with createWebClientAudioProvider() and "web_audio" for browser audio. A preconstructed custom CallHandle is also supported when the adapter already knows how to correlate its events to the managed session.

Managed session lifecycle

agent.start() returns complete(), stop(), and finalSession alongside sessionId and run. Call session.complete() when the caller or host has finished sending input. It asks the CallHandle.endInput() transport hook to half-close input, then resolves with the persisted terminal session. The call handle must implement endInput() for graceful completion. Call session.stop() to cancel only that session; cancellation remains distinct from normal completion and remote hangup. finalSession resolves to the persisted terminal result with an ephemeral usage summary after runtime finalization and transport cleanup. Usage distinguishes runtime-measured and provider-reported fields, counts LLM tool-continuation requests, and does not include prices or currency. It is also available in the metadata-only session trace and is not persisted. Usage summaries are bounded to 128 entries, 32 metrics per entry, and 128 UTF-16 code units per provider, model, or metric identifier. If a bound omits usage detail, usage.truncated is true. Text-to-speech failover usage is grouped under the configured wrapper provider and model; primary and fallback attempts are not reported as separate entries. Custom providers should use usageReporter.report() only for observations available immediately. They must call usageReporter.beginOperation() before starting asynchronous work that may outlive session finalization so an unfinished observation is marked as interrupted in the usage summary. Per-session and agent shutdown use a bounded drain. If cleanup exceeds the deadline, stop() and finalSession reject with voice_runtime.shutdown_failed and lateCleanupPending; cleanup can continue in the background, so keep its dependencies open while it remains active. Start session.run—typically by beginning to consume its event stream—before calling session.complete(). Calling complete() before the run starts rejects with voice_runtime.session_not_running.

The runtime session.run promise still reports execution failure or cancellation by rejecting. Use finalSession when the host needs a persisted terminal status regardless of whether the run succeeded. See the API reference and transport lifecycle guide for the complete contract.

agent.stop() is idempotent. If a session is still running, it cancels and drains that session before closing the runtime and its stores. A deadline failure rejects shutdown and requires the host to retain any dependencies that late cleanup still uses.

Errors on VoiceEvent and MediaEvent payloads are JSON-safe NormalizedError values. Check them with isNormalizedError; the isTvicError marker is reserved for thrown TvicThrowableError wrappers and is not present on plain event payloads.

TVIC-created error codes use lowercase, namespaced identifiers such as auth.invalid_key and provider.open_failed. Custom calls to normalizedError() and normalizeUnknownError() must use one or more lowercase dot-separated segments; older persisted error records retain their legacy codes during migration.

What TVIC does

Accepts call media, transcribes speech, decides when a caller actually finished their turn, runs a model and tools, synthesizes a reply incrementally, streams audio back, and handles interruption, cancellation, provider stalls, and hangup.

TVIC does not store recordings or run recording workflows. Hosts can opt into the audioTap hook to receive copied caller and agent PCM frames, plus clear and playout markers. The host owns recording consent, storage, retention, and access controls. Session traces contain only metadata, audio-tap counters, and usage; they never contain audio bytes or conversation content. The optional onDrained({ sessionId, summary }) callback identifies which session's queued tap callbacks have settled, including a session stopped before its pipeline starts. TVIC does not wait for host recording or storage work to finish; keep synchronous callback work short.

Providers, models, and credentials

Each stage is configured independently. Built-in options currently include:

TVIC's supported provider set is listed in the repository provider overview. For current Sarvam API behavior and voice IDs, use the official Sarvam TTS documentation.

  • STT: Deepgram, Sarvam, Cartesia Ink, ElevenLabs Scribe, AssemblyAI, and Soniox.
  • LLM: Groq Chat Completions (the reference path) and an optional OpenAI Responses adapter.
  • TTS: Cartesia, ElevenLabs (WebSocket, REST, and HTTP stream), and Sarvam Bulbul v3/v4 Flash.
  • Transport: browser audio and inbound Twilio Media Streams.

Maturity is intentionally explicit: Web Client Audio and inbound Twilio Media Streams remain the stable transport paths in 2.0.0. The paid STT, LLM, and TTS adapters are currently experimental. validated is the intermediate label for an exact adapter/model/configuration that has passed the small-scale live evidence profile and is reasonable for low-volume use; stable additionally requires a support matrix, release evidence, ownership, and an upgrade policy. The root exports PROVIDER_STABILITY and PROVIDER_STABILITY_LEVELS contain the machine-readable labels.

Pass an explicit apiKey or use the documented environment variable. Explicit credentials take precedence; missing credentials fail before a live session is created.

| Provider | Environment variable | | ---------- | -------------------- | | Deepgram | DEEPGRAM_API_KEY | | Sarvam | SARVAM_API_KEY | | ElevenLabs | ELEVENLABS_API_KEY | | AssemblyAI | ASSEMBLYAI_API_KEY | | Soniox | SONIOX_API_KEY | | Groq | GROQ_API_KEY | | OpenAI | OPENAI_API_KEY | | Cartesia | CARTESIA_API_KEY | | Sarvam TTS | SARVAM_API_KEY |

STT, LLM, and TTS models are independent settings. TTS voice selection is also independent. Built-in TTS can read CARTESIA_VOICE_ID or ELEVENLABS_VOICE_ID for its voice. For complete provider-specific control, pass a constructed provider instance instead of a built-in configuration object. Sarvam's managed configuration defaults to its WebSocket adapter; set transport: "rest" or transport: "http-stream" on the Sarvam TTS config when the downstream use case needs one-shot HTTP delivery. Built-in STT and LLM configurations reject models outside the dated TVIC catalog unless allowUnknownModel: true is explicitly set.

Tools are application-owned functions. Define their schema and executor once, then include them in the agent:

import { createVoiceAgent, defineTool } from "voice-runtime";

const bookAppointment = defineTool<{ date: string }, { booked: boolean }>({
  id: "book_appointment",
  name: "book_appointment",
  description: "Books an appointment for a requested date.",
  inputSchema: { type: "object", required: ["date"] },
  outputSchema: { type: "object", required: ["booked"] },
  async execute(input) {
    return { booked: input.date.length > 0 };
  },
});

const providers = {
  telephony: { provider: "web-client-audio" as const },
  stt: { provider: "deepgram" as const, apiKey: process.env.DEEPGRAM_API_KEY },
  llm: { provider: "groq" as const, apiKey: process.env.GROQ_API_KEY },
  tts: {
    provider: "cartesia" as const,
    apiKey: process.env.CARTESIA_API_KEY,
    voiceId: process.env.CARTESIA_VOICE_ID,
  },
};

const agentWithTools = createVoiceAgent({
  prompt: "Schedule appointments and use the booking tool when needed.",
  tools: [bookAppointment],
  providers,
});

Tool execution receives a session/turn-scoped AbortSignal; authorization, confirmation for irreversible actions, and domain-state writes remain the host application's responsibility.

Composable API

Advanced runtime, media, provider, normalized-error, and durable-adapter APIs are exported from the package root. There are no advanced subpath imports in 2.0.0. This lets a developer start with createVoiceAgent and later own the runtime/session/pipeline boundaries without changing package names.

Read the voice-agent guide for the managed to composable migration path and the API reference for the root exports.

The host remains responsible for creating authenticated transport connections, owning HTTP/webhook lifecycle, and implementing application-domain tools. TVIC does not automatically create public unauthenticated endpoints or choose a provider fallback policy.

Persistence adapters

The root also exposes the PostgreSQL and Redis durability building blocks. The database drivers are intentionally injected, so install and configure pg and redis in your application:

The persistence guide covers migrations, memory scopes, Docker, ownership, and production retention.

import {
  createPostgresMemory,
  createPostgresRedisDurableRuntimeStore,
  runPostgresMemoryMigrations,
  runPostgresMigrations,
} from "voice-runtime";

await runPostgresMigrations(pgPool);
await runPostgresMemoryMigrations(memoryPool);
const memory = createPostgresMemory({ pool: memoryPool });
const durable = createPostgresRedisDurableRuntimeStore({
  pool: pgPool,
  redis: redisClient,
});

The migration SQL is bundled into the published package and can also be inspected in the package's migration sources. Run each migration function once at deploy time; repeated calls are idempotent. PostgreSQL is authoritative for the composite store, while Redis is used for its cache/projection layer.

License

Apache-2.0