@kodelyth/voice-call 2026.5.42 → 2026.6.2

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.
Files changed (111) hide show
  1. package/package.json +18 -6
  2. package/api.ts +0 -16
  3. package/cli-metadata.ts +0 -10
  4. package/config-api.ts +0 -12
  5. package/index.test.ts +0 -1075
  6. package/index.ts +0 -863
  7. package/runtime-api.ts +0 -20
  8. package/runtime-entry.ts +0 -1
  9. package/setup-api.ts +0 -47
  10. package/src/allowlist.test.ts +0 -18
  11. package/src/allowlist.ts +0 -19
  12. package/src/cli.test.ts +0 -12
  13. package/src/cli.ts +0 -866
  14. package/src/config-compat.test.ts +0 -130
  15. package/src/config-compat.ts +0 -227
  16. package/src/config.test.ts +0 -542
  17. package/src/config.ts +0 -883
  18. package/src/core-bridge.ts +0 -14
  19. package/src/deep-merge.test.ts +0 -40
  20. package/src/deep-merge.ts +0 -23
  21. package/src/gateway-continue-operation.ts +0 -200
  22. package/src/http-headers.test.ts +0 -16
  23. package/src/http-headers.ts +0 -15
  24. package/src/manager/context.ts +0 -50
  25. package/src/manager/events.test.ts +0 -578
  26. package/src/manager/events.ts +0 -332
  27. package/src/manager/lifecycle.ts +0 -53
  28. package/src/manager/lookup.test.ts +0 -52
  29. package/src/manager/lookup.ts +0 -35
  30. package/src/manager/outbound.test.ts +0 -629
  31. package/src/manager/outbound.ts +0 -508
  32. package/src/manager/state.ts +0 -48
  33. package/src/manager/store.ts +0 -107
  34. package/src/manager/timers.test.ts +0 -127
  35. package/src/manager/timers.ts +0 -113
  36. package/src/manager/twiml.test.ts +0 -13
  37. package/src/manager/twiml.ts +0 -17
  38. package/src/manager.closed-loop.test.ts +0 -259
  39. package/src/manager.inbound-allowlist.test.ts +0 -183
  40. package/src/manager.notify.test.ts +0 -390
  41. package/src/manager.restore.test.ts +0 -310
  42. package/src/manager.test-harness.ts +0 -127
  43. package/src/manager.ts +0 -441
  44. package/src/media-stream.test.ts +0 -953
  45. package/src/media-stream.ts +0 -876
  46. package/src/providers/base.ts +0 -99
  47. package/src/providers/mock.test.ts +0 -86
  48. package/src/providers/mock.ts +0 -185
  49. package/src/providers/plivo.test.ts +0 -93
  50. package/src/providers/plivo.ts +0 -601
  51. package/src/providers/shared/call-status.test.ts +0 -24
  52. package/src/providers/shared/call-status.ts +0 -24
  53. package/src/providers/shared/guarded-json-api.test.ts +0 -127
  54. package/src/providers/shared/guarded-json-api.ts +0 -49
  55. package/src/providers/telnyx.test.ts +0 -489
  56. package/src/providers/telnyx.ts +0 -419
  57. package/src/providers/twilio/api.test.ts +0 -184
  58. package/src/providers/twilio/api.ts +0 -100
  59. package/src/providers/twilio/twiml-policy.test.ts +0 -84
  60. package/src/providers/twilio/twiml-policy.ts +0 -87
  61. package/src/providers/twilio/webhook.ts +0 -34
  62. package/src/providers/twilio.test.ts +0 -607
  63. package/src/providers/twilio.ts +0 -861
  64. package/src/providers/twilio.types.ts +0 -17
  65. package/src/realtime-agent-context.test.ts +0 -101
  66. package/src/realtime-agent-context.ts +0 -149
  67. package/src/realtime-defaults.ts +0 -3
  68. package/src/realtime-fast-context.test.ts +0 -74
  69. package/src/realtime-fast-context.ts +0 -27
  70. package/src/realtime-transcription.runtime.ts +0 -4
  71. package/src/realtime-voice.runtime.ts +0 -5
  72. package/src/response-generator.test.ts +0 -385
  73. package/src/response-generator.ts +0 -348
  74. package/src/response-model.test.ts +0 -71
  75. package/src/response-model.ts +0 -23
  76. package/src/runtime.test.ts +0 -625
  77. package/src/runtime.ts +0 -528
  78. package/src/telephony-audio.test.ts +0 -61
  79. package/src/telephony-audio.ts +0 -12
  80. package/src/telephony-tts.test.ts +0 -196
  81. package/src/telephony-tts.ts +0 -235
  82. package/src/test-fixtures.ts +0 -82
  83. package/src/tts-provider-voice.test.ts +0 -34
  84. package/src/tts-provider-voice.ts +0 -21
  85. package/src/tunnel.test.ts +0 -173
  86. package/src/tunnel.ts +0 -314
  87. package/src/types.ts +0 -311
  88. package/src/utils.test.ts +0 -17
  89. package/src/utils.ts +0 -14
  90. package/src/voice-mapping.test.ts +0 -32
  91. package/src/voice-mapping.ts +0 -65
  92. package/src/webhook/realtime-audio-pacer.test.ts +0 -146
  93. package/src/webhook/realtime-audio-pacer.ts +0 -204
  94. package/src/webhook/realtime-handler.test.ts +0 -1450
  95. package/src/webhook/realtime-handler.ts +0 -1382
  96. package/src/webhook/stale-call-reaper.test.ts +0 -89
  97. package/src/webhook/stale-call-reaper.ts +0 -38
  98. package/src/webhook/stream-frame-adapter.test.ts +0 -187
  99. package/src/webhook/stream-frame-adapter.ts +0 -219
  100. package/src/webhook/tailscale.test.ts +0 -216
  101. package/src/webhook/tailscale.ts +0 -129
  102. package/src/webhook-exposure.test.ts +0 -33
  103. package/src/webhook-exposure.ts +0 -84
  104. package/src/webhook-security.test.ts +0 -813
  105. package/src/webhook-security.ts +0 -982
  106. package/src/webhook.hangup-once.lifecycle.test.ts +0 -179
  107. package/src/webhook.test.ts +0 -1615
  108. package/src/webhook.ts +0 -933
  109. package/src/webhook.types.ts +0 -5
  110. package/src/websocket-test-support.ts +0 -72
  111. package/tsconfig.json +0 -16
