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

@kuralle-syrinx/cf-agents

v4.6.3

Published

withVoice(Agent) — add a Syrinx realtime or cascaded voice pipeline to a Cloudflare agents SDK Agent

Downloads

1,513

Readme

@kuralle-syrinx/cf-agents

withVoice(Agent, options) — add a Syrinx voice pipeline to a Cloudflare agents SDK Agent. Supports both a realtime front (Gemini Live / OpenAI Realtime) and a cascaded STT → reasoner → TTS pipeline.

It is a mixin over the Agent, not a raw Durable Object: it reuses the Agent's native hibernation, keepAlive() lease, Connection, and SQL, and hands each connection to Syrinx's published edge runner (runVoiceEdgeWebSocketConnection) wrapped as a ManagedSocket. When the Agent exposes a public kuralle runtime, it is the brain by default (fromKuralleRuntime(this.runtime)); otherwise pass reasoner explicitly.

agents is a peer dependency — install it alongside this package.

Realtime

import { Agent, routeAgentRequest } from "agents";
import { withVoice } from "@kuralle-syrinx/cf-agents";
import { fromGeminiLive } from "@kuralle-syrinx/realtime";

export class SupportVoiceAgent extends withVoice<Env, typeof Agent<Env>>(Agent<Env>, {
  pipeline: {
    kind: "realtime",
    front: (env) => fromGeminiLive({ apiKey: env.GEMINI_API_KEY, tools: [CONSULT] }),
    delegateToolName: "consult_knowledge",
  },
  // reasoner defaults to fromKuralleRuntime(this.runtime, { sessionId })
}) {}

export default {
  fetch: (request: Request, env: Env) =>
    routeAgentRequest(request, env).then((r) => r ?? new Response("Not found", { status: 404 })),
};

Cascaded

import { withVoice } from "@kuralle-syrinx/cf-agents";
import { DeepgramSTTPlugin } from "@kuralle-syrinx/deepgram";
import { CartesiaTTSPlugin } from "@kuralle-syrinx/cartesia";
import { createWorkersSocket } from "@kuralle-syrinx/ws/workers";

export class SupportVoiceAgent extends withVoice<Env, typeof Agent<Env>>(Agent<Env>, {
  pipeline: {
    kind: "cascaded",
    stt: (env) => ({
      plugin: new DeepgramSTTPlugin(createWorkersSocket),
      config: { api_key: env.DEEPGRAM_API_KEY, model: "nova-3", sample_rate: 16000 },
    }),
    tts: (env) => ({
      plugin: new CartesiaTTSPlugin(createWorkersSocket),
      config: { api_key: env.CARTESIA_API_KEY, voice_id: env.CARTESIA_VOICE_ID, model_id: "sonic-3" },
    }),
    // optional: vad, eos (set endpointingOwner: "smart_turn" when supplying eos)
  },
  // reasoner defaults to fromKuralleRuntime(this.runtime); required for non-kuralle agents
}) {}

The Responder-Thinker primitive

withVoice packages Syrinx's bi-model Responder-Thinker shape turnkey (RFC docs/rfc-bimodel-delegate-seam.md): wire a realtime front + a Reasoner and the delegate seam comes with —

  • Structured result envelope (G1, default). The reasoner's answer reaches the front model as { response_text, require_repeat_verbatim: true, render? } so it repeats facts faithfully instead of paraphrasing. Configure per pipeline: toolResultFormat: "envelope" | "string", renderDirective: "translate_faithfully".
  • Delegate observability and client messaging (G2). onDelegateQuery / onDelegateResult hooks fire around every reasoner run with the query, answer, durationMs, and grounded — log or persist the Q&A pair without wrapping the Reasoner. onDelegateResult also carries the originating live connection, so a consumer can call connection.send(...) for a post-result app message without a session-to-connection registry.
  • Typed "thinking" cues (G3). Clients automatically receive tool_call_started / tool_call_delayed (after delayCueAfterMs) / tool_call_complete / tool_call_failed wire messages around the reasoner-latency window — key earcons/indicators on these instead of inventing an app message (@kuralle-syrinx/browser-client parses them).
  • Durable session + resume (G4, default on). The conversation persists to the Agent's DO-SQLite and survives eviction/hibernation: cascaded pipelines re-seed the ReasoningBridge; realtime pipelines feed the durable transcript to delegate turns and expose ctx.resume to the front() factory — resumeHistory: ctx.resume.history on replay providers (OpenAI), sessionResumptionHandle: ctx.resume.providerHandle on native-resume providers (Gemini; never replay on top of a handle).

