@moureau/pipeflow 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +833 -0
- package/dist/cjs/agents/agent.js +2 -0
- package/dist/cjs/agents/agent.js.map +7 -0
- package/dist/cjs/agents/index.js +2 -0
- package/dist/cjs/agents/index.js.map +7 -0
- package/dist/cjs/agents/tools/index.js +2 -0
- package/dist/cjs/agents/tools/index.js.map +7 -0
- package/dist/cjs/agents/tools/tools.js +2 -0
- package/dist/cjs/agents/tools/tools.js.map +7 -0
- package/dist/cjs/conversations/conversation/conversation.js +2 -0
- package/dist/cjs/conversations/conversation/conversation.js.map +7 -0
- package/dist/cjs/conversations/conversation/index.js +2 -0
- package/dist/cjs/conversations/conversation/index.js.map +7 -0
- package/dist/cjs/conversations/conversations.js +2 -0
- package/dist/cjs/conversations/conversations.js.map +7 -0
- package/dist/cjs/conversations/index.js +2 -0
- package/dist/cjs/conversations/index.js.map +7 -0
- package/dist/cjs/conversations/orchestration/orchestrator/index.js +2 -0
- package/dist/cjs/conversations/orchestration/orchestrator/index.js.map +7 -0
- package/dist/cjs/conversations/orchestration/orchestrator/orchestrator.js +3 -0
- package/dist/cjs/conversations/orchestration/orchestrator/orchestrator.js.map +7 -0
- package/dist/cjs/conversations/transcription/index.js +2 -0
- package/dist/cjs/conversations/transcription/index.js.map +7 -0
- package/dist/cjs/conversations/transcription/transcription.js +2 -0
- package/dist/cjs/conversations/transcription/transcription.js.map +7 -0
- package/dist/cjs/conversations/types.js +2 -0
- package/dist/cjs/conversations/types.js.map +7 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/persistence/adapters/index.js +2 -0
- package/dist/cjs/persistence/adapters/index.js.map +7 -0
- package/dist/cjs/persistence/adapters/memory/index.js +2 -0
- package/dist/cjs/persistence/adapters/memory/index.js.map +7 -0
- package/dist/cjs/persistence/adapters/memory/memory.js +2 -0
- package/dist/cjs/persistence/adapters/memory/memory.js.map +7 -0
- package/dist/cjs/persistence/adapters/sqlite/index.js +2 -0
- package/dist/cjs/persistence/adapters/sqlite/index.js.map +7 -0
- package/dist/cjs/persistence/adapters/sqlite/sqlite.js +58 -0
- package/dist/cjs/persistence/adapters/sqlite/sqlite.js.map +7 -0
- package/dist/cjs/persistence/index.js +2 -0
- package/dist/cjs/persistence/index.js.map +7 -0
- package/dist/cjs/persistence/persistence.js +2 -0
- package/dist/cjs/persistence/persistence.js.map +7 -0
- package/dist/cjs/providers/index.js +2 -0
- package/dist/cjs/providers/index.js.map +7 -0
- package/dist/cjs/providers/llm/adapters/deepseek/deepseek.js +2 -0
- package/dist/cjs/providers/llm/adapters/deepseek/deepseek.js.map +7 -0
- package/dist/cjs/providers/llm/adapters/deepseek/index.js +2 -0
- package/dist/cjs/providers/llm/adapters/deepseek/index.js.map +7 -0
- package/dist/cjs/providers/llm/adapters/index.js +2 -0
- package/dist/cjs/providers/llm/adapters/index.js.map +7 -0
- package/dist/cjs/providers/llm/index.js +2 -0
- package/dist/cjs/providers/llm/index.js.map +7 -0
- package/dist/cjs/providers/llm/types.js +2 -0
- package/dist/cjs/providers/llm/types.js.map +7 -0
- package/dist/cjs/providers/shared.js +2 -0
- package/dist/cjs/providers/shared.js.map +7 -0
- package/dist/cjs/providers/stt/adapters/deepgram/deepgram.js +2 -0
- package/dist/cjs/providers/stt/adapters/deepgram/deepgram.js.map +7 -0
- package/dist/cjs/providers/stt/adapters/deepgram/index.js +2 -0
- package/dist/cjs/providers/stt/adapters/deepgram/index.js.map +7 -0
- package/dist/cjs/providers/stt/adapters/index.js +2 -0
- package/dist/cjs/providers/stt/adapters/index.js.map +7 -0
- package/dist/cjs/providers/stt/index.js +2 -0
- package/dist/cjs/providers/stt/index.js.map +7 -0
- package/dist/cjs/providers/stt/types.js +2 -0
- package/dist/cjs/providers/stt/types.js.map +7 -0
- package/dist/cjs/providers/tts/adapters/index.js +2 -0
- package/dist/cjs/providers/tts/adapters/index.js.map +7 -0
- package/dist/cjs/providers/tts/adapters/kokoro/index.js +2 -0
- package/dist/cjs/providers/tts/adapters/kokoro/index.js.map +7 -0
- package/dist/cjs/providers/tts/adapters/kokoro/kokoro.js +2 -0
- package/dist/cjs/providers/tts/adapters/kokoro/kokoro.js.map +7 -0
- package/dist/cjs/providers/tts/index.js +2 -0
- package/dist/cjs/providers/tts/index.js.map +7 -0
- package/dist/cjs/providers/tts/types.js +2 -0
- package/dist/cjs/providers/tts/types.js.map +7 -0
- package/dist/cjs/transport/adapters/index.js +2 -0
- package/dist/cjs/transport/adapters/index.js.map +7 -0
- package/dist/cjs/transport/adapters/memory/index.js +2 -0
- package/dist/cjs/transport/adapters/memory/index.js.map +7 -0
- package/dist/cjs/transport/adapters/memory/memory.js +2 -0
- package/dist/cjs/transport/adapters/memory/memory.js.map +7 -0
- package/dist/cjs/transport/index.js +2 -0
- package/dist/cjs/transport/index.js.map +7 -0
- package/dist/cjs/transport/types.js +2 -0
- package/dist/cjs/transport/types.js.map +7 -0
- package/dist/esm/agents/agent.js +2 -0
- package/dist/esm/agents/agent.js.map +7 -0
- package/dist/esm/agents/index.js +2 -0
- package/dist/esm/agents/index.js.map +7 -0
- package/dist/esm/agents/tools/index.js +2 -0
- package/dist/esm/agents/tools/index.js.map +7 -0
- package/dist/esm/agents/tools/tools.js +2 -0
- package/dist/esm/agents/tools/tools.js.map +7 -0
- package/dist/esm/conversations/conversation/conversation.js +2 -0
- package/dist/esm/conversations/conversation/conversation.js.map +7 -0
- package/dist/esm/conversations/conversation/index.js +2 -0
- package/dist/esm/conversations/conversation/index.js.map +7 -0
- package/dist/esm/conversations/conversations.js +2 -0
- package/dist/esm/conversations/conversations.js.map +7 -0
- package/dist/esm/conversations/index.js +2 -0
- package/dist/esm/conversations/index.js.map +7 -0
- package/dist/esm/conversations/orchestration/orchestrator/index.js +2 -0
- package/dist/esm/conversations/orchestration/orchestrator/index.js.map +7 -0
- package/dist/esm/conversations/orchestration/orchestrator/orchestrator.js +3 -0
- package/dist/esm/conversations/orchestration/orchestrator/orchestrator.js.map +7 -0
- package/dist/esm/conversations/transcription/index.js +2 -0
- package/dist/esm/conversations/transcription/index.js.map +7 -0
- package/dist/esm/conversations/transcription/transcription.js +2 -0
- package/dist/esm/conversations/transcription/transcription.js.map +7 -0
- package/dist/esm/conversations/types.js +2 -0
- package/dist/esm/conversations/types.js.map +7 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/esm/persistence/adapters/index.js +2 -0
- package/dist/esm/persistence/adapters/index.js.map +7 -0
- package/dist/esm/persistence/adapters/memory/index.js +2 -0
- package/dist/esm/persistence/adapters/memory/index.js.map +7 -0
- package/dist/esm/persistence/adapters/memory/memory.js +2 -0
- package/dist/esm/persistence/adapters/memory/memory.js.map +7 -0
- package/dist/esm/persistence/adapters/sqlite/index.js +2 -0
- package/dist/esm/persistence/adapters/sqlite/index.js.map +7 -0
- package/dist/esm/persistence/adapters/sqlite/sqlite.js +58 -0
- package/dist/esm/persistence/adapters/sqlite/sqlite.js.map +7 -0
- package/dist/esm/persistence/index.js +2 -0
- package/dist/esm/persistence/index.js.map +7 -0
- package/dist/esm/persistence/persistence.js +1 -0
- package/dist/esm/persistence/persistence.js.map +7 -0
- package/dist/esm/providers/index.js +2 -0
- package/dist/esm/providers/index.js.map +7 -0
- package/dist/esm/providers/llm/adapters/deepseek/deepseek.js +2 -0
- package/dist/esm/providers/llm/adapters/deepseek/deepseek.js.map +7 -0
- package/dist/esm/providers/llm/adapters/deepseek/index.js +2 -0
- package/dist/esm/providers/llm/adapters/deepseek/index.js.map +7 -0
- package/dist/esm/providers/llm/adapters/index.js +2 -0
- package/dist/esm/providers/llm/adapters/index.js.map +7 -0
- package/dist/esm/providers/llm/index.js +2 -0
- package/dist/esm/providers/llm/index.js.map +7 -0
- package/dist/esm/providers/llm/types.js +1 -0
- package/dist/esm/providers/llm/types.js.map +7 -0
- package/dist/esm/providers/shared.js +1 -0
- package/dist/esm/providers/shared.js.map +7 -0
- package/dist/esm/providers/stt/adapters/deepgram/deepgram.js +2 -0
- package/dist/esm/providers/stt/adapters/deepgram/deepgram.js.map +7 -0
- package/dist/esm/providers/stt/adapters/deepgram/index.js +2 -0
- package/dist/esm/providers/stt/adapters/deepgram/index.js.map +7 -0
- package/dist/esm/providers/stt/adapters/index.js +2 -0
- package/dist/esm/providers/stt/adapters/index.js.map +7 -0
- package/dist/esm/providers/stt/index.js +2 -0
- package/dist/esm/providers/stt/index.js.map +7 -0
- package/dist/esm/providers/stt/types.js +1 -0
- package/dist/esm/providers/stt/types.js.map +7 -0
- package/dist/esm/providers/tts/adapters/index.js +2 -0
- package/dist/esm/providers/tts/adapters/index.js.map +7 -0
- package/dist/esm/providers/tts/adapters/kokoro/index.js +2 -0
- package/dist/esm/providers/tts/adapters/kokoro/index.js.map +7 -0
- package/dist/esm/providers/tts/adapters/kokoro/kokoro.js +2 -0
- package/dist/esm/providers/tts/adapters/kokoro/kokoro.js.map +7 -0
- package/dist/esm/providers/tts/index.js +2 -0
- package/dist/esm/providers/tts/index.js.map +7 -0
- package/dist/esm/providers/tts/types.js +1 -0
- package/dist/esm/providers/tts/types.js.map +7 -0
- package/dist/esm/transport/adapters/index.js +2 -0
- package/dist/esm/transport/adapters/index.js.map +7 -0
- package/dist/esm/transport/adapters/memory/index.js +2 -0
- package/dist/esm/transport/adapters/memory/index.js.map +7 -0
- package/dist/esm/transport/adapters/memory/memory.js +2 -0
- package/dist/esm/transport/adapters/memory/memory.js.map +7 -0
- package/dist/esm/transport/index.js +2 -0
- package/dist/esm/transport/index.js.map +7 -0
- package/dist/esm/transport/types.js +1 -0
- package/dist/esm/transport/types.js.map +7 -0
- package/dist/types/agents/agent.d.ts +60 -0
- package/dist/types/agents/index.d.ts +4 -0
- package/dist/types/agents/tools/index.d.ts +2 -0
- package/dist/types/agents/tools/tools.d.ts +22 -0
- package/dist/types/conversations/conversation/conversation.d.ts +138 -0
- package/dist/types/conversations/conversation/index.d.ts +2 -0
- package/dist/types/conversations/conversations.d.ts +23 -0
- package/dist/types/conversations/index.d.ts +10 -0
- package/dist/types/conversations/orchestration/orchestrator/index.d.ts +2 -0
- package/dist/types/conversations/orchestration/orchestrator/orchestrator.d.ts +98 -0
- package/dist/types/conversations/transcription/index.d.ts +2 -0
- package/dist/types/conversations/transcription/transcription.d.ts +59 -0
- package/dist/types/conversations/types.d.ts +63 -0
- package/dist/types/index.d.ts +45 -0
- package/dist/types/persistence/adapters/index.d.ts +2 -0
- package/dist/types/persistence/adapters/memory/index.d.ts +1 -0
- package/dist/types/persistence/adapters/memory/memory.d.ts +27 -0
- package/dist/types/persistence/adapters/sqlite/index.d.ts +2 -0
- package/dist/types/persistence/adapters/sqlite/sqlite.d.ts +31 -0
- package/dist/types/persistence/index.d.ts +2 -0
- package/dist/types/persistence/persistence.d.ts +38 -0
- package/dist/types/providers/index.d.ts +6 -0
- package/dist/types/providers/llm/adapters/deepseek/deepseek.d.ts +24 -0
- package/dist/types/providers/llm/adapters/deepseek/index.d.ts +2 -0
- package/dist/types/providers/llm/adapters/index.d.ts +1 -0
- package/dist/types/providers/llm/index.d.ts +10 -0
- package/dist/types/providers/llm/types.d.ts +55 -0
- package/dist/types/providers/shared.d.ts +8 -0
- package/dist/types/providers/stt/adapters/deepgram/deepgram.d.ts +57 -0
- package/dist/types/providers/stt/adapters/deepgram/index.d.ts +2 -0
- package/dist/types/providers/stt/adapters/index.d.ts +1 -0
- package/dist/types/providers/stt/index.d.ts +2 -0
- package/dist/types/providers/stt/types.d.ts +30 -0
- package/dist/types/providers/tts/adapters/index.d.ts +1 -0
- package/dist/types/providers/tts/adapters/kokoro/index.d.ts +2 -0
- package/dist/types/providers/tts/adapters/kokoro/kokoro.d.ts +34 -0
- package/dist/types/providers/tts/index.d.ts +2 -0
- package/dist/types/providers/tts/types.d.ts +16 -0
- package/dist/types/transport/adapters/index.d.ts +1 -0
- package/dist/types/transport/adapters/memory/index.d.ts +1 -0
- package/dist/types/transport/adapters/memory/memory.d.ts +20 -0
- package/dist/types/transport/index.d.ts +2 -0
- package/dist/types/transport/types.d.ts +49 -0
- package/package.json +82 -0
package/README.md
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
# Pipeflow
|
|
2
|
+
|
|
3
|
+
Realtime voice infrastructure for TypeScript.
|
|
4
|
+
|
|
5
|
+
Pipeflow is an open-source backend SDK for building voice agents, conversational applications, meeting transcribers, Discord bots, recruiters, assistants, and other realtime audio experiences.
|
|
6
|
+
|
|
7
|
+
It handles the plumbing between audio, speech-to-text, LLMs, text-to-speech, conversations, tools, and persistence—while keeping your application in control.
|
|
8
|
+
|
|
9
|
+
> **Pipeflow is the pipe. You build what flows through it.**
|
|
10
|
+
|
|
11
|
+
## Highlights
|
|
12
|
+
|
|
13
|
+
* **Small** — under 100 kB packed, zero runtime dependencies.
|
|
14
|
+
* **Realtime by default** — audio, transcripts, and speech stream continuously, with built-in interruption and barge-in handling.
|
|
15
|
+
* **Provider-agnostic** — STT, LLM, and TTS are swappable adapters (Deepgram, DeepSeek, and Kokoro today).
|
|
16
|
+
* **Your backend stays yours** — tools and the audio transport are owned by your application; Pipeflow never executes your code.
|
|
17
|
+
|
|
18
|
+
## Status
|
|
19
|
+
|
|
20
|
+
🚧 **Early development**
|
|
21
|
+
|
|
22
|
+
The API is evolving and should be considered experimental.
|
|
23
|
+
|
|
24
|
+
## Philosophy
|
|
25
|
+
|
|
26
|
+
Pipeflow has four core concepts:
|
|
27
|
+
|
|
28
|
+
* **Agent** — intelligence, context, and tools.
|
|
29
|
+
* **Conversation** — a persistent realtime conversation and its participants.
|
|
30
|
+
* **Tool** — application capabilities executed by your backend.
|
|
31
|
+
* **Provider** — an implementation of STT, LLM, or TTS.
|
|
32
|
+
|
|
33
|
+
The goal is to keep these concerns independent.
|
|
34
|
+
|
|
35
|
+
An agent does not inherently own a conversation. A conversation does not require an agent. A provider should not leak into your application logic.
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
Pipeflow
|
|
39
|
+
|
|
40
|
+
┌──────────────┐
|
|
41
|
+
│ Agent │
|
|
42
|
+
│ context │
|
|
43
|
+
│ tools │
|
|
44
|
+
└──────┬───────┘
|
|
45
|
+
│
|
|
46
|
+
▼
|
|
47
|
+
┌──────────────┐
|
|
48
|
+
│ Conversation │
|
|
49
|
+
│ │
|
|
50
|
+
│ participants │
|
|
51
|
+
│ turns │
|
|
52
|
+
│ audio │
|
|
53
|
+
│ interruption │
|
|
54
|
+
└──────┬───────┘
|
|
55
|
+
│
|
|
56
|
+
▼
|
|
57
|
+
┌─────────────────────────┐
|
|
58
|
+
│ Orchestrator │
|
|
59
|
+
└──────┬──────┬──────┬────┘
|
|
60
|
+
│ │ │
|
|
61
|
+
STT LLM TTS
|
|
62
|
+
│ │ │
|
|
63
|
+
▼ ▼ ▼
|
|
64
|
+
Deepgram DeepSeek Kokoro
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bun add pipeflow
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The package is not published to npm yet. Until then, install from the
|
|
74
|
+
repository (`bun add pipeflow`) or build from
|
|
75
|
+
source — see [Development](#development).
|
|
76
|
+
|
|
77
|
+
## Basic voice agent
|
|
78
|
+
|
|
79
|
+
Create an agent:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { Pipeflow, Orchestrator } from "pipeflow";
|
|
83
|
+
import { DeepSeekLLM, DeepgramSTT, KokoroTTS } from "pipeflow/providers";
|
|
84
|
+
|
|
85
|
+
// Providers are configured explicitly with their own credentials —
|
|
86
|
+
// Pipeflow itself does not hold an API key.
|
|
87
|
+
const stt = new DeepgramSTT({ apiKey: process.env.DEEPGRAM_API_KEY });
|
|
88
|
+
const tts = new KokoroTTS();
|
|
89
|
+
|
|
90
|
+
const pipeflow = new Pipeflow({
|
|
91
|
+
llm: new DeepSeekLLM({ apiKey: process.env.DEEPSEEK_API_KEY }),
|
|
92
|
+
stt,
|
|
93
|
+
tts,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const jarvis = pipeflow.agent({
|
|
97
|
+
name: "Jarvis",
|
|
98
|
+
|
|
99
|
+
context: `
|
|
100
|
+
You are Jarvis, a helpful voice assistant.
|
|
101
|
+
Keep your responses concise and conversational.
|
|
102
|
+
`,
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Create a conversation:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const conversation = await pipeflow.conversations.create({
|
|
110
|
+
agents: [jarvis],
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Attach the orchestrator — it runs the realtime pipeline (STT → turns → LLM → TTS) for the conversation:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const orchestrator = new Orchestrator({
|
|
118
|
+
conversation,
|
|
119
|
+
agent: jarvis,
|
|
120
|
+
stt,
|
|
121
|
+
tts,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await orchestrator.start();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Start the conversation and add a participant:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
conversation.start();
|
|
131
|
+
|
|
132
|
+
await conversation.participate({
|
|
133
|
+
userId: "alice",
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Feed it audio as it arrives:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
voice.onAudio((audio) => {
|
|
141
|
+
conversation.listen({
|
|
142
|
+
userId: "alice",
|
|
143
|
+
audio,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Listen for generated audio:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
conversation.on("audio", ({ audio }) => {
|
|
152
|
+
voice.play(audio);
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
When finished:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
await conversation.stop();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The application owns the audio transport. Pipeflow handles the realtime voice pipeline.
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
Your application
|
|
166
|
+
│
|
|
167
|
+
│ audio chunks
|
|
168
|
+
▼
|
|
169
|
+
Pipeflow
|
|
170
|
+
│
|
|
171
|
+
├── STT
|
|
172
|
+
├── conversation orchestration
|
|
173
|
+
├── LLM
|
|
174
|
+
└── TTS
|
|
175
|
+
│
|
|
176
|
+
│ audio chunks
|
|
177
|
+
▼
|
|
178
|
+
Your application
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Conversations
|
|
182
|
+
|
|
183
|
+
A conversation is the persistent entity representing a realtime interaction.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const conversation =
|
|
187
|
+
await pipeflow.conversations.create({
|
|
188
|
+
agents: [jarvis],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
console.log(conversation.id);
|
|
192
|
+
|
|
193
|
+
conversation.start();
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Creation and realtime execution are deliberately separate.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
create() // creates the persistent conversation
|
|
200
|
+
start() // moves the conversation into the started state
|
|
201
|
+
participate() // adds participants
|
|
202
|
+
listen() // sends audio
|
|
203
|
+
stop() // finalizes the realtime session
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Realtime processing itself is the orchestrator's job: once attached, it
|
|
207
|
+
subscribes to `audio-in` events, runs the STT/LLM/TTS pipeline, and pushes
|
|
208
|
+
generated audio, turns, transcripts, and tool calls back through conversation
|
|
209
|
+
events.
|
|
210
|
+
|
|
211
|
+
`listen()` is intentionally synchronous:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
conversation.listen({
|
|
215
|
+
userId,
|
|
216
|
+
audio,
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
It means:
|
|
221
|
+
|
|
222
|
+
> Send this audio packet.
|
|
223
|
+
|
|
224
|
+
It does not mean:
|
|
225
|
+
|
|
226
|
+
> Wait for this utterance to finish.
|
|
227
|
+
|
|
228
|
+
This makes it suitable for high-frequency realtime audio streams.
|
|
229
|
+
|
|
230
|
+
## Participants
|
|
231
|
+
|
|
232
|
+
Participants can be added individually:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
await conversation.participate({
|
|
236
|
+
userId: "alice",
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Or in batches:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
await conversation.participate([
|
|
244
|
+
{ userId: "alice" },
|
|
245
|
+
|
|
246
|
+
{
|
|
247
|
+
userId: "bob",
|
|
248
|
+
aliases: ["robert", "rob"],
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
{
|
|
252
|
+
userId: "charlie",
|
|
253
|
+
aliases: ["charles"],
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Participant information can be used for speaker attribution, conversation history, addressing, and multi-participant floor management.
|
|
259
|
+
|
|
260
|
+
## Interruption
|
|
261
|
+
|
|
262
|
+
Voice conversations need to feel immediate.
|
|
263
|
+
|
|
264
|
+
If an agent is speaking and a participant starts talking, Pipeflow can interrupt
|
|
265
|
+
the current output immediately rather than waiting for the entire utterance to
|
|
266
|
+
be transcribed.
|
|
267
|
+
|
|
268
|
+
Interruptions can be triggered by the application — or automatically: the
|
|
269
|
+
orchestrator detects when a participant starts speaking while the agent is
|
|
270
|
+
responding (barge-in).
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
conversation.interrupt(); // stops TTS, cancels the current generation
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The interrupting speech keeps flowing into STT and becomes the next turn.
|
|
277
|
+
|
|
278
|
+
Conceptually:
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
Agent is speaking
|
|
282
|
+
│
|
|
283
|
+
▼
|
|
284
|
+
Participant starts speaking
|
|
285
|
+
│
|
|
286
|
+
├── stop TTS
|
|
287
|
+
├── cancel current generation
|
|
288
|
+
└── continue receiving speech
|
|
289
|
+
│
|
|
290
|
+
▼
|
|
291
|
+
STT
|
|
292
|
+
│
|
|
293
|
+
▼
|
|
294
|
+
new conversation turn
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
This keeps interactions responsive even when the user interrupts the agent halfway through a sentence.
|
|
298
|
+
|
|
299
|
+
## Conversation events
|
|
300
|
+
|
|
301
|
+
The conversation emits a typed event stream the application can subscribe to:
|
|
302
|
+
|
|
303
|
+
* `audio-in` — raw audio fed in via `listen()`
|
|
304
|
+
* `partial-transcript` — live STT partials (captions)
|
|
305
|
+
* `turn` — a finalized participant turn
|
|
306
|
+
* `transcript` — a transcript entry
|
|
307
|
+
* `audio` — generated audio to play
|
|
308
|
+
* `generation` — an agent generation
|
|
309
|
+
* `tool-call` / `tool-call-result` — tool execution round trips
|
|
310
|
+
* `interrupt` — an interruption occurred
|
|
311
|
+
* `error` — a provider failure
|
|
312
|
+
* `start` / `stop` / `state` — lifecycle
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
conversation.on("audio", ({ audio }) => voice.play(audio));
|
|
316
|
+
conversation.on("partial-transcript", ({ text }) => captions.update(text));
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Multi-participant conversations
|
|
320
|
+
|
|
321
|
+
Conversations can contain multiple participants and agents.
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
const conversation =
|
|
325
|
+
await pipeflow.conversations.create({
|
|
326
|
+
agents: [jarvis],
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await conversation.participate([
|
|
330
|
+
{ userId: "alice" },
|
|
331
|
+
{ userId: "bob" },
|
|
332
|
+
{ userId: "charlie" },
|
|
333
|
+
]);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The conversation orchestrator maintains conversational state such as:
|
|
337
|
+
|
|
338
|
+
* active turns
|
|
339
|
+
* participant identity
|
|
340
|
+
* interruptions
|
|
341
|
+
* agent generations
|
|
342
|
+
* transcript state
|
|
343
|
+
|
|
344
|
+
In a one-to-one conversation, speech is treated as an interaction: every
|
|
345
|
+
finalized participant turn produces an agent generation.
|
|
346
|
+
|
|
347
|
+
Multi-participant floor management and addressing (determining when an agent is
|
|
348
|
+
being spoken to using participant context, agent names, aliases, and floor
|
|
349
|
+
rules) are on the roadmap. A wake word is not intended to be a fundamental
|
|
350
|
+
requirement.
|
|
351
|
+
|
|
352
|
+
## Agents
|
|
353
|
+
|
|
354
|
+
An agent defines an AI persona and its capabilities.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
const jarvis = pipeflow.agent({
|
|
358
|
+
name: "Jarvis",
|
|
359
|
+
|
|
360
|
+
context: `
|
|
361
|
+
You are Jarvis.
|
|
362
|
+
You are concise, helpful, and conversational.
|
|
363
|
+
`,
|
|
364
|
+
|
|
365
|
+
tools: [
|
|
366
|
+
getWeather,
|
|
367
|
+
searchCalendar,
|
|
368
|
+
],
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Agents can also be run independently of conversations:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
const result = await jarvis.run({
|
|
376
|
+
prompt: "Explain how a neural network works.",
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
console.log(result.text);
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
This is useful for ordinary LLM workloads where realtime audio and conversation state are not required.
|
|
383
|
+
|
|
384
|
+
### Conversational agents
|
|
385
|
+
|
|
386
|
+
When an agent is attached to a conversation, it can take conversational turns as part of the realtime orchestration.
|
|
387
|
+
|
|
388
|
+
The conversation owns the runtime.
|
|
389
|
+
|
|
390
|
+
The agent owns the intelligence.
|
|
391
|
+
|
|
392
|
+
```text
|
|
393
|
+
Agent
|
|
394
|
+
├── context
|
|
395
|
+
└── tools
|
|
396
|
+
|
|
397
|
+
Conversation
|
|
398
|
+
├── participants
|
|
399
|
+
├── turns
|
|
400
|
+
├── audio
|
|
401
|
+
├── interruption
|
|
402
|
+
└── orchestration
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Tools
|
|
406
|
+
|
|
407
|
+
Tools expose capabilities from your application to an agent.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
const getWeather = new PipeflowTool({
|
|
411
|
+
name: "get_weather",
|
|
412
|
+
|
|
413
|
+
description: "Get the current weather for a city.",
|
|
414
|
+
|
|
415
|
+
execute: async ({ city }) => {
|
|
416
|
+
return weatherService.getCurrent(city);
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Then:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
const jarvis = pipeflow.agent({
|
|
425
|
+
name: "Jarvis",
|
|
426
|
+
|
|
427
|
+
context: "You are a helpful assistant.",
|
|
428
|
+
|
|
429
|
+
tools: [getWeather],
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Tools execute in **your backend**.
|
|
434
|
+
|
|
435
|
+
Pipeflow does not execute arbitrary application code.
|
|
436
|
+
|
|
437
|
+
```text
|
|
438
|
+
Pipeflow
|
|
439
|
+
│
|
|
440
|
+
LLM requests tool
|
|
441
|
+
│
|
|
442
|
+
▼
|
|
443
|
+
┌─────────────────┐
|
|
444
|
+
│ Your application │
|
|
445
|
+
│ │
|
|
446
|
+
│ execute() │
|
|
447
|
+
└────────┬────────┘
|
|
448
|
+
│
|
|
449
|
+
▼
|
|
450
|
+
tool result
|
|
451
|
+
│
|
|
452
|
+
▼
|
|
453
|
+
LLM
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
This allows tools to access your database, APIs, Discord bot, business logic, filesystem, or anything else your application controls.
|
|
457
|
+
|
|
458
|
+
In a conversation, tool calls never execute inside Pipeflow. The orchestrator
|
|
459
|
+
emits a `tool-call` event with the requested tool and its arguments; your
|
|
460
|
+
backend executes it and reports back:
|
|
461
|
+
|
|
462
|
+
```ts
|
|
463
|
+
conversation.on("tool-call", async ({ call }) => {
|
|
464
|
+
const result = await myBackend.execute(call.name, call.arguments);
|
|
465
|
+
|
|
466
|
+
conversation.resolveToolCall({
|
|
467
|
+
id: call.id,
|
|
468
|
+
result,
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
The agent's narration continues while the tool runs, and the generation resumes
|
|
474
|
+
with the tool result once it is resolved.
|
|
475
|
+
|
|
476
|
+
## Meeting transcription
|
|
477
|
+
|
|
478
|
+
A conversation does not require an agent.
|
|
479
|
+
|
|
480
|
+
This makes Pipeflow useful as a realtime transcription primitive.
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
const conversation =
|
|
484
|
+
await pipeflow.conversations.create();
|
|
485
|
+
|
|
486
|
+
// Without an agent, the orchestrator runs in transcription-only mode:
|
|
487
|
+
// audio in, turns and transcripts out.
|
|
488
|
+
const orchestrator = new Orchestrator({
|
|
489
|
+
conversation,
|
|
490
|
+
stt,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
await orchestrator.start();
|
|
494
|
+
|
|
495
|
+
conversation.start();
|
|
496
|
+
|
|
497
|
+
await conversation.participate([
|
|
498
|
+
{ userId: "alice" },
|
|
499
|
+
{ userId: "bob", aliases: ["robert"] },
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
discordVoice.onAudio((userId, audio) => {
|
|
503
|
+
conversation.listen({
|
|
504
|
+
userId,
|
|
505
|
+
audio,
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
await conversation.stop();
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Retrieve the transcript afterward:
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
const transcript =
|
|
516
|
+
await pipeflow.conversations.transcript(
|
|
517
|
+
conversation.id,
|
|
518
|
+
);
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Transcript retrieval is separate from `stop()` so that ending a conversation does not require loading an arbitrarily large transcript into memory.
|
|
522
|
+
|
|
523
|
+
Pagination can be supported for long conversations.
|
|
524
|
+
|
|
525
|
+
## Meeting summaries
|
|
526
|
+
|
|
527
|
+
A meeting summary can simply be another agent task.
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
const notetaker = pipeflow.agent({
|
|
531
|
+
name: "Meeting Notetaker",
|
|
532
|
+
|
|
533
|
+
context: `
|
|
534
|
+
You are a meeting notetaker.
|
|
535
|
+
|
|
536
|
+
Produce concise notes containing:
|
|
537
|
+
- summary
|
|
538
|
+
- decisions
|
|
539
|
+
- action items
|
|
540
|
+
- unresolved questions
|
|
541
|
+
`,
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Retrieve the transcript:
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
const transcript =
|
|
549
|
+
await pipeflow.conversations.transcript(
|
|
550
|
+
conversation.id,
|
|
551
|
+
);
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Then run the agent:
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
const result = await notetaker.run({
|
|
558
|
+
prompt: `
|
|
559
|
+
The meeting transcription is:
|
|
560
|
+
|
|
561
|
+
${transcript.join("\n")}
|
|
562
|
+
|
|
563
|
+
Produce the meeting notes.
|
|
564
|
+
`,
|
|
565
|
+
});
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
Or the notetaker can retrieve the transcript itself through a tool:
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
const getTranscript = new PipeflowTool({
|
|
572
|
+
name: "get_transcript",
|
|
573
|
+
|
|
574
|
+
description: "Retrieve the meeting transcript.",
|
|
575
|
+
|
|
576
|
+
execute: async () => {
|
|
577
|
+
return pipeflow.conversations.transcript(
|
|
578
|
+
conversation.id,
|
|
579
|
+
);
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
The agent does not receive special access to conversations.
|
|
585
|
+
|
|
586
|
+
If it needs conversation data, **a tool provides that capability**.
|
|
587
|
+
|
|
588
|
+
## Providers
|
|
589
|
+
|
|
590
|
+
Pipeflow separates provider interfaces from provider implementations.
|
|
591
|
+
|
|
592
|
+
```text
|
|
593
|
+
providers/
|
|
594
|
+
├── llm/
|
|
595
|
+
│ ├── types.ts
|
|
596
|
+
│ └── adapters/
|
|
597
|
+
│ └── deepseek/
|
|
598
|
+
│
|
|
599
|
+
├── stt/
|
|
600
|
+
│ ├── types.ts
|
|
601
|
+
│ └── adapters/
|
|
602
|
+
│ └── deepgram/
|
|
603
|
+
│
|
|
604
|
+
└── tts/
|
|
605
|
+
├── types.ts
|
|
606
|
+
└── adapters/
|
|
607
|
+
└── kokoro/
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
The orchestrator works against provider interfaces rather than directly against vendor APIs.
|
|
611
|
+
|
|
612
|
+
This makes it possible to replace providers without changing the conversation layer.
|
|
613
|
+
|
|
614
|
+
### Current providers
|
|
615
|
+
|
|
616
|
+
The project currently contains adapters for:
|
|
617
|
+
|
|
618
|
+
* **STT:** Deepgram
|
|
619
|
+
* **LLM:** DeepSeek
|
|
620
|
+
* **TTS:** Kokoro
|
|
621
|
+
|
|
622
|
+
Providers are configured with their own credentials — Pipeflow itself does
|
|
623
|
+
not hold an API key.
|
|
624
|
+
|
|
625
|
+
Provider availability and configuration are evolving during early development.
|
|
626
|
+
|
|
627
|
+
## Architecture
|
|
628
|
+
|
|
629
|
+
Pipeflow is designed around a small number of independent layers.
|
|
630
|
+
|
|
631
|
+
```text
|
|
632
|
+
src/
|
|
633
|
+
├── agents/
|
|
634
|
+
├── conversations/
|
|
635
|
+
│ ├── conversation/
|
|
636
|
+
│ ├── orchestration/
|
|
637
|
+
│ └── transcription/
|
|
638
|
+
├── persistence/
|
|
639
|
+
│ └── adapters/
|
|
640
|
+
├── providers/
|
|
641
|
+
│ ├── llm/
|
|
642
|
+
│ ├── stt/
|
|
643
|
+
│ └── tts/
|
|
644
|
+
└── transport/
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Conversation
|
|
648
|
+
|
|
649
|
+
Public realtime conversation API and lifecycle.
|
|
650
|
+
|
|
651
|
+
### Orchestration
|
|
652
|
+
|
|
653
|
+
The state machine coordinating:
|
|
654
|
+
|
|
655
|
+
* speech
|
|
656
|
+
* transcription
|
|
657
|
+
* turns
|
|
658
|
+
* floor state
|
|
659
|
+
* agent generation
|
|
660
|
+
* interruptions
|
|
661
|
+
* tools
|
|
662
|
+
* TTS
|
|
663
|
+
|
|
664
|
+
### Transcription
|
|
665
|
+
|
|
666
|
+
Conversation transcription and transcript state.
|
|
667
|
+
|
|
668
|
+
### Providers
|
|
669
|
+
|
|
670
|
+
Vendor-independent interfaces and provider adapters.
|
|
671
|
+
|
|
672
|
+
### Persistence
|
|
673
|
+
|
|
674
|
+
Persistence abstractions with adapters such as SQLite and in-memory storage.
|
|
675
|
+
|
|
676
|
+
### Transport
|
|
677
|
+
|
|
678
|
+
Realtime communication between Pipeflow and the application using Pipeflow.
|
|
679
|
+
|
|
680
|
+
## Persistence
|
|
681
|
+
|
|
682
|
+
Pipeflow separates persistence from the conversation domain.
|
|
683
|
+
|
|
684
|
+
The project currently includes:
|
|
685
|
+
|
|
686
|
+
```text
|
|
687
|
+
persistence/
|
|
688
|
+
└── adapters/
|
|
689
|
+
├── memory/
|
|
690
|
+
└── sqlite/
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
The in-memory adapter is useful for tests and development.
|
|
694
|
+
|
|
695
|
+
SQLite provides a lightweight persistent backend suitable for local applications and early deployments.
|
|
696
|
+
|
|
697
|
+
The persistence interface is intentionally provider-independent so other storage implementations can be added later.
|
|
698
|
+
|
|
699
|
+
## Realtime architecture
|
|
700
|
+
|
|
701
|
+
A typical voice interaction looks like:
|
|
702
|
+
|
|
703
|
+
```text
|
|
704
|
+
Audio input
|
|
705
|
+
│
|
|
706
|
+
▼
|
|
707
|
+
Speech detection
|
|
708
|
+
│
|
|
709
|
+
▼
|
|
710
|
+
STT stream
|
|
711
|
+
│
|
|
712
|
+
partial transcript
|
|
713
|
+
│
|
|
714
|
+
▼
|
|
715
|
+
Conversation state
|
|
716
|
+
│
|
|
717
|
+
turn completed
|
|
718
|
+
│
|
|
719
|
+
▼
|
|
720
|
+
LLM stream
|
|
721
|
+
│
|
|
722
|
+
token stream
|
|
723
|
+
│
|
|
724
|
+
▼
|
|
725
|
+
TTS stream
|
|
726
|
+
│
|
|
727
|
+
audio chunks
|
|
728
|
+
│
|
|
729
|
+
▼
|
|
730
|
+
Application
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Everything happens as a stream.
|
|
734
|
+
|
|
735
|
+
Pipeflow does not wait for a complete recording before beginning transcription, nor does it wait for a complete LLM response before beginning TTS.
|
|
736
|
+
|
|
737
|
+
The intended flow is:
|
|
738
|
+
|
|
739
|
+
```text
|
|
740
|
+
audio
|
|
741
|
+
↓
|
|
742
|
+
partial STT
|
|
743
|
+
↓
|
|
744
|
+
turn detection
|
|
745
|
+
↓
|
|
746
|
+
LLM streaming
|
|
747
|
+
↓
|
|
748
|
+
TTS streaming
|
|
749
|
+
↓
|
|
750
|
+
audio
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
This allows the system to begin producing speech as early as possible.
|
|
754
|
+
|
|
755
|
+
## Open source
|
|
756
|
+
|
|
757
|
+
Pipeflow is open source.
|
|
758
|
+
|
|
759
|
+
The project is designed to make realtime voice infrastructure accessible without requiring applications to implement their own orchestration layer.
|
|
760
|
+
|
|
761
|
+
The provider layer is intentionally modular so applications can choose between hosted and self-hosted services.
|
|
762
|
+
|
|
763
|
+
## Development
|
|
764
|
+
|
|
765
|
+
Clone the repository and install dependencies:
|
|
766
|
+
|
|
767
|
+
```bash
|
|
768
|
+
git clone git@github.com:moureau-dev/pipeflow.git
|
|
769
|
+
cd pipeflow
|
|
770
|
+
bun install
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
Run tests:
|
|
774
|
+
|
|
775
|
+
```bash
|
|
776
|
+
bun test
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
Build and type-check:
|
|
780
|
+
|
|
781
|
+
```bash
|
|
782
|
+
bun run build # transpile to dist/esm + dist/cjs and emit dist/types
|
|
783
|
+
bun run typecheck
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
The project uses Bun and TypeScript.
|
|
787
|
+
|
|
788
|
+
## Design principles
|
|
789
|
+
|
|
790
|
+
### Realtime first
|
|
791
|
+
|
|
792
|
+
Audio is streamed continuously rather than processed as completed recordings.
|
|
793
|
+
|
|
794
|
+
### Provider agnostic
|
|
795
|
+
|
|
796
|
+
STT, LLM, and TTS providers are adapters, not application-level concepts.
|
|
797
|
+
|
|
798
|
+
### Application-owned tools
|
|
799
|
+
|
|
800
|
+
Your application executes your tools.
|
|
801
|
+
|
|
802
|
+
### Conversations are persistent entities
|
|
803
|
+
|
|
804
|
+
A realtime `Conversation` instance is a runtime handle to a persistent conversation.
|
|
805
|
+
|
|
806
|
+
### Agents are independent
|
|
807
|
+
|
|
808
|
+
An agent can participate in a conversation or simply be invoked with `run()`.
|
|
809
|
+
|
|
810
|
+
### Explicit boundaries
|
|
811
|
+
|
|
812
|
+
Pipeflow owns orchestration.
|
|
813
|
+
|
|
814
|
+
Your application owns application logic.
|
|
815
|
+
|
|
816
|
+
Providers own their respective AI services.
|
|
817
|
+
|
|
818
|
+
### Small public API
|
|
819
|
+
|
|
820
|
+
The core API should remain centered around:
|
|
821
|
+
|
|
822
|
+
```text
|
|
823
|
+
Pipeflow
|
|
824
|
+
Agent
|
|
825
|
+
Conversation
|
|
826
|
+
Tool
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
Everything else should remain replaceable implementation detail for as long as possible.
|
|
830
|
+
|
|
831
|
+
## License
|
|
832
|
+
|
|
833
|
+
See [LICENSE](LICENSE).
|