@kuralle-syrinx/cli 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.
@@ -0,0 +1,55 @@
1
+ import { VoiceAgentSession } from "@kuralle-syrinx/core";
2
+ /** A pre-built session, or a zero-arg factory producing one — the same contract examples/02-hello-voice-headless/scripts/dev-server.ts's `--agent` seam uses. */
3
+ export type SessionFactory = () => VoiceAgentSession | Promise<VoiceAgentSession>;
4
+ export interface PerTurnMetrics {
5
+ readonly turnId: string;
6
+ readonly inputAudioMs: number;
7
+ readonly speechEndToFinalTranscriptMs: number;
8
+ readonly speechEndToFirstAudioMs: number;
9
+ readonly endpointingMs: number;
10
+ readonly llmTTFTMs: number;
11
+ readonly ttsTTFBMs: number;
12
+ readonly e2eLatencyMs: number;
13
+ readonly agentTokens: number;
14
+ readonly playedMs: number;
15
+ readonly truncated: boolean;
16
+ readonly toolCalls: number;
17
+ }
18
+ export interface TurnResult {
19
+ readonly sessionDir: string;
20
+ readonly finalTranscript: string;
21
+ readonly agentReply: string;
22
+ readonly agentOutWavPath: string;
23
+ readonly inputWavPath: string;
24
+ readonly eventsJsonlPath: string;
25
+ readonly eventsJsonPath: string;
26
+ readonly transcriptJsonPath: string;
27
+ readonly metricsJsonPath: string;
28
+ readonly metrics: PerTurnMetrics;
29
+ readonly durationMs: number;
30
+ }
31
+ export interface DriveTurnOptions {
32
+ /** An already-registered VoiceAgentSession, or a factory producing one. Built by the caller — this module never constructs providers. */
33
+ readonly session: VoiceAgentSession | SessionFactory;
34
+ readonly inputWavPath: string;
35
+ readonly sessionDir: string;
36
+ /** Skips WAV read; must be mono 16 kHz PCM decoded samples. */
37
+ readonly syntheticMono16kSamples?: Readonly<Int16Array>;
38
+ readonly realtimePacing?: boolean;
39
+ /** Called once per fed audio frame (including the trailing silence pad), right after it is pushed onto the bus — for a VAD implementation (or a scripted test fake) that needs an explicit per-frame tick outside the bus event flow. */
40
+ readonly onAudioFrame?: (contextId: string) => void;
41
+ /** Called once after all audio (including the silence pad) has been fed, before waiting for tts.end — for an STT implementation (or a scripted test fake) that finalizes on an explicit signal. */
42
+ readonly onAudioFed?: (contextId: string) => void | Promise<void>;
43
+ readonly ttsEndTimeoutMs?: number;
44
+ }
45
+ export declare function readPcm16Mono16kWav(filePath: string): Int16Array;
46
+ /** Resolves a WAV path, falling back to the repo's bundled demo fixture for the literal name "hello.wav" — a convenience inherited from the pre-move script, harmless (and inert) outside this monorepo. */
47
+ export declare function resolveInputWavPath(path: string): string;
48
+ /**
49
+ * Feed one turn's worth of audio into an already-built session and report the
50
+ * transcript, reply, per-stage timings, and written artifacts. This is the one
51
+ * implementation of "drive a turn" — it knows nothing about which STT/TTS/
52
+ * reasoner backends the session was assembled from.
53
+ */
54
+ export declare function driveTurn(opts: DriveTurnOptions): Promise<TurnResult>;
55
+ //# sourceMappingURL=turn-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-runner.d.ts","sourceRoot":"","sources":["../src/turn-runner.ts"],"names":[],"mappings":"AAsBA,OAAO,EAEL,iBAAiB,EAKlB,MAAM,sBAAsB,CAAC;AAU9B,iKAAiK;AACjK,MAAM,MAAM,cAAc,GAAG,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAElF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,4BAA4B,EAAE,MAAM,CAAC;IAC9C,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,yIAAyI;IACzI,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAAC;IACrD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,QAAQ,CAAC,uBAAuB,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;IACxD,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,yOAAyO;IACzO,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,mMAAmM;IACnM,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAehE;AAED,4MAA4M;AAC5M,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASxD;AA2DD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CA0M3E"}
@@ -0,0 +1,250 @@
1
+ // src/turn-runner.ts
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFileSync } from "node:fs";
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import { createRequire } from "node:module";
6
+ import { basename, join, resolve } from "node:path";
7
+ import {
8
+ Route
9
+ } from "@kuralle-syrinx/core";
10
+ var require2 = createRequire(import.meta.url);
11
+ var { WaveFile } = require2("wavefile");
12
+ var SAMPLES_PER_FRAME = 320;
13
+ var DEFAULT_FIXTURE_PATH = "/Users/mithushancj/Documents/asyncdot/openscoped/voice-media-transport/research/agents/tests/test_realtime/hello_world.wav";
14
+ var DEFAULT_TTS_END_TIMEOUT_MS = 12e4;
15
+ function readPcm16Mono16kWav(filePath) {
16
+ const buf = readFileSync(filePath);
17
+ const wav = new WaveFile(Buffer.from(buf));
18
+ const fmt = wav.fmt;
19
+ if (fmt.numChannels !== 1) throw new Error(`expected mono WAV, got ${String(fmt.numChannels)} channels`);
20
+ if (fmt.bitsPerSample !== 16 || fmt.audioFormat !== 1) throw new Error("expected 16-bit PCM WAV");
21
+ const raw = wav.getSamples(false, Int16Array);
22
+ const mono = Array.isArray(raw) ? raw[0] : raw;
23
+ if (mono === void 0 || !(mono instanceof Int16Array)) throw new Error("WAV has no mono channel samples");
24
+ return fmt.sampleRate === 16e3 ? mono : resamplePcm16(mono, fmt.sampleRate, 16e3);
25
+ }
26
+ function resolveInputWavPath(path) {
27
+ const resolved = resolve(path);
28
+ try {
29
+ readFileSync(resolved);
30
+ return resolved;
31
+ } catch {
32
+ if (basename(path) === "hello.wav") return DEFAULT_FIXTURE_PATH;
33
+ throw new Error(`input WAV not found: ${resolved}`);
34
+ }
35
+ }
36
+ function resamplePcm16(samples, fromHz, toHz) {
37
+ if (fromHz <= 0 || toHz <= 0) throw new Error("invalid WAV sample rate");
38
+ const outLength = Math.max(1, Math.round(samples.length * toHz / fromHz));
39
+ const out = new Int16Array(outLength);
40
+ const ratio = fromHz / toHz;
41
+ for (let i = 0; i < out.length; i += 1) {
42
+ const src = i * ratio;
43
+ const lo = Math.floor(src);
44
+ const hi = Math.min(samples.length - 1, lo + 1);
45
+ const frac = src - lo;
46
+ out[i] = Math.round(samples[lo] * (1 - frac) + samples[hi] * frac);
47
+ }
48
+ return out;
49
+ }
50
+ function pcmToBytes(samples) {
51
+ return new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength);
52
+ }
53
+ function sliceFramePcm(samples, offset) {
54
+ const end = Math.min(offset + SAMPLES_PER_FRAME, samples.length);
55
+ const frame = new Int16Array(SAMPLES_PER_FRAME);
56
+ if (end > offset) frame.set(samples.subarray(offset, end));
57
+ return frame;
58
+ }
59
+ function mergeBytes(chunks) {
60
+ const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
61
+ const merged = new Uint8Array(total);
62
+ let offset = 0;
63
+ for (const chunk of chunks) {
64
+ merged.set(chunk, offset);
65
+ offset += chunk.byteLength;
66
+ }
67
+ return merged;
68
+ }
69
+ function writePcm16Wav(path, chunks, sampleRateHz) {
70
+ const bytes = mergeBytes(chunks);
71
+ const samples = new Int16Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 2));
72
+ const wav = new WaveFile();
73
+ wav.fromScratch(1, sampleRateHz, "16", samples);
74
+ return writeFile(path, Buffer.from(wav.toBuffer()));
75
+ }
76
+ function eventLine(kind, data) {
77
+ return `${JSON.stringify({ tsMs: Date.now(), kind, ...data })}
78
+ `;
79
+ }
80
+ function sleep(ms) {
81
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
82
+ }
83
+ async function resolveSession(session) {
84
+ return typeof session === "function" ? session() : session;
85
+ }
86
+ async function driveTurn(opts) {
87
+ const session = await resolveSession(opts.session);
88
+ const sessionDir = resolve(opts.sessionDir);
89
+ await mkdir(sessionDir, { recursive: true });
90
+ const pcm = opts.syntheticMono16kSamples !== void 0 ? Int16Array.from(opts.syntheticMono16kSamples) : readPcm16Mono16kWav(resolveInputWavPath(opts.inputWavPath));
91
+ const contextId = randomUUID();
92
+ const inputChunks = [];
93
+ const outputChunks = [];
94
+ const eventLines = [];
95
+ const timeline = {
96
+ feedStartMs: 0,
97
+ speechEndMs: 0,
98
+ finalTranscriptMs: 0,
99
+ firstLlmDeltaMs: 0,
100
+ firstTtsAudioMs: 0,
101
+ ttsEndMs: 0
102
+ };
103
+ let finalTranscript = "";
104
+ let agentReply = "";
105
+ let toolCalls = 0;
106
+ const offRecordUser = session.bus.on("record.user_audio", (pkt) => {
107
+ inputChunks.push(pkt.audio);
108
+ });
109
+ const offTtsAudio = session.bus.on("tts.audio", (pkt) => {
110
+ if (timeline.firstTtsAudioMs === 0) timeline.firstTtsAudioMs = pkt.timestampMs;
111
+ outputChunks.push(pkt.audio);
112
+ });
113
+ const ttsEnd = new Promise((resolveEnd, reject) => {
114
+ const timeout = setTimeout(() => {
115
+ offTtsEnd();
116
+ reject(new Error("tts.end timeout"));
117
+ }, opts.ttsEndTimeoutMs ?? DEFAULT_TTS_END_TIMEOUT_MS);
118
+ const offTtsEnd = session.bus.on("tts.end", (pkt) => {
119
+ if (pkt.contextId !== contextId) return;
120
+ clearTimeout(timeout);
121
+ offTtsEnd();
122
+ timeline.ttsEndMs = pkt.timestampMs;
123
+ resolveEnd();
124
+ });
125
+ });
126
+ const on = (event, handler) => {
127
+ session.on(event, handler);
128
+ };
129
+ on("user_input_final", (event) => {
130
+ finalTranscript = event.text;
131
+ timeline.finalTranscriptMs = event.tsMs;
132
+ eventLines.push(eventLine("user_input_final", { turnId: event.turnId, text: event.text }));
133
+ });
134
+ on("agent_text_delta", (event) => {
135
+ if (timeline.firstLlmDeltaMs === 0) timeline.firstLlmDeltaMs = event.tsMs;
136
+ agentReply += event.delta;
137
+ eventLines.push(eventLine("agent_text_delta", { turnId: event.turnId, delta: event.delta }));
138
+ });
139
+ on("agent_tool_call", (event) => {
140
+ toolCalls += 1;
141
+ eventLines.push(eventLine("agent_tool_call", { turnId: event.turnId, name: event.name }));
142
+ });
143
+ on("agent_finished", (event) => {
144
+ eventLines.push(eventLine("agent_finished", { turnId: event.turnId }));
145
+ });
146
+ on("error", (event) => {
147
+ eventLines.push(eventLine("error", { stage: event.stage, category: event.category, message: event.message }));
148
+ });
149
+ await session.start();
150
+ session.bus.push(Route.Main, {
151
+ kind: "turn.change",
152
+ contextId,
153
+ previousContextId: "",
154
+ reason: "headless_turn_start",
155
+ timestampMs: Date.now()
156
+ });
157
+ let offset = 0;
158
+ while (offset < pcm.length) {
159
+ const frame = sliceFramePcm(pcm, offset);
160
+ const audio = pcmToBytes(frame);
161
+ if (timeline.feedStartMs === 0) timeline.feedStartMs = Date.now();
162
+ session.bus.push(Route.Main, {
163
+ kind: "user.audio_received",
164
+ contextId,
165
+ timestampMs: Date.now(),
166
+ audio
167
+ });
168
+ opts.onAudioFrame?.(contextId);
169
+ offset += SAMPLES_PER_FRAME;
170
+ if (opts.realtimePacing === true) await sleep(20);
171
+ }
172
+ timeline.speechEndMs = Date.now();
173
+ for (let pad = 0; pad < 40; pad += 1) {
174
+ const frame = new Int16Array(SAMPLES_PER_FRAME);
175
+ session.bus.push(Route.Main, {
176
+ kind: "user.audio_received",
177
+ contextId,
178
+ timestampMs: Date.now(),
179
+ audio: pcmToBytes(frame)
180
+ });
181
+ opts.onAudioFrame?.(contextId);
182
+ if (opts.realtimePacing === true) await sleep(20);
183
+ }
184
+ await opts.onAudioFed?.(contextId);
185
+ await ttsEnd;
186
+ offRecordUser();
187
+ offTtsAudio();
188
+ const metrics = {
189
+ turnId: contextId,
190
+ inputAudioMs: Math.round(pcm.length / 16e3 * 1e3),
191
+ speechEndToFinalTranscriptMs: timeline.speechEndMs > 0 && timeline.finalTranscriptMs > 0 ? Math.max(0, timeline.finalTranscriptMs - timeline.speechEndMs) : 0,
192
+ speechEndToFirstAudioMs: timeline.speechEndMs > 0 && timeline.firstTtsAudioMs > 0 ? Math.max(0, timeline.firstTtsAudioMs - timeline.speechEndMs) : 0,
193
+ endpointingMs: timeline.feedStartMs > 0 && timeline.finalTranscriptMs > 0 ? Math.max(0, timeline.finalTranscriptMs - timeline.feedStartMs) : 0,
194
+ llmTTFTMs: timeline.finalTranscriptMs > 0 && timeline.firstLlmDeltaMs > 0 ? Math.max(0, timeline.firstLlmDeltaMs - timeline.finalTranscriptMs) : 0,
195
+ ttsTTFBMs: timeline.firstLlmDeltaMs > 0 && timeline.firstTtsAudioMs > 0 ? Math.max(0, timeline.firstTtsAudioMs - timeline.firstLlmDeltaMs) : 0,
196
+ e2eLatencyMs: timeline.feedStartMs > 0 && timeline.firstTtsAudioMs > 0 ? Math.max(0, timeline.firstTtsAudioMs - timeline.feedStartMs) : 0,
197
+ agentTokens: agentReply.trim().length === 0 ? 0 : agentReply.trim().split(/\s+/).length,
198
+ playedMs: Math.round(mergeBytes(outputChunks).byteLength / 2 / 16e3 * 1e3),
199
+ truncated: false,
200
+ toolCalls
201
+ };
202
+ const inputWavPath = join(sessionDir, "audio-in.wav");
203
+ const agentOutWavPath = join(sessionDir, "audio-out.wav");
204
+ const eventsJsonlPath = join(sessionDir, "events.jsonl");
205
+ const eventsJsonPath = join(sessionDir, "events.json");
206
+ const transcriptJsonPath = join(sessionDir, "transcript.json");
207
+ const metricsJsonPath = join(sessionDir, "metrics.json");
208
+ const events = eventLines.map((line) => line.trim()).filter((line) => line.length > 0).map((line) => JSON.parse(line));
209
+ const qualityGate = {
210
+ passed: finalTranscript.trim().length > 0 && agentReply.trim().length > 0 && outputChunks.length > 0,
211
+ failures: [
212
+ ...finalTranscript.trim().length === 0 ? ["missing final transcript"] : [],
213
+ ...agentReply.trim().length === 0 ? ["missing agent reply"] : [],
214
+ ...outputChunks.length === 0 ? ["missing TTS audio"] : []
215
+ ]
216
+ };
217
+ await writePcm16Wav(inputWavPath, inputChunks, 16e3);
218
+ await writePcm16Wav(agentOutWavPath, outputChunks, 16e3);
219
+ await writeFile(eventsJsonlPath, eventLines.join(""), "utf8");
220
+ await writeFile(eventsJsonPath, `${JSON.stringify({ events, qualityGate }, null, 2)}
221
+ `, "utf8");
222
+ await writeFile(
223
+ transcriptJsonPath,
224
+ `${JSON.stringify({ finalTranscript, agentReply, turnCount: 1, metrics, qualityGate }, null, 2)}
225
+ `,
226
+ "utf8"
227
+ );
228
+ await writeFile(metricsJsonPath, `${JSON.stringify({ ...metrics, turnCount: 1, qualityGate }, null, 2)}
229
+ `, "utf8");
230
+ await session.close();
231
+ return {
232
+ sessionDir,
233
+ finalTranscript,
234
+ agentReply,
235
+ agentOutWavPath,
236
+ inputWavPath,
237
+ eventsJsonlPath,
238
+ eventsJsonPath,
239
+ transcriptJsonPath,
240
+ metricsJsonPath,
241
+ metrics,
242
+ durationMs: timeline.feedStartMs > 0 && timeline.ttsEndMs > 0 ? Math.max(0, timeline.ttsEndMs - timeline.feedStartMs) : 0
243
+ };
244
+ }
245
+ export {
246
+ driveTurn,
247
+ readPcm16Mono16kWav,
248
+ resolveInputWavPath
249
+ };
250
+ //# sourceMappingURL=turn-runner.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/turn-runner.ts"],
4
+ "sourcesContent": ["// SPDX-License-Identifier: MIT\n//\n// Provider-agnostic turn driver (LDT-20, revised). This module owns exactly one\n// thing: feeding audio into an ALREADY-CONSTRUCTED VoiceAgentSession and\n// capturing the transcript/reply/timings/artifacts \u2014 never how that session's\n// STT/TTS/reasoner plugins got built. The CLI itself must not depend on any\n// provider SDK (Deepgram, Cartesia, OpenAI, ...); the caller (the CLI's\n// --agent seam, or examples/02-hello-voice-headless's own hardcoded default\n// kernel) supplies the session.\n//\n// examples/02-hello-voice-headless/src/run-one-turn.ts is the other caller: it\n// keeps its own `runOneTurn(...)` with the hardcoded Deepgram+OpenAI+Cartesia+\n// Silero default (legitimate there \u2014 it's an example harness, not a shipped\n// CLI) and delegates the actual audio-feed/metrics/event-capture work to\n// `driveTurn` here, so there is exactly one implementation of that part.\n\nimport { randomUUID } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { basename, join, resolve } from \"node:path\";\n\nimport {\n Route,\n VoiceAgentSession,\n type RecordUserAudioPacket,\n type TextToSpeechAudioPacket,\n type TextToSpeechEndPacket,\n type VoiceAgentSessionEvents,\n} from \"@kuralle-syrinx/core\";\n\nconst require = createRequire(import.meta.url);\nconst { WaveFile } = require(\"wavefile\") as typeof import(\"wavefile\");\n\nconst SAMPLES_PER_FRAME = 320;\nconst DEFAULT_FIXTURE_PATH =\n \"/Users/mithushancj/Documents/asyncdot/openscoped/voice-media-transport/research/agents/tests/test_realtime/hello_world.wav\";\nconst DEFAULT_TTS_END_TIMEOUT_MS = 120_000;\n\n/** A pre-built session, or a zero-arg factory producing one \u2014 the same contract examples/02-hello-voice-headless/scripts/dev-server.ts's `--agent` seam uses. */\nexport type SessionFactory = () => VoiceAgentSession | Promise<VoiceAgentSession>;\n\nexport interface PerTurnMetrics {\n readonly turnId: string;\n readonly inputAudioMs: number;\n readonly speechEndToFinalTranscriptMs: number;\n readonly speechEndToFirstAudioMs: number;\n readonly endpointingMs: number;\n readonly llmTTFTMs: number;\n readonly ttsTTFBMs: number;\n readonly e2eLatencyMs: number;\n readonly agentTokens: number;\n readonly playedMs: number;\n readonly truncated: boolean;\n readonly toolCalls: number;\n}\n\nexport interface TurnResult {\n readonly sessionDir: string;\n readonly finalTranscript: string;\n readonly agentReply: string;\n readonly agentOutWavPath: string;\n readonly inputWavPath: string;\n readonly eventsJsonlPath: string;\n readonly eventsJsonPath: string;\n readonly transcriptJsonPath: string;\n readonly metricsJsonPath: string;\n readonly metrics: PerTurnMetrics;\n readonly durationMs: number;\n}\n\nexport interface DriveTurnOptions {\n /** An already-registered VoiceAgentSession, or a factory producing one. Built by the caller \u2014 this module never constructs providers. */\n readonly session: VoiceAgentSession | SessionFactory;\n readonly inputWavPath: string;\n readonly sessionDir: string;\n /** Skips WAV read; must be mono 16 kHz PCM decoded samples. */\n readonly syntheticMono16kSamples?: Readonly<Int16Array>;\n readonly realtimePacing?: boolean;\n /** Called once per fed audio frame (including the trailing silence pad), right after it is pushed onto the bus \u2014 for a VAD implementation (or a scripted test fake) that needs an explicit per-frame tick outside the bus event flow. */\n readonly onAudioFrame?: (contextId: string) => void;\n /** Called once after all audio (including the silence pad) has been fed, before waiting for tts.end \u2014 for an STT implementation (or a scripted test fake) that finalizes on an explicit signal. */\n readonly onAudioFed?: (contextId: string) => void | Promise<void>;\n readonly ttsEndTimeoutMs?: number;\n}\n\nexport function readPcm16Mono16kWav(filePath: string): Int16Array {\n const buf = readFileSync(filePath);\n const wav = new WaveFile(Buffer.from(buf));\n const fmt = wav.fmt as {\n sampleRate: number;\n numChannels: number;\n bitsPerSample: number;\n audioFormat: number;\n };\n if (fmt.numChannels !== 1) throw new Error(`expected mono WAV, got ${String(fmt.numChannels)} channels`);\n if (fmt.bitsPerSample !== 16 || fmt.audioFormat !== 1) throw new Error(\"expected 16-bit PCM WAV\");\n const raw = wav.getSamples(false, Int16Array);\n const mono: Int16Array | undefined = Array.isArray(raw) ? raw[0] : raw;\n if (mono === undefined || !(mono instanceof Int16Array)) throw new Error(\"WAV has no mono channel samples\");\n return fmt.sampleRate === 16000 ? mono : resamplePcm16(mono, fmt.sampleRate, 16000);\n}\n\n/** Resolves a WAV path, falling back to the repo's bundled demo fixture for the literal name \"hello.wav\" \u2014 a convenience inherited from the pre-move script, harmless (and inert) outside this monorepo. */\nexport function resolveInputWavPath(path: string): string {\n const resolved = resolve(path);\n try {\n readFileSync(resolved);\n return resolved;\n } catch {\n if (basename(path) === \"hello.wav\") return DEFAULT_FIXTURE_PATH;\n throw new Error(`input WAV not found: ${resolved}`);\n }\n}\n\nfunction resamplePcm16(samples: Int16Array, fromHz: number, toHz: number): Int16Array {\n if (fromHz <= 0 || toHz <= 0) throw new Error(\"invalid WAV sample rate\");\n const outLength = Math.max(1, Math.round((samples.length * toHz) / fromHz));\n const out = new Int16Array(outLength);\n const ratio = fromHz / toHz;\n for (let i = 0; i < out.length; i += 1) {\n const src = i * ratio;\n const lo = Math.floor(src);\n const hi = Math.min(samples.length - 1, lo + 1);\n const frac = src - lo;\n out[i] = Math.round(samples[lo]! * (1 - frac) + samples[hi]! * frac);\n }\n return out;\n}\n\nfunction pcmToBytes(samples: Readonly<Int16Array>): Uint8Array {\n return new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength);\n}\n\nfunction sliceFramePcm(samples: Readonly<Int16Array>, offset: number): Int16Array {\n const end = Math.min(offset + SAMPLES_PER_FRAME, samples.length);\n const frame = new Int16Array(SAMPLES_PER_FRAME);\n if (end > offset) frame.set(samples.subarray(offset, end));\n return frame;\n}\n\nfunction mergeBytes(chunks: readonly Uint8Array[]): Uint8Array {\n const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);\n const merged = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return merged;\n}\n\nfunction writePcm16Wav(path: string, chunks: readonly Uint8Array[], sampleRateHz: number): Promise<void> {\n const bytes = mergeBytes(chunks);\n const samples = new Int16Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 2));\n const wav = new WaveFile();\n wav.fromScratch(1, sampleRateHz, \"16\", samples);\n return writeFile(path, Buffer.from(wav.toBuffer()));\n}\n\nfunction eventLine(kind: string, data: Record<string, unknown>): string {\n return `${JSON.stringify({ tsMs: Date.now(), kind, ...data })}\\n`;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));\n}\n\nasync function resolveSession(session: VoiceAgentSession | SessionFactory): Promise<VoiceAgentSession> {\n return typeof session === \"function\" ? session() : session;\n}\n\n/**\n * Feed one turn's worth of audio into an already-built session and report the\n * transcript, reply, per-stage timings, and written artifacts. This is the one\n * implementation of \"drive a turn\" \u2014 it knows nothing about which STT/TTS/\n * reasoner backends the session was assembled from.\n */\nexport async function driveTurn(opts: DriveTurnOptions): Promise<TurnResult> {\n // Resolve (and, if a factory, construct) the session before touching the\n // filesystem: a caller's agent failing to construct is a different failure\n // class (CONFIG) than a bad input file (USAGE/BACKEND), and should surface\n // as itself rather than being masked by a WAV-read error that never got the\n // chance to matter.\n const session = await resolveSession(opts.session);\n\n const sessionDir = resolve(opts.sessionDir);\n await mkdir(sessionDir, { recursive: true });\n\n const pcm =\n opts.syntheticMono16kSamples !== undefined\n ? Int16Array.from(opts.syntheticMono16kSamples)\n : readPcm16Mono16kWav(resolveInputWavPath(opts.inputWavPath));\n\n const contextId = randomUUID();\n const inputChunks: Uint8Array[] = [];\n const outputChunks: Uint8Array[] = [];\n const eventLines: string[] = [];\n const timeline = {\n feedStartMs: 0,\n speechEndMs: 0,\n finalTranscriptMs: 0,\n firstLlmDeltaMs: 0,\n firstTtsAudioMs: 0,\n ttsEndMs: 0,\n };\n let finalTranscript = \"\";\n let agentReply = \"\";\n let toolCalls = 0;\n\n const offRecordUser = session.bus.on<RecordUserAudioPacket>(\"record.user_audio\", (pkt) => {\n inputChunks.push(pkt.audio);\n });\n const offTtsAudio = session.bus.on<TextToSpeechAudioPacket>(\"tts.audio\", (pkt) => {\n if (timeline.firstTtsAudioMs === 0) timeline.firstTtsAudioMs = pkt.timestampMs;\n outputChunks.push(pkt.audio);\n });\n\n const ttsEnd = new Promise<void>((resolveEnd, reject) => {\n const timeout = setTimeout(() => {\n offTtsEnd();\n reject(new Error(\"tts.end timeout\"));\n }, opts.ttsEndTimeoutMs ?? DEFAULT_TTS_END_TIMEOUT_MS);\n const offTtsEnd = session.bus.on<TextToSpeechEndPacket>(\"tts.end\", (pkt) => {\n if (pkt.contextId !== contextId) return;\n clearTimeout(timeout);\n offTtsEnd();\n timeline.ttsEndMs = pkt.timestampMs;\n resolveEnd();\n });\n });\n\n const on = <K extends keyof VoiceAgentSessionEvents>(event: K, handler: VoiceAgentSessionEvents[K]): void => {\n session.on(event, handler);\n };\n on(\"user_input_final\", (event) => {\n finalTranscript = event.text;\n timeline.finalTranscriptMs = event.tsMs;\n eventLines.push(eventLine(\"user_input_final\", { turnId: event.turnId, text: event.text }));\n });\n on(\"agent_text_delta\", (event) => {\n if (timeline.firstLlmDeltaMs === 0) timeline.firstLlmDeltaMs = event.tsMs;\n agentReply += event.delta;\n eventLines.push(eventLine(\"agent_text_delta\", { turnId: event.turnId, delta: event.delta }));\n });\n on(\"agent_tool_call\", (event) => {\n toolCalls += 1;\n eventLines.push(eventLine(\"agent_tool_call\", { turnId: event.turnId, name: event.name }));\n });\n on(\"agent_finished\", (event) => {\n eventLines.push(eventLine(\"agent_finished\", { turnId: event.turnId }));\n });\n on(\"error\", (event) => {\n eventLines.push(eventLine(\"error\", { stage: event.stage, category: event.category, message: event.message }));\n });\n\n await session.start();\n\n session.bus.push(Route.Main, {\n kind: \"turn.change\",\n contextId,\n previousContextId: \"\",\n reason: \"headless_turn_start\",\n timestampMs: Date.now(),\n });\n\n let offset = 0;\n while (offset < pcm.length) {\n const frame = sliceFramePcm(pcm, offset);\n const audio = pcmToBytes(frame);\n if (timeline.feedStartMs === 0) timeline.feedStartMs = Date.now();\n session.bus.push(Route.Main, {\n kind: \"user.audio_received\",\n contextId,\n timestampMs: Date.now(),\n audio,\n });\n opts.onAudioFrame?.(contextId);\n offset += SAMPLES_PER_FRAME;\n if (opts.realtimePacing === true) await sleep(20);\n }\n timeline.speechEndMs = Date.now();\n\n for (let pad = 0; pad < 40; pad += 1) {\n const frame = new Int16Array(SAMPLES_PER_FRAME);\n session.bus.push(Route.Main, {\n kind: \"user.audio_received\",\n contextId,\n timestampMs: Date.now(),\n audio: pcmToBytes(frame),\n });\n opts.onAudioFrame?.(contextId);\n if (opts.realtimePacing === true) await sleep(20);\n }\n\n await opts.onAudioFed?.(contextId);\n await ttsEnd;\n\n offRecordUser();\n offTtsAudio();\n\n const metrics: PerTurnMetrics = {\n turnId: contextId,\n inputAudioMs: Math.round((pcm.length / 16000) * 1000),\n speechEndToFinalTranscriptMs:\n timeline.speechEndMs > 0 && timeline.finalTranscriptMs > 0\n ? Math.max(0, timeline.finalTranscriptMs - timeline.speechEndMs)\n : 0,\n speechEndToFirstAudioMs:\n timeline.speechEndMs > 0 && timeline.firstTtsAudioMs > 0\n ? Math.max(0, timeline.firstTtsAudioMs - timeline.speechEndMs)\n : 0,\n endpointingMs:\n timeline.feedStartMs > 0 && timeline.finalTranscriptMs > 0\n ? Math.max(0, timeline.finalTranscriptMs - timeline.feedStartMs)\n : 0,\n llmTTFTMs:\n timeline.finalTranscriptMs > 0 && timeline.firstLlmDeltaMs > 0\n ? Math.max(0, timeline.firstLlmDeltaMs - timeline.finalTranscriptMs)\n : 0,\n ttsTTFBMs:\n timeline.firstLlmDeltaMs > 0 && timeline.firstTtsAudioMs > 0\n ? Math.max(0, timeline.firstTtsAudioMs - timeline.firstLlmDeltaMs)\n : 0,\n e2eLatencyMs:\n timeline.feedStartMs > 0 && timeline.firstTtsAudioMs > 0\n ? Math.max(0, timeline.firstTtsAudioMs - timeline.feedStartMs)\n : 0,\n agentTokens: agentReply.trim().length === 0 ? 0 : agentReply.trim().split(/\\s+/).length,\n playedMs: Math.round((mergeBytes(outputChunks).byteLength / 2 / 16000) * 1000),\n truncated: false,\n toolCalls,\n };\n\n const inputWavPath = join(sessionDir, \"audio-in.wav\");\n const agentOutWavPath = join(sessionDir, \"audio-out.wav\");\n const eventsJsonlPath = join(sessionDir, \"events.jsonl\");\n const eventsJsonPath = join(sessionDir, \"events.json\");\n const transcriptJsonPath = join(sessionDir, \"transcript.json\");\n const metricsJsonPath = join(sessionDir, \"metrics.json\");\n const events = eventLines\n .map((line) => line.trim())\n .filter((line) => line.length > 0)\n .map((line) => JSON.parse(line) as Record<string, unknown>);\n const qualityGate = {\n passed: finalTranscript.trim().length > 0 && agentReply.trim().length > 0 && outputChunks.length > 0,\n failures: [\n ...(finalTranscript.trim().length === 0 ? [\"missing final transcript\"] : []),\n ...(agentReply.trim().length === 0 ? [\"missing agent reply\"] : []),\n ...(outputChunks.length === 0 ? [\"missing TTS audio\"] : []),\n ],\n };\n\n await writePcm16Wav(inputWavPath, inputChunks, 16000);\n await writePcm16Wav(agentOutWavPath, outputChunks, 16000);\n await writeFile(eventsJsonlPath, eventLines.join(\"\"), \"utf8\");\n await writeFile(eventsJsonPath, `${JSON.stringify({ events, qualityGate }, null, 2)}\\n`, \"utf8\");\n await writeFile(\n transcriptJsonPath,\n `${JSON.stringify({ finalTranscript, agentReply, turnCount: 1, metrics, qualityGate }, null, 2)}\\n`,\n \"utf8\",\n );\n await writeFile(metricsJsonPath, `${JSON.stringify({ ...metrics, turnCount: 1, qualityGate }, null, 2)}\\n`, \"utf8\");\n\n await session.close();\n\n return {\n sessionDir,\n finalTranscript,\n agentReply,\n agentOutWavPath,\n inputWavPath,\n eventsJsonlPath,\n eventsJsonPath,\n transcriptJsonPath,\n metricsJsonPath,\n metrics,\n durationMs:\n timeline.feedStartMs > 0 && timeline.ttsEndMs > 0 ? Math.max(0, timeline.ttsEndMs - timeline.feedStartMs) : 0,\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["require"]
7
+ }
@@ -0,0 +1,25 @@
1
+ export declare const CLI_VERSION: string;
2
+ export declare const CORE_PACKAGE_NAME = "@kuralle-syrinx/core";
3
+ export interface ResolvedPackageVersion {
4
+ readonly version: string;
5
+ readonly packageJsonPath: string;
6
+ }
7
+ /** First path component of a semver string, e.g. "4.3.0" -> "4". Not validated against full semver — good enough for a major-version skew check. */
8
+ export declare function majorOf(version: string): string;
9
+ /**
10
+ * Resolve `pkgName`'s installed version by walking node_modules resolution from
11
+ * `fromDir` (normally the invoking project's cwd), then walking up from the
12
+ * resolved entry file to the package's own package.json. Returns undefined when
13
+ * the package cannot be resolved at all (not installed in this project).
14
+ */
15
+ export declare function resolveInstalledPackageVersion(pkgName: string, fromDir: string): ResolvedPackageVersion | undefined;
16
+ export interface VersionSkewCheck {
17
+ readonly cliVersion: string;
18
+ readonly coreVersion: string | undefined;
19
+ readonly coreResolved: boolean;
20
+ readonly mismatch: boolean;
21
+ }
22
+ export declare function checkCoreVersionSkew(fromDir: string): VersionSkewCheck;
23
+ /** Prints a loud warning to stderr when the CLI's major and the project's installed core major differ. Never writes to stdout. */
24
+ export declare function warnOnVersionSkew(fromDir: string): VersionSkewCheck;
25
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,WAAW,EAAE,MAA+B,CAAC;AAC1D,eAAO,MAAM,iBAAiB,yBAAyB,CAAC;AAExD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,oJAAoJ;AACpJ,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,sBAAsB,GAAG,SAAS,CA6BnH;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAOtE;AAED,kIAAkI;AAClI,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAUnE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuralle-syrinx/cli",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "private": false,
5
5
  "description": "Agent-facing CLI for Syrinx — run a turn or a text exchange with deterministic, machine-readable (--json) output and exit codes. Not a console: no REPL, no chat loop, no microphone.",
