@livekit/agents 0.7.9 → 1.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (627) hide show
  1. package/dist/_exceptions.cjs +109 -0
  2. package/dist/_exceptions.cjs.map +1 -0
  3. package/dist/_exceptions.d.cts +64 -0
  4. package/dist/_exceptions.d.ts +64 -0
  5. package/dist/_exceptions.d.ts.map +1 -0
  6. package/dist/_exceptions.js +80 -0
  7. package/dist/_exceptions.js.map +1 -0
  8. package/dist/audio.cjs +10 -3
  9. package/dist/audio.cjs.map +1 -1
  10. package/dist/audio.d.cts +2 -0
  11. package/dist/audio.d.ts +2 -0
  12. package/dist/audio.d.ts.map +1 -1
  13. package/dist/audio.js +8 -2
  14. package/dist/audio.js.map +1 -1
  15. package/dist/cli.cjs +25 -0
  16. package/dist/cli.cjs.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +25 -0
  19. package/dist/cli.js.map +1 -1
  20. package/dist/constants.cjs +6 -3
  21. package/dist/constants.cjs.map +1 -1
  22. package/dist/constants.d.cts +2 -1
  23. package/dist/constants.d.ts +2 -1
  24. package/dist/constants.d.ts.map +1 -1
  25. package/dist/constants.js +4 -2
  26. package/dist/constants.js.map +1 -1
  27. package/dist/http_server.cjs.map +1 -1
  28. package/dist/http_server.d.cts +1 -0
  29. package/dist/http_server.d.ts +1 -0
  30. package/dist/http_server.d.ts.map +1 -1
  31. package/dist/http_server.js.map +1 -1
  32. package/dist/index.cjs +27 -20
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +13 -10
  35. package/dist/index.d.ts +13 -10
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +15 -11
  38. package/dist/index.js.map +1 -1
  39. package/dist/inference_runner.cjs +0 -1
  40. package/dist/inference_runner.cjs.map +1 -1
  41. package/dist/inference_runner.d.cts +2 -3
  42. package/dist/inference_runner.d.ts +2 -3
  43. package/dist/inference_runner.d.ts.map +1 -1
  44. package/dist/inference_runner.js +0 -1
  45. package/dist/inference_runner.js.map +1 -1
  46. package/dist/ipc/inference_proc_executor.cjs +2 -2
  47. package/dist/ipc/inference_proc_executor.cjs.map +1 -1
  48. package/dist/ipc/inference_proc_executor.js +2 -2
  49. package/dist/ipc/inference_proc_executor.js.map +1 -1
  50. package/dist/ipc/job_executor.cjs.map +1 -1
  51. package/dist/ipc/job_executor.js.map +1 -1
  52. package/dist/ipc/job_proc_executor.cjs +1 -0
  53. package/dist/ipc/job_proc_executor.cjs.map +1 -1
  54. package/dist/ipc/job_proc_executor.js +1 -0
  55. package/dist/ipc/job_proc_executor.js.map +1 -1
  56. package/dist/ipc/job_proc_lazy_main.cjs +1 -1
  57. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
  58. package/dist/ipc/job_proc_lazy_main.js +1 -1
  59. package/dist/ipc/job_proc_lazy_main.js.map +1 -1
  60. package/dist/ipc/supervised_proc.d.cts +1 -1
  61. package/dist/ipc/supervised_proc.d.ts +1 -1
  62. package/dist/ipc/supervised_proc.d.ts.map +1 -1
  63. package/dist/job.cjs +14 -2
  64. package/dist/job.cjs.map +1 -1
  65. package/dist/job.d.cts +8 -0
  66. package/dist/job.d.ts +8 -0
  67. package/dist/job.d.ts.map +1 -1
  68. package/dist/job.js +12 -1
  69. package/dist/job.js.map +1 -1
  70. package/dist/llm/chat_context.cjs +332 -82
  71. package/dist/llm/chat_context.cjs.map +1 -1
  72. package/dist/llm/chat_context.d.cts +152 -48
  73. package/dist/llm/chat_context.d.ts +152 -48
  74. package/dist/llm/chat_context.d.ts.map +1 -1
  75. package/dist/llm/chat_context.js +327 -81
  76. package/dist/llm/chat_context.js.map +1 -1
  77. package/dist/llm/chat_context.test.cjs +380 -0
  78. package/dist/llm/chat_context.test.cjs.map +1 -0
  79. package/dist/llm/chat_context.test.js +385 -0
  80. package/dist/llm/chat_context.test.js.map +1 -0
  81. package/dist/llm/index.cjs +37 -8
  82. package/dist/llm/index.cjs.map +1 -1
  83. package/dist/llm/index.d.cts +7 -3
  84. package/dist/llm/index.d.ts +7 -3
  85. package/dist/llm/index.d.ts.map +1 -1
  86. package/dist/llm/index.js +39 -9
  87. package/dist/llm/index.js.map +1 -1
  88. package/dist/llm/llm.cjs +97 -33
  89. package/dist/llm/llm.cjs.map +1 -1
  90. package/dist/llm/llm.d.cts +50 -24
  91. package/dist/llm/llm.d.ts +50 -24
  92. package/dist/llm/llm.d.ts.map +1 -1
  93. package/dist/llm/llm.js +98 -33
  94. package/dist/llm/llm.js.map +1 -1
  95. package/dist/llm/provider_format/google.cjs +128 -0
  96. package/dist/llm/provider_format/google.cjs.map +1 -0
  97. package/dist/llm/provider_format/google.d.cts +6 -0
  98. package/dist/llm/provider_format/google.d.ts +6 -0
  99. package/dist/llm/provider_format/google.d.ts.map +1 -0
  100. package/dist/llm/provider_format/google.js +104 -0
  101. package/dist/llm/provider_format/google.js.map +1 -0
  102. package/dist/llm/provider_format/google.test.cjs +676 -0
  103. package/dist/llm/provider_format/google.test.cjs.map +1 -0
  104. package/dist/llm/provider_format/google.test.js +675 -0
  105. package/dist/llm/provider_format/google.test.js.map +1 -0
  106. package/dist/llm/provider_format/index.cjs +40 -0
  107. package/dist/llm/provider_format/index.cjs.map +1 -0
  108. package/dist/llm/provider_format/index.d.cts +4 -0
  109. package/dist/llm/provider_format/index.d.ts +4 -0
  110. package/dist/llm/provider_format/index.d.ts.map +1 -0
  111. package/dist/llm/provider_format/index.js +16 -0
  112. package/dist/llm/provider_format/index.js.map +1 -0
  113. package/dist/llm/provider_format/openai.cjs +116 -0
  114. package/dist/llm/provider_format/openai.cjs.map +1 -0
  115. package/dist/llm/provider_format/openai.d.cts +3 -0
  116. package/dist/llm/provider_format/openai.d.ts +3 -0
  117. package/dist/llm/provider_format/openai.d.ts.map +1 -0
  118. package/dist/llm/provider_format/openai.js +92 -0
  119. package/dist/llm/provider_format/openai.js.map +1 -0
  120. package/dist/llm/provider_format/openai.test.cjs +490 -0
  121. package/dist/llm/provider_format/openai.test.cjs.map +1 -0
  122. package/dist/llm/provider_format/openai.test.js +489 -0
  123. package/dist/llm/provider_format/openai.test.js.map +1 -0
  124. package/dist/llm/provider_format/utils.cjs +146 -0
  125. package/dist/llm/provider_format/utils.cjs.map +1 -0
  126. package/dist/llm/provider_format/utils.d.cts +38 -0
  127. package/dist/llm/provider_format/utils.d.ts +38 -0
  128. package/dist/llm/provider_format/utils.d.ts.map +1 -0
  129. package/dist/llm/provider_format/utils.js +122 -0
  130. package/dist/llm/provider_format/utils.js.map +1 -0
  131. package/dist/llm/realtime.cjs +77 -0
  132. package/dist/llm/realtime.cjs.map +1 -0
  133. package/dist/llm/realtime.d.cts +98 -0
  134. package/dist/llm/realtime.d.ts +98 -0
  135. package/dist/llm/realtime.d.ts.map +1 -0
  136. package/dist/llm/realtime.js +52 -0
  137. package/dist/llm/realtime.js.map +1 -0
  138. package/dist/llm/remote_chat_context.cjs +112 -0
  139. package/dist/llm/remote_chat_context.cjs.map +1 -0
  140. package/dist/llm/remote_chat_context.d.cts +23 -0
  141. package/dist/llm/remote_chat_context.d.ts +23 -0
  142. package/dist/llm/remote_chat_context.d.ts.map +1 -0
  143. package/dist/llm/remote_chat_context.js +88 -0
  144. package/dist/llm/remote_chat_context.js.map +1 -0
  145. package/dist/llm/remote_chat_context.test.cjs +225 -0
  146. package/dist/llm/remote_chat_context.test.cjs.map +1 -0
  147. package/dist/llm/remote_chat_context.test.js +224 -0
  148. package/dist/llm/remote_chat_context.test.js.map +1 -0
  149. package/dist/llm/tool_context.cjs +111 -0
  150. package/dist/llm/tool_context.cjs.map +1 -0
  151. package/dist/llm/tool_context.d.cts +125 -0
  152. package/dist/llm/tool_context.d.ts +125 -0
  153. package/dist/llm/tool_context.d.ts.map +1 -0
  154. package/dist/llm/tool_context.js +80 -0
  155. package/dist/llm/tool_context.js.map +1 -0
  156. package/dist/llm/tool_context.test.cjs +162 -0
  157. package/dist/llm/tool_context.test.cjs.map +1 -0
  158. package/dist/llm/tool_context.test.js +161 -0
  159. package/dist/llm/tool_context.test.js.map +1 -0
  160. package/dist/llm/tool_context.type.test.cjs +92 -0
  161. package/dist/llm/tool_context.type.test.cjs.map +1 -0
  162. package/dist/llm/tool_context.type.test.js +91 -0
  163. package/dist/llm/tool_context.type.test.js.map +1 -0
  164. package/dist/llm/utils.cjs +260 -0
  165. package/dist/llm/utils.cjs.map +1 -0
  166. package/dist/llm/utils.d.cts +42 -0
  167. package/dist/llm/utils.d.ts +42 -0
  168. package/dist/llm/utils.d.ts.map +1 -0
  169. package/dist/llm/utils.js +223 -0
  170. package/dist/llm/utils.js.map +1 -0
  171. package/dist/llm/utils.test.cjs +513 -0
  172. package/dist/llm/utils.test.cjs.map +1 -0
  173. package/dist/llm/utils.test.js +490 -0
  174. package/dist/llm/utils.test.js.map +1 -0
  175. package/dist/metrics/base.cjs +0 -27
  176. package/dist/metrics/base.cjs.map +1 -1
  177. package/dist/metrics/base.d.cts +105 -63
  178. package/dist/metrics/base.d.ts +105 -63
  179. package/dist/metrics/base.d.ts.map +1 -1
  180. package/dist/metrics/base.js +0 -19
  181. package/dist/metrics/base.js.map +1 -1
  182. package/dist/metrics/index.cjs +0 -3
  183. package/dist/metrics/index.cjs.map +1 -1
  184. package/dist/metrics/index.d.cts +2 -3
  185. package/dist/metrics/index.d.ts +2 -3
  186. package/dist/metrics/index.d.ts.map +1 -1
  187. package/dist/metrics/index.js +0 -2
  188. package/dist/metrics/index.js.map +1 -1
  189. package/dist/metrics/usage_collector.cjs +17 -12
  190. package/dist/metrics/usage_collector.cjs.map +1 -1
  191. package/dist/metrics/usage_collector.d.cts +3 -2
  192. package/dist/metrics/usage_collector.d.ts +3 -2
  193. package/dist/metrics/usage_collector.d.ts.map +1 -1
  194. package/dist/metrics/usage_collector.js +17 -12
  195. package/dist/metrics/usage_collector.js.map +1 -1
  196. package/dist/metrics/utils.cjs +22 -59
  197. package/dist/metrics/utils.cjs.map +1 -1
  198. package/dist/metrics/utils.d.cts +1 -8
  199. package/dist/metrics/utils.d.ts +1 -8
  200. package/dist/metrics/utils.d.ts.map +1 -1
  201. package/dist/metrics/utils.js +22 -52
  202. package/dist/metrics/utils.js.map +1 -1
  203. package/dist/multimodal/index.cjs +0 -2
  204. package/dist/multimodal/index.cjs.map +1 -1
  205. package/dist/multimodal/index.d.cts +0 -1
  206. package/dist/multimodal/index.d.ts +0 -1
  207. package/dist/multimodal/index.d.ts.map +1 -1
  208. package/dist/multimodal/index.js +0 -1
  209. package/dist/multimodal/index.js.map +1 -1
  210. package/dist/plugin.cjs +24 -8
  211. package/dist/plugin.cjs.map +1 -1
  212. package/dist/plugin.d.cts +18 -4
  213. package/dist/plugin.d.ts +18 -4
  214. package/dist/plugin.d.ts.map +1 -1
  215. package/dist/plugin.js +22 -7
  216. package/dist/plugin.js.map +1 -1
  217. package/dist/stream/deferred_stream.cjs +98 -0
  218. package/dist/stream/deferred_stream.cjs.map +1 -0
  219. package/dist/stream/deferred_stream.d.cts +27 -0
  220. package/dist/stream/deferred_stream.d.ts +27 -0
  221. package/dist/stream/deferred_stream.d.ts.map +1 -0
  222. package/dist/stream/deferred_stream.js +73 -0
  223. package/dist/stream/deferred_stream.js.map +1 -0
  224. package/dist/stream/deferred_stream.test.cjs +527 -0
  225. package/dist/stream/deferred_stream.test.cjs.map +1 -0
  226. package/dist/stream/deferred_stream.test.js +526 -0
  227. package/dist/stream/deferred_stream.test.js.map +1 -0
  228. package/dist/stream/identity_transform.cjs +42 -0
  229. package/dist/stream/identity_transform.cjs.map +1 -0
  230. package/dist/stream/identity_transform.d.cts +6 -0
  231. package/dist/stream/identity_transform.d.ts +6 -0
  232. package/dist/stream/identity_transform.d.ts.map +1 -0
  233. package/dist/stream/identity_transform.js +18 -0
  234. package/dist/stream/identity_transform.js.map +1 -0
  235. package/dist/stream/identity_transform.test.cjs +125 -0
  236. package/dist/stream/identity_transform.test.cjs.map +1 -0
  237. package/dist/stream/identity_transform.test.js +124 -0
  238. package/dist/stream/identity_transform.test.js.map +1 -0
  239. package/dist/stream/index.cjs +38 -0
  240. package/dist/stream/index.cjs.map +1 -0
  241. package/dist/stream/index.d.cts +5 -0
  242. package/dist/stream/index.d.ts +5 -0
  243. package/dist/stream/index.d.ts.map +1 -0
  244. package/dist/stream/index.js +11 -0
  245. package/dist/stream/index.js.map +1 -0
  246. package/dist/stream/merge_readable_streams.cjs +59 -0
  247. package/dist/stream/merge_readable_streams.cjs.map +1 -0
  248. package/dist/stream/merge_readable_streams.d.cts +4 -0
  249. package/dist/stream/merge_readable_streams.d.ts +4 -0
  250. package/dist/stream/merge_readable_streams.d.ts.map +1 -0
  251. package/dist/stream/merge_readable_streams.js +35 -0
  252. package/dist/stream/merge_readable_streams.js.map +1 -0
  253. package/dist/stream/stream_channel.cjs +47 -0
  254. package/dist/stream/stream_channel.cjs.map +1 -0
  255. package/dist/stream/stream_channel.d.cts +9 -0
  256. package/dist/stream/stream_channel.d.ts +9 -0
  257. package/dist/stream/stream_channel.d.ts.map +1 -0
  258. package/dist/stream/stream_channel.js +23 -0
  259. package/dist/stream/stream_channel.js.map +1 -0
  260. package/dist/stream/stream_channel.test.cjs +97 -0
  261. package/dist/stream/stream_channel.test.cjs.map +1 -0
  262. package/dist/stream/stream_channel.test.js +96 -0
  263. package/dist/stream/stream_channel.test.js.map +1 -0
  264. package/dist/stt/stream_adapter.cjs +3 -4
  265. package/dist/stt/stream_adapter.cjs.map +1 -1
  266. package/dist/stt/stream_adapter.d.cts +1 -0
  267. package/dist/stt/stream_adapter.d.ts +1 -0
  268. package/dist/stt/stream_adapter.d.ts.map +1 -1
  269. package/dist/stt/stream_adapter.js +3 -4
  270. package/dist/stt/stream_adapter.js.map +1 -1
  271. package/dist/stt/stt.cjs +100 -10
  272. package/dist/stt/stt.cjs.map +1 -1
  273. package/dist/stt/stt.d.cts +26 -5
  274. package/dist/stt/stt.d.ts +26 -5
  275. package/dist/stt/stt.d.ts.map +1 -1
  276. package/dist/stt/stt.js +101 -11
  277. package/dist/stt/stt.js.map +1 -1
  278. package/dist/tokenize/basic/basic.cjs +10 -5
  279. package/dist/tokenize/basic/basic.cjs.map +1 -1
  280. package/dist/tokenize/basic/basic.d.cts +7 -1
  281. package/dist/tokenize/basic/basic.d.ts +7 -1
  282. package/dist/tokenize/basic/basic.d.ts.map +1 -1
  283. package/dist/tokenize/basic/basic.js +10 -5
  284. package/dist/tokenize/basic/basic.js.map +1 -1
  285. package/dist/tokenize/basic/sentence.cjs +14 -6
  286. package/dist/tokenize/basic/sentence.cjs.map +1 -1
  287. package/dist/tokenize/basic/sentence.d.cts +1 -1
  288. package/dist/tokenize/basic/sentence.d.ts +1 -1
  289. package/dist/tokenize/basic/sentence.d.ts.map +1 -1
  290. package/dist/tokenize/basic/sentence.js +14 -6
  291. package/dist/tokenize/basic/sentence.js.map +1 -1
  292. package/dist/tokenize/token_stream.cjs +5 -3
  293. package/dist/tokenize/token_stream.cjs.map +1 -1
  294. package/dist/tokenize/token_stream.d.cts +1 -0
  295. package/dist/tokenize/token_stream.d.ts +1 -0
  296. package/dist/tokenize/token_stream.d.ts.map +1 -1
  297. package/dist/tokenize/token_stream.js +6 -4
  298. package/dist/tokenize/token_stream.js.map +1 -1
  299. package/dist/transcription.cjs +1 -2
  300. package/dist/transcription.cjs.map +1 -1
  301. package/dist/transcription.d.ts.map +1 -1
  302. package/dist/transcription.js +2 -3
  303. package/dist/transcription.js.map +1 -1
  304. package/dist/tts/index.cjs +2 -4
  305. package/dist/tts/index.cjs.map +1 -1
  306. package/dist/tts/index.d.cts +1 -1
  307. package/dist/tts/index.d.ts +1 -1
  308. package/dist/tts/index.d.ts.map +1 -1
  309. package/dist/tts/index.js +1 -3
  310. package/dist/tts/index.js.map +1 -1
  311. package/dist/tts/stream_adapter.cjs +26 -13
  312. package/dist/tts/stream_adapter.cjs.map +1 -1
  313. package/dist/tts/stream_adapter.d.cts +1 -1
  314. package/dist/tts/stream_adapter.d.ts +1 -1
  315. package/dist/tts/stream_adapter.d.ts.map +1 -1
  316. package/dist/tts/stream_adapter.js +27 -14
  317. package/dist/tts/stream_adapter.js.map +1 -1
  318. package/dist/tts/tts.cjs +156 -25
  319. package/dist/tts/tts.cjs.map +1 -1
  320. package/dist/tts/tts.d.cts +29 -5
  321. package/dist/tts/tts.d.ts +29 -5
  322. package/dist/tts/tts.d.ts.map +1 -1
  323. package/dist/tts/tts.js +156 -24
  324. package/dist/tts/tts.js.map +1 -1
  325. package/dist/types.cjs +60 -0
  326. package/dist/types.cjs.map +1 -0
  327. package/dist/types.d.cts +13 -0
  328. package/dist/types.d.ts +13 -0
  329. package/dist/types.d.ts.map +1 -0
  330. package/dist/types.js +35 -0
  331. package/dist/types.js.map +1 -0
  332. package/dist/utils.cjs +298 -27
  333. package/dist/utils.cjs.map +1 -1
  334. package/dist/utils.d.cts +145 -9
  335. package/dist/utils.d.ts +145 -9
  336. package/dist/utils.d.ts.map +1 -1
  337. package/dist/utils.js +281 -26
  338. package/dist/utils.js.map +1 -1
  339. package/dist/utils.test.cjs +491 -0
  340. package/dist/utils.test.cjs.map +1 -0
  341. package/dist/utils.test.js +498 -0
  342. package/dist/utils.test.js.map +1 -0
  343. package/dist/vad.cjs +76 -20
  344. package/dist/vad.cjs.map +1 -1
  345. package/dist/vad.d.cts +25 -5
  346. package/dist/vad.d.ts +25 -5
  347. package/dist/vad.d.ts.map +1 -1
  348. package/dist/vad.js +76 -20
  349. package/dist/vad.js.map +1 -1
  350. package/dist/voice/agent.cjs +245 -0
  351. package/dist/voice/agent.cjs.map +1 -0
  352. package/dist/voice/agent.d.cts +78 -0
  353. package/dist/voice/agent.d.ts +78 -0
  354. package/dist/voice/agent.d.ts.map +1 -0
  355. package/dist/voice/agent.js +220 -0
  356. package/dist/voice/agent.js.map +1 -0
  357. package/dist/voice/agent.test.cjs +61 -0
  358. package/dist/voice/agent.test.cjs.map +1 -0
  359. package/dist/voice/agent.test.js +60 -0
  360. package/dist/voice/agent.test.js.map +1 -0
  361. package/dist/voice/agent_activity.cjs +1453 -0
  362. package/dist/voice/agent_activity.cjs.map +1 -0
  363. package/dist/voice/agent_activity.d.cts +94 -0
  364. package/dist/voice/agent_activity.d.ts +94 -0
  365. package/dist/voice/agent_activity.d.ts.map +1 -0
  366. package/dist/voice/agent_activity.js +1449 -0
  367. package/dist/voice/agent_activity.js.map +1 -0
  368. package/dist/voice/agent_session.cjs +312 -0
  369. package/dist/voice/agent_session.cjs.map +1 -0
  370. package/dist/voice/agent_session.d.cts +121 -0
  371. package/dist/voice/agent_session.d.ts +121 -0
  372. package/dist/voice/agent_session.d.ts.map +1 -0
  373. package/dist/voice/agent_session.js +295 -0
  374. package/dist/voice/agent_session.js.map +1 -0
  375. package/dist/voice/audio_recognition.cjs +374 -0
  376. package/dist/voice/audio_recognition.cjs.map +1 -0
  377. package/dist/voice/audio_recognition.d.cts +80 -0
  378. package/dist/voice/audio_recognition.d.ts +80 -0
  379. package/dist/voice/audio_recognition.d.ts.map +1 -0
  380. package/dist/voice/audio_recognition.js +350 -0
  381. package/dist/voice/audio_recognition.js.map +1 -0
  382. package/dist/voice/events.cjs +145 -0
  383. package/dist/voice/events.cjs.map +1 -0
  384. package/dist/voice/events.d.cts +124 -0
  385. package/dist/voice/events.d.ts +124 -0
  386. package/dist/voice/events.d.ts.map +1 -0
  387. package/dist/voice/events.js +110 -0
  388. package/dist/voice/events.js.map +1 -0
  389. package/dist/voice/generation.cjs +700 -0
  390. package/dist/voice/generation.cjs.map +1 -0
  391. package/dist/voice/generation.d.cts +115 -0
  392. package/dist/voice/generation.d.ts +115 -0
  393. package/dist/voice/generation.d.ts.map +1 -0
  394. package/dist/voice/generation.js +672 -0
  395. package/dist/voice/generation.js.map +1 -0
  396. package/dist/voice/index.cjs +40 -0
  397. package/dist/voice/index.cjs.map +1 -0
  398. package/dist/voice/index.d.cts +5 -0
  399. package/dist/voice/index.d.ts +5 -0
  400. package/dist/voice/index.d.ts.map +1 -0
  401. package/dist/voice/index.js +11 -0
  402. package/dist/voice/index.js.map +1 -0
  403. package/dist/voice/io.cjs +245 -0
  404. package/dist/voice/io.cjs.map +1 -0
  405. package/dist/voice/io.d.cts +101 -0
  406. package/dist/voice/io.d.ts +101 -0
  407. package/dist/voice/io.d.ts.map +1 -0
  408. package/dist/voice/io.js +217 -0
  409. package/dist/voice/io.js.map +1 -0
  410. package/dist/voice/room_io/_input.cjs +121 -0
  411. package/dist/voice/room_io/_input.cjs.map +1 -0
  412. package/dist/voice/room_io/_input.d.cts +24 -0
  413. package/dist/voice/room_io/_input.d.ts +24 -0
  414. package/dist/voice/room_io/_input.d.ts.map +1 -0
  415. package/dist/voice/room_io/_input.js +102 -0
  416. package/dist/voice/room_io/_input.js.map +1 -0
  417. package/dist/voice/room_io/_output.cjs +358 -0
  418. package/dist/voice/room_io/_output.cjs.map +1 -0
  419. package/dist/voice/room_io/_output.d.cts +75 -0
  420. package/dist/voice/room_io/_output.d.ts +75 -0
  421. package/dist/voice/room_io/_output.d.ts.map +1 -0
  422. package/dist/voice/room_io/_output.js +342 -0
  423. package/dist/voice/room_io/_output.js.map +1 -0
  424. package/dist/voice/room_io/index.cjs +25 -0
  425. package/dist/voice/room_io/index.cjs.map +1 -0
  426. package/dist/voice/room_io/index.d.cts +3 -0
  427. package/dist/voice/room_io/index.d.ts +3 -0
  428. package/dist/voice/room_io/index.d.ts.map +1 -0
  429. package/dist/voice/room_io/index.js +3 -0
  430. package/dist/voice/room_io/index.js.map +1 -0
  431. package/dist/voice/room_io/room_io.cjs +370 -0
  432. package/dist/voice/room_io/room_io.cjs.map +1 -0
  433. package/dist/voice/room_io/room_io.d.cts +73 -0
  434. package/dist/voice/room_io/room_io.d.ts +73 -0
  435. package/dist/voice/room_io/room_io.d.ts.map +1 -0
  436. package/dist/voice/room_io/room_io.js +361 -0
  437. package/dist/voice/room_io/room_io.js.map +1 -0
  438. package/dist/{pipeline/index.cjs → voice/run_context.cjs} +16 -11
  439. package/dist/voice/run_context.cjs.map +1 -0
  440. package/dist/voice/run_context.d.cts +12 -0
  441. package/dist/voice/run_context.d.ts +12 -0
  442. package/dist/voice/run_context.d.ts.map +1 -0
  443. package/dist/voice/run_context.js +14 -0
  444. package/dist/voice/run_context.js.map +1 -0
  445. package/dist/voice/speech_handle.cjs +105 -0
  446. package/dist/voice/speech_handle.cjs.map +1 -0
  447. package/dist/voice/speech_handle.d.cts +46 -0
  448. package/dist/voice/speech_handle.d.ts +46 -0
  449. package/dist/voice/speech_handle.d.ts.map +1 -0
  450. package/dist/voice/speech_handle.js +81 -0
  451. package/dist/voice/speech_handle.js.map +1 -0
  452. package/dist/voice/transcription/_utils.cjs +45 -0
  453. package/dist/voice/transcription/_utils.cjs.map +1 -0
  454. package/dist/voice/transcription/_utils.d.cts +3 -0
  455. package/dist/voice/transcription/_utils.d.ts +3 -0
  456. package/dist/voice/transcription/_utils.d.ts.map +1 -0
  457. package/dist/voice/transcription/_utils.js +21 -0
  458. package/dist/voice/transcription/_utils.js.map +1 -0
  459. package/dist/voice/transcription/index.cjs +23 -0
  460. package/dist/voice/transcription/index.cjs.map +1 -0
  461. package/dist/voice/transcription/index.d.cts +2 -0
  462. package/dist/voice/transcription/index.d.ts +2 -0
  463. package/dist/voice/transcription/index.d.ts.map +1 -0
  464. package/dist/voice/transcription/index.js +2 -0
  465. package/dist/voice/transcription/index.js.map +1 -0
  466. package/dist/voice/transcription/synchronizer.cjs +379 -0
  467. package/dist/voice/transcription/synchronizer.cjs.map +1 -0
  468. package/dist/voice/transcription/synchronizer.d.cts +86 -0
  469. package/dist/voice/transcription/synchronizer.d.ts +86 -0
  470. package/dist/voice/transcription/synchronizer.d.ts.map +1 -0
  471. package/dist/voice/transcription/synchronizer.js +354 -0
  472. package/dist/voice/transcription/synchronizer.js.map +1 -0
  473. package/dist/worker.cjs +22 -4
  474. package/dist/worker.cjs.map +1 -1
  475. package/dist/worker.d.cts +1 -1
  476. package/dist/worker.d.ts +1 -1
  477. package/dist/worker.d.ts.map +1 -1
  478. package/dist/worker.js +22 -4
  479. package/dist/worker.js.map +1 -1
  480. package/package.json +8 -2
  481. package/src/_exceptions.ts +137 -0
  482. package/src/audio.ts +12 -1
  483. package/src/cli.ts +37 -0
  484. package/src/constants.ts +2 -1
  485. package/src/http_server.ts +1 -0
  486. package/src/index.ts +13 -10
  487. package/src/inference_runner.ts +2 -3
  488. package/src/ipc/inference_proc_executor.ts +2 -2
  489. package/src/ipc/job_executor.ts +1 -1
  490. package/src/ipc/job_proc_executor.ts +1 -1
  491. package/src/ipc/job_proc_lazy_main.ts +1 -1
  492. package/src/job.ts +18 -0
  493. package/src/llm/__snapshots__/chat_context.test.ts.snap +527 -0
  494. package/src/llm/__snapshots__/tool_context.test.ts.snap +177 -0
  495. package/src/llm/__snapshots__/utils.test.ts.snap +65 -0
  496. package/src/llm/chat_context.test.ts +450 -0
  497. package/src/llm/chat_context.ts +501 -103
  498. package/src/llm/index.ts +53 -18
  499. package/src/llm/llm.ts +148 -50
  500. package/src/llm/provider_format/google.test.ts +772 -0
  501. package/src/llm/provider_format/google.ts +130 -0
  502. package/src/llm/provider_format/index.ts +23 -0
  503. package/src/llm/provider_format/openai.test.ts +581 -0
  504. package/src/llm/provider_format/openai.ts +118 -0
  505. package/src/llm/provider_format/utils.ts +183 -0
  506. package/src/llm/realtime.ts +151 -0
  507. package/src/llm/remote_chat_context.test.ts +290 -0
  508. package/src/llm/remote_chat_context.ts +114 -0
  509. package/src/llm/tool_context.test.ts +198 -0
  510. package/src/llm/tool_context.ts +259 -0
  511. package/src/llm/tool_context.type.test.ts +115 -0
  512. package/src/llm/utils.test.ts +670 -0
  513. package/src/llm/utils.ts +324 -0
  514. package/src/metrics/base.ts +110 -78
  515. package/src/metrics/index.ts +3 -9
  516. package/src/metrics/usage_collector.ts +19 -13
  517. package/src/metrics/utils.ts +24 -69
  518. package/src/multimodal/index.ts +0 -1
  519. package/src/plugin.ts +26 -8
  520. package/src/stream/deferred_stream.test.ts +755 -0
  521. package/src/stream/deferred_stream.ts +110 -0
  522. package/src/stream/identity_transform.test.ts +179 -0
  523. package/src/stream/identity_transform.ts +18 -0
  524. package/src/stream/index.ts +7 -0
  525. package/src/stream/merge_readable_streams.ts +40 -0
  526. package/src/stream/stream_channel.test.ts +129 -0
  527. package/src/stream/stream_channel.ts +32 -0
  528. package/src/stt/stream_adapter.ts +3 -5
  529. package/src/stt/stt.ts +134 -17
  530. package/src/tokenize/basic/basic.ts +13 -5
  531. package/src/tokenize/basic/sentence.ts +20 -6
  532. package/src/tokenize/token_stream.ts +7 -4
  533. package/src/transcription.ts +2 -3
  534. package/src/tts/index.ts +0 -1
  535. package/src/tts/stream_adapter.ts +42 -16
  536. package/src/tts/tts.ts +202 -21
  537. package/src/types.ts +42 -0
  538. package/src/utils.test.ts +658 -0
  539. package/src/utils.ts +402 -44
  540. package/src/vad.ts +90 -22
  541. package/src/voice/agent.test.ts +80 -0
  542. package/src/voice/agent.ts +332 -0
  543. package/src/voice/agent_activity.ts +1913 -0
  544. package/src/voice/agent_session.ts +460 -0
  545. package/src/voice/audio_recognition.ts +473 -0
  546. package/src/voice/events.ts +252 -0
  547. package/src/voice/generation.ts +881 -0
  548. package/src/voice/index.ts +7 -0
  549. package/src/voice/io.ts +304 -0
  550. package/src/voice/room_io/_input.ts +144 -0
  551. package/src/voice/room_io/_output.ts +436 -0
  552. package/src/voice/room_io/index.ts +5 -0
  553. package/src/voice/room_io/room_io.ts +495 -0
  554. package/src/voice/run_context.ts +20 -0
  555. package/src/voice/speech_handle.ts +104 -0
  556. package/src/voice/transcription/_utils.ts +25 -0
  557. package/src/voice/transcription/index.ts +4 -0
  558. package/src/voice/transcription/synchronizer.ts +477 -0
  559. package/src/worker.ts +22 -2
  560. package/dist/llm/function_context.cjs +0 -103
  561. package/dist/llm/function_context.cjs.map +0 -1
  562. package/dist/llm/function_context.d.cts +0 -47
  563. package/dist/llm/function_context.d.ts +0 -47
  564. package/dist/llm/function_context.d.ts.map +0 -1
  565. package/dist/llm/function_context.js +0 -78
  566. package/dist/llm/function_context.js.map +0 -1
  567. package/dist/llm/function_context.test.cjs +0 -218
  568. package/dist/llm/function_context.test.cjs.map +0 -1
  569. package/dist/llm/function_context.test.js +0 -217
  570. package/dist/llm/function_context.test.js.map +0 -1
  571. package/dist/multimodal/multimodal_agent.cjs +0 -486
  572. package/dist/multimodal/multimodal_agent.cjs.map +0 -1
  573. package/dist/multimodal/multimodal_agent.d.cts +0 -48
  574. package/dist/multimodal/multimodal_agent.d.ts +0 -48
  575. package/dist/multimodal/multimodal_agent.d.ts.map +0 -1
  576. package/dist/multimodal/multimodal_agent.js +0 -461
  577. package/dist/multimodal/multimodal_agent.js.map +0 -1
  578. package/dist/pipeline/agent_output.cjs +0 -197
  579. package/dist/pipeline/agent_output.cjs.map +0 -1
  580. package/dist/pipeline/agent_output.d.cts +0 -33
  581. package/dist/pipeline/agent_output.d.ts +0 -33
  582. package/dist/pipeline/agent_output.d.ts.map +0 -1
  583. package/dist/pipeline/agent_output.js +0 -172
  584. package/dist/pipeline/agent_output.js.map +0 -1
  585. package/dist/pipeline/agent_playout.cjs +0 -175
  586. package/dist/pipeline/agent_playout.cjs.map +0 -1
  587. package/dist/pipeline/agent_playout.d.cts +0 -40
  588. package/dist/pipeline/agent_playout.d.ts +0 -40
  589. package/dist/pipeline/agent_playout.d.ts.map +0 -1
  590. package/dist/pipeline/agent_playout.js +0 -139
  591. package/dist/pipeline/agent_playout.js.map +0 -1
  592. package/dist/pipeline/human_input.cjs +0 -171
  593. package/dist/pipeline/human_input.cjs.map +0 -1
  594. package/dist/pipeline/human_input.d.cts +0 -30
  595. package/dist/pipeline/human_input.d.ts +0 -30
  596. package/dist/pipeline/human_input.d.ts.map +0 -1
  597. package/dist/pipeline/human_input.js +0 -146
  598. package/dist/pipeline/human_input.js.map +0 -1
  599. package/dist/pipeline/index.cjs.map +0 -1
  600. package/dist/pipeline/index.d.cts +0 -2
  601. package/dist/pipeline/index.d.ts +0 -2
  602. package/dist/pipeline/index.d.ts.map +0 -1
  603. package/dist/pipeline/index.js +0 -11
  604. package/dist/pipeline/index.js.map +0 -1
  605. package/dist/pipeline/pipeline_agent.cjs +0 -859
  606. package/dist/pipeline/pipeline_agent.cjs.map +0 -1
  607. package/dist/pipeline/pipeline_agent.d.cts +0 -150
  608. package/dist/pipeline/pipeline_agent.d.ts +0 -150
  609. package/dist/pipeline/pipeline_agent.d.ts.map +0 -1
  610. package/dist/pipeline/pipeline_agent.js +0 -837
  611. package/dist/pipeline/pipeline_agent.js.map +0 -1
  612. package/dist/pipeline/speech_handle.cjs +0 -176
  613. package/dist/pipeline/speech_handle.cjs.map +0 -1
  614. package/dist/pipeline/speech_handle.d.cts +0 -37
  615. package/dist/pipeline/speech_handle.d.ts +0 -37
  616. package/dist/pipeline/speech_handle.d.ts.map +0 -1
  617. package/dist/pipeline/speech_handle.js +0 -152
  618. package/dist/pipeline/speech_handle.js.map +0 -1
  619. package/src/llm/function_context.test.ts +0 -248
  620. package/src/llm/function_context.ts +0 -142
  621. package/src/multimodal/multimodal_agent.ts +0 -592
  622. package/src/pipeline/agent_output.ts +0 -219
  623. package/src/pipeline/agent_playout.ts +0 -192
  624. package/src/pipeline/human_input.ts +0 -188
  625. package/src/pipeline/index.ts +0 -15
  626. package/src/pipeline/pipeline_agent.ts +0 -1197
  627. package/src/pipeline/speech_handle.ts +0 -201
