Repository navigation
Releases: moureau-dev/pipeflow
Release list
v0.0.8
Added
coordinationsandretryDirectAnsweron the Conversations API —pipeflow.conversations.create()now accepts both options and forwards them to the orchestrator.RestoreConversationOptionsfor rehydrating a conversation —pipeflow.conversations.get(id, options)acceptsagents,coordinations,retryDirectAnswer,maxConcurrentTtsRequests, andaudioReorderMs. Agent instances and per-conversation tuning are not persisted, so a restored handle carries none of them unless they are passed back in. Provider, logger, and tool-execution wiring is instance-level and is inherited from theConversationsinstance.buildClarifyPromptandrenderClarifyQuestionexported — both are documented for registering aclarifycoordination but were unreachable from the published subpaths. They now ship from@moureau/pipeflow/conversations/orchestration/coordination.
Fixed
Conversations.create()dropped orchestrator options —coordinationsandretryDirectAnswerwere accepted byConversationand documented as available through the API, but theConversationslayer discarded them, so orchestrator-level coordinations were unreachable frompipeflow.conversations.create().Conversations.get()returned a handle without the instance wiring — a restored handle loststt,tts, andlogger, and fell back toautoExecuteTools: trueeven when the instance was configured withfalse. Voice restore had no audio intake, and tool calls could be auto-executed behind the application's back.
v0.0.7
Fixed
- Build emits valid relative specifiers for directory-based modules —
fixRelativeSpecifiersin the build script previously appended.jsto
every extensionless relative import, even when the import resolved to a
directory (e.g../pipeflow→./pipeflow.js), but the actual emitted
file was./pipeflow/index.js. The specifier now checks whether the target
is a directory and emits/index.jsinstead. Fixes resolution failures for
@moureau/pipeflowwhen installed from npm.
v0.0.6
Added
- Per-generation context via
ContextFn—Agent.contextnow accepts a function(params: ContextParams) => string | Promise<string>in addition to a static string. Called each time the agent is invoked with the triggering prompt and available conversation context (conversationId,participants,turn,annotations). Works inAgent.run(), direct conversation generations, and delegated sub-generations.ContextFnandContextParamsexported from@moureau/pipeflow. - Conversation annotations —
conversation.annotations(Annotationsinstance) is a scoped key-value store for ephemeral app state. Values are rendered as system messages before every generation. API:.set(key, value),.set({...}),.delete(key),.clear(),.entries(liveReadonlyMap),.size. Accessible fromContextFnviaparams.annotations. Exported from@moureau/pipeflow.
v0.0.5
Added
- Plan-based coordination — the built-in
understandcoordination now outputs a structured plan (action: "plan") with typed steps instead of a reactive delegation loop. Each step has anid, optionalagent,prompt, anddependsOn(string ids). Independent steps run in parallel; dependent steps resolve in order and receive their dependency outputs injected into the prompt. This eliminates infinite re-delegation loops and makes multi-agent execution deterministic, observable, and bounded to one plan round. "plan"event — fires on the conversation when the coordinator produces a plan, giving the application visibility into the execution strategy before it runs."plan-step"event — firesstarted/completed/failedper step with the step's output text, for progress UIs and status tracking."agent-delta"event — a delegated specialist's LLM output streamed in realtime, tagged with the agent name. Distinct fromtext-delta(the top-level reply): this is live progress for UX observation and is not coalesced into the conversation's reply object.finalizeAfterToolRoundoption — sub-generations skip the redundant answer round after tool execution, synthesizing output directly from tool results. Cuts wall time ~30%.agentsaction support — the flatteneddelegatetool still accepts the simpleraction: "agents"/taskssyntax alongsideplan. Both route through the same deterministic executor.- Delegated sub-generation token cap — sub-generations within a plan step bound output to 512 tokens and limit tool iterations to 3 (was 10).
retryDirectAnsweroption — when a multi-agent coordination answers directly without planning, the framework retries once.
Changed
understandprompt tightened — encourages the model to speak the full answer in its narration BEFORE emitting the tool call, eliminating the separate composition LLM round-trip.delegatetool description — updated to emphasize planning as the primary action.
Fixed
- Sub-generation text deltas no longer pollute the conversation stream — specialist output now streams through
agent-deltainstead oftext-delta, so the top-level reply object (coalesced byConversationStream) is not fragmented by intermediary agent narration.
v0.0.4
Added
- Pluggable logging —
Pipeflowacceptslogger(implementsLoggerinterface) orverbose: trueshorthands. ShipsConsoleLoggerandSilentLogger. The logger propagates throughConversations,Conversation,Orchestrator, andCoordinationRunner, logging creation, starts, stops, generation status, errors, and key lifecycle events. Exported from@moureau/pipeflow. - Server transport abstraction —
ServerAdapter/ServerClientinterfaces let any runtime (Bun, Nodews, socket.io) bridgeConversationevents to remote clients. Exported from@moureau/pipeflow/transport. - Per-conversation LLM abort isolation —
LLMRequest.signalallows each orchestrator to abort its own generation without affecting other conversations sharing the same LLM instance.onInterrupt()andstop()now use this per-conversationAbortControllerinstead of callingllm.stop()(which previously aborted all streams on that LLM instance, regardless of which conversation owned them). - Event-driven
whenIdle()— replaced busy-wait polling with a promise-based notification, eliminating CPU spin during test idle detection. - Conversation listing with filters —
pipeflow.conversations.list()acceptsuserId,status,archived,orderBy,orderDir,page, andpageSizefilters. Returns{ conversations, total }for paginated UIs. Excludes archived conversations by default. - Conversation archiving —
pipeflow.conversations.archive(id)soft-deletes by settingarchivedAt. Archived conversations are hidden fromlist()but still retrievable viaget(). Filter withlist({ archived: true }). createdByfield — conversations can be tagged with the creating user's id. Filtered vialist({ userId }).- Client
generation-completeandtext-deltaevents —PipeflowClientnow dispatchesgeneration-completeandtext-deltaevents, letting the server decide whether to forward streaming text deltas to clients.
Fixed
- Cross-conversation interrupt contamination — interrupting one conversation no longer cancels generations in other conversations sharing the same LLM instance. Previously
onInterruptcalledllm.stop()which aborted every in-flight stream on that LLM instance. Now each orchestrator owns its ownAbortControllerwhich only aborts its own streams. - Silent persistence errors —
.catch(() => {})replaced with.catch((err) => logger.error(...))on generation timing writes, so failures surface in logs instead of being silently swallowed.
Changed
- Removed busy-wait loop —
Orchestrator.whenIdle()now uses a microtask check + promise resolve instead ofwhile(…) { Bun.sleep(1) }, eliminating CPU pegging. tsconfig.jsonexcludesdist/— prevents stale build artifacts from polluting typecheck.
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 sta...
v0.0.2
[0.0.2] - 2026-08-22
Added
- OpenRouter LLM adapter —
OpenRouterLLMroutes through any model on the OpenRouter marketplace over a shared OpenAI-compatible streaming engine (now also backing DeepSeek), with app attribution headers (X-Title: pipeflow,HTTP-Refererdefaulting tohttps://moureau.dev). - OpenAI and Claude LLM adapters —
OpenAILLMrides the same OpenAI-compatible engine (default modelgpt-4o-mini);ClaudeLLMimplements the Anthropic Messages API directly (tool_usecontent blocks withinput_json_deltafragment reassembly,tool_resultblocks for tool results, requiredmax_tokens, usage frommessage_start/message_delta). Both exported from@moureau/pipeflow/providers/llm. - Provider timeline hook — both LLM adapters accept an
onTimingcallback (request-start/headers/first-chunk) so application delay, network/queue delay, and model TTFT can be separated (bun scripts/latency-profile.tsnow reports the decomposition). - Structured clarification — the
delegatetool gains aclarifyaction: the coordination declares the missing details in amissingarray, and the framework renders and speaks one batched question for all of them (instead of one question per missing detail, model-permitting). - History windowing — the orchestrator bounds the conversation history each LLM request carries (
historyWindow, default{ maxTurns: 5, maxChars: 4000 }), since provider TTFT grows with input size. Measured on nova: an 8KB history cost ~850ms of pre-first-byte latency; windowing it back to ~5 turns restored the ~430ms regime. - Provider token usage — both LLM adapters accept an
onUsagecallback receiving the provider-reported prompt/completion tokens (bun scripts/latency-profile.tsnow reports tokens per scenario, with a local estimate to expose schema/system-prompt overhead). - Deterministic question budget —
clarifyanduserquestion rounds are capped per coordination run atmaxQuestionRounds(default 2, carried across suspensions); past the cap the coordination states reasonable assumptions and completes. The real-model clarify e2e went from 24.6s / 8 generations (and 60s+ timeouts) to ~5s / bounded rounds. - Thinking deltas — the LLM
deltaevent now carries an optionalreasoningfield when the provider streams thinking tokens separately from content (OpenRouterreasoning, DeepSeekreasoning_content);bun scripts/latency-profile.tsreports whether a model thinks before it speaks, through the same adapter path (the raw-fetch thinking probe was removed). - Tool-mode encodings —
LLMRequest.toolMode(and the matching adapter option, which the request overrides) selects how tool calls are encoded on the wire:native(providertools/tool_calls, default),envelope(response_formatJSON envelope translated back intotool_callevents — for endpoints with structured outputs but no native tool calling), orprompted(the same envelope requested by instruction — the universal fallback). Every mode yields the same event surface, so agents and the orchestrator never change. Inenvelope/promptedmodes the adapter injects a system-level tool contract (names, descriptions, and the envelope output rule) and embeds each tool's argument schema in the envelope, so the model sees the same argument contract it would in native mode. - ToolModeBenchmark — a diagnostic class (exported from
@moureau/pipeflow/providers/llm) that benchmarks the three tool modes through the real adapter path, reporting per-mode availability, p50/p95/p99 decision latency, median per-decision cost, correctness (every emitted call's arguments validated against the probe schema — a call with garbage args is a failed decision), and effective$ / correct decision.bun scripts/envelope-vs-native.tswraps it as a CLI;scripts/tool-envelope-probe.tsgates a model's envelope validity when onboarding. - Favorite models list —
ToolModeBenchmarkshipsFAVORITE_MODELS(the seven models measured as usable: llama-4-scout, gemini-2.5-flash-lite, nova-micro, nova-lite, ling-3.0-flash, lunaris-8b, gpt-oss-20b) plus theStringOr/FavoriteModeltype — any model id, with the favorites autocompleted — andbun scripts/envelope-vs-native.tsbenchmarks them by default (MODELS=...overrides). - Tool argument schemas —
Tool/PipeflowToolacceptsschema: { in, out }(zod):inderives the LLM-facing JSON parameters,out(defaulting toin) validates the arguments atexecute()time and may transform them. One schema keeps the model contract and theexecutesignature in sync and turns garbage model arguments into a clear tool error instead of a crash inside the tool.parametersremains as the hand-written-JSON-schema escape hatch (mutually exclusive). - OpenRouter STT adapter —
OpenRouterSTTtranscribes through the/api/v1/audio/transcriptionsendpoint (default modelopenai/whisper-large-v3-turbo). OpenRouter's STT is batch-only, so the session buffers the incoming linear16 PCM, wraps each utterance in a WAV container, and transcribes a clip aftersilenceMs(default 800ms) of silence, emitting onefinalper clip (end()flushes the tail,cancel()aborts). No interim results — nopartialevents. - OpenRouter TTS adapter —
OpenRouterTTSsynthesizes through the OpenAI-compatible/api/v1/audio/speechendpoint (default modelfish-audio/s2.1-pro-free:free), returning raw audio bytes re-chunked for playback.pcmoutput by default (mp3 on request),voice/speedpass-through, andstop()aborts the in-flight synthesis.
Fixed
- Provider failures surface as errors — the OpenAI-compatible streaming engine now emits an
errorevent when a provider returns HTTP 200 withfinish_reason: "error"/"content_filter"(e.g. gemini models via OpenRouter), instead of silently completing with an empty generation. - Stalled streams abort instead of hanging — an
idleTimeoutMswatchdog (default 8s) cancels a provider stream that delivers no data, closing the connection and surfacing a clear error. Protects against the observed failure where a model emits its full decision (e.g. a tool call) and the stream then never terminates (OpenRouter/nova showed ~28s stalls). RaiseidleTimeoutMsfor providers with slow first tokens. - Errored coordinations finalize their generation — an LLM failure inside a coordination run previously left a dangling
streaminggeneration in persistence; it is now completed (with the error surfaced via theerrorevent), matching the agent path. - Tool calls are emitted exactly once — the streaming engine no longer re-emits a tool call when a provider repeats the
finish_reason: "tool_calls"chunk (gemini does this via OpenRouter), which previously caused a doubleresolveToolCallin the application. - Provider mid-stream aborts surface as errors — providers that abort a stream and deliver the failure as an SSE chunk with no choices (e.g. Amazon Bedrock 504s via OpenRouter, observed on nova models) previously left an empty stream that surfaced as a confusing "model did not return a JSON envelope" error. The abort is now reported as a proper
errorevent ("provider aborted the stream: …"). - Envelope-mode repair —
prompted/envelopereplies that wrap the JSON in markdown fences or prose (common withoutresponse_formatguarantees) are extracted before parsing, and envelopes whose opening bytes were dropped in transport (observed with llama-4-scout via OpenRouter) are repaired by prepending the brace — both otherwise failed with a misleading parse error. - Clarify e2e no longer hard-fails on slow models — the chain test reports a stall gracefully instead of timing out the whole suite.
Full Changelog: v0.0.1...v0.0.2
v0.0.1
v0.0.1
What changed
Renamed the package for NPM to stop whining.
v0.0.1
[0.0.1] - 2026-08-16
Initial release: a realtime voice infrastructure SDK for TypeScript.
Added
- Pipeflow entry point — configure LLM/STT/TTS providers and persistence; create agents and conversations.
- Agents — personas with a system context and tools;
run()for standalone LLM workloads with an agentic tool-call loop (streamed deltas, tool execution, chained history). - Tools — typed
executecallbacks with JSON-schema parameters and validation;PipeflowToolalias. Tools execute in the application's backend, never inside Pipeflow. - Conversations — persistent realtime conversations with
create()/start()/participate()/listen()/stop(), synchronous audio intake, multi-participant support with aliases, and a typed event stream (audio-in,partial-transcript,turn,transcript,audio,generation,tool-call,interrupt,error,start/stop/state). - Interruption —
conversation.interrupt()plus automatic barge-in when a participant speaks while the agent is responding. - Tool calls in conversations — the orchestrator emits
tool-callevents; the application executes tools in its own backend and reports results back throughresolveToolCall(), with configurable timeouts. - Orchestrator — the realtime pipeline (audio → STT → turns → LLM → TTS → audio), sentence-buffered speech so the agent can narrate while tools run, generation epochs that discard stale results after interruption, multi-turn history rehydrated from persistence, and transcription-only mode for meetings without an agent.
- Transcription — transcript entries with speaker attribution and
toString(); transcript retrieval is independent ofstop(). - Providers — vendor-independent LLM/STT/TTS interfaces, plus adapters for DeepSeek (SSE streaming with tool calls), Deepgram (WebSocket streaming with partials/finals), and Kokoro (chunked audio).
- Persistence — provider-independent storage interface with in-memory and SQLite (
bun:sqlite) adapters, backed by a shared contract test suite. - Transport — typed message protocol with an in-memory paired transport.
- Packaging — dual ESM/CJS builds, TypeScript declarations, subpath exports (
pipeflow/providers,pipeflow/persistence,pipeflow/transport), zero runtime dependencies, and a GitHub Actions publish workflow. - Testing — a 196-test suite covering the full pipeline with fake providers: streaming, tool pause/resume and timeouts, interruption and barge-in, multi-turn clarification, persistence contracts, and provider adapters.
Known limitations
- The SQLite adapter uses
bun:sqliteand requires the Bun runtime; Node consumers should use the in-memory adapter. - Multi-participant floor management and addressing (determining when an agent is being spoken to) are planned but not yet implemented.
- The package is not yet published to npm.
Full Changelog: https://github.com/moureau-dev/pipeflow/commits/v0.0.1