Skip to content

0.0.3

Choose a tag to compare

@mococa mococa released this 07 Sep 18:56
· 27 commits to main since this release

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, so listen() now accepts the
    sender's capture-order sequence and 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 for audioReorderMs (default 100, set on create()) 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 a sequence the chunk is
    emitted on arrival exactly as before. Emitted audio-in events keep
    their own conversation-level sequence, renumbered in release order —
    always increasing, so persistence and transcripts see clean ordering.

  • Async client capture transform — audio.transform may now return a
    promise (worker/model-based VAD such as Silero, onnxruntime, anything via
    postMessage). Each chunk's sequence is 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 accepts audio.transform(chunk): return a chunk to send it
    (possibly rewritten) or null to drop it — the seam for client-side VAD
    (energy gate, Silero, WebRTC, …) without reimplementing the capture
    plumbing. It applies only to captured mic audio; manual sendAudio()
    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 an error event and drops the chunk without
    stopping the capture loop.

  • Explicit mic capture constraints — the client's mic now opens with
    echoCancellation, noiseSuppression, and autoGainControl on by
    default (overridable via audio.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/client ships a typed
    PipeflowClient that mirrors the Conversation event surface (start,
    stop, turn, transcript, audio, generation, tool-call,
    tool-call-result, interrupt, error, …) over a swappable Protocol
    wire. Ships a WebSocketProtocol (lazy token provider appended as
    ?token=); bring your own SSE / WebRTC / custom transport by implementing
    the 4-method Protocol contract. Optional mic capture (audio.input),
    exponential-backoff auto-reconnect, and a JSON-serializability guard on
    outbound payloads.

  • Whisper hallucination filter — OpenRouterSTT cleans 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)
    plus fillerPhrases extras, repeated or not (E aí, E aí E aí, ok ok thank you) — are suppressed. filterHallucinations: false returns raw
    transcripts. These artifacts come from near-silence clips
    and speaker echo, so the example also trims trailing silence and enables
    echoCancellation/noiseSuppression at the mic.

  • Clip-level energy floor — OpenRouterSTT accepts minClipRms
    (0–1 RMS): buffered pcm clips below the floor — the near-silence ones
    whisper hallucinates on — are dropped before transcription, so no request
    is sent, no final fires, and no fabricated turn reaches the conversation
    (or interrupts a generation). The client's VAD gates what is sent; this
    gates what is transcribed. onClipEnergy reports 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 — OpenRouterSTT accepts temperature
    and providerOptions (serialized as the multipart provider field).
    OpenRouter ignores whisper's top-level prompt, so per-provider options
    (providerOptions: { options: { groq: { prompt: "…" } } }) are the only
    route to whisper's prompt.

  • STT language pinning — OpenRouterSTT accepts language (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_LANGUAGE through, leaving detection to the provider when unset.

  • OpenRouter TTS output format option — OpenRouterTTS accepts
    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 like Agent.run(): the orchestrator executes the
    matching tool and feeds the result (or a caught error) back into the model
    loop, so no tool-call handler is needed. The tool-call and
    tool-call-result events still fire for visibility. Opt out with
    autoExecuteTools: false (on Pipeflow, conversations.create(), or
    Conversation) to keep the application-managed contract — listen for
    tool-call and resolve each call yourself with resolveToolCall(), 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 by toolTimeoutMs.

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 for idleTimeoutMs (default 15s, option on the
    adapter), and the OpenRouter STT adapter aborts a transcription that takes
    longer than transcriptionTimeoutMs (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
    paid fish-audio/s2.1-pro (TTS_MODEL=...:free to 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-micro via 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
    reasoning field — 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-agent name field even when the
    conversation had one agent, where it is redundant. Some providers render
    that name as 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 like amazon/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; SpeechPipeline accepts
    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 in flush() — the audio that arrives while a transcription
    is in flight starts its own clip.

  • Example tool handler removed — the WebSocket example no longer
    hand-wires tool-call → resolveToolCall (with an unsafe execute cast);
    the agent's get_weather tool 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's interrupt
    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 when voice is omitted; the example now pins voice: "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.