@kodelyth/voice-call 2026.5.39 → 2026.5.42

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 (137) hide show
  1. package/README.md +167 -0
  2. package/api.ts +16 -0
  3. package/cli-metadata.ts +10 -0
  4. package/config-api.ts +12 -0
  5. package/dist/api.js +2 -0
  6. package/dist/cli-metadata.js +12 -0
  7. package/dist/config-DAwbG2aw.js +621 -0
  8. package/dist/config-compat-BYfJ5ueI.js +129 -0
  9. package/dist/guarded-json-api-xAIbFPZh.js +591 -0
  10. package/dist/index.js +1341 -0
  11. package/dist/mock-jtSdKDQN.js +135 -0
  12. package/dist/plivo-L-JTeuEc.js +392 -0
  13. package/dist/realtime-handler-5pSItXxX.js +1227 -0
  14. package/dist/realtime-transcription.runtime-CAbQKwCN.js +2 -0
  15. package/dist/realtime-voice.runtime-vCpCAutg.js +2 -0
  16. package/dist/response-generator-B-MjbtsM.js +199 -0
  17. package/dist/runtime-api.js +6 -0
  18. package/dist/runtime-entry-ohPMJR46.js +3435 -0
  19. package/dist/runtime-entry.js +2 -0
  20. package/dist/setup-api.js +37 -0
  21. package/dist/telnyx-BWr9EZ4x.js +278 -0
  22. package/dist/twilio-D9B0zY1k.js +679 -0
  23. package/index.test.ts +1075 -0
  24. package/index.ts +863 -0
  25. package/klaw.plugin.json +30 -133
  26. package/package.json +3 -3
  27. package/runtime-api.ts +20 -0
  28. package/runtime-entry.ts +1 -0
  29. package/setup-api.ts +47 -0
  30. package/src/allowlist.test.ts +18 -0
  31. package/src/allowlist.ts +19 -0
  32. package/src/cli.test.ts +12 -0
  33. package/src/cli.ts +866 -0
  34. package/src/config-compat.test.ts +130 -0
  35. package/src/config-compat.ts +227 -0
  36. package/src/config.test.ts +542 -0
  37. package/src/config.ts +883 -0
  38. package/src/core-bridge.ts +14 -0
  39. package/src/deep-merge.test.ts +40 -0
  40. package/src/deep-merge.ts +23 -0
  41. package/src/gateway-continue-operation.ts +200 -0
  42. package/src/http-headers.test.ts +16 -0
  43. package/src/http-headers.ts +15 -0
  44. package/src/manager/context.ts +50 -0
  45. package/src/manager/events.test.ts +578 -0
  46. package/src/manager/events.ts +332 -0
  47. package/src/manager/lifecycle.ts +53 -0
  48. package/src/manager/lookup.test.ts +52 -0
  49. package/src/manager/lookup.ts +35 -0
  50. package/src/manager/outbound.test.ts +629 -0
  51. package/src/manager/outbound.ts +508 -0
  52. package/src/manager/state.ts +48 -0
  53. package/src/manager/store.ts +107 -0
  54. package/src/manager/timers.test.ts +127 -0
  55. package/src/manager/timers.ts +113 -0
  56. package/src/manager/twiml.test.ts +13 -0
  57. package/src/manager/twiml.ts +17 -0
  58. package/src/manager.closed-loop.test.ts +259 -0
  59. package/src/manager.inbound-allowlist.test.ts +183 -0
  60. package/src/manager.notify.test.ts +390 -0
  61. package/src/manager.restore.test.ts +310 -0
  62. package/src/manager.test-harness.ts +127 -0
  63. package/src/manager.ts +441 -0
  64. package/src/media-stream.test.ts +953 -0
  65. package/src/media-stream.ts +876 -0
  66. package/src/providers/base.ts +99 -0
  67. package/src/providers/mock.test.ts +86 -0
  68. package/src/providers/mock.ts +185 -0
  69. package/src/providers/plivo.test.ts +93 -0
  70. package/src/providers/plivo.ts +601 -0
  71. package/src/providers/shared/call-status.test.ts +24 -0
  72. package/src/providers/shared/call-status.ts +24 -0
  73. package/src/providers/shared/guarded-json-api.test.ts +127 -0
  74. package/src/providers/shared/guarded-json-api.ts +49 -0
  75. package/src/providers/telnyx.test.ts +489 -0
  76. package/src/providers/telnyx.ts +419 -0
  77. package/src/providers/twilio/api.test.ts +184 -0
  78. package/src/providers/twilio/api.ts +100 -0
  79. package/src/providers/twilio/twiml-policy.test.ts +84 -0
  80. package/src/providers/twilio/twiml-policy.ts +87 -0
  81. package/src/providers/twilio/webhook.ts +34 -0
  82. package/src/providers/twilio.test.ts +607 -0
  83. package/src/providers/twilio.ts +861 -0
  84. package/src/providers/twilio.types.ts +17 -0
  85. package/src/realtime-agent-context.test.ts +101 -0
  86. package/src/realtime-agent-context.ts +149 -0
  87. package/src/realtime-defaults.ts +3 -0
  88. package/src/realtime-fast-context.test.ts +74 -0
  89. package/src/realtime-fast-context.ts +27 -0
  90. package/src/realtime-transcription.runtime.ts +4 -0
  91. package/src/realtime-voice.runtime.ts +5 -0
  92. package/src/response-generator.test.ts +385 -0
  93. package/src/response-generator.ts +348 -0
  94. package/src/response-model.test.ts +71 -0
  95. package/src/response-model.ts +23 -0
  96. package/src/runtime.test.ts +625 -0
  97. package/src/runtime.ts +528 -0
  98. package/src/telephony-audio.test.ts +61 -0
  99. package/src/telephony-audio.ts +12 -0
  100. package/src/telephony-tts.test.ts +196 -0
  101. package/src/telephony-tts.ts +235 -0
  102. package/src/test-fixtures.ts +82 -0
  103. package/src/tts-provider-voice.test.ts +34 -0
  104. package/src/tts-provider-voice.ts +21 -0
  105. package/src/tunnel.test.ts +173 -0
  106. package/src/tunnel.ts +314 -0
  107. package/src/types.ts +311 -0
  108. package/src/utils.test.ts +17 -0
  109. package/src/utils.ts +14 -0
  110. package/src/voice-mapping.test.ts +32 -0
  111. package/src/voice-mapping.ts +65 -0
  112. package/src/webhook/realtime-audio-pacer.test.ts +146 -0
  113. package/src/webhook/realtime-audio-pacer.ts +204 -0
  114. package/src/webhook/realtime-handler.test.ts +1450 -0
  115. package/src/webhook/realtime-handler.ts +1382 -0
  116. package/src/webhook/stale-call-reaper.test.ts +89 -0
  117. package/src/webhook/stale-call-reaper.ts +38 -0
  118. package/src/webhook/stream-frame-adapter.test.ts +187 -0
  119. package/src/webhook/stream-frame-adapter.ts +219 -0
  120. package/src/webhook/tailscale.test.ts +216 -0
  121. package/src/webhook/tailscale.ts +129 -0
  122. package/src/webhook-exposure.test.ts +33 -0
  123. package/src/webhook-exposure.ts +84 -0
  124. package/src/webhook-security.test.ts +813 -0
  125. package/src/webhook-security.ts +982 -0
  126. package/src/webhook.hangup-once.lifecycle.test.ts +179 -0
  127. package/src/webhook.test.ts +1615 -0
  128. package/src/webhook.ts +933 -0
  129. package/src/webhook.types.ts +5 -0
  130. package/src/websocket-test-support.ts +72 -0
  131. package/tsconfig.json +16 -0
  132. package/api.js +0 -7
  133. package/cli-metadata.js +0 -7
  134. package/index.js +0 -7
  135. package/runtime-api.js +0 -7
  136. package/runtime-entry.js +0 -7
  137. package/setup-api.js +0 -7
@@ -0,0 +1,385 @@
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
+ });