@kuralle-syrinx/cf-agents 4.4.0 → 4.5.0
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/dist/build-session.d.ts +105 -0
- package/dist/build-session.d.ts.map +1 -0
- package/dist/build-session.js +74 -0
- package/dist/build-session.js.map +1 -0
- package/dist/connection-socket.d.ts +41 -0
- package/dist/connection-socket.d.ts.map +1 -0
- package/dist/connection-socket.js +90 -0
- package/dist/connection-socket.js.map +1 -0
- package/dist/durable-history.d.ts +17 -0
- package/dist/durable-history.d.ts.map +1 -0
- package/dist/durable-history.js +58 -0
- package/dist/durable-history.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/r2-recorder.d.ts +28 -0
- package/dist/r2-recorder.d.ts.map +1 -0
- package/dist/r2-recorder.js +303 -0
- package/dist/r2-recorder.js.map +1 -0
- package/dist/real-agent.compile-check.d.ts +12 -0
- package/dist/real-agent.compile-check.d.ts.map +1 -0
- package/dist/real-agent.compile-check.js +35 -0
- package/dist/real-agent.compile-check.js.map +1 -0
- package/dist/with-voice.d.ts +150 -0
- package/dist/with-voice.d.ts.map +1 -0
- package/dist/with-voice.js +359 -0
- package/dist/with-voice.js.map +1 -0
- package/package.json +26 -13
- package/src/build-session.test.ts +125 -0
- package/src/connection-socket.test.ts +114 -0
- package/src/r2-recorder.test.ts +185 -0
- package/src/with-voice.test.ts +798 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// withVoice(Agent) — adds a Syrinx voice pipeline (realtime OR cascaded) to a
|
|
4
|
+
// Cloudflare `agents` SDK Agent.
|
|
5
|
+
//
|
|
6
|
+
// Design (issue #7): a mixin OVER the Agent, not a raw Durable Object. It reuses
|
|
7
|
+
// the Agent's native hibernation, `keepAlive()` lease, `Connection`, and SQL —
|
|
8
|
+
// it does not reimplement them. Syrinx is the engine: each connection is handed
|
|
9
|
+
// to the published `runVoiceEdgeWebSocketConnection(socket, request, options)`
|
|
10
|
+
// over the Agent's `Connection` wrapped as a `ManagedSocket`. The agent's own
|
|
11
|
+
// kuralle runtime is the brain by default (`fromKuralleRuntime(this.runtime)`),
|
|
12
|
+
// so an existing agent gets voice with zero brain re-wiring.
|
|
13
|
+
//
|
|
14
|
+
// Lifecycle wrap (capture-and-patch of onConnect/onMessage/onClose) mirrors
|
|
15
|
+
// `@cloudflare/voice`'s withVoice (voice.ts, MIT, © Cloudflare). Unlike that
|
|
16
|
+
// mixin, this one does not implement the voice protocol itself — it delegates the
|
|
17
|
+
// whole connection to Syrinx's edge runner.
|
|
18
|
+
import { runVoiceEdgeWebSocketConnection, } from "@kuralle-syrinx/server-websocket/edge";
|
|
19
|
+
import { runTwilioEdgeWebSocketConnection } from "@kuralle-syrinx/server-websocket/edge-twilio";
|
|
20
|
+
import { runTelnyxEdgeWebSocketConnection } from "@kuralle-syrinx/server-websocket/edge-telnyx";
|
|
21
|
+
import { InMemorySessionStore } from "@kuralle-syrinx/server-websocket/session-store";
|
|
22
|
+
import { fromKuralleRuntime } from "@kuralle-syrinx/kuralle";
|
|
23
|
+
import { connectionManagedSocket, } from "./connection-socket.js";
|
|
24
|
+
import { buildVoiceSession, } from "./build-session.js";
|
|
25
|
+
import { SqliteReasonerSessionStore } from "./durable-history.js";
|
|
26
|
+
/** Bound on the durable realtime transcript (12 turns), mirroring ReasoningBridge's default window. */
|
|
27
|
+
const MAX_DURABLE_HISTORY_MESSAGES = 24;
|
|
28
|
+
function isKuralleRuntime(value) {
|
|
29
|
+
return (typeof value === "object" &&
|
|
30
|
+
value !== null &&
|
|
31
|
+
typeof value.run === "function");
|
|
32
|
+
}
|
|
33
|
+
export function withVoice(Base, options) {
|
|
34
|
+
class VoiceAgentMixin extends Base {
|
|
35
|
+
// One session store per DO instance — resumes a dropped connection within
|
|
36
|
+
// the agent's lifetime (keyed by the stable session id).
|
|
37
|
+
#store = new InMemorySessionStore();
|
|
38
|
+
#controllers = new Map();
|
|
39
|
+
#keepAliveDispose = new Map();
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mixin constructor must accept any args
|
|
41
|
+
constructor(...args) {
|
|
42
|
+
super(...args);
|
|
43
|
+
const host = this;
|
|
44
|
+
const onConnect = host.onConnect?.bind(this);
|
|
45
|
+
const onMessage = host.onMessage?.bind(this);
|
|
46
|
+
const onClose = host.onClose?.bind(this);
|
|
47
|
+
host.onConnect = (connection, ctx) => {
|
|
48
|
+
this.#startVoice(connection, ctx);
|
|
49
|
+
return onConnect?.(connection, ctx);
|
|
50
|
+
};
|
|
51
|
+
host.onMessage = (connection, message) => {
|
|
52
|
+
const controller = this.#controllers.get(connection.id);
|
|
53
|
+
if (controller) {
|
|
54
|
+
controller.message(message);
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return onMessage?.(connection, message);
|
|
58
|
+
};
|
|
59
|
+
host.onClose = (connection, code, reason, wasClean) => {
|
|
60
|
+
this.#endVoice(connection, code, reason);
|
|
61
|
+
return onClose?.(connection, code, reason, wasClean);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
forceEndVoice(connection) {
|
|
65
|
+
// Pump close so the edge runner tears down and the lease releases, then
|
|
66
|
+
// close the underlying socket.
|
|
67
|
+
this.#controllers.get(connection.id)?.close(1000, "force_end");
|
|
68
|
+
try {
|
|
69
|
+
connection.close(1000, "force_end");
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* already closing */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
#startVoice(connection, ctx) {
|
|
76
|
+
const host = this;
|
|
77
|
+
const env = host.env;
|
|
78
|
+
const request = ctx.request;
|
|
79
|
+
const sessionId = this.#resolveSessionId(request);
|
|
80
|
+
// The agents `Connection.send` is runtime-compatible with a ManagedSocket
|
|
81
|
+
// (workerd's WebSocket.send accepts string/ArrayBuffer/ArrayBufferView), but
|
|
82
|
+
// its lib type is nominally stricter (ArrayBuffer- vs ArrayBufferLike-backed
|
|
83
|
+
// views). Bridge that single boundary structurally.
|
|
84
|
+
const voiceConnection = connection;
|
|
85
|
+
const { socket, controller } = connectionManagedSocket(voiceConnection);
|
|
86
|
+
this.#controllers.set(connection.id, controller);
|
|
87
|
+
// Release the keepAlive lease + drop the controller on ANY close — a client
|
|
88
|
+
// close (pumped via the Agent's onClose hook) OR an edge-runner-initiated
|
|
89
|
+
// socket.dispose() (idle / max-duration / startup failure). The socket fires
|
|
90
|
+
// its close handlers in both cases, so this does not depend on the platform
|
|
91
|
+
// delivering a server-initiated onClose.
|
|
92
|
+
socket.onClose(() => this.#releaseConnection(connection.id));
|
|
93
|
+
// Hold a keepAlive lease for the duration of the call so the DO is not
|
|
94
|
+
// evicted mid-conversation.
|
|
95
|
+
void this.keepAlive()
|
|
96
|
+
.then((dispose) => {
|
|
97
|
+
if (this.#controllers.has(connection.id)) {
|
|
98
|
+
this.#keepAliveDispose.set(connection.id, dispose);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Connection already closed before the lease resolved.
|
|
102
|
+
dispose();
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
.catch(() => {
|
|
106
|
+
// keepAlive() failed — the call still runs (the open socket keeps the
|
|
107
|
+
// isolate live), it is just not protected from idle eviction.
|
|
108
|
+
});
|
|
109
|
+
// Both runners assemble the session the same way — pipeline + (resolved) reasoner.
|
|
110
|
+
const createSession = async () => {
|
|
111
|
+
// G4 durable session state: load prior context from DO-SQLite, expose it to the
|
|
112
|
+
// factories via ctx.resume, and wire persistence per pipeline kind.
|
|
113
|
+
const durable = options.durableHistory !== false ? this.#durableStore() : undefined;
|
|
114
|
+
const liveHistory = durable ? [...durable.load(sessionId)] : [];
|
|
115
|
+
const providerHandle = durable?.loadResumeHandle(sessionId);
|
|
116
|
+
const ctx = {
|
|
117
|
+
sessionId,
|
|
118
|
+
...(durable
|
|
119
|
+
? {
|
|
120
|
+
resume: {
|
|
121
|
+
history: () => liveHistory
|
|
122
|
+
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
123
|
+
.map((m) => ({ role: m.role, content: m.content })),
|
|
124
|
+
...(providerHandle ? { providerHandle } : {}),
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
: {}),
|
|
128
|
+
};
|
|
129
|
+
const wiring = {
|
|
130
|
+
...(options.delayCueAfterMs !== undefined ? { delayCueAfterMs: options.delayCueAfterMs } : {}),
|
|
131
|
+
...(options.idleTimeout !== undefined ? { idleTimeout: options.idleTimeout } : {}),
|
|
132
|
+
...(durable
|
|
133
|
+
? options.pipeline.kind === "realtime"
|
|
134
|
+
? { contextProvider: () => liveHistory }
|
|
135
|
+
: { reasonerSessionStore: durable }
|
|
136
|
+
: {}),
|
|
137
|
+
};
|
|
138
|
+
const reasoner = await this.#resolveReasoner(env, ctx);
|
|
139
|
+
const session = buildVoiceSession(options.pipeline, env, reasoner, ctx, wiring);
|
|
140
|
+
// Realtime pipelines have no ReasoningBridge to own history — record the
|
|
141
|
+
// transcript from the bus and persist the bounded snapshot per turn. The
|
|
142
|
+
// cascaded path persists inside ReasoningBridge instead (heard-prefix aware).
|
|
143
|
+
if (durable && options.pipeline.kind === "realtime") {
|
|
144
|
+
const persist = () => {
|
|
145
|
+
if (liveHistory.length > MAX_DURABLE_HISTORY_MESSAGES) {
|
|
146
|
+
liveHistory.splice(0, liveHistory.length - MAX_DURABLE_HISTORY_MESSAGES);
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
durable.save(sessionId, liveHistory);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
/* persistence must never fail the call */
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
// The realtime bridge pushes llm.done (assistant transcript) BEFORE
|
|
156
|
+
// eos.turn_complete (user transcript) for the same turn — buffer the
|
|
157
|
+
// assistant text and commit the pair in conversation order at turn end.
|
|
158
|
+
const pendingAssistant = new Map();
|
|
159
|
+
session.bus.on("llm.done", (pkt) => {
|
|
160
|
+
const done = pkt;
|
|
161
|
+
if (done.text?.trim())
|
|
162
|
+
pendingAssistant.set(done.contextId, done.text);
|
|
163
|
+
});
|
|
164
|
+
session.bus.on("eos.turn_complete", (pkt) => {
|
|
165
|
+
const turn = pkt;
|
|
166
|
+
const assistantText = pendingAssistant.get(turn.contextId);
|
|
167
|
+
pendingAssistant.delete(turn.contextId);
|
|
168
|
+
if (turn.text?.trim())
|
|
169
|
+
liveHistory.push({ role: "user", content: turn.text });
|
|
170
|
+
if (assistantText)
|
|
171
|
+
liveHistory.push({ role: "assistant", content: assistantText });
|
|
172
|
+
if (turn.text?.trim() || assistantText)
|
|
173
|
+
persist();
|
|
174
|
+
});
|
|
175
|
+
session.bus.on("realtime.resumption_handle", (pkt) => {
|
|
176
|
+
const handle = pkt.handle;
|
|
177
|
+
if (!handle)
|
|
178
|
+
return;
|
|
179
|
+
try {
|
|
180
|
+
durable.saveResumeHandle(sessionId, handle);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* persistence must never fail the call */
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// Surface the tool-call-start seam: VoiceAgentSession emits `agent_tool_call` the instant
|
|
188
|
+
// the front model invokes the delegate tool, before the reasoner runs. A throwing app
|
|
189
|
+
// callback must never break the call, so it is fully isolated.
|
|
190
|
+
if (options.onToolCallStart) {
|
|
191
|
+
session.on("agent_tool_call", (e) => {
|
|
192
|
+
try {
|
|
193
|
+
void Promise.resolve(options.onToolCallStart({ toolName: e.name, args: e.args, sessionId, connection: voiceConnection })).catch(() => undefined);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
/* app hook threw synchronously — ignore */
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// Delegate observability (G2): surface the bridge's delegate.query/delegate.result
|
|
201
|
+
// packets as app hooks. Same isolation contract as onToolCallStart — a throwing app
|
|
202
|
+
// callback must never break the call.
|
|
203
|
+
if (options.onDelegateQuery) {
|
|
204
|
+
session.on("delegate_query", (e) => {
|
|
205
|
+
try {
|
|
206
|
+
void Promise.resolve(options.onDelegateQuery({
|
|
207
|
+
query: e.query,
|
|
208
|
+
toolId: e.toolId,
|
|
209
|
+
toolName: e.toolName,
|
|
210
|
+
turnId: e.turnId,
|
|
211
|
+
sessionId,
|
|
212
|
+
connection: voiceConnection,
|
|
213
|
+
env,
|
|
214
|
+
})).catch(() => undefined);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
/* app hook threw synchronously — ignore */
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (options.onDelegateResult) {
|
|
222
|
+
session.on("delegate_result", (e) => {
|
|
223
|
+
try {
|
|
224
|
+
void Promise.resolve(options.onDelegateResult({
|
|
225
|
+
query: e.query,
|
|
226
|
+
answer: e.answer,
|
|
227
|
+
durationMs: e.durationMs,
|
|
228
|
+
grounded: e.grounded,
|
|
229
|
+
toolId: e.toolId,
|
|
230
|
+
toolName: e.toolName,
|
|
231
|
+
control: e.control,
|
|
232
|
+
blocked: e.blocked,
|
|
233
|
+
turnId: e.turnId,
|
|
234
|
+
sessionId,
|
|
235
|
+
connection: voiceConnection,
|
|
236
|
+
env,
|
|
237
|
+
})).catch(() => undefined);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
/* app hook threw synchronously — ignore */
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return session;
|
|
245
|
+
};
|
|
246
|
+
// The runner reports startup failures to the client and disposes the socket
|
|
247
|
+
// itself; nothing to do here beyond not crashing the isolate.
|
|
248
|
+
const onRunnerSettled = () => undefined;
|
|
249
|
+
if (options.transport === "twilio") {
|
|
250
|
+
// Twilio Media Streams: μ-law 8 kHz both ways. The runner derives the session
|
|
251
|
+
// id from the `?sessionId=` query (the callSid), resamples to the engine rate,
|
|
252
|
+
// and manages its own lease/heartbeat. Recorder is edge-only.
|
|
253
|
+
void runTwilioEdgeWebSocketConnection(socket, request, {
|
|
254
|
+
sessionStore: this.#store,
|
|
255
|
+
createSession,
|
|
256
|
+
...(options.inputSampleRateHz !== undefined
|
|
257
|
+
? { engineSampleRateHz: options.inputSampleRateHz }
|
|
258
|
+
: {}),
|
|
259
|
+
...(options.resumeWindowMs !== undefined ? { resumeWindowMs: options.resumeWindowMs } : {}),
|
|
260
|
+
...(options.backgroundAudio ? { backgroundAudio: options.backgroundAudio } : {}),
|
|
261
|
+
}).catch(onRunnerSettled);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (options.transport === "telnyx") {
|
|
265
|
+
// Telnyx Media Streaming: PCMU/PCMA/G722/L16 per negotiated start.media_format.
|
|
266
|
+
// Same lease/heartbeat pattern as Twilio; recorder is edge-only.
|
|
267
|
+
// Live streaming_start → /telnyx is carrier-gated / unit-tested only.
|
|
268
|
+
void runTelnyxEdgeWebSocketConnection(socket, request, {
|
|
269
|
+
sessionStore: this.#store,
|
|
270
|
+
createSession,
|
|
271
|
+
...(options.inputSampleRateHz !== undefined
|
|
272
|
+
? { engineSampleRateHz: options.inputSampleRateHz }
|
|
273
|
+
: {}),
|
|
274
|
+
...(options.resumeWindowMs !== undefined ? { resumeWindowMs: options.resumeWindowMs } : {}),
|
|
275
|
+
...(options.backgroundAudio ? { backgroundAudio: options.backgroundAudio } : {}),
|
|
276
|
+
}).catch(onRunnerSettled);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
let recorder;
|
|
280
|
+
try {
|
|
281
|
+
recorder = options.recorder?.(env, { sessionId });
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// A recorder factory that throws synchronously must not strand the
|
|
285
|
+
// connection (and its lease) — dispose, which fires the close path above.
|
|
286
|
+
socket.dispose();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
void runVoiceEdgeWebSocketConnection(socket, request, {
|
|
290
|
+
sessionStore: this.#store,
|
|
291
|
+
sessionId: () => sessionId,
|
|
292
|
+
recorder,
|
|
293
|
+
...(options.inputSampleRateHz !== undefined
|
|
294
|
+
? { inputSampleRateHz: options.inputSampleRateHz }
|
|
295
|
+
: {}),
|
|
296
|
+
...(options.outputSampleRateHz !== undefined
|
|
297
|
+
? { outputSampleRateHz: options.outputSampleRateHz }
|
|
298
|
+
: {}),
|
|
299
|
+
...(options.resumeWindowMs !== undefined ? { resumeWindowMs: options.resumeWindowMs } : {}),
|
|
300
|
+
...(options.backgroundAudio ? { backgroundAudio: options.backgroundAudio } : {}),
|
|
301
|
+
createSession,
|
|
302
|
+
}).catch(onRunnerSettled);
|
|
303
|
+
}
|
|
304
|
+
/** Pump the close so the edge runner tears down; the lease releases via #releaseConnection. */
|
|
305
|
+
#endVoice(connection, code, reason) {
|
|
306
|
+
this.#controllers.get(connection.id)?.close(code, reason);
|
|
307
|
+
}
|
|
308
|
+
#releaseConnection(id) {
|
|
309
|
+
this.#controllers.delete(id);
|
|
310
|
+
const dispose = this.#keepAliveDispose.get(id);
|
|
311
|
+
if (dispose) {
|
|
312
|
+
this.#keepAliveDispose.delete(id);
|
|
313
|
+
try {
|
|
314
|
+
dispose();
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
/* ignore */
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async #resolveReasoner(env, ctx) {
|
|
322
|
+
if (options.reasoner)
|
|
323
|
+
return options.reasoner(env, ctx);
|
|
324
|
+
const runtime = this.runtime;
|
|
325
|
+
if (isKuralleRuntime(runtime))
|
|
326
|
+
return fromKuralleRuntime(runtime, { sessionId: ctx.sessionId });
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
// One durable store per DO instance; tables are created idempotently on first use.
|
|
330
|
+
#durable = null;
|
|
331
|
+
#durableStore() {
|
|
332
|
+
if (!this.#durable) {
|
|
333
|
+
const host = this;
|
|
334
|
+
this.#durable = new SqliteReasonerSessionStore((strings, ...values) => host.sql(strings, ...values));
|
|
335
|
+
}
|
|
336
|
+
return this.#durable;
|
|
337
|
+
}
|
|
338
|
+
// Default: the client-supplied `?sessionId=` (so a reconnecting client can
|
|
339
|
+
// resume its session within the resume window), else a per-connection random
|
|
340
|
+
// id. Crucially NOT the Agent name — two concurrent connections to one
|
|
341
|
+
// instance must not silently share (and cross-wire) a single VoiceAgentSession.
|
|
342
|
+
#resolveSessionId(request) {
|
|
343
|
+
const name = this.name;
|
|
344
|
+
if (options.sessionId)
|
|
345
|
+
return options.sessionId(request, name);
|
|
346
|
+
try {
|
|
347
|
+
const fromQuery = new URL(request.url).searchParams.get("sessionId");
|
|
348
|
+
if (fromQuery)
|
|
349
|
+
return fromQuery;
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
/* malformed URL — fall through to a random id */
|
|
353
|
+
}
|
|
354
|
+
return crypto.randomUUID();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return VoiceAgentMixin;
|
|
358
|
+
}
|
|
359
|
+
//# sourceMappingURL=with-voice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-voice.js","sourceRoot":"","sources":["../src/with-voice.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuralle-syrinx/cf-agents",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "withVoice(Agent) — add a Syrinx realtime or cascaded voice pipeline to a Cloudflare agents SDK Agent",
|
|
6
6
|
"keywords": [
|
|
@@ -26,32 +26,45 @@
|
|
|
26
26
|
"url": "https://github.com/kuralle/syrinx/issues"
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
|
-
"main": "./
|
|
30
|
-
"types": "./
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
31
|
"exports": {
|
|
32
|
-
".":
|
|
33
|
-
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./r2-recorder": {
|
|
37
|
+
"types": "./dist/r2-recorder.d.ts",
|
|
38
|
+
"default": "./dist/r2-recorder.js"
|
|
39
|
+
}
|
|
34
40
|
},
|
|
35
41
|
"dependencies": {
|
|
36
|
-
"@kuralle-syrinx/
|
|
37
|
-
"@kuralle-syrinx/
|
|
38
|
-
"@kuralle-syrinx/
|
|
39
|
-
"@kuralle-syrinx/
|
|
40
|
-
"@kuralle-syrinx/
|
|
41
|
-
"@kuralle-syrinx/
|
|
42
|
-
"@kuralle-syrinx/
|
|
42
|
+
"@kuralle-syrinx/realtime": "4.5.0",
|
|
43
|
+
"@kuralle-syrinx/server-websocket": "4.5.0",
|
|
44
|
+
"@kuralle-syrinx/core": "4.5.0",
|
|
45
|
+
"@kuralle-syrinx/recorder": "4.5.0",
|
|
46
|
+
"@kuralle-syrinx/ws": "4.5.0",
|
|
47
|
+
"@kuralle-syrinx/aisdk": "4.5.0",
|
|
48
|
+
"@kuralle-syrinx/kuralle": "4.5.0"
|
|
43
49
|
},
|
|
44
50
|
"peerDependencies": {
|
|
45
51
|
"agents": ">=0.14.0 <1.0.0"
|
|
46
52
|
},
|
|
47
53
|
"devDependencies": {
|
|
48
54
|
"@cloudflare/workers-types": "^4.20260601.0",
|
|
55
|
+
"@types/node": "^22.0.0",
|
|
49
56
|
"agents": "0.14.0",
|
|
50
57
|
"typescript": "^5.7.0",
|
|
51
58
|
"vitest": "^3.2.6"
|
|
52
59
|
},
|
|
60
|
+
"files": [
|
|
61
|
+
"dist",
|
|
62
|
+
"src",
|
|
63
|
+
"README.md"
|
|
64
|
+
],
|
|
53
65
|
"scripts": {
|
|
54
66
|
"typecheck": "tsc --noEmit",
|
|
55
|
-
"test": "vitest run"
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"build": "tsc -p tsconfig.build.json"
|
|
56
69
|
}
|
|
57
70
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
import { VoiceAgentSession, type Reasoner, type VoicePlugin } from "@kuralle-syrinx/core";
|
|
5
|
+
import type { RealtimeAdapter } from "@kuralle-syrinx/realtime";
|
|
6
|
+
import { buildVoiceSession, type VoicePipeline } from "./build-session.js";
|
|
7
|
+
|
|
8
|
+
const stubPlugin = (): VoicePlugin => ({
|
|
9
|
+
initialize: async () => {},
|
|
10
|
+
close: async () => {},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const stubReasoner = (): Reasoner => ({
|
|
14
|
+
// eslint-disable-next-line require-yield
|
|
15
|
+
stream: async function* () {
|
|
16
|
+
return;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const stubFront = (): RealtimeAdapter => ({}) as unknown as RealtimeAdapter;
|
|
21
|
+
|
|
22
|
+
const ctx = { sessionId: "s1" };
|
|
23
|
+
|
|
24
|
+
describe("buildVoiceSession", () => {
|
|
25
|
+
it("builds a realtime session from a realtime pipeline", () => {
|
|
26
|
+
const pipeline: VoicePipeline<unknown> = {
|
|
27
|
+
kind: "realtime",
|
|
28
|
+
front: () => stubFront(),
|
|
29
|
+
delegateToolName: "consult_knowledge",
|
|
30
|
+
};
|
|
31
|
+
const session = buildVoiceSession(pipeline, {}, stubReasoner(), ctx);
|
|
32
|
+
expect(session).toBeInstanceOf(VoiceAgentSession);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("allows a realtime session with no reasoner (front-only)", () => {
|
|
36
|
+
const pipeline: VoicePipeline<unknown> = { kind: "realtime", front: () => stubFront() };
|
|
37
|
+
const session = buildVoiceSession(pipeline, {}, undefined, ctx);
|
|
38
|
+
expect(session).toBeInstanceOf(VoiceAgentSession);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("builds a cascaded session from a cascaded pipeline", () => {
|
|
42
|
+
const pipeline: VoicePipeline<unknown> = {
|
|
43
|
+
kind: "cascaded",
|
|
44
|
+
stt: () => ({ plugin: stubPlugin(), config: { model: "nova-3" } }),
|
|
45
|
+
tts: () => ({ plugin: stubPlugin(), config: { voice_id: "v" } }),
|
|
46
|
+
};
|
|
47
|
+
const session = buildVoiceSession(pipeline, {}, stubReasoner(), ctx);
|
|
48
|
+
expect(session).toBeInstanceOf(VoiceAgentSession);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("carries sttForceFinalizeTimeoutMs through to the cascaded session", () => {
|
|
52
|
+
// Provider-endpointed cascades (e.g. Deepgram) tune this below the engine default; the mixin
|
|
53
|
+
// must thread it through instead of silently reverting to 7000ms.
|
|
54
|
+
const pipeline: VoicePipeline<unknown> = {
|
|
55
|
+
kind: "cascaded",
|
|
56
|
+
stt: () => ({ plugin: stubPlugin(), config: { model: "nova-3" } }),
|
|
57
|
+
tts: () => ({ plugin: stubPlugin(), config: { voice_id: "v" } }),
|
|
58
|
+
endpointingOwner: "provider_stt",
|
|
59
|
+
sttForceFinalizeTimeoutMs: 3500,
|
|
60
|
+
};
|
|
61
|
+
const session = buildVoiceSession(pipeline, {}, stubReasoner(), ctx);
|
|
62
|
+
expect(session).toBeInstanceOf(VoiceAgentSession);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("throws a clear error when a cascaded pipeline has no reasoner", () => {
|
|
66
|
+
const pipeline: VoicePipeline<unknown> = {
|
|
67
|
+
kind: "cascaded",
|
|
68
|
+
stt: () => ({ plugin: stubPlugin() }),
|
|
69
|
+
tts: () => ({ plugin: stubPlugin() }),
|
|
70
|
+
};
|
|
71
|
+
expect(() => buildVoiceSession(pipeline, {}, undefined, ctx)).toThrow(/cascaded pipeline needs a reasoner/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('throws when endpointingOwner is "smart_turn" but no eos stage is provided', () => {
|
|
75
|
+
const pipeline: VoicePipeline<unknown> = {
|
|
76
|
+
kind: "cascaded",
|
|
77
|
+
stt: () => ({ plugin: stubPlugin() }),
|
|
78
|
+
tts: () => ({ plugin: stubPlugin() }),
|
|
79
|
+
endpointingOwner: "smart_turn",
|
|
80
|
+
};
|
|
81
|
+
expect(() => buildVoiceSession(pipeline, {}, stubReasoner(), ctx)).toThrow(/smart_turn/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("resolves endpointingOwner when provided as an (env) => owner factory", () => {
|
|
85
|
+
let seenEnv: { ai: boolean } | undefined;
|
|
86
|
+
const pipeline: VoicePipeline<{ ai: boolean }> = {
|
|
87
|
+
kind: "cascaded",
|
|
88
|
+
stt: () => ({ plugin: stubPlugin() }),
|
|
89
|
+
tts: () => ({ plugin: stubPlugin() }),
|
|
90
|
+
endpointingOwner: (env) => {
|
|
91
|
+
seenEnv = env;
|
|
92
|
+
return env.ai ? "smart_turn" : "provider_stt";
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const providerSession = buildVoiceSession(pipeline, { ai: false }, stubReasoner(), ctx);
|
|
97
|
+
expect(providerSession).toBeInstanceOf(VoiceAgentSession);
|
|
98
|
+
expect(seenEnv).toEqual({ ai: false });
|
|
99
|
+
|
|
100
|
+
expect(() => buildVoiceSession(pipeline, { ai: true }, stubReasoner(), ctx)).toThrow(/smart_turn/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("builds smart_turn when the factory returns smart_turn and eos is present", () => {
|
|
104
|
+
const pipeline: VoicePipeline<{ ai: boolean }> = {
|
|
105
|
+
kind: "cascaded",
|
|
106
|
+
stt: () => ({ plugin: stubPlugin() }),
|
|
107
|
+
tts: () => ({ plugin: stubPlugin() }),
|
|
108
|
+
eos: () => ({ plugin: stubPlugin(), config: {} }),
|
|
109
|
+
endpointingOwner: (env) => (env.ai ? "smart_turn" : "provider_stt"),
|
|
110
|
+
};
|
|
111
|
+
const session = buildVoiceSession(pipeline, { ai: true }, stubReasoner(), ctx);
|
|
112
|
+
expect(session).toBeInstanceOf(VoiceAgentSession);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("treats an eos factory that returns undefined as no eos stage", () => {
|
|
116
|
+
const pipeline: VoicePipeline<unknown> = {
|
|
117
|
+
kind: "cascaded",
|
|
118
|
+
stt: () => ({ plugin: stubPlugin() }),
|
|
119
|
+
tts: () => ({ plugin: stubPlugin() }),
|
|
120
|
+
eos: () => undefined,
|
|
121
|
+
endpointingOwner: "provider_stt",
|
|
122
|
+
};
|
|
123
|
+
expect(buildVoiceSession(pipeline, {}, stubReasoner(), ctx)).toBeInstanceOf(VoiceAgentSession);
|
|
124
|
+
});
|
|
125
|
+
});
|