Options

| Option | Description | | --- | --- | | transport | "edge" (default — Syrinx browser/edge protocol over /ws) or "twilio" (Twilio Media Streams, μ-law 8 kHz, for a PSTN leg). One transport per Agent class. | | pipeline | { kind: "realtime", front, delegateToolName?, toolResultFormat?, renderDirective? } or { kind: "cascaded", stt, tts, vad?, eos?, endpointingOwner?, sttForceFinalizeTimeoutMs? }. | | reasoner | (env, ctx) => Reasoner (ctx: { sessionId, resume? }). Defaults to fromKuralleRuntime(this.runtime) when the Agent exposes a kuralle runtime. Required for cascaded agents without one. | | recorder | (env, { sessionId }) => EdgeRecorder \| undefined — optional per-call recorder (e.g. the R2 recorder at @kuralle-syrinx/cf-agents/r2-recorder). Edge transport. | | onToolCallStart | (ctx: { toolName, args, sessionId, connection }) => void \| Promise<void> — fired the instant the front model invokes the delegate tool, before the reasoner runs — for app-specific cues beyond the standard tool_call_* wire messages. A throwing callback never affects the call. | | onDelegateQuery / onDelegateResult | G2 hooks around the reasoner run. onDelegateResult is self-contained ({ query, answer, durationMs, grounded, toolId?, toolName?, turnId, sessionId, connection }) — log/persist the grounded Q&A pair or use connection.send(...) to message the originating client. Throwing never affects the call. | | durableHistory | G4 durable session state over the Agent's SQLite (default true). Set false for ephemeral pre-G4 behavior. | | delayCueAfterMs | G3: ms before a pending tool call fires the tool_call_delayed ("still working") cue. 0 disables. Default 2000. | | backgroundAudio | { ambient?, thinking?, duckWhileSpeaking? } — looped ambient bed + thinking loop (raw mono PCM16 sources), mixed (ducked) under assistant speech; on the "twilio" transport the bed also fills between-turn gaps as comfort noise. Thinking follows the G3 cues. | | inputSampleRateHz / outputSampleRateHz | Edge audio rates (default 16000). | | resumeWindowMs | How long a dropped connection can resume its session. | | sessionId | (request, agentName) => string. Defaults to the ?sessionId= query param (so a reconnecting client can resume), else a per-connection random id. (Not the Agent name — concurrent connections to one instance must not share a session.) |

The client speaks Syrinx's edge voice protocol — connect a @kuralle-syrinx/browser-client to wss://<worker>/agents/<agent-class-kebab>/<instance>.

See examples/03-cf-agent-voice for a runnable worker.

Recording to object storage

R2EdgeRecorder writes a time-aligned stereo call recording to R2 while the call is still running — caller left, assistant right, plus per-speaker stems and a manifest.

import { R2EdgeRecorder } from '@kuralle-syrinx/cf-agents/r2-recorder';

const recorder = new R2EdgeRecorder({
  bucket: env.RECORDINGS,
  sessionId,
  startedAtMs: Date.now(),
  storageClass: 'InfrequentAccess',
});

For any S3-compatible bucket instead — AWS, R2's S3 endpoint, MinIO, Backblaze B2 — use @kuralle-syrinx/cf-agents/s3-store, which signs SigV4 over fetch rather than bundling an AWS SDK. Both implement the same ObjectStore seam (./object-store), so the timeline logic has one implementation.

Two behaviours worth knowing, both learned against a real bucket:

  • Storage class is fixed at createMultipartUpload. Setting it on the parts does nothing, so objects over 5 MiB silently stay Standard.
  • A failed finalize aborts the multipart. An abandoned upload is billed until R2 auto-aborts it at 7 days.

See Recording a call for the full reference.