@@ -1,385 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { VoiceCallConfigSchema } from "./config.js";
3
- import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
4
- import { generateVoiceResponse } from "./response-generator.js";
5
-
6
- type TestSessionEntry = {
7
- sessionId: string;
8
- updatedAt: number;
9
- providerOverride?: string;
10
- modelOverride?: string;
11
- modelOverrideSource?: string;
12
- };
13
-
14
- type EmbeddedAgentArgs = {
15
- extraSystemPrompt: string;
16
- provider?: string;
17
- model?: string;
18
- sessionKey?: string;
19
- sandboxSessionKey?: string;
20
- agentDir?: string;
21
- agentId?: string;
22
- workspaceDir?: string;
23
- sessionFile?: string;
24
- toolsAllow?: string[];
25
- };
26
-
27
- function createAgentRuntime(payloads: Array<Record<string, unknown>>) {
28
- const sessionStore: Record<string, TestSessionEntry> = {};
29
- const saveSessionStore = vi.fn(async () => {});
30
- const updateSessionStore = vi.fn(
31
- async (_storePath: string, mutator: (store: Record<string, TestSessionEntry>) => unknown) => {
32
- return await mutator(sessionStore);
33
- },
34
- );
35
- const runEmbeddedPiAgent = vi.fn(async () => ({
36
- payloads,
37
- meta: { durationMs: 12, aborted: false },
38
- }));
39
- const resolveAgentDir = vi.fn((_cfg: CoreConfig, agentId: string) => {
40
- return `/tmp/klaw/agents/${agentId}`;
41
- });
42
- const resolveAgentWorkspaceDir = vi.fn((_cfg: CoreConfig, agentId: string) => {
43
- return `/tmp/klaw/workspace/${agentId}`;
44
- });
45
- const resolveAgentIdentity = vi.fn((_cfg: CoreConfig, agentId: string) => ({
46
- name: `${agentId} tester`,
47
- }));
48
- const resolveStorePath = vi.fn((_store: string | undefined, params: { agentId?: string }) => {
49
- return `/tmp/klaw/${params.agentId ?? "main"}/sessions.json`;
50
- });
51
- const resolveSessionFilePath = vi.fn(
52
- (_sessionId: string, _entry: unknown, params: { agentId?: string }) => {
53
- return `/tmp/klaw/${params.agentId ?? "main"}/sessions/session.jsonl`;
54
- },
55
- );
56
-
57
- const runtime = {
58
- defaults: {
59
- provider: "together",
60
- model: "Qwen/Qwen2.5-7B-Instruct-Turbo",
61
- },
62
- resolveAgentDir,
63
- resolveAgentWorkspaceDir,
64
- resolveAgentIdentity,
65
- resolveThinkingDefault: () => "off",
66
- resolveAgentTimeoutMs: () => 30_000,
67
- ensureAgentWorkspace: async () => {},
68
- runEmbeddedPiAgent,
69
- session: {
70
- resolveStorePath,
71
- loadSessionStore: () => sessionStore,
72
- saveSessionStore,
73
- updateSessionStore,
74
- resolveSessionFilePath,
75
- },
76
- } as unknown as CoreAgentDeps;
77
-
78
- return {
79
- runtime,
80
- runEmbeddedPiAgent,
81
- saveSessionStore,
82
- updateSessionStore,
83
- sessionStore,
84
- resolveAgentDir,
85
- resolveAgentWorkspaceDir,
86
- resolveAgentIdentity,
87
- resolveStorePath,
88
- resolveSessionFilePath,
89
- };
90
- }
91
-
92
- function requireEmbeddedAgentArgs(runEmbeddedPiAgent: ReturnType<typeof vi.fn>) {
93
- const calls = runEmbeddedPiAgent.mock.calls as unknown[][];
94
- const firstCall = requireFirstMockCall(
95
- calls,
96
- "voice response generator embedded agent invocation",
97
- );
98
- const args = firstCall[0] as Partial<EmbeddedAgentArgs> | undefined;
99
- if (!args?.extraSystemPrompt) {
100
- throw new Error("voice response generator did not pass the spoken-output contract prompt");
101
- }
102
- return args as EmbeddedAgentArgs;
103
- }
104
-
105
- function requireFirstMockCall(calls: readonly unknown[][], label: string): unknown[] {
106
- const call = calls.at(0);
107
- if (!call) {
108
- throw new Error(`expected ${label} call`);
109
- }
110
- return call;
111
- }
112
-
113
- async function runGenerateVoiceResponse(
114
- payloads: Array<Record<string, unknown>>,
115
- overrides?: {
116
- runtime?: CoreAgentDeps;
117
- transcript?: Array<{ speaker: "user" | "bot"; text: string }>;
118
- },
119
- ) {
120
- const voiceConfig = VoiceCallConfigSchema.parse({
121
- responseTimeoutMs: 5000,
122
- });
123
- const coreConfig = {} as CoreConfig;
124
- const runtime = overrides?.runtime ?? createAgentRuntime(payloads).runtime;
125
-
126
- const result = await generateVoiceResponse({
127
- voiceConfig,
128
- coreConfig,
129
- agentRuntime: runtime,
130
- callId: "call-123",
131
- from: "+15550001111",
132
- transcript: overrides?.transcript ?? [{ speaker: "user", text: "hello there" }],
133
- userMessage: "hello there",
134
- });
135
-
136
- return { result };
137
- }
138
-
139
- describe("generateVoiceResponse", () => {
140
- it("suppresses reasoning payloads and reads structured spoken output", async () => {
141
- const { runtime, runEmbeddedPiAgent } = createAgentRuntime([
142
- { text: "Reasoning: hidden", isReasoning: true },
143
- { text: '{"spoken":"Hello from JSON."}' },
144
- ]);
145
- const { result } = await runGenerateVoiceResponse([], { runtime });
146
-
147
- expect(result.text).toBe("Hello from JSON.");
148
- expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
149
- const args = requireEmbeddedAgentArgs(runEmbeddedPiAgent);
150
- expect(args.extraSystemPrompt).toContain('{"spoken":"..."}');
151
- expect(args.provider).toBe("together");
152
- expect(args.model).toBe("Qwen/Qwen2.5-7B-Instruct-Turbo");
153
- });
154
-
155
- it("extracts spoken text from fenced JSON", async () => {
156
- const { result } = await runGenerateVoiceResponse([
157
- { text: '```json\n{"spoken":"Fenced JSON works."}\n```' },
158
- ]);
159
-
160
- expect(result.text).toBe("Fenced JSON works.");
161
- });
162
-
163
- it("returns silence for an explicit empty spoken contract response", async () => {
164
- const { result } = await runGenerateVoiceResponse([{ text: '{"spoken":""}' }]);
165
-
166
- expect(result.text).toBeNull();
167
- });
168
-
169
- it("strips leading planning text when model returns plain text", async () => {
170
- const { result } = await runGenerateVoiceResponse([
171
- {
172
- text:
173
- "The user responded with short text. I should keep the response concise.\n\n" +
174
- "Sounds good. I can help with the next step whenever you are ready.",
175
- },
176
- ]);
177
-
178
- expect(result.text).toBe("Sounds good. I can help with the next step whenever you are ready.");
179
- });
180
-
181
- it("keeps plain conversational output when no JSON contract is followed", async () => {
182
- const { result } = await runGenerateVoiceResponse([
183
- { text: "Absolutely. Tell me what you want to do next." },
184
- ]);
185
-
186
- expect(result.text).toBe("Absolutely. Tell me what you want to do next.");
187
- });
188
-
189
- it("pins the voice session to responseModel before running the embedded agent", async () => {
190
- const { runtime, runEmbeddedPiAgent, updateSessionStore, sessionStore } = createAgentRuntime([
191
- { text: '{"spoken":"Pinned model works."}' },
192
- ]);
193
- const voiceConfig = VoiceCallConfigSchema.parse({
194
- responseModel: "openai/gpt-4.1-nano",
195
- responseTimeoutMs: 5000,
196
- });
197
-
198
- const result = await generateVoiceResponse({
199
- voiceConfig,
200
- coreConfig: {} as CoreConfig,
201
- agentRuntime: runtime,
202
- callId: "call-123",
203
- from: "+15550001111",
204
- transcript: [{ speaker: "user", text: "hello there" }],
205
- userMessage: "hello there",
206
- });
207
-
208
- expect(result.text).toBe("Pinned model works.");
209
- const pinnedSessionEntry = sessionStore["voice:15550001111"];
210
- expect(pinnedSessionEntry?.providerOverride).toBe("openai");
211
- expect(pinnedSessionEntry?.modelOverride).toBe("gpt-4.1-nano");
212
- expect(pinnedSessionEntry?.modelOverrideSource).toBe("auto");
213
- const updateSessionStoreCall = requireFirstMockCall(
214
- updateSessionStore.mock.calls,
215
- "session store update",
216
- );
217
- expect(updateSessionStoreCall[0]).toBe("/tmp/klaw/main/sessions.json");
218
- expect(updateSessionStoreCall[1]).toBeTypeOf("function");
219
- const args = requireEmbeddedAgentArgs(runEmbeddedPiAgent);
220
- expect(args.provider).toBe("openai");
221
- expect(args.model).toBe("gpt-4.1-nano");
222
- expect(args.sessionKey).toBe("voice:15550001111");
223
- });
224
-
225
- it("uses the persisted per-call session key for classic responses", async () => {
226
- const { runtime, runEmbeddedPiAgent, sessionStore } = createAgentRuntime([
227
- { text: '{"spoken":"Fresh call context."}' },
228
- ]);
229
- const voiceConfig = VoiceCallConfigSchema.parse({
230
- sessionScope: "per-call",
231
- responseTimeoutMs: 5000,
232
- });
233
-
234
- const result = await generateVoiceResponse({
235
- voiceConfig,
236
- coreConfig: {} as CoreConfig,
237
- agentRuntime: runtime,
238
- callId: "call-123",
239
- sessionKey: "voice:call:call-123",
240
- from: "+15550001111",
241
- transcript: [{ speaker: "user", text: "hello there" }],
242
- userMessage: "hello there",
243
- });
244
-
245
- expect(result.text).toBe("Fresh call context.");
246
- const perCallSessionEntry = sessionStore["voice:call:call-123"];
247
- expect(perCallSessionEntry?.sessionId).toBeTypeOf("string");
248
- expect(perCallSessionEntry?.sessionId).not.toBe("");
249
- expect(sessionStore["voice:15550001111"]).toBeUndefined();
250
- const args = requireEmbeddedAgentArgs(runEmbeddedPiAgent);
251
- expect(args.sessionKey).toBe("voice:call:call-123");
252
- expect(args.sandboxSessionKey).toBe("agent:main:voice:call:call-123");
253
- });
254
-
255
- it("uses the main agent workspace when voice config omits agentId", async () => {
256
- const {
257
- runtime,
258
- runEmbeddedPiAgent,
259
- resolveAgentDir,
260
- resolveAgentWorkspaceDir,
261
- resolveAgentIdentity,
262
- resolveStorePath,
263
- resolveSessionFilePath,
264
- sessionStore,
265
- } = createAgentRuntime([{ text: '{"spoken":"Default agent."}' }]);
266
- const coreConfig = {} as CoreConfig;
267
-
268
- await generateVoiceResponse({
269
- voiceConfig: VoiceCallConfigSchema.parse({ responseTimeoutMs: 5000 }),
270
- coreConfig,
271
- agentRuntime: runtime,
272
- callId: "call-123",
273
- from: "+15550001111",
274
- transcript: [],
275
- userMessage: "hello there",
276
- });
277
-
278
- expect(resolveStorePath).toHaveBeenCalledWith(undefined, { agentId: "main" });
279
- expect(resolveAgentDir).toHaveBeenCalledWith(coreConfig, "main");
280
- expect(resolveAgentWorkspaceDir).toHaveBeenCalledWith(coreConfig, "main");
281
- expect(resolveAgentIdentity).toHaveBeenCalledWith(coreConfig, "main");
282
- const defaultSessionEntry = sessionStore["voice:15550001111"];
283
- if (!defaultSessionEntry) {
284
- throw new Error("Expected default voice session entry");
285
- }
286
- expect(resolveSessionFilePath).toHaveBeenCalledWith(
287
- defaultSessionEntry.sessionId,
288
- defaultSessionEntry,
289
- {
290
- agentId: "main",
291
- },
292
- );
293
- const args = requireEmbeddedAgentArgs(runEmbeddedPiAgent);
294
- expect(args.agentDir).toBe("/tmp/klaw/agents/main");
295
- expect(args.agentId).toBe("main");
296
- expect(args.sandboxSessionKey).toBe("agent:main:voice:15550001111");
297
- expect(args.workspaceDir).toBe("/tmp/klaw/workspace/main");
298
- expect(args.sessionFile).toBe("/tmp/klaw/main/sessions/session.jsonl");
299
- });
300
-
301
- it("uses the configured voice response agent workspace", async () => {
302
- const {
303
- runtime,
304
- runEmbeddedPiAgent,
305
- resolveAgentDir,
306
- resolveAgentWorkspaceDir,
307
- resolveAgentIdentity,
308
- resolveStorePath,
309
- resolveSessionFilePath,
310
- sessionStore,
311
- } = createAgentRuntime([{ text: '{"spoken":"Voice agent."}' }]);
312
- const coreConfig = {} as CoreConfig;
313
-
314
- const result = await generateVoiceResponse({
315
- voiceConfig: VoiceCallConfigSchema.parse({
316
- agentId: "voice",
317
- responseTimeoutMs: 5000,
318
- }),
319
- coreConfig,
320
- agentRuntime: runtime,
321
- callId: "call-123",
322
- from: "+15550001111",
323
- transcript: [],
324
- userMessage: "hello there",
325
- });
326
-
327
- expect(result.text).toBe("Voice agent.");
328
- expect(resolveStorePath).toHaveBeenCalledWith(undefined, { agentId: "voice" });
329
- expect(resolveAgentDir).toHaveBeenCalledWith(coreConfig, "voice");
330
- expect(resolveAgentWorkspaceDir).toHaveBeenCalledWith(coreConfig, "voice");
331
- expect(resolveAgentIdentity).toHaveBeenCalledWith(coreConfig, "voice");
332
- const voiceSessionEntry = sessionStore["voice:15550001111"];
333
- if (!voiceSessionEntry) {
334
- throw new Error("Expected routed voice session entry");
335
- }
336
- expect(resolveSessionFilePath).toHaveBeenCalledWith(
337
- voiceSessionEntry.sessionId,
338
- voiceSessionEntry,
339
- {
340
- agentId: "voice",
341
- },
342
- );
343
- const args = requireEmbeddedAgentArgs(runEmbeddedPiAgent);
344
- expect(args.agentDir).toBe("/tmp/klaw/agents/voice");
345
- expect(args.agentId).toBe("voice");
346
- expect(args.sandboxSessionKey).toBe("agent:voice:voice:15550001111");
347
- expect(args.workspaceDir).toBe("/tmp/klaw/workspace/voice");
348
- expect(args.sessionFile).toBe("/tmp/klaw/voice/sessions/session.jsonl");
349
- });
350
-
351
- it("passes the routed voice agent explicit tool allowlist to the embedded run", async () => {
352
- const { runtime, runEmbeddedPiAgent } = createAgentRuntime([
353
- { text: '{"spoken":"No tools needed."}' },
354
- ]);
355
- const coreConfig = {
356
- agents: {
357
- list: [
358
- {
359
- id: "voice",
360
- tools: { allow: [] },
361
- },
362
- ],
363
- },
364
- } as CoreConfig;
365
-
366
- const result = await generateVoiceResponse({
367
- voiceConfig: VoiceCallConfigSchema.parse({
368
- agentId: "voice",
369
- responseModel: "ollama/qwen2.5:1.5b",
370
- responseTimeoutMs: 5000,
371
- }),
372
- coreConfig,
373
- agentRuntime: runtime,
374
- callId: "call-123",
375
- from: "+15550001111",
376
- transcript: [],
377
- userMessage: "hello there",
378
- });
379
-
380
- expect(result.text).toBe("No tools needed.");
381
- const args = requireEmbeddedAgentArgs(runEmbeddedPiAgent);
382
- expect(args.agentId).toBe("voice");
383
- expect(args.toolsAllow).toStrictEqual([]);
384
- });
385
- });