Repository navigation
0.0.3
Added
-
Server-side audio reordering (
listen({ sequence })) —Conversation
gains netcode-style reordering before STT, like client-side prediction in
multiplayer games: transports and client transforms (async VAD, worker
inference) can deliver audio out of order, solisten()now accepts the
sender's capture-ordersequenceand reorders per participant before
anything reaches the orchestrator. In-order chunks (the common case) are
emitted immediately with no added latency; a chunk arriving ahead of a
gap is held foraudioReorderMs(default 100, set oncreate()) so a
packet still in flight can fill it; when the window expires the gap is
skipped and the buffer is released in order, and the late chunk is
dropped. Late and duplicate sequences are always dropped, each
participant's stream is independent (first chunk anchors the baseline),
stop()clears the buffers, and without asequencethe chunk is
emitted on arrival exactly as before. Emittedaudio-inevents keep
their own conversation-level sequence, renumbered in release order —
always increasing, so persistence and transcripts see clean ordering. -
Async client capture transform —
audio.transformmay now return a
promise (worker/model-based VAD such as Silero, onnxruntime, anything via
postMessage). Each chunk'ssequenceis assigned at capture time,
pre-transform, so chunks sent out of order still mark their capture
position and the server's reorder buffer restores capture order before
STT. A transform slower than the capture rate loses audio at the gaps
past the server's hold window — keep it faster than real-time. Sync
transforms (energy gates) are unchanged. -
Client capture gate (
audio.transform) —PipeflowClient's built-in
mic capture acceptsaudio.transform(chunk): return a chunk to send it
(possibly rewritten) ornullto drop it — the seam for client-side VAD
(energy gate, Silero, WebRTC, …) without reimplementing the capture
plumbing. It applies only to captured mic audio; manualsendAudio()
calls hit the wire untouched, keeping the send path a faithful transport
so gating policy lives only where capture was explicitly opted into. A
throwing transform surfaces anerrorevent and drops the chunk without
stopping the capture loop. -
Explicit mic capture constraints — the client's mic now opens with
echoCancellation,noiseSuppression, andautoGainControlon by
default (overridable viaaudio.constraints). Without echo cancellation
the speaker's audio re-enters the mic, the server hears the agent's own
reply, and barge-in self-triggers in a feedback loop; noise suppression
keeps pops and room tone from becoming transcribed stage directions. The
example's client already set these by hand — the SDK now does it by
default. -
Browser/Node client SDK —
@moureau/pipeflow/clientships a typed
PipeflowClientthat mirrors theConversationevent surface (start,
stop,turn,transcript,audio,generation,tool-call,
tool-call-result,interrupt,error, …) over a swappableProtocol
wire. Ships aWebSocketProtocol(lazy token provider appended as
?token=); bring your own SSE / WebRTC / custom transport by implementing
the 4-methodProtocolcontract. Optional mic capture (audio.input),
exponential-backoff auto-reconnect, and a JSON-serializability guard on
outbound payloads. -
Whisper hallucination filter —
OpenRouterSTTcleans transcripts by
default before emission: asterisk stage directions (*Dramatic music*) are
dropped, consecutive repeated sentences (Thank you. Thank you.) collapse
to one, and transcripts built entirely from conversational fillers — the
built-in multilingual list (English, Portuguese, Spanish, French, German)
plusfillerPhrasesextras, repeated or not (E aí,E aí E aí,ok ok thank you) — are suppressed.filterHallucinations: falsereturns raw
transcripts. These artifacts come from near-silence clips
and speaker echo, so the example also trims trailing silence and enables
echoCancellation/noiseSuppressionat the mic. -
Clip-level energy floor —
OpenRouterSTTacceptsminClipRms
(0–1 RMS): bufferedpcmclips below the floor — the near-silence ones
whisper hallucinates on — are dropped before transcription, so no request
is sent, nofinalfires, and no fabricated turn reaches the conversation
(or interrupts a generation). The client's VAD gates what is sent; this
gates what is transcribed.onClipEnergyreports every measured clip's RMS
and whether it was transcribed, for tuning the floor against your real
distribution (speech clips sit far above artifact clips). The floor is
live-tunable (stt.minClipRms = …, no session restart) and the example's
client has a slider for it, showing each clip's RMS as you drag. -
STT sampling/passthrough options —
OpenRouterSTTacceptstemperature
andproviderOptions(serialized as the multipartproviderfield).
OpenRouter ignores whisper's top-levelprompt, so per-provider options
(providerOptions: { options: { groq: { prompt: "…" } } }) are the only
route to whisper's prompt. -
STT language pinning —
OpenRouterSTTacceptslanguage(ISO-639-1):
whisper's auto-detection drifts to unrelated scripts on short or quiet
clips (Portuguese speech transcribed as Japanese/Korean), so forcing the
code (language: "pt") keeps transcripts on the spoken language and
reduces hallucinations; the whisper-idiomatic"auto"is normalized to
"omit" (provider-side detection). The WebSocket example passes
STT_LANGUAGEthrough, leaving detection to the provider when unset. -
OpenRouter TTS output format option —
OpenRouterTTSaccepts
format: "pcm" | "mp3"for requests that don't specify one.pcm's sample
rate is provider-defined and opaque, so mp3 (self-describing) is the choice
when the client decodes. -
WebSocket voice-chat example —
example/runs a full voice loop in one
Bun process (whisper → llama-4-scout → fish s2.1 free) over a WebSocket,
with client-side VAD and per-sentence, pipelined TTS delivered as decodable
mp3 frames. -
Conversation tools auto-execute — agents' tools now run automatically in
a conversation, exactly likeAgent.run(): the orchestrator executes the
matching tool and feeds the result (or a caught error) back into the model
loop, so notool-callhandler is needed. Thetool-calland
tool-call-resultevents still fire for visibility. Opt out with
autoExecuteTools: false(onPipeflow,conversations.create(), or
Conversation) to keep the application-managed contract — listen for
tool-calland resolve each call yourself withresolveToolCall(), e.g.
for approval flows or tools that run in a different backend. Unknown tool
names and thrown tool errors surface to the model as{ "error": ... }so
it can recover, and a hung tool is bounded bytoolTimeoutMs.
Fixed
-
Transient no-output LLM failures are retried once — nova models served
from Bedrock via OpenRouter intermittently abort the chat stream right
after it starts (the client saw "OpenRouter provider aborted the stream:
The operation was aborted"), killing the whole generation and forcing the
user to re-ask. When a generation fails before any text was produced and
no tool executed, the orchestrator retries it once with a fresh request
(mid-stream aborts, idle timeouts, 429/5xx). Partial output or executed
tools are never retried — that would double-speak or re-run side effects. -
TTS and STT requests can no longer hang the pipeline indefinitely — a
provider connection that never responded (or went silent mid-stream) held
the pipeline open forever: a stuck TTS synthesis wedged the speech delivery
chain (the reply waited for the next user input, whose barge-in aborted it),
and a stuck transcription blocked the STT session's serialized queue for the
rest of the server's life. The OpenRouter TTS adapter now aborts a synthesis
that delivers no audio foridleTimeoutMs(default 15s, option on the
adapter), and the OpenRouter STT adapter aborts a transcription that takes
longer thantranscriptionTimeoutMs(default 30s) — both surface a clear
error and the pipeline moves on. -
TTS concurrency is tunable and the example stops defaulting to a flaky
model — remote TTS takes ~1.3-2s per sentence request, so with the old
2-in-flight window the sentences of a reply arrived ~1s apart (audible gaps)
and the free fish variant OpenRouter served was intermittently unavailable
(404 "No endpoints found" → dropped sentence audio). Conversations now accept
maxConcurrentTtsRequests(threaded to the speech pipeline, default 2) so
sentences synthesize in parallel, and the example defaults to the steady
paidfish-audio/s2.1-pro(TTS_MODEL=...:freeto opt back into the free
variant). -
Inline
<thinking>tags no longer leak into the reply — reasoning
models whose serving layer lacks a native reasoning channel (e.g.
amazon/nova-microvia OpenRouter in a thinking mode) stream their chain
of thought as visible<thinking>…</thinking>tags inside the content, so
the "thoughts" were displayed and read aloud by TTS. The OpenAI-compatible
adapter now partitions the tags out of the content stream (handling tags
split across chunks) and surfaces the inner text on the existing
reasoningfield — measured/displayable, but never spoken or written into
the reply text, transcript, or history. -
Single-agent conversations stop leaking the agent's name into prompts —
system and assistant messages carried a per-agentnamefield even when the
conversation had one agent, where it is redundant. Some providers render
thatnameas a role header ("Scout:"), and smaller models imitate it —
every reply then starts by speaking the agent's name. Multi-agent
conversations (where names disambiguate who said what) keep them. -
Prompted tool mode answers plain prose instead of failing — models that
ignore the envelope instruction and reply conversationally (common on
smaller chat models likeamazon/nova-micro) previously errored the whole
generation with "model did not return a JSON envelope". The reply is now
treated as the model's answer and spoken normally; the prompt also tells the
model plain text is fine when no tool is needed. Output that clearly
attempted a (broken/truncated) envelope still errors. -
TTS synthesis is bounded to a small queue — the speech pipeline fired
one synthesis request per sentence the moment it was flushed, so a reply
with many sentences launched an unbounded burst of concurrent provider
requests (free TTS variants rate-limit those, and later sentences were
frequently cut off or errored). Sentences now synthesize through a bounded
window (2 in-flight requests by default;SpeechPipelineaccepts
maxConcurrentRequests), started as soon as a slot is free so the next
sentence overlaps the current one's playback and the first piece is never
held up by the last.waitForIdle()now also covers sentences still waiting
on a slot. -
Clip boundaries freeze at silence detection — the OpenRouter STT
adapter previously took its buffer when the serialized transcription
callback ran, not when the silence that ended the clip was detected. If a
previous clip was still being transcribed, a late-running flush swept the
start of the next utterance into the wrong clip (or merged utterances
separated by a full silence gap into one turn). The buffer is now captured
synchronously inflush()— the audio that arrives while a transcription
is in flight starts its own clip. -
Example tool handler removed — the WebSocket example no longer
hand-wirestool-call→resolveToolCall(with an unsafeexecutecast);
the agent'sget_weathertool auto-executes. -
Barge-in cuts the agent's audio on the client — the example previously
kept playing already-synthesized sentences after an interrupt (the server
aborts generation, but queued mp3 frames were still in the client's
playback queue). The server now forwards the conversation'sinterrupt
event and the client cuts the current buffer and drops the queue the moment
the mic hears the user, on a new turn, or on the server message. -
Mic taps no longer interrupt the agent — the client previously cut
playback on the first voiced buffer, so a tap or pop (a single 256ms
transient) stopped the agent mid-sentence. Barge-in now fires only on
confirmed speech — a short burst of voiced ~32ms frames — so a tap never
interrupts, and the gate now reacts in ~200ms instead of the ~770ms the old
three-buffer streak cost. -
Example mic VAD stops eating and duplicating audio — the buffer-level
VAD dropped the first voiced 256ms buffer (clipped word onsets — the eaten
first letter) and then sent the confirming buffer a second time (whisper
heard one 256ms window twice and echoed words back). Clips also began
abruptly at speech, with no lead-in silence, and ended after a single
silent block — the exact input whisper loop-hallucinates on ("to the back,
to the back…", repeated words). The client now decides on ~32ms frames,
holds an idle pre-roll so every clip starts with real lead-in silence
before the first voiced frame, requires a genuine speech burst so short
words ("sim") register, and appends ~450ms of trailing silence so the last
word isn't cut off. -
Truncated replies are marked — when an interrupt or a new user turn
cuts the agent mid-response, the client appends "…" to the partial line so
a cut-off answer is visually distinct from a completed one. The user's own
lines get the same treatment: when a new utterance starts before the
previous clip's turn comes back from STT, that turn was a slice of
continued speech and is marked "…" too. -
Example voice drift — fish's free TTS variant varies the voice per
request whenvoiceis omitted; the example now pinsvoice: "alloy". -
Example playback speed — raw pcm has a provider-defined sample rate
(fish ≈44.1 kHz), so playing it at a guessed 16 kHz ran ≈2.7× slow; the
example now requests mp3 and the client decodes each frame at its real
rate.