6
6
  "keywords": [
@@ -32,21 +32,24 @@
32
32
  "README.md"
33
33
  ],
34
34
  "exports": {
35
- "./turn-runner": "./src/turn-runner.ts"
35
+ "./turn-runner": {
36
+ "types": "./dist/turn-runner.d.ts",
37
+ "default": "./dist/turn-runner.js"
38
+ }
36
39
  },
37
40
  "dependencies": {
38
41
  "wavefile": "^11.0.0",
39
- "@kuralle-syrinx/core": "4.4.0"
42
+ "@kuralle-syrinx/core": "4.5.0"
40
43
  },
41
44
  "devDependencies": {
42
45
  "@types/node": "^22.0.0",
43
46
  "esbuild": "^0.28.1",
44
47
  "typescript": "^5.7.0",
45
48
  "vitest": "^3.2.6",
46
- "@kuralle-syrinx/test": "4.4.0"
49
+ "@kuralle-syrinx/test": "4.5.0"
47
50
  },
48
51
  "scripts": {
49
- "build": "node scripts/build.mjs",
52
+ "build": "node scripts/build.mjs && tsc -p tsconfig.build.json --emitDeclarationOnly",
50
53
  "typecheck": "tsc --noEmit",
51
54
  "test": "vitest run"
52
55
  }