@@ -0,0 +1,1913 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { Mutex } from '@livekit/mutex';
5
+ import type { AudioFrame } from '@livekit/rtc-node';
6
+ import { Heap } from 'heap-js';
7
+ import { AsyncLocalStorage } from 'node:async_hooks';
8
+ import { ReadableStream } from 'node:stream/web';
9
+ import { type ChatContext, ChatMessage } from '../llm/chat_context.js';
10
+ import {
11
+ type ChatItem,
12
+ type FunctionCall,
13
+ type GenerationCreatedEvent,
14
+ type InputSpeechStartedEvent,
15
+ type InputSpeechStoppedEvent,
16
+ type InputTranscriptionCompleted,
17
+ LLM,
18
+ RealtimeModel,
19
+ type RealtimeModelError,
20
+ type RealtimeSession,
21
+ type ToolChoice,
22
+ type ToolContext,
23
+ } from '../llm/index.js';
24
+ import type { LLMError } from '../llm/llm.js';
25
+ import { log } from '../log.js';
26
+ import type {
27
+ EOUMetrics,
28
+ LLMMetrics,
29
+ RealtimeModelMetrics,
30
+ STTMetrics,
31
+ TTSMetrics,
32
+ VADMetrics,
33
+ } from '../metrics/base.js';
34
+ import { DeferredReadableStream } from '../stream/deferred_stream.js';
35
+ import { STT, type STTError, type SpeechEvent } from '../stt/stt.js';
36
+ import { splitWords } from '../tokenize/basic/word.js';
37
+ import { TTS, type TTSError } from '../tts/tts.js';
38
+ import { Future, Task, cancelAndWait, waitFor } from '../utils.js';
39
+ import { VAD, type VADEvent } from '../vad.js';
40
+ import type { Agent, ModelSettings } from './agent.js';
41
+ import { StopResponse, asyncLocalStorage } from './agent.js';
42
+ import { type AgentSession, type TurnDetectionMode } from './agent_session.js';
43
+ import {
44
+ AudioRecognition,
45
+ type EndOfTurnInfo,
46
+ type RecognitionHooks,
47
+ type _TurnDetector,
48
+ } from './audio_recognition.js';
49
+ import {
50
+ AgentSessionEventTypes,
51
+ createErrorEvent,
52
+ createFunctionToolsExecutedEvent,
53
+ createMetricsCollectedEvent,
54
+ createSpeechCreatedEvent,
55
+ createUserInputTranscribedEvent,
56
+ } from './events.js';
57
+ import type { ToolExecutionOutput } from './generation.js';
58
+ import {
59
+ type _AudioOut,
60
+ type _TextOut,
61
+ performAudioForwarding,
62
+ performLLMInference,
63
+ performTTSInference,
64
+ performTextForwarding,
65
+ performToolExecutions,
66
+ removeInstructions,
67
+ updateInstructions,
68
+ } from './generation.js';
69
+ import { SpeechHandle } from './speech_handle.js';
70
+
71
+ // equivalent to Python's contextvars
72
+ const speechHandleStorage = new AsyncLocalStorage<SpeechHandle>();
73
+
74
+ export class AgentActivity implements RecognitionHooks {
75
+ private static readonly REPLY_TASK_CANCEL_TIMEOUT = 5000;
76
+ private started = false;
77
+ private audioRecognition?: AudioRecognition;
78
+ private realtimeSession?: RealtimeSession;
79
+ private turnDetectionMode?: Exclude<TurnDetectionMode, _TurnDetector>;
80
+ private logger = log();
81
+ private _draining = false;
82
+ private _currentSpeech?: SpeechHandle;
83
+ private speechQueue: Heap<[number, number, SpeechHandle]>; // [priority, timestamp, speechHandle]
84
+ private q_updated: Future;
85
+ private speechTasks: Set<Promise<unknown>> = new Set();
86
+ private lock = new Mutex();
87
+ private audioStream = new DeferredReadableStream<AudioFrame>();
88
+ // default to null as None, which maps to the default provider tool choice value
89
+ private toolChoice: ToolChoice | null = null;
90
+
91
+ agent: Agent;
92
+ agentSession: AgentSession;
93
+
94
+ /** @internal */
95
+ _mainTask?: Task<void>;
96
+ _userTurnCompletedTask?: Promise<void>;
97
+
98
+ constructor(agent: Agent, agentSession: AgentSession) {
99
+ this.agent = agent;
100
+ this.agentSession = agentSession;
101
+
102
+ /**
103
+ * Custom comparator to prioritize speech handles with higher priority
104
+ * - Prefer higher priority
105
+ * - Prefer earlier timestamp (so calling a sequence of generateReply() will execute in FIFO order)
106
+ */
107
+ this.speechQueue = new Heap<[number, number, SpeechHandle]>(([p1, t1, _], [p2, t2, __]) => {
108
+ return p1 === p2 ? t1 - t2 : p2 - p1;
109
+ });
110
+ this.q_updated = new Future();
111
+
112
+ this.turnDetectionMode =
113
+ typeof this.turnDetection === 'string' ? this.turnDetection : undefined;
114
+
115
+ if (this.turnDetectionMode === 'vad' && this.vad === undefined) {
116
+ this.logger.warn(
117
+ 'turnDetection is set to "vad", but no VAD model is provided, ignoring the turnDdetection setting',
118
+ );
119
+ this.turnDetectionMode = undefined;
120
+ }
121
+
122
+ if (this.turnDetectionMode === 'stt' && this.stt === undefined) {
123
+ this.logger.warn(
124
+ 'turnDetection is set to "stt", but no STT model is provided, ignoring the turnDetection setting',
125
+ );
126
+ this.turnDetectionMode = undefined;
127
+ }
128
+
129
+ if (this.llm instanceof RealtimeModel) {
130
+ if (this.llm.capabilities.turnDetection && !this.allowInterruptions) {
131
+ this.logger.warn(
132
+ 'the RealtimeModel uses a server-side turn detection, allowInterruptions cannot be false, ' +
133
+ 'disable turnDetection in the RealtimeModel and use VAD on the AgentSession instead',
134
+ );
135
+ }
136
+
137
+ if (this.turnDetectionMode === 'realtime_llm' && !this.llm.capabilities.turnDetection) {
138
+ this.logger.warn(
139
+ 'turnDetection is set to "realtime_llm", but the LLM is not a RealtimeModel or the server-side turn detection is not supported/enabled, ignoring the turnDetection setting',
140
+ );
141
+ this.turnDetectionMode = undefined;
142
+ }
143
+
144
+ if (this.turnDetectionMode === 'stt') {
145
+ this.logger.warn(
146
+ 'turnDetection is set to "stt", but the LLM is a RealtimeModel, ignoring the turnDetection setting',
147
+ );
148
+ this.turnDetectionMode = undefined;
149
+ }
150
+
151
+ if (
152
+ this.turnDetectionMode &&
153
+ this.turnDetectionMode !== 'realtime_llm' &&
154
+ this.llm.capabilities.turnDetection
155
+ ) {
156
+ this.logger.warn(
157
+ `turnDetection is set to "${this.turnDetectionMode}", but the LLM is a RealtimeModel and server-side turn detection enabled, ignoring the turnDetection setting`,
158
+ );
159
+ this.turnDetectionMode = undefined;
160
+ }
161
+
162
+ // fallback to VAD if server side turn detection is disabled and VAD is available
163
+ if (
164
+ !this.llm.capabilities.turnDetection &&
165
+ this.vad &&
166
+ this.turnDetectionMode === undefined
167
+ ) {
168
+ this.turnDetectionMode = 'vad';
169
+ }
170
+ } else if (this.turnDetectionMode === 'realtime_llm') {
171
+ this.logger.warn(
172
+ 'turnDetection is set to "realtime_llm", but the LLM is not a RealtimeModel',
173
+ );
174
+ this.turnDetectionMode = undefined;
175
+ }
176
+
177
+ if (
178
+ !this.vad &&
179
+ this.stt &&
180
+ this.llm instanceof LLM &&
181
+ this.allowInterruptions &&
182
+ this.turnDetectionMode === undefined
183
+ ) {
184
+ this.logger.warn(
185
+ 'VAD is not set. Enabling VAD is recommended when using LLM and STT ' +
186
+ 'for more responsive interruption handling.',
187
+ );
188
+ }
189
+ }
190
+
191
+ async start(): Promise<void> {
192
+ const unlock = await this.lock.lock();
193
+ try {
194
+ this.agent._agentActivity = this;
195
+
196
+ if (this.llm instanceof RealtimeModel) {
197
+ this.realtimeSession = this.llm.session();
198
+ this.realtimeSession.on('generation_created', (ev) => this.onGenerationCreated(ev));
199
+ this.realtimeSession.on('input_speech_started', (ev) => this.onInputSpeechStarted(ev));
200
+ this.realtimeSession.on('input_speech_stopped', (ev) => this.onInputSpeechStopped(ev));
201
+ this.realtimeSession.on('input_audio_transcription_completed', (ev) =>
202
+ this.onInputAudioTranscriptionCompleted(ev),
203
+ );
204
+ this.realtimeSession.on('metrics_collected', (ev) => this.onMetricsCollected(ev));
205
+ this.realtimeSession.on('error', (ev) => this.onError(ev));
206
+
207
+ removeInstructions(this.agent._chatCtx);
208
+ try {
209
+ await this.realtimeSession.updateInstructions(this.agent.instructions);
210
+ } catch (error) {
211
+ this.logger.error(error, 'failed to update the instructions');
212
+ }
213
+
214
+ try {
215
+ await this.realtimeSession.updateChatCtx(this.agent.chatCtx);
216
+ } catch (error) {
217
+ this.logger.error(error, 'failed to update the chat context');
218
+ }
219
+
220
+ try {
221
+ await this.realtimeSession.updateTools(this.tools);
222
+ } catch (error) {
223
+ this.logger.error(error, 'failed to update the tools');
224
+ }
225
+ } else if (this.llm instanceof LLM) {
226
+ try {
227
+ updateInstructions({
228
+ chatCtx: this.agent._chatCtx,
229
+ instructions: this.agent.instructions,
230
+ addIfMissing: true,
231
+ });
232
+ } catch (error) {
233
+ this.logger.error('failed to update the instructions', error);
234
+ }
235
+ }
236
+
237
+ // metrics and error handling
238
+ if (this.llm instanceof LLM) {
239
+ this.llm.on('metrics_collected', (ev) => this.onMetricsCollected(ev));
240
+ this.llm.on('error', (ev) => this.onError(ev));
241
+ }
242
+
243
+ if (this.stt instanceof STT) {
244
+ this.stt.on('metrics_collected', (ev) => this.onMetricsCollected(ev));
245
+ this.stt.on('error', (ev) => this.onError(ev));
246
+ }
247
+
248
+ if (this.tts instanceof TTS) {
249
+ this.tts.on('metrics_collected', (ev) => this.onMetricsCollected(ev));
250
+ this.tts.on('error', (ev) => this.onError(ev));
251
+ }
252
+
253
+ if (this.vad instanceof VAD) {
254
+ this.vad.on('metrics_collected', (ev) => this.onMetricsCollected(ev));
255
+ }
256
+
257
+ this.audioRecognition = new AudioRecognition({
258
+ recognitionHooks: this,
259
+ // Disable stt node if stt is not provided
260
+ stt: this.stt ? (...args) => this.agent.sttNode(...args) : undefined,
261
+ vad: this.vad,
262
+ turnDetector: typeof this.turnDetection === 'string' ? undefined : this.turnDetection,
263
+ turnDetectionMode: this.turnDetectionMode,
264
+ minEndpointingDelay: this.agentSession.options.minEndpointingDelay,
265
+ maxEndpointingDelay: this.agentSession.options.maxEndpointingDelay,
266
+ });
267
+ this.audioRecognition.start();
268
+ this.started = true;
269
+
270
+ this._mainTask = Task.from(({ signal }) => this.mainTask(signal));
271
+ this.createSpeechTask({
272
+ promise: this.agent.onEnter(),
273
+ name: 'AgentActivity_onEnter',
274
+ });
275
+ } finally {
276
+ unlock();
277
+ }
278
+ }
279
+
280
+ get currentSpeech(): SpeechHandle | undefined {
281
+ return this._currentSpeech;
282
+ }
283
+
284
+ get vad(): VAD | undefined {
285
+ return this.agent.vad || this.agentSession.vad;
286
+ }
287
+
288
+ get stt(): STT | undefined {
289
+ return this.agent.stt || this.agentSession.stt;
290
+ }
291
+
292
+ get llm(): LLM | RealtimeModel | undefined {
293
+ return this.agent.llm || this.agentSession.llm;
294
+ }
295
+
296
+ get tts(): TTS | undefined {
297
+ return this.agent.tts || this.agentSession.tts;
298
+ }
299
+
300
+ get tools(): ToolContext {
301
+ return this.agent.toolCtx;
302
+ }
303
+
304
+ get draining(): boolean {
305
+ return this._draining;
306
+ }
307
+
308
+ get realtimeLLMSession(): RealtimeSession | undefined {
309
+ return this.realtimeSession;
310
+ }
311
+
312
+ get allowInterruptions(): boolean {
313
+ // TODO(AJS-51): Allow options to be defined in Agent class
314
+ return this.agentSession.options.allowInterruptions;
315
+ }
316
+
317
+ get turnDetection(): TurnDetectionMode | undefined {
318
+ // TODO(brian): prioritize using agent.turn_detection
319
+ return this.agentSession.turnDetection;
320
+ }
321
+
322
+ get toolCtx(): ToolContext {
323
+ return this.agent.toolCtx;
324
+ }
325
+
326
+ async updateChatCtx(chatCtx: ChatContext): Promise<void> {
327
+ chatCtx = chatCtx.copy({ toolCtx: this.toolCtx });
328
+
329
+ this.agent._chatCtx = chatCtx;
330
+
331
+ if (this.realtimeSession) {
332
+ removeInstructions(chatCtx);
333
+ this.realtimeSession.updateChatCtx(chatCtx);
334
+ } else {
335
+ updateInstructions({
336
+ chatCtx,
337
+ instructions: this.agent.instructions,
338
+ addIfMissing: true,
339
+ });
340
+ }
341
+ }
342
+
343
+ updateOptions({ toolChoice }: { toolChoice?: ToolChoice | null }): void {
344
+ if (toolChoice !== undefined) {
345
+ this.toolChoice = toolChoice;
346
+ }
347
+
348
+ if (this.realtimeSession) {
349
+ this.realtimeSession.updateOptions({ toolChoice: this.toolChoice });
350
+ }
351
+ }
352
+
353
+ attachAudioInput(audioStream: ReadableStream<AudioFrame>): void {
354
+ if (this.audioStream.isSourceSet) {
355
+ this.logger.debug('detaching existing audio input in agent activity');
356
+ this.audioStream.detachSource();
357
+ }
358
+
359
+ /**
360
+ * We need to add a deferred ReadableStream layer on top of the audioStream from the agent session.
361
+ * The tee() operation should be applied to the deferred stream, not the original audioStream.
362
+ * This is important because teeing the original stream directly makes it very difficult—if not
363
+ * impossible—to implement stream unlock logic cleanly.
364
+ */
365
+ this.audioStream.setSource(audioStream);
366
+ const [realtimeAudioStream, recognitionAudioStream] = this.audioStream.stream.tee();
367
+
368
+ if (this.realtimeSession) {
369
+ this.realtimeSession.setInputAudioStream(realtimeAudioStream);
370
+ }
371
+
372
+ if (this.audioRecognition) {
373
+ this.audioRecognition.setInputAudioStream(recognitionAudioStream);
374
+ }
375
+ }
376
+
377
+ detachAudioInput(): void {
378
+ this.audioStream.detachSource();
379
+ }
380
+
381
+ commitUserTurn() {
382
+ if (!this.audioRecognition) {
383
+ throw new Error('AudioRecognition is not initialized');
384
+ }
385
+
386
+ // TODO(brian): add audio_detached flag
387
+ const audioDetached = false;
388
+ this.audioRecognition.commitUserTurn(audioDetached);
389
+ }
390
+
391
+ clearUserTurn() {
392
+ this.audioRecognition?.clearUserTurn();
393
+ this.realtimeSession?.clearAudio();
394
+ }
395
+
396
+ say(
397
+ text: string | ReadableStream<string>,
398
+ options?: {
399
+ audio?: ReadableStream<AudioFrame>;
400
+ allowInterruptions?: boolean;
401
+ addToChatCtx?: boolean;
402
+ },
403
+ ): SpeechHandle {
404
+ const {
405
+ audio,
406
+ allowInterruptions: defaultAllowInterruptions,
407
+ addToChatCtx = true,
408
+ } = options ?? {};
409
+ let allowInterruptions = defaultAllowInterruptions;
410
+
411
+ if (
412
+ !audio &&
413
+ !this.tts &&
414
+ this.agentSession.output.audio &&
415
+ this.agentSession.output.audioEnabled
416
+ ) {
417
+ throw new Error('trying to generate speech from text without a TTS model');
418
+ }
419
+
420
+ if (
421
+ this.llm instanceof RealtimeModel &&
422
+ this.llm.capabilities.turnDetection &&
423
+ allowInterruptions === false
424
+ ) {
425
+ this.logger.warn(
426
+ 'the RealtimeModel uses a server-side turn detection, allowInterruptions cannot be false when using VoiceAgent.say(), ' +
427
+ 'disable turnDetection in the RealtimeModel and use VAD on the AgentTask/VoiceAgent instead',
428
+ );
429
+ allowInterruptions = true;
430
+ }
431
+
432
+ const handle = SpeechHandle.create({
433
+ allowInterruptions: allowInterruptions ?? this.allowInterruptions,
434
+ });
435
+
436
+ this.agentSession.emit(
437
+ AgentSessionEventTypes.SpeechCreated,
438
+ createSpeechCreatedEvent({
439
+ userInitiated: true,
440
+ source: 'say',
441
+ speechHandle: handle,
442
+ }),
443
+ );
444
+
445
+ const task = this.createSpeechTask({
446
+ promise: this.ttsTask(handle, text, addToChatCtx, {}, audio),
447
+ ownedSpeechHandle: handle,
448
+ name: 'AgentActivity.say_tts',
449
+ });
450
+
451
+ task.finally(() => this.onPipelineReplyDone());
452
+ this.scheduleSpeech(handle, SpeechHandle.SPEECH_PRIORITY_NORMAL);
453
+ return handle;
454
+ }
455
+
456
+ // -- Metrics and errors --
457
+
458
+ private onMetricsCollected = (
459
+ ev: STTMetrics | TTSMetrics | VADMetrics | LLMMetrics | RealtimeModelMetrics,
460
+ ) => {
461
+ const speechHandle = speechHandleStorage.getStore();
462
+ if (speechHandle && (ev.type === 'llm_metrics' || ev.type === 'tts_metrics')) {
463
+ ev.speechId = speechHandle.id;
464
+ }
465
+ this.agentSession.emit(
466
+ AgentSessionEventTypes.MetricsCollected,
467
+ createMetricsCollectedEvent({ metrics: ev }),
468
+ );
469
+ };
470
+
471
+ private onError(ev: RealtimeModelError | STTError | TTSError | LLMError): void {
472
+ if (ev.type === 'realtime_model_error') {
473
+ const errorEvent = createErrorEvent(ev.error, this.llm);
474
+ this.agentSession.emit(AgentSessionEventTypes.Error, errorEvent);
475
+ } else if (ev.type === 'stt_error') {
476
+ const errorEvent = createErrorEvent(ev.error, this.stt);
477
+ this.agentSession.emit(AgentSessionEventTypes.Error, errorEvent);
478
+ } else if (ev.type === 'tts_error') {
479
+ const errorEvent = createErrorEvent(ev.error, this.tts);
480
+ this.agentSession.emit(AgentSessionEventTypes.Error, errorEvent);
481
+ } else if (ev.type === 'llm_error') {
482
+ const errorEvent = createErrorEvent(ev.error, this.llm);
483
+ this.agentSession.emit(AgentSessionEventTypes.Error, errorEvent);
484
+ }
485
+
486
+ this.agentSession._onError(ev);
487
+ }
488
+
489
+ // -- Realtime Session events --
490
+
491
+ onInputSpeechStarted(_ev: InputSpeechStartedEvent): void {
492
+ this.logger.info('onInputSpeechStarted');
493
+
494
+ if (!this.vad) {
495
+ this.agentSession._updateUserState('speaking');
496
+ }
497
+
498
+ // this.interrupt() is going to raise when allow_interruptions is False,
499
+ // llm.InputSpeechStartedEvent is only fired by the server when the turn_detection is enabled.
500
+ try {
501
+ this.interrupt();
502
+ } catch (error) {
503
+ this.logger.error(
504
+ 'RealtimeAPI input_speech_started, but current speech is not interruptable, this should never happen!',
505
+ error,
506
+ );
507
+ }
508
+ }
509
+
510
+ onInputSpeechStopped(ev: InputSpeechStoppedEvent): void {
511
+ this.logger.info(ev, 'onInputSpeechStopped');
512
+
513
+ if (!this.vad) {
514
+ this.agentSession._updateUserState('listening');
515
+ }
516
+
517
+ if (ev.userTranscriptionEnabled) {
518
+ this.agentSession.emit(
519
+ AgentSessionEventTypes.UserInputTranscribed,
520
+ createUserInputTranscribedEvent({
521
+ isFinal: false,
522
+ transcript: '',
523
+ }),
524
+ );
525
+ }
526
+ }
527
+
528
+ onInputAudioTranscriptionCompleted(ev: InputTranscriptionCompleted): void {
529
+ this.agentSession.emit(
530
+ AgentSessionEventTypes.UserInputTranscribed,
531
+ createUserInputTranscribedEvent({
532
+ transcript: ev.transcript,
533
+ isFinal: ev.isFinal,
534
+ }),
535
+ );
536
+
537
+ if (ev.isFinal) {
538
+ const message = ChatMessage.create({
539
+ role: 'user',
540
+ content: ev.transcript,
541
+ id: ev.itemId,
542
+ });
543
+ this.agent._chatCtx.items.push(message);
544
+ this.agentSession._conversationItemAdded(message);
545
+ }
546
+ }
547
+
548
+ onGenerationCreated(ev: GenerationCreatedEvent): void {
549
+ if (ev.userInitiated) {
550
+ // user initiated generations are directly handled inside _realtime_reply_task
551
+ return;
552
+ }
553
+
554
+ if (this.draining) {
555
+ // copied from python:
556
+ // TODO(shubhra): should we "forward" this new turn to the next agent?
557
+ this.logger.warn('skipping new realtime generation, the agent is draining');
558
+ return;
559
+ }
560
+
561
+ const handle = SpeechHandle.create({
562
+ allowInterruptions: this.allowInterruptions,
563
+ });
564
+ this.agentSession.emit(
565
+ AgentSessionEventTypes.SpeechCreated,
566
+ createSpeechCreatedEvent({
567
+ userInitiated: false,
568
+ source: 'generate_reply',
569
+ speechHandle: handle,
570
+ }),
571
+ );
572
+ this.logger.info({ speech_id: handle.id }, 'Creating speech handle');
573
+
574
+ this.createSpeechTask({
575
+ promise: this.realtimeGenerationTask(handle, ev, {}),
576
+ ownedSpeechHandle: handle,
577
+ name: 'AgentActivity.realtimeGeneration',
578
+ });
579
+
580
+ this.scheduleSpeech(handle, SpeechHandle.SPEECH_PRIORITY_NORMAL);
581
+ }
582
+
583
+ // recognition hooks
584
+
585
+ onStartOfSpeech(_ev: VADEvent): void {
586
+ this.agentSession._updateUserState('speaking');
587
+ }
588
+
589
+ onEndOfSpeech(_ev: VADEvent): void {
590
+ this.agentSession._updateUserState('listening');
591
+ }
592
+
593
+ onVADInferenceDone(ev: VADEvent): void {
594
+ if (this.turnDetection === 'manual' || this.turnDetection === 'realtime_llm') {
595
+ // skip speech handle interruption for manual and realtime model
596
+ return;
597
+ }
598
+
599
+ if (this.llm instanceof RealtimeModel && this.llm.capabilities.turnDetection) {
600
+ // skip speech handle interruption if server side turn detection is enabled
601
+ return;
602
+ }
603
+
604
+ if (ev.speechDuration < this.agentSession.options.minInterruptionDuration) {
605
+ return;
606
+ }
607
+
608
+ if (this.stt && this.agentSession.options.minInterruptionWords > 0 && this.audioRecognition) {
609
+ const text = this.audioRecognition.currentTranscript;
610
+
611
+ // TODO(shubhra): better word splitting for multi-language
612
+ if (text && splitWords(text, true).length < this.agentSession.options.minInterruptionWords) {
613
+ return;
614
+ }
615
+ }
616
+
617
+ this.realtimeSession?.startUserActivity();
618
+
619
+ if (
620
+ this._currentSpeech &&
621
+ !this._currentSpeech.interrupted &&
622
+ this._currentSpeech.allowInterruptions
623
+ ) {
624
+ this.logger.info({ 'speech id': this._currentSpeech.id }, 'speech interrupted by VAD');
625
+ this.realtimeSession?.interrupt();
626
+ this._currentSpeech.interrupt();
627
+ }
628
+ }
629
+
630
+ onInterimTranscript(ev: SpeechEvent): void {
631
+ if (this.llm instanceof RealtimeModel && this.llm.capabilities.userTranscription) {
632
+ // skip stt transcription if userTranscription is enabled on the realtime model
633
+ return;
634
+ }
635
+
636
+ this.agentSession.emit(
637
+ AgentSessionEventTypes.UserInputTranscribed,
638
+ createUserInputTranscribedEvent({
639
+ transcript: ev.alternatives![0].text,
640
+ isFinal: false,
641
+ // TODO(AJS-106): add multi participant support
642
+ }),
643
+ );
644
+ }
645
+
646
+ onFinalTranscript(ev: SpeechEvent): void {
647
+ if (this.llm instanceof RealtimeModel && this.llm.capabilities.userTranscription) {
648
+ // skip stt transcription if userTranscription is enabled on the realtime model
649
+ return;
650
+ }
651
+
652
+ this.agentSession.emit(
653
+ AgentSessionEventTypes.UserInputTranscribed,
654
+ createUserInputTranscribedEvent({
655
+ transcript: ev.alternatives![0].text,
656
+ isFinal: true,
657
+ // TODO(AJS-106): add multi participant support
658
+ }),
659
+ );
660
+ }
661
+
662
+ private createSpeechTask<T>(options: {
663
+ promise: Promise<T>;
664
+ ownedSpeechHandle?: SpeechHandle;
665
+ name?: string;
666
+ }): Promise<T> {
667
+ const { promise, ownedSpeechHandle } = options;
668
+
669
+ this.speechTasks.add(promise);
670
+
671
+ promise.finally(() => {
672
+ this.speechTasks.delete(promise);
673
+
674
+ if (ownedSpeechHandle) {
675
+ ownedSpeechHandle._markPlayoutDone();
676
+ }
677
+
678
+ this.wakeupMainTask();
679
+ });
680
+
681
+ return promise;
682
+ }
683
+
684
+ async onEndOfTurn(info: EndOfTurnInfo): Promise<boolean> {
685
+ if (this.draining) {
686
+ this.logger.warn({ user_input: info.newTranscript }, 'skipping user input, task is draining');
687
+ // copied from python:
688
+ // TODO(shubhra): should we "forward" this new turn to the next agent/activity?
689
+ return true;
690
+ }
691
+
692
+ if (
693
+ this.stt &&
694
+ this.turnDetection !== 'manual' &&
695
+ this._currentSpeech &&
696
+ this._currentSpeech.allowInterruptions &&
697
+ !this._currentSpeech.interrupted &&
698
+ this.agentSession.options.minInterruptionWords > 0 &&
699
+ info.newTranscript.split(' ').length < this.agentSession.options.minInterruptionWords
700
+ ) {
701
+ // avoid interruption if the new_transcript is too short
702
+ this.logger.info('skipping user input, new_transcript is too short');
703
+ return false;
704
+ }
705
+
706
+ const oldTask = this._userTurnCompletedTask;
707
+ this._userTurnCompletedTask = this.createSpeechTask({
708
+ promise: this.userTurnCompleted(info, oldTask),
709
+ name: 'AgentActivity.userTurnCompleted',
710
+ });
711
+ return true;
712
+ }
713
+
714
+ retrieveChatCtx(): ChatContext {
715
+ return this.agentSession.chatCtx;
716
+ }
717
+
718
+ private async mainTask(signal: AbortSignal): Promise<void> {
719
+ const abortFuture = new Future();
720
+ const abortHandler = () => {
721
+ abortFuture.resolve();
722
+ signal.removeEventListener('abort', abortHandler);
723
+ };
724
+ signal.addEventListener('abort', abortHandler);
725
+
726
+ while (true) {
727
+ await Promise.race([this.q_updated.await, abortFuture.await]);
728
+ if (signal.aborted) break;
729
+
730
+ while (this.speechQueue.size() > 0) {
731
+ if (signal.aborted) break;
732
+
733
+ const heapItem = this.speechQueue.pop();
734
+ if (!heapItem) {
735
+ throw new Error('Speech queue is empty');
736
+ }
737
+ const speechHandle = heapItem[2];
738
+ this._currentSpeech = speechHandle;
739
+ speechHandle._authorizePlayout();
740
+ await speechHandle.waitForPlayout();
741
+ this._currentSpeech = undefined;
742
+ }
743
+
744
+ // If we're draining and there are no more speech tasks, we can exit.
745
+ // Only speech tasks can bypass draining to create a tool response
746
+ if (this.draining && this.speechTasks.size === 0) {
747
+ this.logger.info('mainTask: draining and no more speech tasks');
748
+ break;
749
+ }
750
+
751
+ this.q_updated = new Future();
752
+ }
753
+
754
+ this.logger.info('AgentActivity mainTask: exiting');
755
+ }
756
+
757
+ private wakeupMainTask(): void {
758
+ this.q_updated.resolve();
759
+ }
760
+
761
+ generateReply(options: {
762
+ userMessage?: ChatMessage;
763
+ chatCtx?: ChatContext;
764
+ instructions?: string;
765
+ toolChoice?: ToolChoice | null;
766
+ allowInterruptions?: boolean;
767
+ }): SpeechHandle {
768
+ const {
769
+ userMessage,
770
+ chatCtx,
771
+ instructions: defaultInstructions,
772
+ toolChoice: defaultToolChoice,
773
+ allowInterruptions: defaultAllowInterruptions,
774
+ } = options;
775
+
776
+ let instructions = defaultInstructions;
777
+ let toolChoice = defaultToolChoice;
778
+ let allowInterruptions = defaultAllowInterruptions;
779
+
780
+ if (
781
+ this.llm instanceof RealtimeModel &&
782
+ this.llm.capabilities.turnDetection &&
783
+ allowInterruptions === false
784
+ ) {
785
+ this.logger.warn(
786
+ 'the RealtimeModel uses a server-side turn detection, allowInterruptions cannot be false when using VoiceAgent.generateReply(), ' +
787
+ 'disable turnDetection in the RealtimeModel and use VAD on the AgentTask/VoiceAgent instead',
788
+ );
789
+ allowInterruptions = true;
790
+ }
791
+
792
+ if (this.llm === undefined) {
793
+ throw new Error('trying to generate reply without an LLM model');
794
+ }
795
+
796
+ const functionCall = asyncLocalStorage.getStore()?.functionCall;
797
+ if (toolChoice === undefined && functionCall !== undefined) {
798
+ // when generateReply is called inside a tool, set toolChoice to 'none' by default
799
+ toolChoice = 'none';
800
+ }
801
+
802
+ const handle = SpeechHandle.create({
803
+ allowInterruptions: allowInterruptions ?? this.allowInterruptions,
804
+ });
805
+
806
+ this.agentSession.emit(
807
+ AgentSessionEventTypes.SpeechCreated,
808
+ createSpeechCreatedEvent({
809
+ userInitiated: true,
810
+ source: 'generate_reply',
811
+ speechHandle: handle,
812
+ }),
813
+ );
814
+ this.logger.info({ speech_id: handle.id }, 'Creating speech handle');
815
+
816
+ if (this.llm instanceof RealtimeModel) {
817
+ this.createSpeechTask({
818
+ promise: this.realtimeReplyTask({
819
+ speechHandle: handle,
820
+ // TODO(brian): support llm.ChatMessage for the realtime model
821
+ userInput: userMessage?.textContent,
822
+ instructions,
823
+ modelSettings: {
824
+ // isGiven(toolChoice) = toolChoice !== undefined
825
+ toolChoice: toOaiToolChoice(toolChoice !== undefined ? toolChoice : this.toolChoice),
826
+ },
827
+ }),
828
+ ownedSpeechHandle: handle,
829
+ name: 'AgentActivity.realtimeReply',
830
+ });
831
+ } else if (this.llm instanceof LLM) {
832
+ // instructions used inside generateReply are "extra" instructions.
833
+ // this matches the behavior of the Realtime API:
834
+ // https://platform.openai.com/docs/api-reference/realtime-client-events/response/create
835
+ if (instructions) {
836
+ instructions = `${this.agent.instructions}\n${instructions}`;
837
+ }
838
+
839
+ const task = this.createSpeechTask({
840
+ promise: this.pipelineReplyTask(
841
+ handle,
842
+ chatCtx ?? this.agent.chatCtx,
843
+ this.agent.toolCtx,
844
+ { toolChoice: toOaiToolChoice(toolChoice !== undefined ? toolChoice : this.toolChoice) },
845
+ instructions ? `${this.agent.instructions}\n${instructions}` : instructions,
846
+ userMessage,
847
+ ),
848
+ ownedSpeechHandle: handle,
849
+ name: 'AgentActivity.pipelineReply',
850
+ });
851
+
852
+ task.finally(() => this.onPipelineReplyDone());
853
+ }
854
+
855
+ this.scheduleSpeech(handle, SpeechHandle.SPEECH_PRIORITY_NORMAL);
856
+ return handle;
857
+ }
858
+
859
+ interrupt(): Future<void> {
860
+ const future = new Future<void>();
861
+ const currentSpeech = this._currentSpeech;
862
+
863
+ currentSpeech?.interrupt();
864
+
865
+ for (const [_, __, speech] of this.speechQueue) {
866
+ speech.interrupt();
867
+ }
868
+
869
+ this.realtimeSession?.interrupt();
870
+
871
+ if (currentSpeech === undefined) {
872
+ future.resolve();
873
+ } else {
874
+ currentSpeech.then(() => {
875
+ if (future.done) return;
876
+ future.resolve();
877
+ });
878
+ }
879
+
880
+ return future;
881
+ }
882
+
883
+ private onPipelineReplyDone(): void {
884
+ if (!this.speechQueue.peek() && (!this._currentSpeech || this._currentSpeech.done)) {
885
+ this.agentSession._updateAgentState('listening');
886
+ }
887
+ }
888
+
889
+ private async userTurnCompleted(info: EndOfTurnInfo, oldTask?: Promise<void>): Promise<void> {
890
+ if (oldTask) {
891
+ // We never cancel user code as this is very confusing.
892
+ // So we wait for the old execution of onUserTurnCompleted to finish.
893
+ // In practice this is OK because most speeches will be interrupted if a new turn
894
+ // is detected. So the previous execution should complete quickly.
895
+ await oldTask;
896
+ }
897
+
898
+ // When the audio recognition detects the end of a user turn:
899
+ // - check if realtime model server-side turn detection is enabled
900
+ // - check if there is no current generation happening
901
+ // - cancel the current generation if it allows interruptions (otherwise skip this current
902
+ // turn)
903
+ // - generate a reply to the user input
904
+
905
+ if (this.llm instanceof RealtimeModel) {
906
+ if (this.llm.capabilities.turnDetection) {
907
+ return;
908
+ }
909
+ this.realtimeSession?.commitAudio();
910
+ }
911
+
912
+ if (this._currentSpeech) {
913
+ if (!this._currentSpeech.allowInterruptions) {
914
+ this.logger.warn(
915
+ { user_input: info.newTranscript },
916
+ 'skipping user input, current speech generation cannot be interrupted',
917
+ );
918
+ return;
919
+ }
920
+
921
+ this.logger.info(
922
+ { 'speech id': this._currentSpeech.id },
923
+ 'speech interrupted, new user turn detected',
924
+ );
925
+
926
+ this._currentSpeech.interrupt();
927
+ this.realtimeSession?.interrupt();
928
+ }
929
+
930
+ let userMessage: ChatMessage | undefined = ChatMessage.create({
931
+ role: 'user',
932
+ content: info.newTranscript,
933
+ });
934
+
935
+ // create a temporary mutable chat context to pass to onUserTurnCompleted
936
+ // the user can edit it for the current generation, but changes will not be kept inside the
937
+ // Agent.chatCtx
938
+ const chatCtx = this.agent.chatCtx.copy();
939
+ const startTime = Date.now();
940
+
941
+ try {
942
+ await this.agent.onUserTurnCompleted(chatCtx, userMessage);
943
+ } catch (e) {
944
+ if (e instanceof StopResponse) {
945
+ return;
946
+ }
947
+ this.logger.error({ error: e }, 'error occurred during onUserTurnCompleted');
948
+ }
949
+
950
+ const callbackDuration = Date.now() - startTime;
951
+
952
+ if (this.llm instanceof RealtimeModel) {
953
+ // ignore stt transcription for realtime model
954
+ userMessage = undefined;
955
+ } else if (this.llm === undefined) {
956
+ return;
957
+ }
958
+
959
+ // Ensure the new message is passed to generateReply
960
+ // This preserves the original message id, making it easier for users to track responses
961
+ const speechHandle = this.generateReply({ userMessage, chatCtx });
962
+
963
+ const eouMetrics: EOUMetrics = {
964
+ type: 'eou_metrics',
965
+ timestamp: Date.now(),
966
+ endOfUtteranceDelay: info.endOfUtteranceDelay,
967
+ transcriptionDelay: info.transcriptionDelay,
968
+ onUserTurnCompletedDelay: callbackDuration,
969
+ speechId: speechHandle.id,
970
+ };
971
+
972
+ this.agentSession.emit(
973
+ AgentSessionEventTypes.MetricsCollected,
974
+ createMetricsCollectedEvent({ metrics: eouMetrics }),
975
+ );
976
+ }
977
+
978
+ private async ttsTask(
979
+ speechHandle: SpeechHandle,
980
+ text: string | ReadableStream<string>,
981
+ addToChatCtx: boolean,
982
+ modelSettings: ModelSettings,
983
+ audio?: ReadableStream<AudioFrame> | null,
984
+ ): Promise<void> {
985
+ speechHandleStorage.enterWith(speechHandle);
986
+
987
+ const transcriptionOutput = this.agentSession.output.transcriptionEnabled
988
+ ? this.agentSession.output.transcription
989
+ : null;
990
+
991
+ const audioOutput = this.agentSession.output.audioEnabled
992
+ ? this.agentSession.output.audio
993
+ : null;
994
+
995
+ const replyAbortController = new AbortController();
996
+ await speechHandle.waitIfNotInterrupted([speechHandle._waitForAuthorization()]);
997
+
998
+ if (speechHandle.interrupted) {
999
+ return;
1000
+ }
1001
+
1002
+ let baseStream: ReadableStream<string>;
1003
+ if (text instanceof ReadableStream) {
1004
+ baseStream = text;
1005
+ } else {
1006
+ baseStream = new ReadableStream({
1007
+ start(controller) {
1008
+ controller.enqueue(text);
1009
+ controller.close();
1010
+ },
1011
+ });
1012
+ }
1013
+
1014
+ const [textSource, audioSource] = baseStream.tee();
1015
+
1016
+ const tasks: Array<Task<void>> = [];
1017
+
1018
+ const trNode = await this.agent.transcriptionNode(textSource, {});
1019
+ let textOut: _TextOut | null = null;
1020
+ if (trNode) {
1021
+ const [textForwardTask, _textOut] = performTextForwarding(
1022
+ trNode,
1023
+ replyAbortController,
1024
+ transcriptionOutput,
1025
+ );
1026
+ textOut = _textOut;
1027
+ tasks.push(textForwardTask);
1028
+ }
1029
+
1030
+ const onFirstFrame = () => {
1031
+ this.agentSession._updateAgentState('speaking');
1032
+ };
1033
+
1034
+ if (!audioOutput) {
1035
+ if (textOut) {
1036
+ textOut.firstTextFut.await.finally(onFirstFrame);
1037
+ }
1038
+ } else {
1039
+ let audioOut: _AudioOut | null = null;
1040
+ if (!audio) {
1041
+ // generate audio using TTS
1042
+ const [ttsTask, ttsStream] = performTTSInference(
1043
+ (...args) => this.agent.ttsNode(...args),
1044
+ audioSource,
1045
+ modelSettings,
1046
+ replyAbortController,
1047
+ );
1048
+ tasks.push(ttsTask);
1049
+
1050
+ const [forwardTask, _audioOut] = performAudioForwarding(
1051
+ ttsStream,
1052
+ audioOutput,
1053
+ replyAbortController,
1054
+ );
1055
+ tasks.push(forwardTask);
1056
+ audioOut = _audioOut;
1057
+ } else {
1058
+ // use the provided audio
1059
+ const [forwardTask, _audioOut] = performAudioForwarding(
1060
+ audio,
1061
+ audioOutput,
1062
+ replyAbortController,
1063
+ );
1064
+ tasks.push(forwardTask);
1065
+ audioOut = _audioOut;
1066
+ }
1067
+ audioOut.firstFrameFut.await.finally(onFirstFrame);
1068
+ }
1069
+
1070
+ await speechHandle.waitIfNotInterrupted(tasks.map((task) => task.result));
1071
+
1072
+ if (audioOutput) {
1073
+ await speechHandle.waitIfNotInterrupted([audioOutput.waitForPlayout()]);
1074
+ }
1075
+
1076
+ if (speechHandle.interrupted) {
1077
+ replyAbortController.abort();
1078
+ await cancelAndWait(tasks, AgentActivity.REPLY_TASK_CANCEL_TIMEOUT);
1079
+ if (audioOutput) {
1080
+ audioOutput.clearBuffer();
1081
+ await audioOutput.waitForPlayout();
1082
+ }
1083
+ }
1084
+
1085
+ if (addToChatCtx) {
1086
+ const message = ChatMessage.create({
1087
+ role: 'assistant',
1088
+ content: textOut?.text || '',
1089
+ interrupted: speechHandle.interrupted,
1090
+ });
1091
+ this.agent._chatCtx.insert(message);
1092
+ this.agentSession._conversationItemAdded(message);
1093
+ }
1094
+
1095
+ if (this.agentSession.agentState === 'speaking') {
1096
+ this.agentSession._updateAgentState('listening');
1097
+ }
1098
+ }
1099
+
1100
+ private async pipelineReplyTask(
1101
+ speechHandle: SpeechHandle,
1102
+ chatCtx: ChatContext,
1103
+ toolCtx: ToolContext,
1104
+ modelSettings: ModelSettings,
1105
+ instructions?: string,
1106
+ newMessage?: ChatMessage,
1107
+ toolsMessages?: ChatItem[],
1108
+ ): Promise<void> {
1109
+ speechHandleStorage.enterWith(speechHandle);
1110
+
1111
+ const replyAbortController = new AbortController();
1112
+
1113
+ const audioOutput = this.agentSession.output.audioEnabled
1114
+ ? this.agentSession.output.audio
1115
+ : null;
1116
+ const transcriptionOutput = this.agentSession.output.transcriptionEnabled
1117
+ ? this.agentSession.output.transcription
1118
+ : null;
1119
+
1120
+ chatCtx = chatCtx.copy();
1121
+
1122
+ if (newMessage) {
1123
+ chatCtx.insert(newMessage);
1124
+ this.agent._chatCtx.insert(newMessage);
1125
+ this.agentSession._conversationItemAdded(newMessage);
1126
+ }
1127
+
1128
+ if (instructions) {
1129
+ try {
1130
+ updateInstructions({
1131
+ chatCtx,
1132
+ instructions,
1133
+ addIfMissing: true,
1134
+ });
1135
+ } catch (e) {
1136
+ this.logger.error({ error: e }, 'error occurred during updateInstructions');
1137
+ }
1138
+ }
1139
+
1140
+ this.agentSession._updateAgentState('thinking');
1141
+ const tasks: Array<Task<void>> = [];
1142
+ const [llmTask, llmGenData] = performLLMInference(
1143
+ // preserve `this` context in llmNode
1144
+ (...args) => this.agent.llmNode(...args),
1145
+ chatCtx,
1146
+ toolCtx,
1147
+ modelSettings,
1148
+ replyAbortController,
1149
+ );
1150
+ tasks.push(llmTask);
1151
+
1152
+ const [ttsTextInput, llmOutput] = llmGenData.textStream.tee();
1153
+
1154
+ let ttsTask: Task<void> | null = null;
1155
+ let ttsStream: ReadableStream<AudioFrame> | null = null;
1156
+ if (audioOutput) {
1157
+ [ttsTask, ttsStream] = performTTSInference(
1158
+ (...args) => this.agent.ttsNode(...args),
1159
+ ttsTextInput,
1160
+ modelSettings,
1161
+ replyAbortController,
1162
+ );
1163
+ tasks.push(ttsTask);
1164
+ }
1165
+
1166
+ await speechHandle.waitIfNotInterrupted([speechHandle._waitForAuthorization()]);
1167
+ if (speechHandle.interrupted) {
1168
+ replyAbortController.abort();
1169
+ await cancelAndWait(tasks, AgentActivity.REPLY_TASK_CANCEL_TIMEOUT);
1170
+ return;
1171
+ }
1172
+
1173
+ const replyStartedAt = Date.now();
1174
+ const trNodeResult = await this.agent.transcriptionNode(llmOutput, modelSettings);
1175
+ let textOut: _TextOut | null = null;
1176
+ if (trNodeResult) {
1177
+ const [textForwardTask, _textOut] = performTextForwarding(
1178
+ trNodeResult,
1179
+ replyAbortController,
1180
+ transcriptionOutput,
1181
+ );
1182
+ tasks.push(textForwardTask);
1183
+ textOut = _textOut;
1184
+ }
1185
+
1186
+ const onFirstFrame = () => {
1187
+ this.agentSession._updateAgentState('speaking');
1188
+ };
1189
+
1190
+ let audioOut: _AudioOut | null = null;
1191
+ if (audioOutput) {
1192
+ if (ttsStream) {
1193
+ const [forwardTask, _audioOut] = performAudioForwarding(
1194
+ ttsStream,
1195
+ audioOutput,
1196
+ replyAbortController,
1197
+ );
1198
+ audioOut = _audioOut;
1199
+ tasks.push(forwardTask);
1200
+ audioOut.firstFrameFut.await.finally(onFirstFrame);
1201
+ } else {
1202
+ throw Error('ttsStream is null when audioOutput is enabled');
1203
+ }
1204
+ } else {
1205
+ textOut?.firstTextFut.await.finally(onFirstFrame);
1206
+ }
1207
+
1208
+ const onToolExecutionStarted = (_: FunctionCall) => {
1209
+ // TODO(brian): handle speech_handle item_added
1210
+ };
1211
+
1212
+ const onToolExecutionCompleted = (_: ToolExecutionOutput) => {
1213
+ // TODO(brian): handle speech_handle item_added
1214
+ };
1215
+
1216
+ const [executeToolsTask, toolOutput] = performToolExecutions({
1217
+ session: this.agentSession,
1218
+ speechHandle,
1219
+ toolCtx,
1220
+ toolChoice: modelSettings.toolChoice,
1221
+ toolCallStream: llmGenData.toolCallStream,
1222
+ controller: replyAbortController,
1223
+ onToolExecutionStarted,
1224
+ onToolExecutionCompleted,
1225
+ });
1226
+ tasks.push(executeToolsTask);
1227
+
1228
+ await speechHandle.waitIfNotInterrupted(tasks.map((task) => task.result));
1229
+
1230
+ if (audioOutput) {
1231
+ await speechHandle.waitIfNotInterrupted([audioOutput.waitForPlayout()]);
1232
+ }
1233
+
1234
+ // add the tools messages that triggers this reply to the chat context
1235
+ if (toolsMessages) {
1236
+ for (const msg of toolsMessages) {
1237
+ msg.createdAt = replyStartedAt;
1238
+ }
1239
+ this.agent._chatCtx.insert(toolsMessages);
1240
+ }
1241
+
1242
+ if (speechHandle.interrupted) {
1243
+ this.logger.debug(
1244
+ { speech_id: speechHandle.id },
1245
+ 'Aborting all pipeline reply tasks due to interruption',
1246
+ );
1247
+ replyAbortController.abort();
1248
+ await Promise.allSettled(
1249
+ tasks.map((task) => task.cancelAndWait(AgentActivity.REPLY_TASK_CANCEL_TIMEOUT)),
1250
+ );
1251
+
1252
+ let forwardedText = textOut?.text || '';
1253
+
1254
+ if (audioOutput) {
1255
+ audioOutput.clearBuffer();
1256
+ const playbackEv = await audioOutput.waitForPlayout();
1257
+ if (audioOut?.firstFrameFut.done) {
1258
+ // playback EV is valid only if the first frame was already played
1259
+ this.logger.info(
1260
+ { speech_id: speechHandle.id, playbackPosition: playbackEv.playbackPosition },
1261
+ 'playout interrupted',
1262
+ );
1263
+ if (playbackEv.synchronizedTranscript) {
1264
+ forwardedText = playbackEv.synchronizedTranscript;
1265
+ }
1266
+ } else {
1267
+ forwardedText = '';
1268
+ }
1269
+ }
1270
+
1271
+ if (forwardedText) {
1272
+ const message = ChatMessage.create({
1273
+ role: 'assistant',
1274
+ content: forwardedText,
1275
+ id: llmGenData.id,
1276
+ interrupted: true,
1277
+ createdAt: replyStartedAt,
1278
+ });
1279
+ chatCtx.insert(message);
1280
+ this.agent._chatCtx.insert(message);
1281
+ this.agentSession._conversationItemAdded(message);
1282
+ }
1283
+
1284
+ if (this.agentSession.agentState === 'speaking') {
1285
+ this.agentSession._updateAgentState('listening');
1286
+ }
1287
+
1288
+ this.logger.info(
1289
+ { speech_id: speechHandle.id, message: forwardedText },
1290
+ 'playout completed with interrupt',
1291
+ );
1292
+ // TODO(shubhra) add chat message to speech handle
1293
+ speechHandle._markPlayoutDone();
1294
+ await executeToolsTask.cancelAndWait(AgentActivity.REPLY_TASK_CANCEL_TIMEOUT);
1295
+ return;
1296
+ }
1297
+
1298
+ if (textOut && textOut.text) {
1299
+ const message = ChatMessage.create({
1300
+ role: 'assistant',
1301
+ id: llmGenData.id,
1302
+ interrupted: false,
1303
+ createdAt: replyStartedAt,
1304
+ content: textOut.text,
1305
+ });
1306
+ chatCtx.insert(message);
1307
+ this.agent._chatCtx.insert(message);
1308
+ this.agentSession._conversationItemAdded(message);
1309
+ this.logger.info(
1310
+ { speech_id: speechHandle.id, message: textOut.text },
1311
+ 'playout completed without interruption',
1312
+ );
1313
+ }
1314
+
1315
+ if (toolOutput.output.length > 0) {
1316
+ this.agentSession._updateAgentState('thinking');
1317
+ } else if (this.agentSession.agentState === 'speaking') {
1318
+ this.agentSession._updateAgentState('listening');
1319
+ }
1320
+
1321
+ speechHandle._markPlayoutDone();
1322
+ await executeToolsTask.result;
1323
+
1324
+ if (toolOutput.output.length === 0) return;
1325
+
1326
+ // important: no agent output should be used after this point
1327
+ const { maxToolSteps } = this.agentSession.options;
1328
+ if (speechHandle.stepIndex >= maxToolSteps) {
1329
+ this.logger.warn(
1330
+ { speech_id: speechHandle.id, max_tool_steps: maxToolSteps },
1331
+ 'maximum number of function calls steps reached',
1332
+ );
1333
+ return;
1334
+ }
1335
+
1336
+ const functionToolsExecutedEvent = createFunctionToolsExecutedEvent({
1337
+ functionCalls: [],
1338
+ functionCallOutputs: [],
1339
+ });
1340
+ let shouldGenerateToolReply: boolean = false;
1341
+ let newAgentTask: Agent | null = null;
1342
+ let ignoreTaskSwitch: boolean = false;
1343
+
1344
+ for (const sanitizedOut of toolOutput.output) {
1345
+ if (sanitizedOut.toolCallOutput !== undefined) {
1346
+ functionToolsExecutedEvent.functionCalls.push(sanitizedOut.toolCall);
1347
+ functionToolsExecutedEvent.functionCallOutputs.push(sanitizedOut.toolCallOutput);
1348
+ if (sanitizedOut.replyRequired) {
1349
+ shouldGenerateToolReply = true;
1350
+ }
1351
+ }
1352
+
1353
+ if (newAgentTask !== null && sanitizedOut.agentTask !== undefined) {
1354
+ this.logger.error('expected to receive only one agent task from the tool executions');
1355
+ ignoreTaskSwitch = true;
1356
+ // TODO(brian): should we mark the function call as failed to notify the LLM?
1357
+ }
1358
+
1359
+ newAgentTask = sanitizedOut.agentTask ?? null;
1360
+
1361
+ this.logger.debug(
1362
+ {
1363
+ speechId: speechHandle.id,
1364
+ name: sanitizedOut.toolCall?.name,
1365
+ args: sanitizedOut.toolCall.args,
1366
+ output: sanitizedOut.toolCallOutput?.output,
1367
+ isError: sanitizedOut.toolCallOutput?.isError,
1368
+ },
1369
+ 'Tool call execution finished',
1370
+ );
1371
+ }
1372
+
1373
+ this.agentSession.emit(
1374
+ AgentSessionEventTypes.FunctionToolsExecuted,
1375
+ functionToolsExecutedEvent,
1376
+ );
1377
+
1378
+ let draining = this.draining;
1379
+ if (!ignoreTaskSwitch && newAgentTask !== null) {
1380
+ this.agentSession.updateAgent(newAgentTask);
1381
+ draining = true;
1382
+ }
1383
+
1384
+ const toolMessages = [
1385
+ ...functionToolsExecutedEvent.functionCalls,
1386
+ ...functionToolsExecutedEvent.functionCallOutputs,
1387
+ ] as ChatItem[];
1388
+ if (shouldGenerateToolReply) {
1389
+ chatCtx.insert(toolMessages);
1390
+
1391
+ const handle = SpeechHandle.create({
1392
+ allowInterruptions: speechHandle.allowInterruptions,
1393
+ stepIndex: speechHandle.stepIndex + 1,
1394
+ parent: speechHandle,
1395
+ });
1396
+ this.agentSession.emit(
1397
+ AgentSessionEventTypes.SpeechCreated,
1398
+ createSpeechCreatedEvent({
1399
+ userInitiated: false,
1400
+ source: 'tool_response',
1401
+ speechHandle: handle,
1402
+ }),
1403
+ );
1404
+
1405
+ // Avoid setting tool_choice to "required" or a specific function when
1406
+ // passing tool response back to the LLM
1407
+ const respondToolChoice = draining || modelSettings.toolChoice === 'none' ? 'none' : 'auto';
1408
+
1409
+ const toolResponseTask = this.createSpeechTask({
1410
+ promise: this.pipelineReplyTask(
1411
+ handle,
1412
+ chatCtx,
1413
+ toolCtx,
1414
+ { toolChoice: respondToolChoice },
1415
+ instructions,
1416
+ undefined,
1417
+ toolMessages,
1418
+ ),
1419
+ ownedSpeechHandle: handle,
1420
+ name: 'AgentActivity.pipelineReply',
1421
+ });
1422
+
1423
+ toolResponseTask.finally(() => this.onPipelineReplyDone());
1424
+
1425
+ this.scheduleSpeech(handle, SpeechHandle.SPEECH_PRIORITY_NORMAL, true);
1426
+ } else if (functionToolsExecutedEvent.functionCallOutputs.length > 0) {
1427
+ for (const msg of toolMessages) {
1428
+ msg.createdAt = replyStartedAt;
1429
+ }
1430
+ this.agent._chatCtx.insert(toolMessages);
1431
+ }
1432
+ }
1433
+
1434
+ private async realtimeGenerationTask(
1435
+ speechHandle: SpeechHandle,
1436
+ ev: GenerationCreatedEvent,
1437
+ modelSettings: ModelSettings,
1438
+ ): Promise<void> {
1439
+ speechHandleStorage.enterWith(speechHandle);
1440
+
1441
+ if (!this.realtimeSession) {
1442
+ throw new Error('realtime session is not initialized');
1443
+ }
1444
+ if (!(this.llm instanceof RealtimeModel)) {
1445
+ throw new Error('llm is not a realtime model');
1446
+ }
1447
+
1448
+ this.logger.debug(
1449
+ { speech_id: speechHandle.id, stepIndex: speechHandle.stepIndex },
1450
+ 'realtime generation started',
1451
+ );
1452
+
1453
+ const audioOutput = this.agentSession.output.audioEnabled
1454
+ ? this.agentSession.output.audio
1455
+ : null;
1456
+ const textOutput = this.agentSession.output.transcriptionEnabled
1457
+ ? this.agentSession.output.transcription
1458
+ : null;
1459
+ const toolCtx = this.realtimeSession.tools;
1460
+
1461
+ await speechHandle.waitIfNotInterrupted([speechHandle._waitForAuthorization()]);
1462
+
1463
+ if (speechHandle.interrupted) {
1464
+ return;
1465
+ }
1466
+
1467
+ const onFirstFrame = () => {
1468
+ this.agentSession._updateAgentState('speaking');
1469
+ };
1470
+
1471
+ const replyAbortController = new AbortController();
1472
+
1473
+ const readMessages = async (
1474
+ abortController: AbortController,
1475
+ outputs: Array<[string, _TextOut | null, _AudioOut | null]>,
1476
+ ) => {
1477
+ const forwardTasks: Array<Task<void>> = [];
1478
+ try {
1479
+ for await (const msg of ev.messageStream) {
1480
+ if (forwardTasks.length > 0) {
1481
+ this.logger.warn(
1482
+ 'expected to receive only one message generation from the realtime API',
1483
+ );
1484
+ break;
1485
+ }
1486
+ const trNodeResult = await this.agent.transcriptionNode(msg.textStream, modelSettings);
1487
+ let textOut: _TextOut | null = null;
1488
+ if (trNodeResult) {
1489
+ const [textForwardTask, _textOut] = performTextForwarding(
1490
+ trNodeResult,
1491
+ abortController,
1492
+ textOutput,
1493
+ );
1494
+ forwardTasks.push(textForwardTask);
1495
+ textOut = _textOut;
1496
+ }
1497
+ let audioOut: _AudioOut | null = null;
1498
+ if (audioOutput) {
1499
+ const realtimeAudio = await this.agent.realtimeAudioOutputNode(
1500
+ msg.audioStream,
1501
+ modelSettings,
1502
+ );
1503
+ if (realtimeAudio) {
1504
+ const [forwardTask, _audioOut] = performAudioForwarding(
1505
+ realtimeAudio,
1506
+ audioOutput,
1507
+ abortController,
1508
+ );
1509
+ forwardTasks.push(forwardTask);
1510
+ audioOut = _audioOut;
1511
+ audioOut.firstFrameFut.await.finally(onFirstFrame);
1512
+ } else {
1513
+ this.logger.warn(
1514
+ 'audio output is enabled but neither tts nor realtime audio is available',
1515
+ );
1516
+ }
1517
+ } else if (textOut) {
1518
+ textOut.firstTextFut.await.finally(onFirstFrame);
1519
+ }
1520
+ outputs.push([msg.messageId, textOut, audioOut]);
1521
+ }
1522
+ await waitFor(forwardTasks);
1523
+ } catch (error) {
1524
+ this.logger.error(error, 'error reading messages from the realtime API');
1525
+ } finally {
1526
+ await cancelAndWait(forwardTasks, AgentActivity.REPLY_TASK_CANCEL_TIMEOUT);
1527
+ }
1528
+ };
1529
+
1530
+ const messageOutputs: Array<[string, _TextOut | null, _AudioOut | null]> = [];
1531
+ const tasks = [
1532
+ Task.from(
1533
+ (controller) => readMessages(controller, messageOutputs),
1534
+ replyAbortController,
1535
+ 'AgentActivity.realtime_generation.read_messages',
1536
+ ),
1537
+ ];
1538
+
1539
+ const [toolCallStream, toolCallStreamForTracing] = ev.functionStream.tee();
1540
+ // TODO(brian): append to tracing tees
1541
+ const toolCalls: FunctionCall[] = [];
1542
+
1543
+ const readToolStreamTask = async (
1544
+ controller: AbortController,
1545
+ stream: ReadableStream<FunctionCall>,
1546
+ ) => {
1547
+ const reader = stream.getReader();
1548
+ try {
1549
+ while (!controller.signal.aborted) {
1550
+ const { done, value } = await reader.read();
1551
+ if (done) break;
1552
+
1553
+ this.logger.debug({ tool_call: value }, 'received tool call from the realtime API');
1554
+ toolCalls.push(value);
1555
+ }
1556
+ } finally {
1557
+ reader.releaseLock();
1558
+ }
1559
+ };
1560
+
1561
+ tasks.push(
1562
+ Task.from(
1563
+ (controller) => readToolStreamTask(controller, toolCallStreamForTracing),
1564
+ replyAbortController,
1565
+ 'AgentActivity.realtime_generation.read_tool_stream',
1566
+ ),
1567
+ );
1568
+
1569
+ const onToolExecutionStarted = (_: FunctionCall) => {
1570
+ // TODO(brian): handle speech_handle item_added
1571
+ };
1572
+
1573
+ const onToolExecutionCompleted = (_: ToolExecutionOutput) => {
1574
+ // TODO(brian): handle speech_handle item_added
1575
+ };
1576
+
1577
+ const [executeToolsTask, toolOutput] = performToolExecutions({
1578
+ session: this.agentSession,
1579
+ speechHandle,
1580
+ toolCtx,
1581
+ toolCallStream,
1582
+ toolChoice: modelSettings.toolChoice,
1583
+ controller: replyAbortController,
1584
+ onToolExecutionStarted,
1585
+ onToolExecutionCompleted,
1586
+ });
1587
+
1588
+ await speechHandle.waitIfNotInterrupted(tasks.map((task) => task.result));
1589
+
1590
+ // TODO(brian): add tracing span
1591
+
1592
+ if (audioOutput) {
1593
+ await speechHandle.waitIfNotInterrupted([audioOutput.waitForPlayout()]);
1594
+ this.agentSession._updateAgentState('listening');
1595
+ }
1596
+
1597
+ if (speechHandle.interrupted) {
1598
+ this.logger.debug(
1599
+ { speech_id: speechHandle.id },
1600
+ 'Aborting all realtime generation tasks due to interruption',
1601
+ );
1602
+ replyAbortController.abort();
1603
+ await cancelAndWait(tasks, AgentActivity.REPLY_TASK_CANCEL_TIMEOUT);
1604
+
1605
+ if (messageOutputs.length > 0) {
1606
+ // there should be only one message
1607
+ const [msgId, textOut, audioOut] = messageOutputs[0]!;
1608
+ let forwardedText = textOut?.text || '';
1609
+
1610
+ if (audioOutput) {
1611
+ audioOutput.clearBuffer();
1612
+ const playbackEv = await audioOutput.waitForPlayout();
1613
+ let playbackPosition = playbackEv.playbackPosition;
1614
+ if (audioOut?.firstFrameFut.done) {
1615
+ // playback EV is valid only if the first frame was already played
1616
+ this.logger.info(
1617
+ { speech_id: speechHandle.id, playbackPosition: playbackEv.playbackPosition },
1618
+ 'playout interrupted',
1619
+ );
1620
+ if (playbackEv.synchronizedTranscript) {
1621
+ forwardedText = playbackEv.synchronizedTranscript;
1622
+ }
1623
+ } else {
1624
+ forwardedText = '';
1625
+ playbackPosition = 0;
1626
+ }
1627
+
1628
+ // truncate server-side message
1629
+ this.realtimeSession.truncate({
1630
+ messageId: msgId,
1631
+ audioEndMs: Math.floor(playbackPosition),
1632
+ });
1633
+ }
1634
+
1635
+ if (forwardedText) {
1636
+ const message = ChatMessage.create({
1637
+ role: 'assistant',
1638
+ content: forwardedText,
1639
+ id: msgId,
1640
+ interrupted: true,
1641
+ });
1642
+ this.agent._chatCtx.insert(message);
1643
+ speechHandle._setChatMessage(message);
1644
+ this.agentSession._conversationItemAdded(message);
1645
+
1646
+ // TODO(brian): add tracing span
1647
+ }
1648
+ this.logger.info(
1649
+ { speech_id: speechHandle.id, message: forwardedText },
1650
+ 'playout completed with interrupt',
1651
+ );
1652
+ }
1653
+ // TODO(shubhra) add chat message to speech handle
1654
+ speechHandle._markPlayoutDone();
1655
+ await executeToolsTask.cancelAndWait(AgentActivity.REPLY_TASK_CANCEL_TIMEOUT);
1656
+
1657
+ // TODO(brian): close tees
1658
+ return;
1659
+ }
1660
+
1661
+ if (messageOutputs.length > 0) {
1662
+ // there should be only one message
1663
+ const [msgId, textOut, _] = messageOutputs[0]!;
1664
+ const message = ChatMessage.create({
1665
+ role: 'assistant',
1666
+ content: textOut?.text || '',
1667
+ id: msgId,
1668
+ interrupted: false,
1669
+ });
1670
+ this.agent._chatCtx.insert(message);
1671
+ speechHandle._setChatMessage(message);
1672
+ this.agentSession._conversationItemAdded(message); // mark the playout done before waiting for the tool execution\
1673
+ // TODO(brian): add tracing span
1674
+ }
1675
+
1676
+ // mark the playout done before waiting for the tool execution
1677
+ speechHandle._markPlayoutDone();
1678
+ // TODO(brian): close tees
1679
+
1680
+ toolOutput.firstToolStartedFuture.await.finally(() => {
1681
+ this.agentSession._updateAgentState('thinking');
1682
+ });
1683
+
1684
+ await executeToolsTask.result;
1685
+
1686
+ if (toolOutput.output.length === 0) return;
1687
+
1688
+ // important: no agent ouput should be used after this point
1689
+ const { maxToolSteps } = this.agentSession.options;
1690
+ if (speechHandle.stepIndex >= maxToolSteps) {
1691
+ this.logger.warn(
1692
+ { speech_id: speechHandle.id, max_tool_steps: maxToolSteps },
1693
+ 'maximum number of function calls steps reached',
1694
+ );
1695
+ return;
1696
+ }
1697
+
1698
+ const functionToolsExecutedEvent = createFunctionToolsExecutedEvent({
1699
+ functionCalls: [],
1700
+ functionCallOutputs: [],
1701
+ });
1702
+ let shouldGenerateToolReply: boolean = false;
1703
+ let newAgentTask: Agent | null = null;
1704
+ let ignoreTaskSwitch: boolean = false;
1705
+
1706
+ for (const sanitizedOut of toolOutput.output) {
1707
+ if (sanitizedOut.toolCallOutput !== undefined) {
1708
+ functionToolsExecutedEvent.functionCallOutputs.push(sanitizedOut.toolCallOutput);
1709
+ if (sanitizedOut.replyRequired) {
1710
+ shouldGenerateToolReply = true;
1711
+ }
1712
+ }
1713
+
1714
+ if (newAgentTask !== null && sanitizedOut.agentTask !== undefined) {
1715
+ this.logger.error('expected to receive only one agent task from the tool executions');
1716
+ ignoreTaskSwitch = true;
1717
+ }
1718
+
1719
+ newAgentTask = sanitizedOut.agentTask ?? null;
1720
+
1721
+ this.logger.debug(
1722
+ {
1723
+ speechId: speechHandle.id,
1724
+ name: sanitizedOut.toolCall?.name,
1725
+ args: sanitizedOut.toolCall.args,
1726
+ output: sanitizedOut.toolCallOutput?.output,
1727
+ isError: sanitizedOut.toolCallOutput?.isError,
1728
+ },
1729
+ 'Tool call execution finished',
1730
+ );
1731
+ }
1732
+
1733
+ this.agentSession.emit(
1734
+ AgentSessionEventTypes.FunctionToolsExecuted,
1735
+ functionToolsExecutedEvent,
1736
+ );
1737
+
1738
+ let draining = this.draining;
1739
+ if (!ignoreTaskSwitch && newAgentTask !== null) {
1740
+ this.agentSession.updateAgent(newAgentTask);
1741
+ draining = true;
1742
+ }
1743
+
1744
+ if (functionToolsExecutedEvent.functionCallOutputs.length > 0) {
1745
+ const chatCtx = this.realtimeSession.chatCtx.copy();
1746
+ chatCtx.items.push(...functionToolsExecutedEvent.functionCallOutputs);
1747
+ try {
1748
+ await this.realtimeSession.updateChatCtx(chatCtx);
1749
+ } catch (error) {
1750
+ this.logger.warn(
1751
+ { error },
1752
+ 'failed to update chat context before generating the function calls results',
1753
+ );
1754
+ }
1755
+ }
1756
+
1757
+ // skip realtime reply if not required or auto-generated
1758
+ if (!shouldGenerateToolReply || this.llm.capabilities.autoToolReplyGeneration) {
1759
+ return;
1760
+ }
1761
+
1762
+ this.realtimeSession.interrupt();
1763
+
1764
+ const replySpeechHandle = SpeechHandle.create({
1765
+ allowInterruptions: speechHandle.allowInterruptions,
1766
+ stepIndex: speechHandle.stepIndex + 1,
1767
+ parent: speechHandle,
1768
+ });
1769
+ this.agentSession.emit(
1770
+ AgentSessionEventTypes.SpeechCreated,
1771
+ createSpeechCreatedEvent({
1772
+ userInitiated: false,
1773
+ source: 'tool_response',
1774
+ speechHandle: replySpeechHandle,
1775
+ }),
1776
+ );
1777
+
1778
+ const toolChoice = draining || modelSettings.toolChoice === 'none' ? 'none' : 'auto';
1779
+ this.createSpeechTask({
1780
+ promise: this.realtimeReplyTask({
1781
+ speechHandle: replySpeechHandle,
1782
+ modelSettings: { toolChoice },
1783
+ }),
1784
+ ownedSpeechHandle: replySpeechHandle,
1785
+ name: 'AgentActivity.realtime_reply',
1786
+ });
1787
+
1788
+ this.scheduleSpeech(replySpeechHandle, SpeechHandle.SPEECH_PRIORITY_NORMAL, true);
1789
+ }
1790
+
1791
+ private async realtimeReplyTask({
1792
+ speechHandle,
1793
+ modelSettings: { toolChoice },
1794
+ userInput,
1795
+ instructions,
1796
+ }: {
1797
+ speechHandle: SpeechHandle;
1798
+ modelSettings: ModelSettings;
1799
+ userInput?: string;
1800
+ instructions?: string;
1801
+ }): Promise<void> {
1802
+ speechHandleStorage.enterWith(speechHandle);
1803
+
1804
+ if (!this.realtimeSession) {
1805
+ throw new Error('realtime session is not available');
1806
+ }
1807
+
1808
+ await speechHandle.waitIfNotInterrupted([speechHandle._waitForAuthorization()]);
1809
+
1810
+ if (userInput) {
1811
+ const chatCtx = this.realtimeSession.chatCtx.copy();
1812
+ const message = chatCtx.addMessage({
1813
+ role: 'user',
1814
+ content: userInput,
1815
+ });
1816
+ await this.realtimeSession.updateChatCtx(chatCtx);
1817
+ this.agent._chatCtx.insert(message);
1818
+ this.agentSession._conversationItemAdded(message);
1819
+ }
1820
+
1821
+ const originalToolChoice = this.toolChoice;
1822
+ if (toolChoice !== undefined) {
1823
+ this.realtimeSession.updateOptions({ toolChoice });
1824
+ }
1825
+
1826
+ try {
1827
+ const generationEvent = await this.realtimeSession.generateReply(instructions);
1828
+ await this.realtimeGenerationTask(speechHandle, generationEvent, { toolChoice });
1829
+ } finally {
1830
+ // reset toolChoice value
1831
+ if (toolChoice !== undefined && toolChoice !== originalToolChoice) {
1832
+ this.realtimeSession.updateOptions({ toolChoice: originalToolChoice });
1833
+ }
1834
+ }
1835
+ }
1836
+
1837
+ private scheduleSpeech(
1838
+ speechHandle: SpeechHandle,
1839
+ priority: number,
1840
+ bypassDraining: boolean = false,
1841
+ ): void {
1842
+ if (this.draining && !bypassDraining) {
1843
+ throw new Error('cannot schedule new speech, the agent is draining');
1844
+ }
1845
+
1846
+ // Monotonic time to avoid near 0 collisions
1847
+ this.speechQueue.push([priority, Number(process.hrtime.bigint()), speechHandle]);
1848
+ this.wakeupMainTask();
1849
+ }
1850
+
1851
+ async drain(): Promise<void> {
1852
+ const unlock = await this.lock.lock();
1853
+ try {
1854
+ if (this._draining) return;
1855
+
1856
+ this.createSpeechTask({
1857
+ promise: this.agent.onExit(),
1858
+ name: 'AgentActivity_onExit',
1859
+ });
1860
+
1861
+ this.wakeupMainTask();
1862
+ this._draining = true;
1863
+ await this._mainTask?.result;
1864
+ } finally {
1865
+ unlock();
1866
+ }
1867
+ }
1868
+
1869
+ async close(): Promise<void> {
1870
+ const unlock = await this.lock.lock();
1871
+ try {
1872
+ if (!this._draining) {
1873
+ this.logger.warn('task closing without draining');
1874
+ }
1875
+
1876
+ // Unregister event handlers to prevent duplicate metrics
1877
+ if (this.llm instanceof LLM) {
1878
+ this.llm.off('metrics_collected', this.onMetricsCollected);
1879
+ }
1880
+ if (this.realtimeSession) {
1881
+ this.realtimeSession.off('generation_created', this.onGenerationCreated);
1882
+ this.realtimeSession.off('input_speech_started', this.onInputSpeechStarted);
1883
+ this.realtimeSession.off('input_speech_stopped', this.onInputSpeechStopped);
1884
+ this.realtimeSession.off(
1885
+ 'input_audio_transcription_completed',
1886
+ this.onInputAudioTranscriptionCompleted,
1887
+ );
1888
+ this.realtimeSession.off('metrics_collected', this.onMetricsCollected);
1889
+ }
1890
+ if (this.stt instanceof STT) {
1891
+ this.stt.off('metrics_collected', this.onMetricsCollected);
1892
+ }
1893
+ if (this.tts instanceof TTS) {
1894
+ this.tts.off('metrics_collected', this.onMetricsCollected);
1895
+ }
1896
+ if (this.vad instanceof VAD) {
1897
+ this.vad.off('metrics_collected', this.onMetricsCollected);
1898
+ }
1899
+
1900
+ this.detachAudioInput();
1901
+ await this.realtimeSession?.close();
1902
+ await this.audioRecognition?.close();
1903
+ await this._mainTask?.cancelAndWait();
1904
+ } finally {
1905
+ unlock();
1906
+ }
1907
+ }
1908
+ }
1909
+
1910
+ function toOaiToolChoice(toolChoice: ToolChoice | null): ToolChoice | undefined {
1911
+ // we convert null to undefined, which maps to the default provider tool choice value
1912
+ return toolChoice !== null ? toolChoice : undefined;
1913
+ }