@pipedog/core 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (736) hide show
  1. package/LICENSE +25 -0
  2. package/NOTICE +9 -0
  3. package/dist/adapters/base_llm_adapter.d.ts +266 -0
  4. package/dist/adapters/base_llm_adapter.js +489 -0
  5. package/dist/adapters/index.d.ts +2 -0
  6. package/dist/adapters/index.js +8 -0
  7. package/dist/adapters/schemas/direct_function.d.ts +202 -0
  8. package/dist/adapters/schemas/direct_function.js +302 -0
  9. package/dist/adapters/schemas/function_schema.d.ts +67 -0
  10. package/dist/adapters/schemas/function_schema.js +72 -0
  11. package/dist/adapters/schemas/index.d.ts +2 -0
  12. package/dist/adapters/schemas/index.js +8 -0
  13. package/dist/adapters/schemas/tools_schema.d.ts +57 -0
  14. package/dist/adapters/schemas/tools_schema.js +79 -0
  15. package/dist/adapters/services/index.d.ts +2 -0
  16. package/dist/adapters/services/index.js +8 -0
  17. package/dist/adapters/services/open_ai_adapter.d.ts +147 -0
  18. package/dist/adapters/services/open_ai_adapter.js +250 -0
  19. package/dist/audio/dtmf/dtmf_data.d.ts +3 -0
  20. package/dist/audio/dtmf/dtmf_data.js +22 -0
  21. package/dist/audio/dtmf/index.d.ts +2 -0
  22. package/dist/audio/dtmf/index.js +8 -0
  23. package/dist/audio/dtmf/types.d.ts +31 -0
  24. package/dist/audio/dtmf/types.js +35 -0
  25. package/dist/audio/dtmf/utils.d.ts +13 -0
  26. package/dist/audio/dtmf/utils.js +62 -0
  27. package/dist/audio/filters/base_audio_filter.d.ts +51 -0
  28. package/dist/audio/filters/base_audio_filter.js +17 -0
  29. package/dist/audio/filters/index.d.ts +2 -0
  30. package/dist/audio/filters/index.js +8 -0
  31. package/dist/audio/index.d.ts +2 -0
  32. package/dist/audio/index.js +8 -0
  33. package/dist/audio/krisp_instance.d.ts +95 -0
  34. package/dist/audio/krisp_instance.js +229 -0
  35. package/dist/audio/mixers/base_audio_mixer.d.ts +56 -0
  36. package/dist/audio/mixers/base_audio_mixer.js +16 -0
  37. package/dist/audio/mixers/index.d.ts +2 -0
  38. package/dist/audio/mixers/index.js +8 -0
  39. package/dist/audio/resamplers/base_audio_resampler.d.ts +46 -0
  40. package/dist/audio/resamplers/base_audio_resampler.js +35 -0
  41. package/dist/audio/resamplers/engine.d.ts +60 -0
  42. package/dist/audio/resamplers/engine.js +89 -0
  43. package/dist/audio/resamplers/index.d.ts +2 -0
  44. package/dist/audio/resamplers/index.js +8 -0
  45. package/dist/audio/resamplers/resampy_resampler.d.ts +10 -0
  46. package/dist/audio/resamplers/resampy_resampler.js +15 -0
  47. package/dist/audio/resamplers/sinc_engine.d.ts +10 -0
  48. package/dist/audio/resamplers/sinc_engine.js +235 -0
  49. package/dist/audio/resamplers/soxr_resampler.d.ts +58 -0
  50. package/dist/audio/resamplers/soxr_resampler.js +79 -0
  51. package/dist/audio/resamplers/soxr_stream_resampler.d.ts +78 -0
  52. package/dist/audio/resamplers/soxr_stream_resampler.js +159 -0
  53. package/dist/audio/turn/base_turn_analyzer.d.ts +84 -0
  54. package/dist/audio/turn/base_turn_analyzer.js +61 -0
  55. package/dist/audio/turn/index.d.ts +2 -0
  56. package/dist/audio/turn/index.js +8 -0
  57. package/dist/audio/turn/smart_turn/_whisper_features.d.ts +30 -0
  58. package/dist/audio/turn/smart_turn/_whisper_features.js +225 -0
  59. package/dist/audio/turn/smart_turn/base_smart_turn.d.ts +94 -0
  60. package/dist/audio/turn/smart_turn/base_smart_turn.js +239 -0
  61. package/dist/audio/turn/smart_turn/local_smart_turn_v3.d.ts +76 -0
  62. package/dist/audio/turn/smart_turn/local_smart_turn_v3.js +151 -0
  63. package/dist/audio/utils.d.ts +224 -0
  64. package/dist/audio/utils.js +634 -0
  65. package/dist/audio/vad/data/index.d.ts +2 -0
  66. package/dist/audio/vad/data/index.js +8 -0
  67. package/dist/audio/vad/index.d.ts +2 -0
  68. package/dist/audio/vad/index.js +8 -0
  69. package/dist/audio/vad/vad_analyzer.d.ts +134 -0
  70. package/dist/audio/vad/vad_analyzer.js +249 -0
  71. package/dist/audio/vad/vad_controller.d.ts +143 -0
  72. package/dist/audio/vad/vad_controller.js +228 -0
  73. package/dist/audio/volume.d.ts +38 -0
  74. package/dist/audio/volume.js +75 -0
  75. package/dist/bus/adapters/base.d.ts +35 -0
  76. package/dist/bus/adapters/base.js +19 -0
  77. package/dist/bus/adapters/index.d.ts +10 -0
  78. package/dist/bus/adapters/index.js +15 -0
  79. package/dist/bus/adapters/llm_context_adapter.d.ts +36 -0
  80. package/dist/bus/adapters/llm_context_adapter.js +82 -0
  81. package/dist/bus/adapters/tools_schema_adapter.d.ts +25 -0
  82. package/dist/bus/adapters/tools_schema_adapter.js +70 -0
  83. package/dist/bus/bridge_processor.d.ts +111 -0
  84. package/dist/bus/bridge_processor.js +166 -0
  85. package/dist/bus/bus.d.ts +110 -0
  86. package/dist/bus/bus.js +200 -0
  87. package/dist/bus/index.d.ts +22 -0
  88. package/dist/bus/index.js +27 -0
  89. package/dist/bus/local/async_queue.d.ts +12 -0
  90. package/dist/bus/local/async_queue.js +21 -0
  91. package/dist/bus/local/index.d.ts +3 -0
  92. package/dist/bus/local/index.js +8 -0
  93. package/dist/bus/messages.d.ts +378 -0
  94. package/dist/bus/messages.js +357 -0
  95. package/dist/bus/network/index.d.ts +16 -0
  96. package/dist/bus/network/index.js +21 -0
  97. package/dist/bus/queue.d.ts +27 -0
  98. package/dist/bus/queue.js +51 -0
  99. package/dist/bus/serializers/base.d.ts +25 -0
  100. package/dist/bus/serializers/base.js +14 -0
  101. package/dist/bus/serializers/index.d.ts +9 -0
  102. package/dist/bus/serializers/index.js +14 -0
  103. package/dist/bus/serializers/json.d.ts +77 -0
  104. package/dist/bus/serializers/json.js +367 -0
  105. package/dist/bus/subscriber.d.ts +46 -0
  106. package/dist/bus/subscriber.js +27 -0
  107. package/dist/bus/ui/index.d.ts +3 -0
  108. package/dist/bus/ui/index.js +8 -0
  109. package/dist/bus/ui/messages.d.ts +186 -0
  110. package/dist/bus/ui/messages.js +171 -0
  111. package/dist/classifiers/base_classifier.d.ts +263 -0
  112. package/dist/classifiers/base_classifier.js +346 -0
  113. package/dist/classifiers/cloudflare/clef/classifier.d.ts +70 -0
  114. package/dist/classifiers/cloudflare/clef/classifier.js +204 -0
  115. package/dist/classifiers/cloudflare/clef/client.d.ts +104 -0
  116. package/dist/classifiers/cloudflare/clef/client.js +234 -0
  117. package/dist/classifiers/cloudflare/clef/index.d.ts +2 -0
  118. package/dist/classifiers/cloudflare/clef/index.js +8 -0
  119. package/dist/classifiers/cloudflare/index.d.ts +2 -0
  120. package/dist/classifiers/cloudflare/index.js +8 -0
  121. package/dist/classifiers/http_client.d.ts +70 -0
  122. package/dist/classifiers/http_client.js +161 -0
  123. package/dist/classifiers/index.d.ts +2 -0
  124. package/dist/classifiers/index.js +8 -0
  125. package/dist/classifiers/jev/classifier.d.ts +8 -0
  126. package/dist/classifiers/jev/classifier.js +13 -0
  127. package/dist/classifiers/jev/client.d.ts +8 -0
  128. package/dist/classifiers/jev/client.js +13 -0
  129. package/dist/classifiers/jev/index.d.ts +2 -0
  130. package/dist/classifiers/jev/index.js +8 -0
  131. package/dist/classifiers/llm/classifier.d.ts +72 -0
  132. package/dist/classifiers/llm/classifier.js +304 -0
  133. package/dist/classifiers/llm/index.d.ts +2 -0
  134. package/dist/classifiers/llm/index.js +8 -0
  135. package/dist/classifiers/openai/decisions/classifier.d.ts +77 -0
  136. package/dist/classifiers/openai/decisions/classifier.js +241 -0
  137. package/dist/classifiers/openai/decisions/client.d.ts +108 -0
  138. package/dist/classifiers/openai/decisions/client.js +234 -0
  139. package/dist/classifiers/openai/decisions/index.d.ts +2 -0
  140. package/dist/classifiers/openai/decisions/index.js +8 -0
  141. package/dist/classifiers/openai/index.d.ts +2 -0
  142. package/dist/classifiers/openai/index.js +8 -0
  143. package/dist/classifiers/typesafe/index.d.ts +2 -0
  144. package/dist/classifiers/typesafe/index.js +8 -0
  145. package/dist/classifiers/typesafe/jev/classifier.d.ts +70 -0
  146. package/dist/classifiers/typesafe/jev/classifier.js +220 -0
  147. package/dist/classifiers/typesafe/jev/client.d.ts +105 -0
  148. package/dist/classifiers/typesafe/jev/client.js +233 -0
  149. package/dist/classifiers/typesafe/jev/index.d.ts +2 -0
  150. package/dist/classifiers/typesafe/jev/index.js +8 -0
  151. package/dist/clocks/base_clock.d.ts +13 -0
  152. package/dist/clocks/base_clock.js +14 -0
  153. package/dist/clocks/index.d.ts +2 -0
  154. package/dist/clocks/index.js +8 -0
  155. package/dist/clocks/system_clock.d.ts +15 -0
  156. package/dist/clocks/system_clock.js +25 -0
  157. package/dist/frames/frames.d.ts +2533 -0
  158. package/dist/frames/frames.js +2485 -0
  159. package/dist/frames/index.d.ts +2 -0
  160. package/dist/frames/index.js +8 -0
  161. package/dist/frames/protobufs/frames_pb2.d.ts +229 -0
  162. package/dist/frames/protobufs/frames_pb2.js +466 -0
  163. package/dist/index.d.ts +11 -0
  164. package/dist/index.js +56 -0
  165. package/dist/metrics/index.d.ts +2 -0
  166. package/dist/metrics/index.js +8 -0
  167. package/dist/metrics/metrics.d.ts +276 -0
  168. package/dist/metrics/metrics.js +254 -0
  169. package/dist/observers/base_observer.d.ts +173 -0
  170. package/dist/observers/base_observer.js +151 -0
  171. package/dist/observers/error_observer.d.ts +75 -0
  172. package/dist/observers/error_observer.js +87 -0
  173. package/dist/observers/function_call_observer.d.ts +157 -0
  174. package/dist/observers/function_call_observer.js +222 -0
  175. package/dist/observers/index.d.ts +2 -0
  176. package/dist/observers/index.js +8 -0
  177. package/dist/observers/loggers/debug_log_observer.d.ts +91 -0
  178. package/dist/observers/loggers/debug_log_observer.js +228 -0
  179. package/dist/observers/loggers/index.d.ts +2 -0
  180. package/dist/observers/loggers/index.js +8 -0
  181. package/dist/observers/loggers/llm_log_observer.d.ts +24 -0
  182. package/dist/observers/loggers/llm_log_observer.js +110 -0
  183. package/dist/observers/loggers/metrics_log_observer.d.ts +85 -0
  184. package/dist/observers/loggers/metrics_log_observer.js +186 -0
  185. package/dist/observers/loggers/transcription_log_observer.d.ts +23 -0
  186. package/dist/observers/loggers/transcription_log_observer.js +74 -0
  187. package/dist/observers/service_metrics_observer.d.ts +168 -0
  188. package/dist/observers/service_metrics_observer.js +236 -0
  189. package/dist/observers/speaking_observer.d.ts +104 -0
  190. package/dist/observers/speaking_observer.js +150 -0
  191. package/dist/observers/startup_timing_observer.d.ts +221 -0
  192. package/dist/observers/startup_timing_observer.js +365 -0
  193. package/dist/observers/turn_tracking_observer.d.ts +78 -0
  194. package/dist/observers/turn_tracking_observer.js +189 -0
  195. package/dist/observers/user_bot_latency_observer.d.ts +411 -0
  196. package/dist/observers/user_bot_latency_observer.js +974 -0
  197. package/dist/pipeline/base_pipeline.d.ts +49 -0
  198. package/dist/pipeline/base_pipeline.js +101 -0
  199. package/dist/pipeline/capabilities.d.ts +76 -0
  200. package/dist/pipeline/capabilities.js +97 -0
  201. package/dist/pipeline/index.d.ts +2 -0
  202. package/dist/pipeline/index.js +8 -0
  203. package/dist/pipeline/job_context.d.ts +317 -0
  204. package/dist/pipeline/job_context.js +450 -0
  205. package/dist/pipeline/job_decorator.d.ts +54 -0
  206. package/dist/pipeline/job_decorator.js +92 -0
  207. package/dist/pipeline/llm_switcher.d.ts +84 -0
  208. package/dist/pipeline/llm_switcher.js +107 -0
  209. package/dist/pipeline/llm_with_backend.d.ts +435 -0
  210. package/dist/pipeline/llm_with_backend.js +806 -0
  211. package/dist/pipeline/parallel_pipeline.d.ts +83 -0
  212. package/dist/pipeline/parallel_pipeline.js +200 -0
  213. package/dist/pipeline/pipeline.d.ts +126 -0
  214. package/dist/pipeline/pipeline.js +196 -0
  215. package/dist/pipeline/service_switcher.d.ts +287 -0
  216. package/dist/pipeline/service_switcher.js +509 -0
  217. package/dist/pipeline/sync_parallel_pipeline.d.ts +192 -0
  218. package/dist/pipeline/sync_parallel_pipeline.js +387 -0
  219. package/dist/pipeline/worker.d.ts +821 -0
  220. package/dist/pipeline/worker.js +1656 -0
  221. package/dist/pipeline/worker_observer.d.ts +131 -0
  222. package/dist/pipeline/worker_observer.js +274 -0
  223. package/dist/pipeline/worker_ready_decorator.d.ts +45 -0
  224. package/dist/pipeline/worker_ready_decorator.js +76 -0
  225. package/dist/processors/aggregators/async_tool_messages.d.ts +146 -0
  226. package/dist/processors/aggregators/async_tool_messages.js +291 -0
  227. package/dist/processors/aggregators/dtmf_aggregator.d.ts +61 -0
  228. package/dist/processors/aggregators/dtmf_aggregator.js +144 -0
  229. package/dist/processors/aggregators/gated.d.ts +43 -0
  230. package/dist/processors/aggregators/gated.js +86 -0
  231. package/dist/processors/aggregators/gated_llm_context.d.ts +42 -0
  232. package/dist/processors/aggregators/gated_llm_context.js +86 -0
  233. package/dist/processors/aggregators/index.d.ts +2 -0
  234. package/dist/processors/aggregators/index.js +8 -0
  235. package/dist/processors/aggregators/llm_context.d.ts +370 -0
  236. package/dist/processors/aggregators/llm_context.js +580 -0
  237. package/dist/processors/aggregators/llm_context_summarizer.d.ts +219 -0
  238. package/dist/processors/aggregators/llm_context_summarizer.js +447 -0
  239. package/dist/processors/aggregators/llm_response.d.ts +37 -0
  240. package/dist/processors/aggregators/llm_response.js +71 -0
  241. package/dist/processors/aggregators/llm_response_universal.d.ts +966 -0
  242. package/dist/processors/aggregators/llm_response_universal.js +2394 -0
  243. package/dist/processors/aggregators/llm_text_processor.d.ts +53 -0
  244. package/dist/processors/aggregators/llm_text_processor.js +102 -0
  245. package/dist/processors/aggregators/sentence.d.ts +39 -0
  246. package/dist/processors/aggregators/sentence.js +69 -0
  247. package/dist/processors/async_generator.d.ts +42 -0
  248. package/dist/processors/async_generator.js +76 -0
  249. package/dist/processors/audio/audio_buffer_processor.d.ts +241 -0
  250. package/dist/processors/audio/audio_buffer_processor.js +565 -0
  251. package/dist/processors/audio/index.d.ts +2 -0
  252. package/dist/processors/audio/index.js +8 -0
  253. package/dist/processors/audio/vad_processor.d.ts +67 -0
  254. package/dist/processors/audio/vad_processor.js +103 -0
  255. package/dist/processors/consumer_processor.d.ts +49 -0
  256. package/dist/processors/consumer_processor.js +85 -0
  257. package/dist/processors/filters/frame_filter.d.ts +39 -0
  258. package/dist/processors/filters/frame_filter.js +52 -0
  259. package/dist/processors/filters/function_filter.d.ts +59 -0
  260. package/dist/processors/filters/function_filter.js +78 -0
  261. package/dist/processors/filters/identity_filter.d.ts +33 -0
  262. package/dist/processors/filters/identity_filter.js +37 -0
  263. package/dist/processors/filters/index.d.ts +2 -0
  264. package/dist/processors/filters/index.js +8 -0
  265. package/dist/processors/filters/null_filter.d.ts +34 -0
  266. package/dist/processors/filters/null_filter.js +47 -0
  267. package/dist/processors/filters/wake_notifier_filter.d.ts +48 -0
  268. package/dist/processors/filters/wake_notifier_filter.js +49 -0
  269. package/dist/processors/frame_processor.d.ts +360 -0
  270. package/dist/processors/frame_processor.js +932 -0
  271. package/dist/processors/frameworks/index.d.ts +2 -0
  272. package/dist/processors/frameworks/index.js +8 -0
  273. package/dist/processors/frameworks/rtvi/frames.d.ts +241 -0
  274. package/dist/processors/frameworks/rtvi/frames.js +259 -0
  275. package/dist/processors/frameworks/rtvi/index.d.ts +6 -0
  276. package/dist/processors/frameworks/rtvi/index.js +11 -0
  277. package/dist/processors/frameworks/rtvi/models.d.ts +1741 -0
  278. package/dist/processors/frameworks/rtvi/models.js +1807 -0
  279. package/dist/processors/frameworks/rtvi/observer.d.ts +340 -0
  280. package/dist/processors/frameworks/rtvi/observer.js +1113 -0
  281. package/dist/processors/frameworks/rtvi/processor.d.ts +182 -0
  282. package/dist/processors/frameworks/rtvi/processor.js +676 -0
  283. package/dist/processors/idle_frame_processor.d.ts +44 -0
  284. package/dist/processors/idle_frame_processor.js +86 -0
  285. package/dist/processors/index.d.ts +2 -0
  286. package/dist/processors/index.js +8 -0
  287. package/dist/processors/logger.d.ts +42 -0
  288. package/dist/processors/logger.js +87 -0
  289. package/dist/processors/metrics/frame_processor_metrics.d.ts +172 -0
  290. package/dist/processors/metrics/frame_processor_metrics.js +372 -0
  291. package/dist/processors/metrics/index.d.ts +2 -0
  292. package/dist/processors/metrics/index.js +8 -0
  293. package/dist/processors/producer_processor.d.ts +68 -0
  294. package/dist/processors/producer_processor.js +78 -0
  295. package/dist/processors/text_transformer.d.ts +30 -0
  296. package/dist/processors/text_transformer.js +48 -0
  297. package/dist/registry/index.d.ts +3 -0
  298. package/dist/registry/index.js +8 -0
  299. package/dist/registry/registry.d.ts +69 -0
  300. package/dist/registry/registry.js +149 -0
  301. package/dist/registry/types.d.ts +47 -0
  302. package/dist/registry/types.js +39 -0
  303. package/dist/serializers/base_serializer.d.ts +94 -0
  304. package/dist/serializers/base_serializer.js +82 -0
  305. package/dist/serializers/index.d.ts +2 -0
  306. package/dist/serializers/index.js +8 -0
  307. package/dist/services/ai_service.d.ts +145 -0
  308. package/dist/services/ai_service.js +262 -0
  309. package/dist/services/anthropic/index.d.ts +2 -0
  310. package/dist/services/anthropic/index.js +7 -0
  311. package/dist/services/assemblyai/index.d.ts +2 -0
  312. package/dist/services/assemblyai/index.js +7 -0
  313. package/dist/services/asyncai/index.d.ts +2 -0
  314. package/dist/services/asyncai/index.js +7 -0
  315. package/dist/services/aws/index.d.ts +2 -0
  316. package/dist/services/aws/index.js +7 -0
  317. package/dist/services/azure/index.d.ts +2 -0
  318. package/dist/services/azure/index.js +7 -0
  319. package/dist/services/baseten/index.d.ts +2 -0
  320. package/dist/services/baseten/index.js +7 -0
  321. package/dist/services/bland/index.d.ts +2 -0
  322. package/dist/services/bland/index.js +7 -0
  323. package/dist/services/camb/index.d.ts +2 -0
  324. package/dist/services/camb/index.js +7 -0
  325. package/dist/services/cartesia/index.d.ts +2 -0
  326. package/dist/services/cartesia/index.js +7 -0
  327. package/dist/services/cerebras/index.d.ts +2 -0
  328. package/dist/services/cerebras/index.js +7 -0
  329. package/dist/services/crusoe/index.d.ts +2 -0
  330. package/dist/services/crusoe/index.js +7 -0
  331. package/dist/services/deepgram/index.d.ts +2 -0
  332. package/dist/services/deepgram/index.js +7 -0
  333. package/dist/services/deepseek/index.d.ts +2 -0
  334. package/dist/services/deepseek/index.js +7 -0
  335. package/dist/services/elevenlabs/index.d.ts +2 -0
  336. package/dist/services/elevenlabs/index.js +7 -0
  337. package/dist/services/fal/index.d.ts +2 -0
  338. package/dist/services/fal/index.js +7 -0
  339. package/dist/services/fireworks/index.d.ts +2 -0
  340. package/dist/services/fireworks/index.js +7 -0
  341. package/dist/services/fish/index.d.ts +2 -0
  342. package/dist/services/fish/index.js +7 -0
  343. package/dist/services/funasr/index.d.ts +3 -0
  344. package/dist/services/funasr/index.js +8 -0
  345. package/dist/services/gladia/index.d.ts +2 -0
  346. package/dist/services/gladia/index.js +7 -0
  347. package/dist/services/google/index.d.ts +2 -0
  348. package/dist/services/google/index.js +7 -0
  349. package/dist/services/gradium/index.d.ts +2 -0
  350. package/dist/services/gradium/index.js +7 -0
  351. package/dist/services/grok/index.d.ts +2 -0
  352. package/dist/services/grok/index.js +7 -0
  353. package/dist/services/groq/index.d.ts +2 -0
  354. package/dist/services/groq/index.js +7 -0
  355. package/dist/services/heygen/index.d.ts +2 -0
  356. package/dist/services/heygen/index.js +7 -0
  357. package/dist/services/hume/index.d.ts +2 -0
  358. package/dist/services/hume/index.js +7 -0
  359. package/dist/services/image_service.d.ts +52 -0
  360. package/dist/services/image_service.js +60 -0
  361. package/dist/services/inception/index.d.ts +2 -0
  362. package/dist/services/inception/index.js +7 -0
  363. package/dist/services/index.d.ts +2 -0
  364. package/dist/services/index.js +8 -0
  365. package/dist/services/inworld/index.d.ts +2 -0
  366. package/dist/services/inworld/index.js +7 -0
  367. package/dist/services/keenable/index.d.ts +2 -0
  368. package/dist/services/keenable/index.js +7 -0
  369. package/dist/services/kokoro/index.d.ts +2 -0
  370. package/dist/services/kokoro/index.js +7 -0
  371. package/dist/services/llm_service.d.ts +1106 -0
  372. package/dist/services/llm_service.js +2231 -0
  373. package/dist/services/mem0/index.d.ts +2 -0
  374. package/dist/services/mem0/index.js +7 -0
  375. package/dist/services/meta/index.d.ts +2 -0
  376. package/dist/services/meta/index.js +7 -0
  377. package/dist/services/minimax/index.d.ts +2 -0
  378. package/dist/services/minimax/index.js +7 -0
  379. package/dist/services/mistral/index.d.ts +2 -0
  380. package/dist/services/mistral/index.js +7 -0
  381. package/dist/services/moondream/index.d.ts +2 -0
  382. package/dist/services/moondream/index.js +7 -0
  383. package/dist/services/moonshine/index.d.ts +2 -0
  384. package/dist/services/moonshine/index.js +7 -0
  385. package/dist/services/nebius/index.d.ts +2 -0
  386. package/dist/services/nebius/index.js +7 -0
  387. package/dist/services/neuphonic/index.d.ts +2 -0
  388. package/dist/services/neuphonic/index.js +7 -0
  389. package/dist/services/novita/index.d.ts +2 -0
  390. package/dist/services/novita/index.js +7 -0
  391. package/dist/services/nvidia/index.d.ts +2 -0
  392. package/dist/services/nvidia/index.js +11 -0
  393. package/dist/services/ollama/index.d.ts +2 -0
  394. package/dist/services/ollama/index.js +7 -0
  395. package/dist/services/openai/index.d.ts +2 -0
  396. package/dist/services/openai/index.js +7 -0
  397. package/dist/services/openclaw/index.d.ts +14 -0
  398. package/dist/services/openclaw/index.js +19 -0
  399. package/dist/services/openrouter/index.d.ts +2 -0
  400. package/dist/services/openrouter/index.js +7 -0
  401. package/dist/services/perplexity/index.d.ts +2 -0
  402. package/dist/services/perplexity/index.js +7 -0
  403. package/dist/services/piper/index.d.ts +2 -0
  404. package/dist/services/piper/index.js +7 -0
  405. package/dist/services/pocket_tts/index.d.ts +2 -0
  406. package/dist/services/pocket_tts/index.js +7 -0
  407. package/dist/services/qwen/index.d.ts +2 -0
  408. package/dist/services/qwen/index.js +7 -0
  409. package/dist/services/resembleai/index.d.ts +2 -0
  410. package/dist/services/resembleai/index.js +7 -0
  411. package/dist/services/rime/index.d.ts +2 -0
  412. package/dist/services/rime/index.js +7 -0
  413. package/dist/services/sambanova/index.d.ts +2 -0
  414. package/dist/services/sambanova/index.js +7 -0
  415. package/dist/services/sarvam/index.d.ts +2 -0
  416. package/dist/services/sarvam/index.js +7 -0
  417. package/dist/services/settings.d.ts +356 -0
  418. package/dist/services/settings.js +437 -0
  419. package/dist/services/simli/index.d.ts +2 -0
  420. package/dist/services/simli/index.js +7 -0
  421. package/dist/services/smallest/index.d.ts +2 -0
  422. package/dist/services/smallest/index.js +7 -0
  423. package/dist/services/soniox/index.d.ts +2 -0
  424. package/dist/services/soniox/index.js +7 -0
  425. package/dist/services/speechify/index.d.ts +2 -0
  426. package/dist/services/speechify/index.js +7 -0
  427. package/dist/services/speechmatics/index.d.ts +2 -0
  428. package/dist/services/speechmatics/index.js +7 -0
  429. package/dist/services/stt_latency.d.ts +62 -0
  430. package/dist/services/stt_latency.js +71 -0
  431. package/dist/services/stt_service.d.ts +601 -0
  432. package/dist/services/stt_service.js +1044 -0
  433. package/dist/services/tavus/index.d.ts +2 -0
  434. package/dist/services/tavus/index.js +7 -0
  435. package/dist/services/together/index.d.ts +2 -0
  436. package/dist/services/together/index.js +7 -0
  437. package/dist/services/tts_service.d.ts +909 -0
  438. package/dist/services/tts_service.js +2018 -0
  439. package/dist/services/ultravox/index.d.ts +2 -0
  440. package/dist/services/ultravox/index.js +9 -0
  441. package/dist/services/vision_service.d.ts +56 -0
  442. package/dist/services/vision_service.js +62 -0
  443. package/dist/services/websocket_service.d.ts +213 -0
  444. package/dist/services/websocket_service.js +654 -0
  445. package/dist/services/whisper/index.d.ts +2 -0
  446. package/dist/services/whisper/index.js +9 -0
  447. package/dist/services/xai/index.d.ts +2 -0
  448. package/dist/services/xai/index.js +9 -0
  449. package/dist/services/xtts/index.d.ts +2 -0
  450. package/dist/services/xtts/index.js +9 -0
  451. package/dist/tests/index.d.ts +2 -0
  452. package/dist/tests/index.js +8 -0
  453. package/dist/tests/utils.d.ts +123 -0
  454. package/dist/tests/utils.js +212 -0
  455. package/dist/transcriptions/index.d.ts +2 -0
  456. package/dist/transcriptions/index.js +8 -0
  457. package/dist/transcriptions/language.d.ts +376 -0
  458. package/dist/transcriptions/language.js +543 -0
  459. package/dist/transports/base_input.d.ts +149 -0
  460. package/dist/transports/base_input.js +338 -0
  461. package/dist/transports/base_output.d.ts +432 -0
  462. package/dist/transports/base_output.js +1231 -0
  463. package/dist/transports/base_transport.d.ts +223 -0
  464. package/dist/transports/base_transport.js +167 -0
  465. package/dist/transports/daily/index.d.ts +2 -0
  466. package/dist/transports/daily/index.js +9 -0
  467. package/dist/transports/heygen/index.d.ts +2 -0
  468. package/dist/transports/heygen/index.js +9 -0
  469. package/dist/transports/index.d.ts +2 -0
  470. package/dist/transports/index.js +8 -0
  471. package/dist/transports/lemonslice/index.d.ts +2 -0
  472. package/dist/transports/lemonslice/index.js +9 -0
  473. package/dist/transports/livekit/index.d.ts +2 -0
  474. package/dist/transports/livekit/index.js +9 -0
  475. package/dist/transports/local/index.d.ts +2 -0
  476. package/dist/transports/local/index.js +9 -0
  477. package/dist/transports/moq/index.d.ts +2 -0
  478. package/dist/transports/moq/index.js +9 -0
  479. package/dist/transports/smallwebrtc/index.d.ts +2 -0
  480. package/dist/transports/smallwebrtc/index.js +9 -0
  481. package/dist/transports/tavus/index.d.ts +2 -0
  482. package/dist/transports/tavus/index.js +9 -0
  483. package/dist/transports/video_in_sampler.d.ts +157 -0
  484. package/dist/transports/video_in_sampler.js +258 -0
  485. package/dist/transports/vonage/index.d.ts +2 -0
  486. package/dist/transports/vonage/index.js +9 -0
  487. package/dist/transports/websocket/index.d.ts +2 -0
  488. package/dist/transports/websocket/index.js +9 -0
  489. package/dist/transports/whatsapp/index.d.ts +2 -0
  490. package/dist/transports/whatsapp/index.js +9 -0
  491. package/dist/turns/eager_end_of_turn_mixin.d.ts +70 -0
  492. package/dist/turns/eager_end_of_turn_mixin.js +122 -0
  493. package/dist/turns/empty_user_turn.d.ts +55 -0
  494. package/dist/turns/empty_user_turn.js +53 -0
  495. package/dist/turns/index.d.ts +2 -0
  496. package/dist/turns/index.js +8 -0
  497. package/dist/turns/speculation_gate.d.ts +169 -0
  498. package/dist/turns/speculation_gate.js +294 -0
  499. package/dist/turns/types.d.ts +33 -0
  500. package/dist/turns/types.js +35 -0
  501. package/dist/turns/user_idle_controller.d.ts +94 -0
  502. package/dist/turns/user_idle_controller.js +187 -0
  503. package/dist/turns/user_mute/always_user_mute_strategy.d.ts +15 -0
  504. package/dist/turns/user_mute/always_user_mute_strategy.js +29 -0
  505. package/dist/turns/user_mute/base_user_mute_strategy.d.ts +39 -0
  506. package/dist/turns/user_mute/base_user_mute_strategy.js +45 -0
  507. package/dist/turns/user_mute/first_speech_user_mute_strategy.d.ts +28 -0
  508. package/dist/turns/user_mute/first_speech_user_mute_strategy.js +52 -0
  509. package/dist/turns/user_mute/function_call_user_mute_strategy.d.ts +25 -0
  510. package/dist/turns/user_mute/function_call_user_mute_strategy.js +50 -0
  511. package/dist/turns/user_mute/index.d.ts +6 -0
  512. package/dist/turns/user_mute/index.js +11 -0
  513. package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.d.ts +43 -0
  514. package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.js +76 -0
  515. package/dist/turns/user_start/base_user_turn_start_strategy.d.ts +135 -0
  516. package/dist/turns/user_start/base_user_turn_start_strategy.js +134 -0
  517. package/dist/turns/user_start/external_user_turn_start_strategy.d.ts +45 -0
  518. package/dist/turns/user_start/external_user_turn_start_strategy.js +64 -0
  519. package/dist/turns/user_start/index.d.ts +8 -0
  520. package/dist/turns/user_start/index.js +16 -0
  521. package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.d.ts +133 -0
  522. package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.js +243 -0
  523. package/dist/turns/user_start/min_words_user_turn_start_strategy.d.ts +60 -0
  524. package/dist/turns/user_start/min_words_user_turn_start_strategy.js +102 -0
  525. package/dist/turns/user_start/transcription_user_turn_start_strategy.d.ts +29 -0
  526. package/dist/turns/user_start/transcription_user_turn_start_strategy.js +43 -0
  527. package/dist/turns/user_start/vad_user_turn_start_strategy.d.ts +20 -0
  528. package/dist/turns/user_start/vad_user_turn_start_strategy.js +31 -0
  529. package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.d.ts +141 -0
  530. package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.js +238 -0
  531. package/dist/turns/user_stop/base_user_turn_stop_strategy.d.ts +174 -0
  532. package/dist/turns/user_stop/base_user_turn_stop_strategy.js +170 -0
  533. package/dist/turns/user_stop/deferred_user_turn_stop_strategy.d.ts +81 -0
  534. package/dist/turns/user_stop/deferred_user_turn_stop_strategy.js +107 -0
  535. package/dist/turns/user_stop/eager_match_policy.d.ts +36 -0
  536. package/dist/turns/user_stop/eager_match_policy.js +50 -0
  537. package/dist/turns/user_stop/eager_user_turn_stop_strategy.d.ts +84 -0
  538. package/dist/turns/user_stop/eager_user_turn_stop_strategy.js +154 -0
  539. package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.d.ts +45 -0
  540. package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.js +55 -0
  541. package/dist/turns/user_stop/external_user_turn_stop_strategy.d.ts +115 -0
  542. package/dist/turns/user_stop/external_user_turn_stop_strategy.js +205 -0
  543. package/dist/turns/user_stop/index.d.ts +10 -0
  544. package/dist/turns/user_stop/index.js +15 -0
  545. package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.d.ts +44 -0
  546. package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.js +59 -0
  547. package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.d.ts +140 -0
  548. package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.js +303 -0
  549. package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.d.ts +135 -0
  550. package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.js +332 -0
  551. package/dist/turns/user_turn_completion_mixin.d.ts +147 -0
  552. package/dist/turns/user_turn_completion_mixin.js +591 -0
  553. package/dist/turns/user_turn_controller.d.ts +134 -0
  554. package/dist/turns/user_turn_controller.js +335 -0
  555. package/dist/turns/user_turn_processor.d.ts +87 -0
  556. package/dist/turns/user_turn_processor.js +180 -0
  557. package/dist/turns/user_turn_strategies.d.ts +187 -0
  558. package/dist/turns/user_turn_strategies.js +222 -0
  559. package/dist/utils/async_tool_cancellation.d.ts +35 -0
  560. package/dist/utils/async_tool_cancellation.js +78 -0
  561. package/dist/utils/asyncio/index.d.ts +2 -0
  562. package/dist/utils/asyncio/index.js +7 -0
  563. package/dist/utils/asyncio/task_manager.d.ts +49 -0
  564. package/dist/utils/asyncio/task_manager.js +105 -0
  565. package/dist/utils/base_object.d.ts +76 -0
  566. package/dist/utils/base_object.js +154 -0
  567. package/dist/utils/context/aggregated_frame_sequencer.d.ts +491 -0
  568. package/dist/utils/context/aggregated_frame_sequencer.js +803 -0
  569. package/dist/utils/context/audio_context_manager.d.ts +131 -0
  570. package/dist/utils/context/audio_context_manager.js +233 -0
  571. package/dist/utils/context/index.d.ts +2 -0
  572. package/dist/utils/context/index.js +7 -0
  573. package/dist/utils/context/llm_context_summarization.d.ts +258 -0
  574. package/dist/utils/context/llm_context_summarization.js +511 -0
  575. package/dist/utils/context/text_segment_map.d.ts +611 -0
  576. package/dist/utils/context/text_segment_map.js +1132 -0
  577. package/dist/utils/context/word_completion_tracker.d.ts +273 -0
  578. package/dist/utils/context/word_completion_tracker.js +441 -0
  579. package/dist/utils/context/word_timestamp_tracker.d.ts +72 -0
  580. package/dist/utils/context/word_timestamp_tracker.js +98 -0
  581. package/dist/utils/deprecation.d.ts +28 -0
  582. package/dist/utils/deprecation.js +75 -0
  583. package/dist/utils/env.d.ts +38 -0
  584. package/dist/utils/env.js +62 -0
  585. package/dist/utils/errors.d.ts +75 -0
  586. package/dist/utils/errors.js +202 -0
  587. package/dist/utils/file_resolver.d.ts +126 -0
  588. package/dist/utils/file_resolver.js +314 -0
  589. package/dist/utils/file_storage.d.ts +115 -0
  590. package/dist/utils/file_storage.js +178 -0
  591. package/dist/utils/frame_queue.d.ts +36 -0
  592. package/dist/utils/frame_queue.js +66 -0
  593. package/dist/utils/http.d.ts +39 -0
  594. package/dist/utils/http.js +53 -0
  595. package/dist/utils/image.d.ts +14 -0
  596. package/dist/utils/image.js +63 -0
  597. package/dist/utils/index.d.ts +2 -0
  598. package/dist/utils/index.js +8 -0
  599. package/dist/utils/jpeg_encoder.d.ts +14 -0
  600. package/dist/utils/jpeg_encoder.js +315 -0
  601. package/dist/utils/log_config.d.ts +34 -0
  602. package/dist/utils/log_config.js +76 -0
  603. package/dist/utils/logger.d.ts +40 -0
  604. package/dist/utils/logger.js +91 -0
  605. package/dist/utils/mixin.d.ts +14 -0
  606. package/dist/utils/mixin.js +63 -0
  607. package/dist/utils/network.d.ts +73 -0
  608. package/dist/utils/network.js +90 -0
  609. package/dist/utils/security/allowed_origins.d.ts +16 -0
  610. package/dist/utils/security/allowed_origins.js +34 -0
  611. package/dist/utils/security/index.d.ts +2 -0
  612. package/dist/utils/security/index.js +7 -0
  613. package/dist/utils/security/redaction.d.ts +27 -0
  614. package/dist/utils/security/redaction.js +123 -0
  615. package/dist/utils/security/ssrf.d.ts +79 -0
  616. package/dist/utils/security/ssrf.js +327 -0
  617. package/dist/utils/shared.d.ts +35 -0
  618. package/dist/utils/shared.js +136 -0
  619. package/dist/utils/startup.d.ts +17 -0
  620. package/dist/utils/startup.js +130 -0
  621. package/dist/utils/string.d.ts +116 -0
  622. package/dist/utils/string.js +460 -0
  623. package/dist/utils/sync/base_notifier.d.ts +25 -0
  624. package/dist/utils/sync/base_notifier.js +16 -0
  625. package/dist/utils/sync/event_notifier.d.ts +38 -0
  626. package/dist/utils/sync/event_notifier.js +51 -0
  627. package/dist/utils/sync/index.d.ts +2 -0
  628. package/dist/utils/sync/index.js +7 -0
  629. package/dist/utils/text/alnum_utils.d.ts +109 -0
  630. package/dist/utils/text/alnum_utils.js +262 -0
  631. package/dist/utils/text/base_text_aggregator.d.ts +146 -0
  632. package/dist/utils/text/base_text_aggregator.js +102 -0
  633. package/dist/utils/text/base_text_filter.d.ts +54 -0
  634. package/dist/utils/text/base_text_filter.js +50 -0
  635. package/dist/utils/text/index.d.ts +2 -0
  636. package/dist/utils/text/index.js +7 -0
  637. package/dist/utils/text/markdown_text_filter.d.ts +147 -0
  638. package/dist/utils/text/markdown_text_filter.js +522 -0
  639. package/dist/utils/text/markup_utils.d.ts +54 -0
  640. package/dist/utils/text/markup_utils.js +156 -0
  641. package/dist/utils/text/pattern_pair_aggregator.d.ts +251 -0
  642. package/dist/utils/text/pattern_pair_aggregator.js +478 -0
  643. package/dist/utils/text/phonemes.d.ts +60 -0
  644. package/dist/utils/text/phonemes.js +172 -0
  645. package/dist/utils/text/simple_text_aggregator.d.ts +135 -0
  646. package/dist/utils/text/simple_text_aggregator.js +254 -0
  647. package/dist/utils/text/skip_tags_aggregator.d.ts +85 -0
  648. package/dist/utils/text/skip_tags_aggregator.js +147 -0
  649. package/dist/utils/text/transforms/acronyms.d.ts +21 -0
  650. package/dist/utils/text/transforms/acronyms.js +31 -0
  651. package/dist/utils/text/transforms/currency.d.ts +16 -0
  652. package/dist/utils/text/transforms/currency.js +56 -0
  653. package/dist/utils/text/transforms/dates.d.ts +18 -0
  654. package/dist/utils/text/transforms/dates.js +102 -0
  655. package/dist/utils/text/transforms/email.d.ts +17 -0
  656. package/dist/utils/text/transforms/email.js +35 -0
  657. package/dist/utils/text/transforms/index.d.ts +37 -0
  658. package/dist/utils/text/transforms/index.js +42 -0
  659. package/dist/utils/text/transforms/numbers.d.ts +25 -0
  660. package/dist/utils/text/transforms/numbers.js +59 -0
  661. package/dist/utils/text/transforms/percentages.d.ts +16 -0
  662. package/dist/utils/text/transforms/percentages.js +32 -0
  663. package/dist/utils/text/transforms/phone.d.ts +21 -0
  664. package/dist/utils/text/transforms/phone.js +39 -0
  665. package/dist/utils/text/transforms/pronunciations.d.ts +65 -0
  666. package/dist/utils/text/transforms/pronunciations.js +112 -0
  667. package/dist/utils/text/transforms/replacements.d.ts +39 -0
  668. package/dist/utils/text/transforms/replacements.js +46 -0
  669. package/dist/utils/text/transforms/strip_markdown.d.ts +25 -0
  670. package/dist/utils/text/transforms/strip_markdown.js +60 -0
  671. package/dist/utils/text/transforms/units.d.ts +22 -0
  672. package/dist/utils/text/transforms/units.js +81 -0
  673. package/dist/utils/text/transforms/voice_formatter.d.ts +86 -0
  674. package/dist/utils/text/transforms/voice_formatter.js +98 -0
  675. package/dist/utils/text/unicode_digits.d.ts +3 -0
  676. package/dist/utils/text/unicode_digits.js +34 -0
  677. package/dist/utils/text/word_timestamp_utils.d.ts +26 -0
  678. package/dist/utils/text/word_timestamp_utils.js +52 -0
  679. package/dist/utils/time.d.ts +13 -0
  680. package/dist/utils/time.js +31 -0
  681. package/dist/utils/tracing/index.d.ts +3 -0
  682. package/dist/utils/tracing/index.js +8 -0
  683. package/dist/utils/tracing/service_attributes.d.ts +198 -0
  684. package/dist/utils/tracing/service_attributes.js +434 -0
  685. package/dist/utils/tracing/service_decorators.d.ts +214 -0
  686. package/dist/utils/tracing/service_decorators.js +1662 -0
  687. package/dist/utils/tracing/setup.d.ts +107 -0
  688. package/dist/utils/tracing/setup.js +160 -0
  689. package/dist/utils/tracing/tracing_context.d.ts +55 -0
  690. package/dist/utils/tracing/tracing_context.js +97 -0
  691. package/dist/utils/tracing/turn_trace_observer.d.ts +95 -0
  692. package/dist/utils/tracing/turn_trace_observer.js +233 -0
  693. package/dist/utils/types.d.ts +80 -0
  694. package/dist/utils/types.js +108 -0
  695. package/dist/utils/utils.d.ts +5 -0
  696. package/dist/utils/utils.js +22 -0
  697. package/dist/utils/yaml.d.ts +179 -0
  698. package/dist/utils/yaml.js +556 -0
  699. package/dist/version.d.ts +3 -0
  700. package/dist/version.js +7 -0
  701. package/dist/workers/base_worker.d.ts +772 -0
  702. package/dist/workers/base_worker.js +1298 -0
  703. package/dist/workers/index.d.ts +2 -0
  704. package/dist/workers/index.js +8 -0
  705. package/dist/workers/llm/backend_llm_worker.d.ts +552 -0
  706. package/dist/workers/llm/backend_llm_worker.js +1055 -0
  707. package/dist/workers/llm/index.d.ts +6 -0
  708. package/dist/workers/llm/index.js +11 -0
  709. package/dist/workers/llm/llm_context_worker.d.ts +66 -0
  710. package/dist/workers/llm/llm_context_worker.js +72 -0
  711. package/dist/workers/llm/llm_worker.d.ts +180 -0
  712. package/dist/workers/llm/llm_worker.js +323 -0
  713. package/dist/workers/llm/tool_decorator.d.ts +68 -0
  714. package/dist/workers/llm/tool_decorator.js +60 -0
  715. package/dist/workers/proxy/index.d.ts +3 -0
  716. package/dist/workers/proxy/index.js +9 -0
  717. package/dist/workers/proxy/websocket/client.d.ts +110 -0
  718. package/dist/workers/proxy/websocket/client.js +196 -0
  719. package/dist/workers/proxy/websocket/index.d.ts +3 -0
  720. package/dist/workers/proxy/websocket/index.js +10 -0
  721. package/dist/workers/runner.d.ts +268 -0
  722. package/dist/workers/runner.js +489 -0
  723. package/dist/workers/ui/index.d.ts +22 -0
  724. package/dist/workers/ui/index.js +27 -0
  725. package/dist/workers/ui/ui_event_decorator.d.ts +43 -0
  726. package/dist/workers/ui/ui_event_decorator.js +75 -0
  727. package/dist/workers/ui/ui_job_context.d.ts +12 -0
  728. package/dist/workers/ui/ui_job_context.js +17 -0
  729. package/dist/workers/ui/ui_prompts.d.ts +21 -0
  730. package/dist/workers/ui/ui_prompts.js +41 -0
  731. package/dist/workers/ui/ui_tools.d.ts +32 -0
  732. package/dist/workers/ui/ui_tools.js +112 -0
  733. package/dist/workers/ui/ui_worker.d.ts +571 -0
  734. package/dist/workers/ui/ui_worker.js +1156 -0
  735. package/package.json +61 -3
  736. package/README.md +0 -5
@@ -0,0 +1,2394 @@
1
+ // Copyright (c) 2024-2026, Daily
2
+ // Copyright (c) 2026, pipedog contributors
3
+ // SPDX-License-Identifier: BSD-2-Clause
4
+ //
5
+ // Ported from pipecat: src/pipecat/processors/aggregators/llm_response_universal.py
6
+ /**
7
+ * LLM response aggregators for handling conversation context and message aggregation.
8
+ *
9
+ * This module provides aggregators that process and accumulate LLM responses,
10
+ * user inputs, and conversation context. These aggregators handle the flow
11
+ * between speech-to-text, LLM processing, and text-to-speech components in
12
+ * conversational AI pipelines.
13
+ *
14
+ * Porting notes: the deprecated `LLMUserAggregatorParams.filterIncompleteUserTurns`
15
+ * / `userTurnCompletionConfig` and `LLMAssistantAggregatorParams.enableContextSummarization`
16
+ * / `contextSummarizationConfig` fields are not ported.
17
+ */
18
+ import { rethrowIfCancelled, sleep } from "@pipedog/asyncio";
19
+ import { VADController } from "../../audio/vad/vad_controller.js";
20
+ import { AggregatedTextFrame, AssistantImageRawFrame, BotStartedSpeakingFrame, BotStoppedSpeakingFrame, CancelFrame, EagerEndOfTurnCancelFrame, EagerTranscriptionFrame, EndFrame, FunctionCallCancelFrame, FunctionCallInProgressFrame, FunctionCallResultFrame, FunctionCallsStartedFrame, InputAudioRawFrame, InterimTranscriptionFrame, InterruptionFrame, LLMAssistantPushAggregationFrame, LLMContextAssistantTimestampFrame, LLMContextAssistantTurnFrame, LLMContextFrame, LLMContextUserTurnFrame, LLMFullResponseEndFrame, LLMFullResponseStartFrame, LLMMarkerFrame, LLMMessagesAppendFrame, LLMMessagesTransformFrame, LLMMessagesUpdateFrame, LLMRunFrame, LLMServiceMetadataFrame, LLMSetToolChoiceFrame, LLMSetToolsFrame, LLMThoughtEndFrame, LLMThoughtStartFrame, LLMThoughtTextFrame, ProposedUserStartedSpeakingFrame, ProposedUserStoppedSpeakingFrame, ServiceMetadataFrame, STTMetadataFrame, StartFrame, TextFrame, TranscriptionFrame, TranslationFrame, TTSStartedFrame, TTSTextFrame, UserBackchannelFrame, UserFileRawFrame, UserImageRawFrame, UserMuteStartedFrame, UserMuteStoppedFrame, UserSpeakingFrame, UserStartedSpeakingFrame, UserStoppedSpeakingFrame, VADUserStartedSpeakingFrame, VADUserStoppedSpeakingFrame, } from "../../frames/frames.js";
21
+ import { DEFAULT_TTFS_P99 } from "../../services/stt_latency.js";
22
+ import { EmptyUserTurnConfig } from "../../turns/empty_user_turn.js";
23
+ import { UserIdleController } from "../../turns/user_idle_controller.js";
24
+ import { TranscriptionUserTurnStartStrategy } from "../../turns/user_start/transcription_user_turn_start_strategy.js";
25
+ import { USER_TURN_COMPLETE_MARKER, USER_TURN_INCOMPLETE_LONG_MARKER, USER_TURN_INCOMPLETE_SHORT_MARKER, } from "../../turns/user_turn_completion_mixin.js";
26
+ import { UserTurnController } from "../../turns/user_turn_controller.js";
27
+ import { ExternalUserTurnStrategies, UserTurnStrategies, } from "../../turns/user_turn_strategies.js";
28
+ import { logger } from "../../utils/logger.js";
29
+ import { concatenateAggregatedText, TextPartForConcatenation } from "../../utils/string.js";
30
+ import { TextType } from "../../utils/text/base_text_aggregator.js";
31
+ import { timeNowIso8601 } from "../../utils/time.js";
32
+ import { FrameDirection, FrameProcessor, } from "../frame_processor.js";
33
+ import * as asyncToolMessages from "./async_tool_messages.js";
34
+ import { isGiven, LLMContext, LLMSpecificMessage, } from "./llm_context.js";
35
+ import { LLMContextSummarizer } from "./llm_context_summarizer.js";
36
+ /**
37
+ * Python `json.dumps(value, ensure_ascii=False)`: same output as
38
+ * `JSON.stringify`, but with Python's `", "` and `": "` separators so
39
+ * contexts are identical across ports.
40
+ */
41
+ function jsonDumps(value) {
42
+ if (value === undefined)
43
+ return "null";
44
+ if (value === null || typeof value !== "object") {
45
+ return JSON.stringify(value) ?? "null";
46
+ }
47
+ const withToJson = value;
48
+ if (typeof withToJson.toJSON === "function") {
49
+ return jsonDumps(withToJson.toJSON());
50
+ }
51
+ if (Array.isArray(value)) {
52
+ return `[${value.map((v) => jsonDumps(v)).join(", ")}]`;
53
+ }
54
+ const entries = Object.entries(value)
55
+ .filter(([, v]) => v !== undefined && typeof v !== "function")
56
+ .map(([k, v]) => `${JSON.stringify(k)}: ${jsonDumps(v)}`);
57
+ return `{${entries.join(", ")}}`;
58
+ }
59
+ /** Python truthiness for a function call result (empty dicts/lists/strings are falsy). */
60
+ function isTruthy(value) {
61
+ if (value === null || value === undefined || value === false || value === 0 || value === "") {
62
+ return false;
63
+ }
64
+ if (Array.isArray(value))
65
+ return value.length > 0;
66
+ if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
67
+ return Object.keys(value).length > 0;
68
+ }
69
+ return true;
70
+ }
71
+ /** Python `repr()` of a list of strings, as used in log lines. */
72
+ function reprList(items) {
73
+ return `[${items.map((i) => `'${i}'`).join(", ")}]`;
74
+ }
75
+ /**
76
+ * Parameters for configuring LLM user aggregation behavior.
77
+ *
78
+ * See `LLMUserAggregatorParamsOptions` for the fields.
79
+ */
80
+ export class LLMUserAggregatorParams {
81
+ addToolChangeMessages;
82
+ audioIdleTimeout;
83
+ userTurnStrategies;
84
+ userMuteStrategies;
85
+ userTurnStopTimeout;
86
+ userIdleTimeout;
87
+ vadAnalyzer;
88
+ emptyUserTurn;
89
+ constructor(options = {}) {
90
+ this.addToolChangeMessages = options.addToolChangeMessages ?? false;
91
+ this.audioIdleTimeout = options.audioIdleTimeout ?? 1.0;
92
+ this.userTurnStrategies = options.userTurnStrategies ?? null;
93
+ this.userMuteStrategies = options.userMuteStrategies ?? [];
94
+ this.userTurnStopTimeout = options.userTurnStopTimeout ?? 5.0;
95
+ this.userIdleTimeout = options.userIdleTimeout ?? 0;
96
+ this.vadAnalyzer = options.vadAnalyzer ?? null;
97
+ this.emptyUserTurn =
98
+ options.emptyUserTurn === undefined ? new EmptyUserTurnConfig() : options.emptyUserTurn;
99
+ }
100
+ }
101
+ /**
102
+ * Parameters for configuring LLM assistant aggregation behavior.
103
+ *
104
+ * See `LLMAssistantAggregatorParamsOptions` for the fields.
105
+ */
106
+ export class LLMAssistantAggregatorParams {
107
+ enableAutoContextSummarization;
108
+ autoContextSummarizationConfig;
109
+ addToolChangeMessages;
110
+ constructor(options = {}) {
111
+ this.enableAutoContextSummarization = options.enableAutoContextSummarization ?? false;
112
+ this.autoContextSummarizationConfig = options.autoContextSummarizationConfig ?? null;
113
+ this.addToolChangeMessages = options.addToolChangeMessages ?? false;
114
+ }
115
+ }
116
+ /**
117
+ * A user turn stopped message containing a user transcript update.
118
+ *
119
+ * A message in a conversation transcript containing the user content. This
120
+ * is the aggregated transcript that is then used in the context.
121
+ *
122
+ * - `content`: The message content/text. `null` in realtime mode
123
+ * (`realtimeServiceMode: true`) when fired from a user-turn-stop frame: in
124
+ * realtime mode the user message isn't finalized until the assistant
125
+ * response start acts as the effective end-of-turn signal. Subscribers that
126
+ * need the finalized text should listen to `on_user_turn_message_added`
127
+ * instead.
128
+ * - `timestamp`: When the user turn started.
129
+ * - `userId`: Optional identifier for the user.
130
+ */
131
+ export class UserTurnStoppedMessage {
132
+ content;
133
+ timestamp;
134
+ userId;
135
+ constructor(options) {
136
+ this.content = options.content;
137
+ this.timestamp = options.timestamp;
138
+ this.userId = options.userId ?? null;
139
+ }
140
+ }
141
+ /**
142
+ * A message accompanying `on_user_turn_message_added`.
143
+ *
144
+ * Fired when a user message is written to the LLM context. In cascade mode
145
+ * (`realtimeServiceMode: false`) this coincides with `on_user_turn_stopped`.
146
+ * In realtime mode (`realtimeServiceMode: true`) the write is triggered by
147
+ * the assistant response start (or session end), so this event fires
148
+ * decoupled from user-turn-end frames. `content` is always populated.
149
+ *
150
+ * - `content`: The aggregated user transcript.
151
+ * - `timestamp`: When the user turn started.
152
+ * - `userId`: Optional identifier for the user.
153
+ */
154
+ export class UserTurnMessageAddedMessage {
155
+ content;
156
+ timestamp;
157
+ userId;
158
+ constructor(options) {
159
+ this.content = options.content;
160
+ this.timestamp = options.timestamp;
161
+ this.userId = options.userId ?? null;
162
+ }
163
+ }
164
+ /**
165
+ * A message accompanying `on_user_backchannel`.
166
+ *
167
+ * A short acknowledgment the user said while the bot talks, such as "mhm",
168
+ * which stays out of the LLM context.
169
+ *
170
+ * - `content`: What the user said.
171
+ * - `timestamp`: When the user said it.
172
+ * - `userId`: Optional identifier for the user.
173
+ */
174
+ export class UserBackchannelMessage {
175
+ content;
176
+ timestamp;
177
+ userId;
178
+ constructor(options) {
179
+ this.content = options.content;
180
+ this.timestamp = options.timestamp;
181
+ this.userId = options.userId ?? null;
182
+ }
183
+ }
184
+ /**
185
+ * An assistant turn stopped message containing an assistant transcript update.
186
+ *
187
+ * A message in a conversation transcript containing the assistant content.
188
+ * This is the aggregated transcript that is then used in the context.
189
+ *
190
+ * - `content`: The message content/text. May be empty if the LLM returned
191
+ * zero tokens (e.g. turn was interrupted before any tokens were received or
192
+ * pushed).
193
+ * - `interrupted`: Whether the assistant turn was interrupted.
194
+ * - `timestamp`: When the assistant turn started.
195
+ */
196
+ export class AssistantTurnStoppedMessage {
197
+ content;
198
+ interrupted;
199
+ timestamp;
200
+ constructor(options) {
201
+ this.content = options.content;
202
+ this.interrupted = options.interrupted;
203
+ this.timestamp = options.timestamp;
204
+ }
205
+ }
206
+ /**
207
+ * A message accompanying `on_assistant_backchannel`.
208
+ *
209
+ * A short acknowledgment the assistant said while the user talks, such as
210
+ * "Mm-hmm.".
211
+ *
212
+ * - `content`: What the assistant said.
213
+ * - `timestamp`: When the assistant started saying it.
214
+ */
215
+ export class AssistantBackchannelMessage {
216
+ content;
217
+ timestamp;
218
+ constructor(options) {
219
+ this.content = options.content;
220
+ this.timestamp = options.timestamp;
221
+ }
222
+ }
223
+ /**
224
+ * An assistant thought message containing an assistant thought update.
225
+ *
226
+ * A message in a conversation transcript containing the assistant thought
227
+ * content.
228
+ *
229
+ * - `content`: The message content/text.
230
+ * - `timestamp`: When the thought started.
231
+ */
232
+ export class AssistantThoughtMessage {
233
+ content;
234
+ timestamp;
235
+ constructor(options) {
236
+ this.content = options.content;
237
+ this.timestamp = options.timestamp;
238
+ }
239
+ }
240
+ /**
241
+ * Base LLM aggregator that uses an LLMContext for conversation storage.
242
+ *
243
+ * This aggregator maintains conversation state using an LLMContext and pushes
244
+ * LLMContextFrame objects as aggregation frames. It provides common
245
+ * functionality for context-based conversation management.
246
+ */
247
+ export class LLMContextAggregator extends FrameProcessor {
248
+ // Developer-role messages appended to the context when tools are added/
249
+ // removed via `LLMSetToolsFrame` (only when `addToolChangeMessages` is
250
+ // enabled on the aggregator's params). `{function_names}` is substituted
251
+ // with a sorted, comma-separated, backtick-wrapped list.
252
+ static TOOL_ACTIVATION_MESSAGE_TEMPLATE = "The following function(s) have just been added and may now be called: " +
253
+ "{function_names}. Any previously available functions remain available.";
254
+ static TOOL_DEACTIVATION_MESSAGE_TEMPLATE = "The following function(s) have just been removed and should not be called: " +
255
+ "{function_names}. Any previously available functions remain available. " +
256
+ "The removed function(s) may become available again later, in which case " +
257
+ "you will be informed.";
258
+ _context;
259
+ _role;
260
+ _addToolChangeMessages;
261
+ _aggregation = [];
262
+ /** Initialize the context response aggregator. */
263
+ constructor(options) {
264
+ super(options);
265
+ this._context = options.context;
266
+ this._role = options.role;
267
+ this._addToolChangeMessages = options.addToolChangeMessages ?? false;
268
+ }
269
+ /**
270
+ * Append a developer message describing tool add/remove deltas.
271
+ *
272
+ * No-op unless `addToolChangeMessages` was enabled on the aggregator, and
273
+ * no-op when the diff against the currently advertised tools is empty.
274
+ * Custom (LLM-specific) tools are ignored — only standard tools are diffed.
275
+ *
276
+ * Both aggregators call this on every `LLMSetToolsFrame` they handle.
277
+ * Whichever aggregator handles the frame first computes a real diff against
278
+ * the shared context and adds the announcement; by the time the other
279
+ * aggregator sees it (if at all), the context already reflects the new
280
+ * tools, so its diff is empty and no duplicate message is added. This is
281
+ * order-independent: it works whether the frame flows downstream (user
282
+ * aggregator first) or upstream (assistant aggregator first, and consumed
283
+ * without being forwarded).
284
+ */
285
+ _maybeAddToolChangeMessages(newTools) {
286
+ if (!this._addToolChangeMessages) {
287
+ return;
288
+ }
289
+ const names = (tools) => {
290
+ if (!isGiven(tools)) {
291
+ return new Set();
292
+ }
293
+ return new Set(tools.standardTools.map((s) => s.name));
294
+ };
295
+ const oldNames = names(this._context.tools);
296
+ const newNames = names(newTools);
297
+ const added = [...newNames].filter((n) => !oldNames.has(n));
298
+ const removed = [...oldNames].filter((n) => !newNames.has(n));
299
+ if (added.length === 0 && removed.length === 0) {
300
+ return;
301
+ }
302
+ const ctor = this.constructor;
303
+ const parts = [];
304
+ if (added.length > 0) {
305
+ const list = added
306
+ .sort()
307
+ .map((n) => `\`${n}\``)
308
+ .join(", ");
309
+ parts.push(ctor.TOOL_ACTIVATION_MESSAGE_TEMPLATE.replaceAll("{function_names}", list));
310
+ }
311
+ if (removed.length > 0) {
312
+ const list = removed
313
+ .sort()
314
+ .map((n) => `\`${n}\``)
315
+ .join(", ");
316
+ parts.push(ctor.TOOL_DEACTIVATION_MESSAGE_TEMPLATE.replaceAll("{function_names}", list));
317
+ }
318
+ this._context.addMessage({ role: "developer", content: parts.join(" ") });
319
+ }
320
+ /** Messages from the LLM context. */
321
+ get messages() {
322
+ return this._context.getMessages();
323
+ }
324
+ /** The role for this aggregator. */
325
+ get role() {
326
+ return this._role;
327
+ }
328
+ /** The LLMContext instance used by this aggregator. */
329
+ get context() {
330
+ return this._context;
331
+ }
332
+ /**
333
+ * Create a context frame with the current context.
334
+ *
335
+ * @returns LLMContextFrame containing the current context.
336
+ */
337
+ _getContextFrame() {
338
+ return new LLMContextFrame({ context: this._context });
339
+ }
340
+ /**
341
+ * Push a context frame in the specified direction.
342
+ *
343
+ * @param direction The direction to push the frame (upstream or downstream).
344
+ */
345
+ async pushContextFrame(direction = FrameDirection.DOWNSTREAM) {
346
+ const frame = this._getContextFrame();
347
+ await this.pushFrame(frame, direction);
348
+ }
349
+ /**
350
+ * Add messages to the context.
351
+ *
352
+ * @param messages Messages to add to the conversation context.
353
+ */
354
+ addMessages(messages) {
355
+ this._context.addMessages(messages);
356
+ }
357
+ /**
358
+ * Set the context messages.
359
+ *
360
+ * @param messages Messages to replace the current context messages.
361
+ */
362
+ setMessages(messages) {
363
+ this._context.setMessages(messages);
364
+ }
365
+ /**
366
+ * Transform the context messages using a provided function.
367
+ *
368
+ * @param transform A function that takes the current list of messages and
369
+ * returns a modified list of messages to set in the context.
370
+ */
371
+ transformMessages(transform) {
372
+ this._context.transformMessages(transform);
373
+ }
374
+ /**
375
+ * Set tools in the context.
376
+ *
377
+ * @param tools List of tool definitions to set in the context.
378
+ */
379
+ setTools(tools) {
380
+ this._context.setTools(tools);
381
+ }
382
+ /**
383
+ * Set tool choice in the context.
384
+ *
385
+ * @param toolChoice Tool choice configuration for the context.
386
+ */
387
+ setToolChoice(toolChoice) {
388
+ this._context.setToolChoice(toolChoice);
389
+ }
390
+ /** Reset the aggregation state. */
391
+ async reset() {
392
+ this._aggregation = [];
393
+ }
394
+ /**
395
+ * Get the current aggregation as a string.
396
+ *
397
+ * @returns The concatenated aggregation string.
398
+ */
399
+ aggregationString() {
400
+ return concatenateAggregatedText(this._aggregation);
401
+ }
402
+ }
403
+ /**
404
+ * User LLM aggregator that aggregates user input during active user turns.
405
+ *
406
+ * This aggregator uses a turn controller and operates within turn boundaries
407
+ * defined by the controller's configured user turn strategies. User turn
408
+ * start strategies indicate when a user turn begins, while user turn stop
409
+ * strategies signal when the user turn has ended.
410
+ *
411
+ * The aggregator collects and aggregates speech-to-text transcriptions that
412
+ * occur while a user turn is active and pushes the final aggregation when the
413
+ * user turn is finished.
414
+ *
415
+ * Event handlers available:
416
+ *
417
+ * - on_user_turn_started: Called when the user turn starts
418
+ * - on_user_turn_stopped: Called when the user turn ends
419
+ * - on_user_turn_stop_timeout: Called when no user turn stop strategy triggers
420
+ * - on_user_turn_idle: Called when the user has been idle for the configured timeout
421
+ * - on_user_turn_message_added: Called when a user message is written to
422
+ * context. In realtime mode (`realtimeServiceMode: true`) the write is
423
+ * triggered by the assistant response start rather than the user-turn-end
424
+ * frame, so this fires decoupled from `on_user_turn_stopped` — subscribe
425
+ * here for the finalized user text.
426
+ * - on_user_mute_started: Called when the user becomes muted
427
+ * - on_user_mute_stopped: Called when the user becomes unmuted
428
+ * - on_user_backchannel: Called when the user acknowledges the bot without
429
+ * interrupting it, from a `UserBackchannelFrame`
430
+ *
431
+ * @example
432
+ * aggregator.eventHandler("on_user_turn_started", async (aggregator, strategy: BaseUserTurnStartStrategy) => {});
433
+ * aggregator.eventHandler(
434
+ * "on_user_turn_stopped",
435
+ * async (aggregator, strategy: BaseUserTurnStopStrategy, message: UserTurnStoppedMessage) => {},
436
+ * );
437
+ * aggregator.eventHandler("on_user_turn_stop_timeout", async (aggregator) => {});
438
+ * aggregator.eventHandler("on_user_turn_idle", async (aggregator) => {});
439
+ * // In realtime mode the user message is written when the assistant response
440
+ * // starts, not at user-turn-end — subscribe here for the finalized text.
441
+ * aggregator.eventHandler(
442
+ * "on_user_turn_message_added",
443
+ * async (aggregator, message: UserTurnMessageAddedMessage) => {},
444
+ * );
445
+ * aggregator.eventHandler("on_user_mute_started", async (aggregator) => {});
446
+ * aggregator.eventHandler("on_user_mute_stopped", async (aggregator) => {});
447
+ * aggregator.eventHandler("on_user_backchannel", async (aggregator, message: UserBackchannelMessage) => {});
448
+ */
449
+ export class LLMUserAggregator extends LLMContextAggregator {
450
+ _params;
451
+ // Realtime-mode wiring. null (the default) means auto-configure from
452
+ // service metadata: cascade behavior until a realtime service announces
453
+ // itself, at which point _handleLlmServiceMetadata flips this to true.
454
+ // Explicit true/false force the mode on or off (legacy context writes).
455
+ _realtimeServiceMode;
456
+ _userIsMuted = false;
457
+ _userTurnStartTimestamp = "";
458
+ // One-shot guard: whether the first realtime LLM service metadata frame has
459
+ // been handled this session — see _handleLlmServiceMetadata.
460
+ _realtimeMetadataHandled = false;
461
+ // Realtime-mode deferred-flush state. Realtime mode treats the assistant
462
+ // response start as the "user turn ended" signal for context-writing
463
+ // purposes, then waits up to this many seconds for any in-flight
464
+ // `TranscriptionFrame` to land before flushing — mirroring the cascade-mode
465
+ // pattern of waiting up to `ttfsP99Latency` after
466
+ // `UserStoppedSpeakingFrame`. Captured from the most recent
467
+ // `STTMetadataFrame` (which a realtime LLM service may broadcast for its
468
+ // internal STT pass), falling back to `DEFAULT_TTFS_P99`.
469
+ _ttfsP99Latency = null;
470
+ _realtimeHandoffFlushTask = null;
471
+ // Full transcript across the user turn. Each
472
+ // `_onUserTurnInferenceTriggered` push captures only the new segment since
473
+ // the previous push (pushAggregation resets `_aggregation` after writing to
474
+ // context); we accumulate those segments here so the eventual
475
+ // `on_user_turn_stopped` event surfaces the full turn transcript even when
476
+ // several inferences fire before finalization.
477
+ _fullUserTurnAggregation = null;
478
+ // Whether the current user turn interrupted the bot.
479
+ _userTurnInterruptedBot = false;
480
+ _consecutiveEmptyUserTurnRecoveries = 0;
481
+ // Pair-internal: read by the paired LLMAssistantAggregator.
482
+ _userTurnController;
483
+ _userIdleController;
484
+ // VAD controller
485
+ _vadController = null;
486
+ /**
487
+ * Initialize the user context aggregator.
488
+ *
489
+ * @param context The LLM context for conversation storage.
490
+ */
491
+ constructor(context, options = {}) {
492
+ const params = options.params || new LLMUserAggregatorParams();
493
+ super({
494
+ ...options,
495
+ context,
496
+ role: "user",
497
+ addToolChangeMessages: params.addToolChangeMessages,
498
+ });
499
+ this._params = params;
500
+ this._registerEventHandler("on_user_turn_started");
501
+ this._registerEventHandler("on_user_turn_stopped");
502
+ this._registerEventHandler("on_user_turn_stop_timeout");
503
+ this._registerEventHandler("on_user_turn_idle");
504
+ this._registerEventHandler("on_user_turn_inference_triggered");
505
+ this._registerEventHandler("on_user_turn_message_added");
506
+ this._registerEventHandler("on_user_mute_started");
507
+ this._registerEventHandler("on_user_mute_stopped");
508
+ this._registerEventHandler("on_user_backchannel");
509
+ this._realtimeServiceMode = options._realtimeServiceMode ?? null;
510
+ const userTurnStrategies = this._params.userTurnStrategies || new UserTurnStrategies();
511
+ // Realtime-mode strategy mutation: drop the transcription-based start
512
+ // strategy and flip waitForTranscript on stop strategies that expose it,
513
+ // so turn-stop fires as soon as VAD / the turn analyzer / external frames
514
+ // report end-of-speech.
515
+ //
516
+ // This only runs here when realtime mode was explicitly enabled. When
517
+ // it's auto-configured (the default — _realtimeServiceMode is null until a
518
+ // realtime service announces itself), the mutation is deferred to
519
+ // _handleLlmServiceMetadata, which also applies it on top of any
520
+ // service-recommended external strategies.
521
+ if (this._realtimeServiceMode) {
522
+ this._applyRealtimeModeStrategyMutations(userTurnStrategies, this._params.userTurnStrategies !== null);
523
+ this._disableEmptyUserTurnRecovery();
524
+ }
525
+ this._userTurnController = new UserTurnController({
526
+ userTurnStrategies,
527
+ userTurnStopTimeout: this._params.userTurnStopTimeout,
528
+ });
529
+ this._userTurnController.addEventHandler("on_push_frame", (controller, frame, direction) => this._onPushFrame(controller, frame, direction));
530
+ this._userTurnController.addEventHandler("on_broadcast_frame", (controller, frameCls, frameOptions) => this._onBroadcastFrame(controller, frameCls, frameOptions));
531
+ this._userTurnController.addEventHandler("on_user_turn_speculation_cancelled", (controller) => this._onUserTurnSpeculationCancelled(controller));
532
+ this._userTurnController.addEventHandler("on_user_turn_started", (controller, strategy, params) => this._onUserTurnStarted(controller, strategy, params));
533
+ this._userTurnController.addEventHandler("on_user_turn_inference_triggered", (controller, strategy, speculation) => this._onUserTurnInferenceTriggered(controller, strategy, speculation));
534
+ this._userTurnController.addEventHandler("on_user_turn_stopped", (controller, strategy, params) => this._onUserTurnStopped(controller, strategy, params));
535
+ this._userTurnController.addEventHandler("on_user_turn_stop_timeout", (controller) => this._onUserTurnStopTimeout(controller));
536
+ this._userTurnController.addEventHandler("on_reset_aggregation", (controller, strategy) => this._onResetAggregation(controller, strategy));
537
+ this._userIdleController = new UserIdleController({
538
+ userIdleTimeout: this._params.userIdleTimeout,
539
+ });
540
+ this._userIdleController.addEventHandler("on_user_turn_idle", (controller) => this._onUserTurnIdle(controller));
541
+ // VAD controller
542
+ if (this._params.vadAnalyzer) {
543
+ this._vadController = new VADController(this._params.vadAnalyzer, {
544
+ audioIdleTimeout: this._params.audioIdleTimeout,
545
+ });
546
+ this._vadController.addEventHandler("on_speech_started", (controller) => this._onVadSpeechStarted(controller));
547
+ this._vadController.addEventHandler("on_speech_stopped", (controller) => this._onVadSpeechStopped(controller));
548
+ this._vadController.addEventHandler("on_speech_activity", (controller) => this._onVadSpeechActivity(controller));
549
+ this._vadController.addEventHandler("on_push_frame", (controller, frame, direction) => this._onPushFrame(controller, frame, direction));
550
+ this._vadController.addEventHandler("on_broadcast_frame", (controller, frameCls, frameOptions) => this._onBroadcastFrame(controller, frameCls, frameOptions));
551
+ }
552
+ }
553
+ /**
554
+ * Set up the aggregator.
555
+ *
556
+ * @param setup Configuration object containing setup parameters.
557
+ */
558
+ async setup(setup) {
559
+ await super.setup(setup);
560
+ if (this._vadController) {
561
+ await this._vadController.setup(setup);
562
+ }
563
+ await this._userTurnController.setup(setup);
564
+ await this._userIdleController.setup(setup);
565
+ for (const s of this._params.userMuteStrategies) {
566
+ await s.setup(setup);
567
+ }
568
+ }
569
+ /** Release this aggregator's resources at teardown. */
570
+ async cleanup() {
571
+ await super.cleanup();
572
+ await this._cancelRealtimeHandoffFlushTask();
573
+ if (this._vadController) {
574
+ await this._vadController.cleanup();
575
+ }
576
+ await this._userTurnController.cleanup();
577
+ await this._userIdleController.cleanup();
578
+ for (const s of this._params.userMuteStrategies) {
579
+ await s.cleanup();
580
+ }
581
+ }
582
+ /**
583
+ * Process frames for user speech aggregation and context management.
584
+ *
585
+ * @param frame The frame to process.
586
+ * @param direction The direction of frame flow in the pipeline.
587
+ */
588
+ async processFrame(frame, direction) {
589
+ await super.processFrame(frame, direction);
590
+ if (await this._maybeMuteFrame(frame)) {
591
+ return;
592
+ }
593
+ if (frame instanceof StartFrame) {
594
+ await this.pushFrame(frame, direction);
595
+ await this._start(frame);
596
+ }
597
+ else if (frame instanceof EndFrame) {
598
+ // Push EndFrame before stop(), because stop() waits on the task to
599
+ // finish and the task finishes when EndFrame is processed.
600
+ await this.pushFrame(frame, direction);
601
+ await this._stop(frame);
602
+ }
603
+ else if (frame instanceof CancelFrame) {
604
+ await this._cancel(frame);
605
+ await this.pushFrame(frame, direction);
606
+ }
607
+ else if (frame instanceof TranscriptionFrame) {
608
+ await this._handleTranscription(frame);
609
+ }
610
+ else if (frame instanceof InterimTranscriptionFrame ||
611
+ frame instanceof TranslationFrame ||
612
+ frame instanceof EagerTranscriptionFrame) {
613
+ // Interim transcriptions, translations and eager end-of-turn
614
+ // transcripts are consumed here and not pushed downstream, same as
615
+ // final TranscriptionFrame. The turn strategies still see them: the
616
+ // controller is fed every frame below.
617
+ }
618
+ else if (frame instanceof UserBackchannelFrame) {
619
+ const message = new UserBackchannelMessage({
620
+ content: frame.text,
621
+ timestamp: frame.timestamp,
622
+ userId: frame.userId,
623
+ });
624
+ await this._callEventHandler("on_user_backchannel", message);
625
+ await this.pushFrame(frame, direction);
626
+ }
627
+ else if (frame instanceof LLMRunFrame) {
628
+ await this._handleLlmRun(frame);
629
+ }
630
+ else if (frame instanceof LLMMessagesAppendFrame) {
631
+ await this._handleLlmMessagesAppend(frame);
632
+ }
633
+ else if (frame instanceof LLMMessagesUpdateFrame) {
634
+ await this._handleLlmMessagesUpdate(frame);
635
+ }
636
+ else if (frame instanceof LLMMessagesTransformFrame) {
637
+ await this._handleLlmMessagesTransform(frame);
638
+ }
639
+ else if (frame instanceof LLMSetToolsFrame) {
640
+ // Normalize and validate (a plain list of direct functions /
641
+ // FunctionSchema objects becomes a ToolsSchema) so the tool-change diff
642
+ // and setTools see a consistent type.
643
+ const normalizedTools = LLMContext._normalizeAndValidateTools(frame.tools);
644
+ this._maybeAddToolChangeMessages(normalizedTools);
645
+ this.setTools(normalizedTools);
646
+ // Push the LLMSetToolsFrame as well, since speech-to-speech LLM
647
+ // services (like OpenAI Realtime) may need to know about tool changes;
648
+ // unlike text-based LLM services they won't just "pick up the change"
649
+ // on the next LLM run, as the LLM is continuously running.
650
+ await this.pushFrame(frame, direction);
651
+ }
652
+ else if (frame instanceof LLMSetToolChoiceFrame) {
653
+ this.setToolChoice(frame.toolChoice);
654
+ }
655
+ else if (frame instanceof ServiceMetadataFrame) {
656
+ await this._handleServiceMetadata(frame);
657
+ await this.pushFrame(frame, direction);
658
+ }
659
+ else if (frame instanceof ProposedUserStartedSpeakingFrame) {
660
+ // A proposal is resolved once. Forwarding one our own strategies
661
+ // resolve would let a resolver further down the pipeline decide the
662
+ // same turn a second time.
663
+ if (!this._userTurnController.resolvesProposedTurnStartFrames) {
664
+ await this.pushFrame(frame, direction);
665
+ }
666
+ }
667
+ else if (frame instanceof ProposedUserStoppedSpeakingFrame) {
668
+ if (!this._userTurnController.resolvesProposedTurnStopFrames) {
669
+ await this.pushFrame(frame, direction);
670
+ }
671
+ }
672
+ else {
673
+ await this.pushFrame(frame, direction);
674
+ }
675
+ if (this._vadController) {
676
+ await this._vadController.processFrame(frame);
677
+ }
678
+ await this._userTurnController.processFrame(frame);
679
+ await this._userIdleController.processFrame(frame);
680
+ }
681
+ /** Push the current aggregation. */
682
+ async pushAggregation() {
683
+ return await this._pushAggregation();
684
+ }
685
+ /**
686
+ * Write the aggregated user turn to the context.
687
+ *
688
+ * @param options.runLlm Whether to emit the LLMContextFrame that runs
689
+ * inference. False when the response is already generated — an eager end
690
+ * of turn that held — and only the context write is still owed.
691
+ * @returns The text written, or "" when there was nothing to write.
692
+ */
693
+ async _pushAggregation(options = {}) {
694
+ const runLlm = options.runLlm ?? true;
695
+ if (this._aggregation.length === 0) {
696
+ return "";
697
+ }
698
+ const aggregation = this.aggregationString();
699
+ await this.reset();
700
+ this._context.addMessage({ role: this.role, content: aggregation });
701
+ await this.broadcastFrame(LLMContextUserTurnFrame, {
702
+ text: aggregation,
703
+ timestamp: this._userTurnStartTimestamp,
704
+ });
705
+ if (runLlm) {
706
+ await this.pushContextFrame();
707
+ }
708
+ const message = new UserTurnMessageAddedMessage({
709
+ content: aggregation,
710
+ timestamp: this._userTurnStartTimestamp,
711
+ });
712
+ await this._callEventHandler("on_user_turn_message_added", message);
713
+ return aggregation;
714
+ }
715
+ async _start(_frame) {
716
+ await this._startControllers();
717
+ }
718
+ async _stop(_frame) {
719
+ if (this._realtimeServiceMode) {
720
+ // Realtime mode: cancel any pending deferred handoff flush and commit
721
+ // trailing user content directly. The on_user_turn_stopped event
722
+ // already fired (if turn frames were emitted), so don't re-fire it from
723
+ // session end.
724
+ await this._cancelRealtimeHandoffFlushTask();
725
+ await this.pushAggregation();
726
+ }
727
+ else {
728
+ await this._maybeEmitUserTurnStopped({ onSessionEnd: true });
729
+ }
730
+ await this._stopControllers();
731
+ }
732
+ async _cancel(_frame) {
733
+ // See _stop — same realtime-mode vs cascade dispatch.
734
+ if (this._realtimeServiceMode) {
735
+ await this._cancelRealtimeHandoffFlushTask();
736
+ await this.pushAggregation();
737
+ }
738
+ else {
739
+ await this._maybeEmitUserTurnStopped({ onSessionEnd: true });
740
+ }
741
+ await this._stopControllers();
742
+ }
743
+ /**
744
+ * Mutate turn strategies for realtime mode.
745
+ *
746
+ * Drops `TranscriptionUserTurnStartStrategy` from the start strategies
747
+ * (transcripts shouldn't start a turn when the realtime service drives the
748
+ * conversation) and flips `waitForTranscript = false` on stop strategies
749
+ * that expose the flag, so end-of-turn fires as soon as VAD / the turn
750
+ * analyzer reports end-of-speech.
751
+ */
752
+ _applyRealtimeModeStrategyMutations(userTurnStrategies, areUserProvidedCustomStrategies) {
753
+ const startStrategies = userTurnStrategies.start ?? [];
754
+ const dropped = [];
755
+ const keptStart = [];
756
+ for (const s of startStrategies) {
757
+ if (s instanceof TranscriptionUserTurnStartStrategy) {
758
+ dropped.push(s.constructor.name);
759
+ }
760
+ else {
761
+ keptStart.push(s);
762
+ }
763
+ }
764
+ userTurnStrategies.start = keptStart;
765
+ const flipped = [];
766
+ for (const s of userTurnStrategies.stop ?? []) {
767
+ // The flag is a property on some stop strategies, not on the base.
768
+ const withFlag = s;
769
+ if (withFlag.waitForTranscript) {
770
+ try {
771
+ withFlag.waitForTranscript = false;
772
+ flipped.push(s.constructor.name);
773
+ }
774
+ catch (e) {
775
+ rethrowIfCancelled(e);
776
+ // Strategy exposes the property but no setter — skip.
777
+ if (!(e instanceof TypeError))
778
+ throw e;
779
+ }
780
+ }
781
+ }
782
+ if (dropped.length === 0 && flipped.length === 0) {
783
+ return;
784
+ }
785
+ const msg = `${this}: realtime mode — mutated turn strategies: ` +
786
+ `dropped ${dropped.length > 0 ? reprList(dropped) : "no"} start strategy(ies); set ` +
787
+ `wait_for_transcript=False on ${flipped.length > 0 ? reprList(flipped) : "no"} stop strategy(ies).`;
788
+ if (areUserProvidedCustomStrategies) {
789
+ logger.warning(msg);
790
+ }
791
+ else {
792
+ logger.debug(msg);
793
+ }
794
+ }
795
+ /**
796
+ * Turn off empty user turn recovery for realtime mode.
797
+ *
798
+ * A realtime LLM service hears the user's audio directly, so an empty
799
+ * transcript doesn't mean the model missed the speech.
800
+ */
801
+ _disableEmptyUserTurnRecovery() {
802
+ if (this._params.emptyUserTurn === null) {
803
+ return;
804
+ }
805
+ this._params.emptyUserTurn = null;
806
+ logger.debug(`${this}: realtime mode — empty user turn recovery disabled; the realtime ` +
807
+ "LLM service hears the user's audio directly.");
808
+ }
809
+ /**
810
+ * Dispatch a service metadata frame.
811
+ *
812
+ * Any service may recommend user turn strategies via the base
813
+ * `userTurnStrategies` field, so apply that first — regardless of service
814
+ * type or realtime mode — then run type-specific handling.
815
+ */
816
+ async _handleServiceMetadata(frame) {
817
+ await this._handleServiceUserTurnStrategies(frame.serviceName, frame.userTurnStrategies);
818
+ if (frame instanceof STTMetadataFrame) {
819
+ await this._handleSttMetadata(frame);
820
+ }
821
+ else if (frame instanceof LLMServiceMetadataFrame) {
822
+ await this._handleLlmServiceMetadata(frame);
823
+ }
824
+ }
825
+ /**
826
+ * Handle an `STTMetadataFrame` broadcast by an STT service at start.
827
+ *
828
+ * Captures the STT TTFS P99 so the realtime-mode deferred handoff flush can
829
+ * size itself to the real transcript-arrival latency. Any recommended user
830
+ * turn strategies were already applied by `_handleServiceMetadata`.
831
+ */
832
+ async _handleSttMetadata(frame) {
833
+ this._ttfsP99Latency = frame.ttfsP99Latency;
834
+ }
835
+ /**
836
+ * Apply user turn strategies a service recommends via its metadata frame.
837
+ *
838
+ * Honored only when the user did not pass their own `userTurnStrategies`,
839
+ * which always wins. `updateStrategies` is idempotent, so this is safe to
840
+ * re-run on a later broadcast (e.g. a ServiceSwitcher switch).
841
+ */
842
+ async _handleServiceUserTurnStrategies(serviceName, userTurnStrategies) {
843
+ if (userTurnStrategies === null || userTurnStrategies === undefined) {
844
+ return;
845
+ }
846
+ if (this._params.userTurnStrategies !== null) {
847
+ logger.debug(`${this}: ignoring user turn strategies recommended by ` +
848
+ `\`${serviceName}\`; using the user-provided strategies.`);
849
+ this._warnOnDiscardedInterruptionSetting(serviceName, userTurnStrategies);
850
+ return;
851
+ }
852
+ logger.debug(`${this}: applying user turn strategies recommended by \`${serviceName}\`.`);
853
+ await this._userTurnController.updateStrategies(userTurnStrategies);
854
+ }
855
+ /**
856
+ * Warn when (maybe unintentionally) overruling a service silently turns interruptions back on.
857
+ *
858
+ * A service recommends turn strategies by putting them on its
859
+ * `ServiceMetadataFrame.userTurnStrategies`, and one configured with
860
+ * `shouldInterrupt: false` carries that setting there. User-provided
861
+ * strategies discard the recommendation whole — so the pipeline would
862
+ * start interrupting with no indication why.
863
+ *
864
+ * Passing `ExternalUserTurnStrategies` explicitly was a safe way to opt out
865
+ * of the recommendation until 1.8.0, because the external start strategy
866
+ * never interrupted no matter how it was constructed. It broadcasts the
867
+ * interruption now, so explicit strategies can contradict the service, and
868
+ * this warning names the two ways to reconcile them.
869
+ *
870
+ * Only that container is checked. A `UserTurnStrategies` assembled by hand
871
+ * around a bare `ExternalUserTurnStartStrategy` contradicts the service the
872
+ * same way and goes unwarned — not expected to come up often enough to
873
+ * justify inspecting the individual strategies.
874
+ */
875
+ _warnOnDiscardedInterruptionSetting(serviceName, recommended) {
876
+ if (!(recommended instanceof ExternalUserTurnStrategies)) {
877
+ return;
878
+ }
879
+ if (recommended.enableInterruptions) {
880
+ return;
881
+ }
882
+ const provided = this._params.userTurnStrategies;
883
+ if (!(provided instanceof ExternalUserTurnStrategies) || !provided.enableInterruptions) {
884
+ return;
885
+ }
886
+ logger.warning(`${this}: \`${serviceName}\` asked for interruptions to stay off, but the ` +
887
+ "user-provided `ExternalUserTurnStrategies` leaves them on, so the bot will " +
888
+ "be interrupted when the user starts speaking. Drop `user_turn_strategies` to " +
889
+ "use the service's recommendation, or pass " +
890
+ "`ExternalUserTurnStrategies(enable_interruptions=False)`.");
891
+ }
892
+ /**
893
+ * Handle an `LLMServiceMetadataFrame` broadcast by an LLM service.
894
+ *
895
+ * Non-realtime services need no configuration. For a realtime service,
896
+ * auto-configure `realtimeServiceMode` (once) when the user left it unset
897
+ * (`null`); an explicit `true`/`false` is honored as-is (`false` opts out
898
+ * silently). When the mode is active, strip transcript dependence from the
899
+ * active strategies — including any external strategies
900
+ * `_handleServiceMetadata` just adopted.
901
+ *
902
+ * The strategy mutation re-runs on every broadcast (so a re-adopted
903
+ * recommendation is re-mutated, e.g. after a ServiceSwitcher switch); only
904
+ * the auto-configuration + log is one-shot. `updateStrategies` is
905
+ * idempotent, so re-applying is safe.
906
+ */
907
+ async _handleLlmServiceMetadata(frame) {
908
+ if (!frame.isRealtimeService) {
909
+ return;
910
+ }
911
+ // Auto-configure + log once per session.
912
+ if (!this._realtimeMetadataHandled) {
913
+ this._realtimeMetadataHandled = true;
914
+ if (this._realtimeServiceMode === null) {
915
+ this._realtimeServiceMode = true;
916
+ logger.debug(`${this}: detected realtime service \`${frame.serviceName}\`; ` +
917
+ "auto-enabled realtime_service_mode.");
918
+ }
919
+ }
920
+ if (!this._realtimeServiceMode) {
921
+ // Explicitly disabled — honor it silently; the user opted out.
922
+ return;
923
+ }
924
+ this._disableEmptyUserTurnRecovery();
925
+ const strategies = this._userTurnController.userTurnStrategies;
926
+ this._applyRealtimeModeStrategyMutations(strategies, this._params.userTurnStrategies !== null);
927
+ await this._userTurnController.updateStrategies(strategies);
928
+ }
929
+ /**
930
+ * Commit the user message in realtime mode, allowing for late transcripts.
931
+ *
932
+ * Called by the paired assistant half when the assistant response starts —
933
+ * realtime mode treats that as the user turn's end signal for
934
+ * context-writing purposes (in place of `UserStoppedSpeakingFrame`).
935
+ * Realtime services may deliver the finalized user `TranscriptionFrame` in
936
+ * pieces, and some deliver it only after the assistant starts responding,
937
+ * so we always defer the flush by `ttfsP99Latency` to let any in-flight
938
+ * transcript land. The `_realtimeHandoffFlushImmediate` failsafe on
939
+ * `LLMFullResponseEndFrame` catches any stragglers.
940
+ *
941
+ * Pair-internal: called by the paired LLMAssistantAggregator.
942
+ */
943
+ async _realtimeHandoffFlush() {
944
+ // Cancel any prior deferred flush task — a new turn supersedes it.
945
+ await this._cancelRealtimeHandoffFlushTask();
946
+ this._realtimeHandoffFlushTask = this.createTask(() => this._realtimeDeferredHandoffFlush(), `${this}::realtime_handoff_flush`);
947
+ }
948
+ /** Wait one `ttfsP99Latency` window, then flush whatever has arrived. */
949
+ async _realtimeDeferredHandoffFlush() {
950
+ const wait = this._ttfsP99Latency !== null ? this._ttfsP99Latency : DEFAULT_TTFS_P99;
951
+ await sleep(wait);
952
+ if (this._aggregation.length > 0) {
953
+ await this.pushAggregation();
954
+ this._userTurnStartTimestamp = "";
955
+ }
956
+ }
957
+ /**
958
+ * Failsafe flush invoked when the assistant response ends.
959
+ *
960
+ * By the time the assistant response has fully completed, even slow
961
+ * transcripts should have landed. Cancel any pending deferred flush and
962
+ * write the user message now so it precedes the assistant message in
963
+ * context.
964
+ *
965
+ * Pair-internal: called by the paired LLMAssistantAggregator.
966
+ */
967
+ async _realtimeHandoffFlushImmediate() {
968
+ await this._cancelRealtimeHandoffFlushTask();
969
+ if (this._aggregation.length > 0) {
970
+ await this.pushAggregation();
971
+ this._userTurnStartTimestamp = "";
972
+ }
973
+ }
974
+ async _cancelRealtimeHandoffFlushTask() {
975
+ if (this._realtimeHandoffFlushTask === null) {
976
+ return;
977
+ }
978
+ if (!this._realtimeHandoffFlushTask.done()) {
979
+ await this.cancelTask(this._realtimeHandoffFlushTask);
980
+ }
981
+ this._realtimeHandoffFlushTask = null;
982
+ }
983
+ async _startControllers() {
984
+ if (this._vadController) {
985
+ await this._vadController.start();
986
+ }
987
+ await this._userTurnController.start();
988
+ }
989
+ async _stopControllers() {
990
+ // At session end the controllers' timers can only report what ending
991
+ // looks like: no audio arriving, no turn finishing, the user idle. They
992
+ // stop here, while what they hold (the VAD analyzer, the turn strategies)
993
+ // may be shared and is released in cleanup() instead.
994
+ await this._cancelRealtimeHandoffFlushTask();
995
+ if (this._vadController) {
996
+ await this._vadController.stop();
997
+ }
998
+ await this._userTurnController.stop();
999
+ await this._userIdleController.stop();
1000
+ }
1001
+ async _maybeMuteFrame(frame) {
1002
+ // Lifecycle frames should never be muted and should not trigger mute
1003
+ // state changes. Evaluating mute strategies on StartFrame would broadcast
1004
+ // UserMuteStartedFrame before StartFrame reaches downstream processors.
1005
+ if (frame instanceof StartFrame || frame instanceof EndFrame || frame instanceof CancelFrame) {
1006
+ return false;
1007
+ }
1008
+ const shouldMuteFrame = this._userIsMuted &&
1009
+ (frame instanceof InterruptionFrame ||
1010
+ frame instanceof VADUserStartedSpeakingFrame ||
1011
+ frame instanceof VADUserStoppedSpeakingFrame ||
1012
+ frame instanceof ProposedUserStartedSpeakingFrame ||
1013
+ frame instanceof ProposedUserStoppedSpeakingFrame ||
1014
+ frame instanceof UserStartedSpeakingFrame ||
1015
+ frame instanceof UserStoppedSpeakingFrame ||
1016
+ frame instanceof InputAudioRawFrame ||
1017
+ frame instanceof InterimTranscriptionFrame ||
1018
+ frame instanceof TranscriptionFrame ||
1019
+ frame instanceof EagerTranscriptionFrame ||
1020
+ frame instanceof UserBackchannelFrame);
1021
+ if (shouldMuteFrame) {
1022
+ logger.trace(`${frame.name} suppressed - user currently muted`);
1023
+ }
1024
+ let shouldMuteNextTime = false;
1025
+ for (const s of this._params.userMuteStrategies) {
1026
+ // Every strategy sees the frame, so no short-circuiting.
1027
+ const mute = await s.processFrame(frame);
1028
+ shouldMuteNextTime = shouldMuteNextTime || Boolean(mute);
1029
+ }
1030
+ if (shouldMuteNextTime !== this._userIsMuted) {
1031
+ logger.debug(`${this}: user is now ${shouldMuteNextTime ? "muted" : "unmuted"}`);
1032
+ this._userIsMuted = shouldMuteNextTime;
1033
+ // Emit mute state change events
1034
+ if (this._userIsMuted) {
1035
+ await this._callEventHandler("on_user_mute_started");
1036
+ await this.broadcastFrame(UserMuteStartedFrame);
1037
+ }
1038
+ else {
1039
+ await this._callEventHandler("on_user_mute_stopped");
1040
+ await this.broadcastFrame(UserMuteStoppedFrame);
1041
+ }
1042
+ }
1043
+ return shouldMuteFrame;
1044
+ }
1045
+ async _handleLlmRun(_frame) {
1046
+ await this.pushContextFrame();
1047
+ }
1048
+ async _handleLlmMessagesAppend(frame) {
1049
+ this.addMessages(frame.messages);
1050
+ if (frame.runLlm) {
1051
+ await this.pushContextFrame();
1052
+ }
1053
+ }
1054
+ async _handleLlmMessagesUpdate(frame) {
1055
+ this.setMessages(frame.messages);
1056
+ if (frame.runLlm) {
1057
+ await this.pushContextFrame();
1058
+ }
1059
+ }
1060
+ async _handleLlmMessagesTransform(frame) {
1061
+ this.transformMessages(frame.transform);
1062
+ if (frame.runLlm) {
1063
+ await this.pushContextFrame();
1064
+ }
1065
+ }
1066
+ async _handleTranscription(frame) {
1067
+ const text = frame.text;
1068
+ // Make sure we really have some text.
1069
+ if (!text.trim()) {
1070
+ return;
1071
+ }
1072
+ // In realtime mode with a service that doesn't emit user-turn frames
1073
+ // (Gemini Live, AWS Nova Sonic, Ultravox), `_onUserTurnStarted` never
1074
+ // fires, so seed `_userTurnStartTimestamp` here from the first transcript
1075
+ // so the eventual `on_user_turn_message_added` event carries one.
1076
+ // Harmless in cascade mode — turn frames set it before this runs (the
1077
+ // timestamp check is a no-op).
1078
+ if (!this._userTurnStartTimestamp) {
1079
+ this._userTurnStartTimestamp = timeNowIso8601();
1080
+ }
1081
+ // Transcriptions never include inter-part spaces (so far).
1082
+ this._aggregation.push(new TextPartForConcatenation({
1083
+ text,
1084
+ includesInterPartSpaces: frame.includesInterFrameSpaces,
1085
+ }));
1086
+ }
1087
+ /**
1088
+ * Broadcasts a frame upstream and queues it for internal processing.
1089
+ *
1090
+ * Queues the frame so it flows through `processFrame` and is handled
1091
+ * internally (e.g. by the `UserTurnController`). The upstream frame is
1092
+ * pushed directly.
1093
+ *
1094
+ * @param frameCls The class of the frame to be broadcasted.
1095
+ * @param options Options to be passed to the frame's constructor.
1096
+ */
1097
+ async _queuedBroadcastFrame(frameCls, options) {
1098
+ await this.queueFrame(new frameCls(options));
1099
+ await this.pushFrame(new frameCls(options), FrameDirection.UPSTREAM);
1100
+ }
1101
+ async _onPushFrame(_controller, frame, direction = FrameDirection.DOWNSTREAM) {
1102
+ await this.queueFrame(frame, direction);
1103
+ }
1104
+ async _onBroadcastFrame(_controller, frameCls, options) {
1105
+ await this._queuedBroadcastFrame(frameCls, options);
1106
+ }
1107
+ async _onUserTurnSpeculationCancelled(_controller) {
1108
+ // Broadcast rather than queued, so it reaches a downstream gate ahead of
1109
+ // the turn end that follows it. Queued, it would arrive after, and the
1110
+ // gate would release the response this withdraws.
1111
+ await this.broadcastFrame(EagerEndOfTurnCancelFrame);
1112
+ }
1113
+ async _onVadSpeechStarted(controller) {
1114
+ const analyzer = controller._vadAnalyzer;
1115
+ await this._queuedBroadcastFrame(VADUserStartedSpeakingFrame, {
1116
+ startSecs: analyzer.params.startSecs,
1117
+ });
1118
+ }
1119
+ async _onVadSpeechStopped(controller) {
1120
+ const analyzer = controller._vadAnalyzer;
1121
+ await this._queuedBroadcastFrame(VADUserStoppedSpeakingFrame, {
1122
+ stopSecs: analyzer.params.stopSecs,
1123
+ });
1124
+ }
1125
+ async _onVadSpeechActivity(_controller) {
1126
+ await this._queuedBroadcastFrame(UserSpeakingFrame);
1127
+ }
1128
+ async _onUserTurnStarted(_controller, strategy, params) {
1129
+ logger.debug(`${this}: User started speaking (strategy: ${strategy})`);
1130
+ this._userTurnStartTimestamp = timeNowIso8601();
1131
+ this._fullUserTurnAggregation = null;
1132
+ // Unless the bot is waiting for the user, it's thinking, speaking or
1133
+ // running a function call, and the interruption below cancels that.
1134
+ this._userTurnInterruptedBot =
1135
+ params.enableInterruptions && !this._userIdleController.waitingForUser;
1136
+ if (params.enableUserSpeakingFrames) {
1137
+ await this.broadcastFrame(UserStartedSpeakingFrame);
1138
+ }
1139
+ await this._userIdleController.processFrame(new UserStartedSpeakingFrame());
1140
+ if (params.enableInterruptions) {
1141
+ await this.broadcastInterruption();
1142
+ }
1143
+ await this._callEventHandler("on_user_turn_started", strategy);
1144
+ }
1145
+ async _onUserTurnInferenceTriggered(_controller, strategy, speculation = null) {
1146
+ if (this._realtimeServiceMode) {
1147
+ // Realtime mode: the assistant response start, not turn frames, is the
1148
+ // signal that the user turn has ended for context-writing purposes.
1149
+ // Fire the event without pushing aggregation here.
1150
+ logger.debug(`${this}: User turn inference triggered (strategy: ${strategy}) ` +
1151
+ "[realtime mode: event-only, no context push]");
1152
+ await this._callEventHandler("on_user_turn_inference_triggered", strategy);
1153
+ return;
1154
+ }
1155
+ if (speculation) {
1156
+ logger.debug(`${this}: User turn inference triggered speculatively (strategy: ${strategy})`);
1157
+ await this._runSpeculativeInference(speculation);
1158
+ await this._callEventHandler("on_user_turn_inference_triggered", strategy);
1159
+ return;
1160
+ }
1161
+ logger.debug(`${this}: User turn inference triggered (strategy: ${strategy})`);
1162
+ // Push aggregation now: this writes the user message segment to the
1163
+ // context and emits LLMContextFrame, which kicks LLM inference.
1164
+ // Concatenate the segment into `_fullUserTurnAggregation` so multiple
1165
+ // inferences in the same turn don't lose earlier segments from the
1166
+ // eventual `on_user_turn_stopped` event.
1167
+ const segment = await this.pushAggregation();
1168
+ if (segment) {
1169
+ if (this._fullUserTurnAggregation) {
1170
+ this._fullUserTurnAggregation = `${this._fullUserTurnAggregation} ${segment}`.trim();
1171
+ }
1172
+ else {
1173
+ this._fullUserTurnAggregation = segment;
1174
+ }
1175
+ }
1176
+ await this._callEventHandler("on_user_turn_inference_triggered", strategy);
1177
+ }
1178
+ /**
1179
+ * Run an inference for a turn that hasn't ended yet.
1180
+ *
1181
+ * The turn is still open, so the context must not record it. The inference
1182
+ * runs against a provisional copy carrying the speculated turn text:
1183
+ * nothing here mutates the real context, and `_aggregation` keeps
1184
+ * accumulating for whenever the turn does end. The response is held
1185
+ * downstream until the turn is confirmed, and discarded if it isn't.
1186
+ *
1187
+ * @param speculation The speculation to run, from the stop strategy that
1188
+ * started it.
1189
+ */
1190
+ async _runSpeculativeInference(speculation) {
1191
+ const provisional = new LLMContext({
1192
+ messages: [...this._context.messages, { role: this.role, content: speculation.text }],
1193
+ tools: this._context.tools,
1194
+ toolChoice: this._context.toolChoice,
1195
+ });
1196
+ await this.pushFrame(new LLMContextFrame({ context: provisional, speculation: true }));
1197
+ }
1198
+ async _onUserTurnStopped(_controller, strategy, params) {
1199
+ logger.debug(`${this}: User stopped speaking (strategy: ${strategy})`);
1200
+ if (params.enableUserSpeakingFrames) {
1201
+ await this.broadcastFrame(UserStoppedSpeakingFrame);
1202
+ }
1203
+ await this._userIdleController.processFrame(new UserStoppedSpeakingFrame());
1204
+ if (this._realtimeServiceMode) {
1205
+ // Realtime mode: the user message isn't finalized at turn-stop time —
1206
+ // the assistant response start is the effective end-of-turn signal,
1207
+ // and the user message is written then. Content is null here;
1208
+ // subscribers wanting the finalized text use on_user_turn_message_added
1209
+ // instead.
1210
+ const message = new UserTurnStoppedMessage({
1211
+ content: null,
1212
+ timestamp: this._userTurnStartTimestamp,
1213
+ });
1214
+ await this._callEventHandler("on_user_turn_stopped", strategy, message);
1215
+ return;
1216
+ }
1217
+ // A turn end that confirms a speculation has its response already: the
1218
+ // context write is all that's left, and running inference again would
1219
+ // answer the same turn twice.
1220
+ await this._maybeEmitUserTurnStopped({
1221
+ strategy,
1222
+ runLlm: !params.confirmsSpeculation,
1223
+ });
1224
+ }
1225
+ async _onResetAggregation(_controller, strategy) {
1226
+ logger.debug(`${this}: Resetting aggregation (strategy: ${strategy})`);
1227
+ await this.reset();
1228
+ }
1229
+ async _onUserTurnStopTimeout(_controller) {
1230
+ await this._callEventHandler("on_user_turn_stop_timeout");
1231
+ }
1232
+ async _onUserTurnIdle(_controller) {
1233
+ await this._callEventHandler("on_user_turn_idle");
1234
+ }
1235
+ /**
1236
+ * Maybe emit user turn stopped event.
1237
+ *
1238
+ * Earlier inference triggers in the same turn have already pushed their
1239
+ * segments to the context and accumulated them into
1240
+ * `this._fullUserTurnAggregation`. Any aggregation that arrived after the
1241
+ * last inference trigger is flushed here so end-of-turn content is never
1242
+ * lost from the public event.
1243
+ *
1244
+ * @param options.strategy The strategy that triggered the turn stop.
1245
+ * @param options.onSessionEnd If true, only emit if there's unemitted
1246
+ * content (avoids duplicate events when session ends).
1247
+ * @param options.runLlm Whether the context write should run inference.
1248
+ */
1249
+ async _maybeEmitUserTurnStopped(options = {}) {
1250
+ const strategy = options.strategy ?? null;
1251
+ const onSessionEnd = options.onSessionEnd ?? false;
1252
+ const runLlm = options.runLlm ?? true;
1253
+ const segment = await this._pushAggregation({ runLlm });
1254
+ const fullAggregation = this._fullUserTurnAggregation;
1255
+ this._fullUserTurnAggregation = null;
1256
+ let content;
1257
+ if (segment && fullAggregation) {
1258
+ content = `${fullAggregation} ${segment}`.trim();
1259
+ }
1260
+ else {
1261
+ content = fullAggregation || segment;
1262
+ }
1263
+ if (!onSessionEnd || content) {
1264
+ const message = new UserTurnStoppedMessage({
1265
+ content,
1266
+ timestamp: this._userTurnStartTimestamp,
1267
+ });
1268
+ await this._callEventHandler("on_user_turn_stopped", strategy, message);
1269
+ this._userTurnStartTimestamp = "";
1270
+ }
1271
+ const interruptedBot = this._userTurnInterruptedBot;
1272
+ this._userTurnInterruptedBot = false;
1273
+ if (content) {
1274
+ this._consecutiveEmptyUserTurnRecoveries = 0;
1275
+ }
1276
+ else if (!onSessionEnd) {
1277
+ // An empty turn doesn't run the LLM, so the bot stays silent unless a
1278
+ // recovery runs it. Without one, restart the idle timer, which this
1279
+ // turn's start cancelled.
1280
+ if (!(await this._maybeRecoverEmptyUserTurn(interruptedBot))) {
1281
+ await this._userIdleController.waitForUser();
1282
+ }
1283
+ }
1284
+ }
1285
+ /**
1286
+ * Run the LLM for a user turn that ended with no transcript.
1287
+ *
1288
+ * @param interruptedBot Whether the turn interrupted a response in progress.
1289
+ * @returns Whether the LLM was asked to respond.
1290
+ */
1291
+ async _maybeRecoverEmptyUserTurn(interruptedBot) {
1292
+ const config = this._params.emptyUserTurn;
1293
+ if (!config) {
1294
+ return false;
1295
+ }
1296
+ const prompt = interruptedBot ? config.interruptedPrompt : config.idlePrompt;
1297
+ if (!prompt) {
1298
+ return false;
1299
+ }
1300
+ if (this._consecutiveEmptyUserTurnRecoveries >= config.maxConsecutiveRecoveries) {
1301
+ logger.debug(`${this}: Empty user turn left unanswered (too many in a row)`);
1302
+ return false;
1303
+ }
1304
+ // A pending function call result runs the LLM itself, and a muted user
1305
+ // shouldn't be prompted to speak.
1306
+ if (this._userIsMuted || this._userIdleController.functionCallsInProgress) {
1307
+ return false;
1308
+ }
1309
+ logger.debug(`${this}: Empty user turn (${interruptedBot ? "interrupted" : "idle"}), running LLM`);
1310
+ this._consecutiveEmptyUserTurnRecoveries += 1;
1311
+ this._context.addMessage({ role: "developer", content: prompt });
1312
+ await this.pushContextFrame();
1313
+ return true;
1314
+ }
1315
+ }
1316
+ /**
1317
+ * Assistant LLM aggregator that processes bot responses and function calls.
1318
+ *
1319
+ * This aggregator handles the complex logic of processing assistant
1320
+ * responses including:
1321
+ *
1322
+ * - Text frame aggregation between response start/end markers
1323
+ * - Function call lifecycle management
1324
+ * - Context updates with timestamps
1325
+ * - Tool execution and result handling
1326
+ * - Interruption handling during responses
1327
+ *
1328
+ * The aggregator manages function calls in progress and coordinates between
1329
+ * text generation and tool execution phases of LLM responses.
1330
+ *
1331
+ * Event handlers available:
1332
+ *
1333
+ * - on_assistant_turn_started: Called when the assistant turn starts
1334
+ * - on_assistant_turn_stopped: Called when the assistant turn ends
1335
+ * - on_assistant_thought: Called when an assistant thought is available
1336
+ * - on_assistant_backchannel: Called when the assistant starts saying text of
1337
+ * type `backchannel`, such as a `BotBackchannelFrame`
1338
+ * - on_summary_applied: Called when a context summarization is applied
1339
+ *
1340
+ * @example
1341
+ * aggregator.eventHandler("on_assistant_turn_started", async (aggregator) => {});
1342
+ * aggregator.eventHandler(
1343
+ * "on_assistant_turn_stopped",
1344
+ * async (aggregator, message: AssistantTurnStoppedMessage) => {},
1345
+ * );
1346
+ * aggregator.eventHandler("on_assistant_thought", async (aggregator, message: AssistantThoughtMessage) => {});
1347
+ * aggregator.eventHandler(
1348
+ * "on_assistant_backchannel",
1349
+ * async (aggregator, message: AssistantBackchannelMessage) => {},
1350
+ * );
1351
+ * aggregator.eventHandler(
1352
+ * "on_summary_applied",
1353
+ * async (aggregator, summarizer: LLMContextSummarizer, event: SummaryAppliedEvent) => {},
1354
+ * );
1355
+ */
1356
+ export class LLMAssistantAggregator extends LLMContextAggregator {
1357
+ _params;
1358
+ // Realtime-mode wiring. null (the default) auto-configures from service
1359
+ // metadata, mirroring the user half (see LLMUserAggregator constructor).
1360
+ _realtimeServiceMode;
1361
+ _pairedUserAggregator;
1362
+ _functionCallsInProgress = new Map();
1363
+ _functionCallsImageResults = new Map();
1364
+ // Markers seen on LLMMarkerFrames, so a service configured with markers
1365
+ // other than the defaults still gets them stripped from transcripts.
1366
+ _seenTurnMarkers = new Set();
1367
+ _contextUpdatedTasks = new Set();
1368
+ _userSpeaking = false;
1369
+ _botSpeaking = false;
1370
+ // When a function call result arrives while the bot is speaking, we defer
1371
+ // the LLM re-invocation until the bot stops speaking. This flag is set to
1372
+ // true in that case so that `BotStoppedSpeakingFrame` knows to push a
1373
+ // context frame. Multiple results arriving in the same speaking window are
1374
+ // bundled into a single deferred push.
1375
+ _pushContextOnBotStoppedSpeaking = false;
1376
+ // A function call result asked to run inference and the push hasn't
1377
+ // happened yet: it was held for results still queued, or for a user who is
1378
+ // speaking. Whichever result is handled once the way is clear makes the
1379
+ // push, whatever its own `runLlm` says. A run that starts meanwhile, such
1380
+ // as the one the user's turn brings, settles it: that run sees the context
1381
+ // as it stands. A user turn with nothing in it brings no run, so the push
1382
+ // stays owed until the next arrival.
1383
+ _contextPushOwed = false;
1384
+ _assistantTurnStartTimestamp = "";
1385
+ _thoughtAppendToContext = false;
1386
+ _thoughtLlm = "";
1387
+ _thoughtAggregation = [];
1388
+ _thoughtStartTime = "";
1389
+ _summarizer;
1390
+ /**
1391
+ * Initialize the assistant context aggregator.
1392
+ *
1393
+ * @param context The LLM context for conversation storage.
1394
+ */
1395
+ constructor(context, options = {}) {
1396
+ const params = options.params || new LLMAssistantAggregatorParams();
1397
+ super({
1398
+ ...options,
1399
+ context,
1400
+ role: "assistant",
1401
+ addToolChangeMessages: params.addToolChangeMessages,
1402
+ });
1403
+ this._params = params;
1404
+ this._realtimeServiceMode = options._realtimeServiceMode ?? null;
1405
+ this._pairedUserAggregator = options._pairedUserAggregator ?? null;
1406
+ // Context summarization — always create the summarizer so that manually
1407
+ // pushed LLMSummarizeContextFrame frames are always handled.
1408
+ // Auto-triggering based on thresholds is only enabled when
1409
+ // enableAutoContextSummarization is true.
1410
+ this._summarizer = new LLMContextSummarizer({
1411
+ context: this._context,
1412
+ config: this._params.autoContextSummarizationConfig,
1413
+ autoTrigger: this._params.enableAutoContextSummarization,
1414
+ });
1415
+ this._summarizer.addEventHandler("on_request_summarization", (summarizer, frame) => this._onRequestSummarization(summarizer, frame));
1416
+ this._summarizer.addEventHandler("on_summary_applied", (summarizer, event) => this._onSummaryApplied(summarizer, event));
1417
+ this._registerEventHandler("on_assistant_turn_started");
1418
+ this._registerEventHandler("on_assistant_turn_stopped");
1419
+ this._registerEventHandler("on_assistant_thought");
1420
+ this._registerEventHandler("on_assistant_backchannel");
1421
+ this._registerEventHandler("on_summary_applied");
1422
+ }
1423
+ /** Whether there are any function calls currently in progress. */
1424
+ get hasFunctionCallsInProgress() {
1425
+ return this._functionCallsInProgress.size > 0;
1426
+ }
1427
+ /**
1428
+ * Whether a synchronous call is in flight, one whose result the model waits for.
1429
+ *
1430
+ * A synchronous call leaves the context without its result until it
1431
+ * returns, so the model cannot run in the meantime; an asynchronous one
1432
+ * (`cancelOnInterruption: false`) has a placeholder result in the context
1433
+ * and the model runs on. A call announced but not yet started counts as
1434
+ * synchronous.
1435
+ */
1436
+ get hasSynchronousFunctionCallsInProgress() {
1437
+ for (const frame of this._functionCallsInProgress.values()) {
1438
+ if (frame === null || frame.cancelOnInterruption) {
1439
+ return true;
1440
+ }
1441
+ }
1442
+ return false;
1443
+ }
1444
+ /**
1445
+ * Set up the aggregator.
1446
+ *
1447
+ * @param setup Configuration object containing setup parameters.
1448
+ */
1449
+ async setup(setup) {
1450
+ await super.setup(setup);
1451
+ if (this._summarizer) {
1452
+ await this._summarizer.setup(setup);
1453
+ }
1454
+ }
1455
+ /** Release this aggregator's resources at teardown. */
1456
+ async cleanup() {
1457
+ await super.cleanup();
1458
+ await this._cleanup();
1459
+ }
1460
+ /** Reset the aggregation state. */
1461
+ async reset() {
1462
+ await super.reset();
1463
+ await this._resetThoughtAggregation(); // Just to be safe
1464
+ this._pushContextOnBotStoppedSpeaking = false;
1465
+ this._contextPushOwed = false;
1466
+ }
1467
+ /** Reset the thought aggregation state. */
1468
+ async _resetThoughtAggregation() {
1469
+ this._thoughtAppendToContext = false;
1470
+ this._thoughtLlm = "";
1471
+ this._thoughtAggregation = [];
1472
+ }
1473
+ /**
1474
+ * Process frames for assistant response aggregation and function call management.
1475
+ *
1476
+ * @param frame The frame to process.
1477
+ * @param direction The direction of frame flow in the pipeline.
1478
+ */
1479
+ async processFrame(frame, direction) {
1480
+ await super.processFrame(frame, direction);
1481
+ if (frame instanceof StartFrame) {
1482
+ // Push StartFrame before start(), because we want StartFrame to be
1483
+ // processed by every processor before any other frame is processed.
1484
+ await this.pushFrame(frame, direction);
1485
+ await this._start(frame);
1486
+ }
1487
+ else if (frame instanceof InterruptionFrame) {
1488
+ await this._handleInterruptions(frame);
1489
+ await this.pushFrame(frame, direction);
1490
+ }
1491
+ else if (frame instanceof EndFrame || frame instanceof CancelFrame) {
1492
+ await this._handleEndOrCancel(frame);
1493
+ await this.pushFrame(frame, direction);
1494
+ }
1495
+ else if (frame instanceof LLMAssistantPushAggregationFrame) {
1496
+ await this._handlePushAggregation();
1497
+ }
1498
+ else if (frame instanceof TTSStartedFrame) {
1499
+ await this._handleTtsStarted(frame);
1500
+ await this.pushFrame(frame, direction);
1501
+ }
1502
+ else if (frame instanceof LLMFullResponseStartFrame) {
1503
+ this._contextPushOwed = false;
1504
+ await this._handleLlmStart(frame);
1505
+ }
1506
+ else if (frame instanceof LLMFullResponseEndFrame) {
1507
+ await this._handleLlmEnd(frame);
1508
+ }
1509
+ else if (frame instanceof TextFrame) {
1510
+ await this._handleText(frame);
1511
+ }
1512
+ else if (frame instanceof LLMMarkerFrame) {
1513
+ await this._handleMarkerFrame(frame);
1514
+ }
1515
+ else if (frame instanceof LLMThoughtStartFrame) {
1516
+ await this._handleThoughtStart(frame);
1517
+ }
1518
+ else if (frame instanceof LLMThoughtTextFrame) {
1519
+ await this._handleThoughtText(frame);
1520
+ }
1521
+ else if (frame instanceof LLMThoughtEndFrame) {
1522
+ await this._handleThoughtEnd(frame);
1523
+ }
1524
+ else if (frame instanceof LLMRunFrame) {
1525
+ await this._handleLlmRun(frame);
1526
+ }
1527
+ else if (frame instanceof LLMMessagesAppendFrame) {
1528
+ await this._handleLlmMessagesAppend(frame);
1529
+ }
1530
+ else if (frame instanceof LLMMessagesUpdateFrame) {
1531
+ await this._handleLlmMessagesUpdate(frame);
1532
+ }
1533
+ else if (frame instanceof LLMMessagesTransformFrame) {
1534
+ await this._handleLlmMessagesTransform(frame);
1535
+ }
1536
+ else if (frame instanceof LLMSetToolsFrame) {
1537
+ // Normalize and validate (a plain list of direct functions /
1538
+ // FunctionSchema objects becomes a ToolsSchema) so the tool-change diff
1539
+ // and setTools see a consistent type.
1540
+ const normalizedTools = LLMContext._normalizeAndValidateTools(frame.tools);
1541
+ this._maybeAddToolChangeMessages(normalizedTools);
1542
+ this.setTools(normalizedTools);
1543
+ }
1544
+ else if (frame instanceof LLMSetToolChoiceFrame) {
1545
+ this.setToolChoice(frame.toolChoice);
1546
+ }
1547
+ else if (frame instanceof FunctionCallsStartedFrame) {
1548
+ await this._handleFunctionCallsStarted(frame);
1549
+ }
1550
+ else if (frame instanceof FunctionCallInProgressFrame) {
1551
+ await this._handleFunctionCallInProgress(frame);
1552
+ }
1553
+ else if (frame instanceof FunctionCallResultFrame) {
1554
+ await this._handleFunctionCallResult(frame);
1555
+ }
1556
+ else if (frame instanceof FunctionCallCancelFrame) {
1557
+ await this._handleFunctionCallCancel(frame);
1558
+ }
1559
+ else if (frame instanceof UserImageRawFrame) {
1560
+ await this._handleUserImageFrame(frame);
1561
+ }
1562
+ else if (frame instanceof UserFileRawFrame) {
1563
+ await this._handleUserFileFrame(frame);
1564
+ }
1565
+ else if (frame instanceof AssistantImageRawFrame) {
1566
+ await this._handleAssistantImageFrame(frame);
1567
+ }
1568
+ else if (frame instanceof UserStartedSpeakingFrame) {
1569
+ this._userSpeaking = true;
1570
+ await this.pushFrame(frame, direction);
1571
+ }
1572
+ else if (frame instanceof UserStoppedSpeakingFrame) {
1573
+ this._userSpeaking = false;
1574
+ await this.pushFrame(frame, direction);
1575
+ }
1576
+ else if (frame instanceof BotStartedSpeakingFrame) {
1577
+ this._botSpeaking = true;
1578
+ await this.pushFrame(frame, direction);
1579
+ }
1580
+ else if (frame instanceof BotStoppedSpeakingFrame) {
1581
+ this._botSpeaking = false;
1582
+ await this.pushFrame(frame, direction);
1583
+ if (this._pushContextOnBotStoppedSpeaking && !this._userSpeaking) {
1584
+ logger.debug(`${this}: Bot stopped speaking — pushing deferred context frame!`);
1585
+ await this.pushContextFrame(FrameDirection.UPSTREAM);
1586
+ }
1587
+ }
1588
+ else if (frame instanceof LLMServiceMetadataFrame) {
1589
+ // Auto-configure realtime mode on the assistant half too — the
1590
+ // broadcast reaches both halves. The assistant only needs the flag for
1591
+ // its trailing flush; the user half owns the strategy mutations.
1592
+ if (frame.isRealtimeService && this._realtimeServiceMode === null) {
1593
+ this._realtimeServiceMode = true;
1594
+ // The mode just auto-enabled: the trailing flush needs the
1595
+ // back-reference to the user half (always wired by the pair — this
1596
+ // surfaces unsupported direct construction instead of silently
1597
+ // dropping user messages).
1598
+ this._requirePairedUserAggregator();
1599
+ }
1600
+ await this.pushFrame(frame, direction);
1601
+ }
1602
+ else if (frame instanceof ProposedUserStartedSpeakingFrame) {
1603
+ // A broadcast sends a copy each way, so a proposal from a service
1604
+ // sitting between the two halves — a realtime LLM — reaches the user
1605
+ // half as the upstream copy while this copy travels on. A proposal
1606
+ // should be resolved once, so stop it here if the user half resolves
1607
+ // it. No standard pipeline has a resolver downstream of this half, so
1608
+ // this guards the invariant rather than fixing an observed escape.
1609
+ const user = this._pairedUserAggregator;
1610
+ if (!user?._userTurnController.resolvesProposedTurnStartFrames) {
1611
+ await this.pushFrame(frame, direction);
1612
+ }
1613
+ }
1614
+ else if (frame instanceof ProposedUserStoppedSpeakingFrame) {
1615
+ const user = this._pairedUserAggregator;
1616
+ if (!user?._userTurnController.resolvesProposedTurnStopFrames) {
1617
+ await this.pushFrame(frame, direction);
1618
+ }
1619
+ }
1620
+ else {
1621
+ await this.pushFrame(frame, direction);
1622
+ }
1623
+ // Pass frames to summarizer for monitoring
1624
+ if (this._summarizer) {
1625
+ await this._summarizer.processFrame(frame);
1626
+ }
1627
+ }
1628
+ async _start(_frame) {
1629
+ if (this._realtimeServiceMode) {
1630
+ this._requirePairedUserAggregator();
1631
+ }
1632
+ }
1633
+ /**
1634
+ * Throw if realtime mode is active without a paired user aggregator.
1635
+ *
1636
+ * Realtime mode needs the assistant half to hold a back-reference to the
1637
+ * user half so it can flush the user message on `LLMFullResponseStartFrame`.
1638
+ * The pair wires this unconditionally (so the halves are always configured
1639
+ * together); this guards unsupported direct construction, surfacing the
1640
+ * missing flush loudly instead of silently dropping user messages. Checked
1641
+ * at `StartFrame` for an explicit mode and again when the mode auto-enables.
1642
+ *
1643
+ * @throws Error (upstream: RuntimeError) when no user aggregator is paired.
1644
+ */
1645
+ _requirePairedUserAggregator() {
1646
+ if (this._pairedUserAggregator === null) {
1647
+ throw new Error(`${this}: realtime_service_mode is enabled but this assistant ` +
1648
+ "aggregator has no paired user aggregator. Build both aggregators " +
1649
+ "with LLMContextAggregatorPair(context) rather than constructing " +
1650
+ "them individually, so realtime mode is configured once and shared " +
1651
+ "across both halves.");
1652
+ }
1653
+ }
1654
+ /** Push the current assistant aggregation with timestamp. */
1655
+ async pushAggregation() {
1656
+ const aggregation = await this._addAggregationToContext();
1657
+ if (!aggregation) {
1658
+ return "";
1659
+ }
1660
+ // Push context frame
1661
+ await this.pushContextFrame();
1662
+ // Push timestamp frame with current time
1663
+ const timestampFrame = new LLMContextAssistantTimestampFrame({ timestamp: timeNowIso8601() });
1664
+ await this.pushFrame(timestampFrame);
1665
+ return aggregation;
1666
+ }
1667
+ /**
1668
+ * Commit the held assistant aggregation as a context message, pushing no frames.
1669
+ *
1670
+ * For flushes in the middle of an open assistant turn (a user file or image
1671
+ * arriving mid-reply), where the downstream context and timestamp frames —
1672
+ * and pushContextFrame()'s side effects — belong to the turn's real end,
1673
+ * not to this commit.
1674
+ *
1675
+ * @returns The committed aggregation, or an empty string if none was held.
1676
+ */
1677
+ async _addAggregationToContext() {
1678
+ if (this._aggregation.length === 0) {
1679
+ return "";
1680
+ }
1681
+ const aggregation = this.aggregationString();
1682
+ await this.reset();
1683
+ this._context.addMessage({ role: "assistant", content: aggregation });
1684
+ return aggregation;
1685
+ }
1686
+ /**
1687
+ * Push a context frame in the specified direction.
1688
+ *
1689
+ * @param direction The direction to push the frame (upstream or downstream).
1690
+ */
1691
+ async pushContextFrame(direction = FrameDirection.DOWNSTREAM) {
1692
+ await super.pushContextFrame(direction);
1693
+ this._pushContextOnBotStoppedSpeaking = false;
1694
+ this._contextPushOwed = false;
1695
+ }
1696
+ async _handleLlmRun(_frame) {
1697
+ await this.pushContextFrame(FrameDirection.UPSTREAM);
1698
+ }
1699
+ async _handleLlmMessagesAppend(frame) {
1700
+ // An LLMMessagesAppendFrame with runLlm=true is treated as a
1701
+ // FunctionCallResultFrame with runLlm=true is: held while the bot speaks,
1702
+ // owed while the user speaks, and bundled with the appends and results
1703
+ // queued behind it. Run at once, a message appended mid-sentence runs the
1704
+ // model on a context that lacks the answer in progress, and the model
1705
+ // answers it again.
1706
+ this.addMessages(frame.messages);
1707
+ if (frame.runLlm) {
1708
+ this._contextPushOwed = true;
1709
+ }
1710
+ if (this._contextPushOwed && !this._userSpeaking) {
1711
+ await this._maybePushContext();
1712
+ }
1713
+ else if (this._contextPushOwed) {
1714
+ logger.debug(`${this}: User is speaking — context frame push owed until the turn ends.`);
1715
+ }
1716
+ }
1717
+ async _handleLlmMessagesUpdate(frame) {
1718
+ this.setMessages(frame.messages);
1719
+ if (frame.runLlm) {
1720
+ await this.pushContextFrame(FrameDirection.UPSTREAM);
1721
+ }
1722
+ }
1723
+ async _handleLlmMessagesTransform(frame) {
1724
+ this.transformMessages(frame.transform);
1725
+ if (frame.runLlm) {
1726
+ await this.pushContextFrame(FrameDirection.UPSTREAM);
1727
+ }
1728
+ }
1729
+ async _handleInterruptions(_frame) {
1730
+ await this._triggerAssistantTurnStopped({ interrupted: true });
1731
+ await this.reset();
1732
+ }
1733
+ async _handleEndOrCancel(frame) {
1734
+ await this._triggerAssistantTurnStopped({ interrupted: frame instanceof CancelFrame });
1735
+ await this._cleanup();
1736
+ }
1737
+ async _cleanup() {
1738
+ // Cancel any in-flight onContextUpdated callback tasks and tear down the
1739
+ // summarizer. Idempotent so it is safe to run from both the
1740
+ // EndFrame/CancelFrame handler and cleanup().
1741
+ for (const task of [...this._contextUpdatedTasks]) {
1742
+ await this.cancelTask(task);
1743
+ }
1744
+ this._contextUpdatedTasks.clear();
1745
+ if (this._summarizer) {
1746
+ await this._summarizer.cleanup();
1747
+ }
1748
+ }
1749
+ async _handleFunctionCallsStarted(frame) {
1750
+ const functionNames = frame.functionCalls.map((f) => `${f.functionName}:${f.toolCallId}`);
1751
+ logger.debug(`${this} FunctionCallsStartedFrame: ${reprList(functionNames)}`);
1752
+ for (const functionCall of frame.functionCalls) {
1753
+ this._functionCallsInProgress.set(functionCall.toolCallId, null);
1754
+ }
1755
+ }
1756
+ async _handleFunctionCallInProgress(frame) {
1757
+ logger.debug(`${this} FunctionCallInProgressFrame: [${frame.functionName}:${frame.toolCallId}]`);
1758
+ // Update context with the in-progress function call
1759
+ this._context.addMessage({
1760
+ role: "assistant",
1761
+ tool_calls: [
1762
+ {
1763
+ id: frame.toolCallId,
1764
+ function: {
1765
+ name: frame.functionName,
1766
+ arguments: jsonDumps(frame.arguments),
1767
+ },
1768
+ type: "function",
1769
+ },
1770
+ ],
1771
+ });
1772
+ const isAsync = !frame.cancelOnInterruption;
1773
+ if (isAsync) {
1774
+ this._context.addMessage(asyncToolMessages.buildStartedMessage(frame.toolCallId));
1775
+ }
1776
+ else {
1777
+ this._context.addMessage({
1778
+ role: "tool",
1779
+ content: "IN_PROGRESS",
1780
+ tool_call_id: frame.toolCallId,
1781
+ });
1782
+ }
1783
+ this._functionCallsInProgress.set(frame.toolCallId, frame);
1784
+ }
1785
+ async _handleFunctionCallResult(frame) {
1786
+ logger.debug(`${this} FunctionCallResultFrame: [${frame.functionName}:${frame.toolCallId}]`);
1787
+ if (!this._functionCallsInProgress.has(frame.toolCallId)) {
1788
+ logger.warning(`FunctionCallResultFrame tool_call_id [${frame.toolCallId}] is not running`);
1789
+ return;
1790
+ }
1791
+ const inProgressFrame = this._functionCallsInProgress.get(frame.toolCallId) ?? null;
1792
+ if (inProgressFrame === null) {
1793
+ // Started, but its in-progress frame hasn't arrived: there's nothing
1794
+ // yet to attach the result to.
1795
+ logger.warning(`FunctionCallResultFrame tool_call_id [${frame.toolCallId}] is not in progress`);
1796
+ return;
1797
+ }
1798
+ const groupId = inProgressFrame.groupId;
1799
+ const properties = frame.properties;
1800
+ const isFinal = frame.properties ? frame.properties.isFinal : true;
1801
+ if (isFinal) {
1802
+ await this._handleFunctionCallFinished(frame, inProgressFrame);
1803
+ }
1804
+ else {
1805
+ await this._handleFunctionCallIntermediateResult(frame, inProgressFrame);
1806
+ }
1807
+ let runLlm = false;
1808
+ // Append any images that were generated by function calls.
1809
+ const imageFrame = this._functionCallsImageResults.get(frame.toolCallId);
1810
+ if (imageFrame !== undefined) {
1811
+ this._functionCallsImageResults.delete(frame.toolCallId);
1812
+ // If an image frame has been added to the context, let's run inference.
1813
+ runLlm = await this._maybeAppendImageToContext(imageFrame);
1814
+ }
1815
+ // Run inference if the function call result requires it.
1816
+ if (isTruthy(frame.result)) {
1817
+ if (properties && properties.runLlm !== null && properties.runLlm !== undefined) {
1818
+ // If the tool call result has a runLlm property, use it.
1819
+ runLlm = properties.runLlm;
1820
+ }
1821
+ else if (frame.runLlm !== null && frame.runLlm !== undefined) {
1822
+ // If the frame is indicating we should run the LLM, do it.
1823
+ runLlm = frame.runLlm;
1824
+ }
1825
+ else {
1826
+ // Run the LLM when this is the last function call in the group to
1827
+ // complete. If groupId is set, only consider sibling calls; otherwise
1828
+ // always execute as soon as we receive the result.
1829
+ if (groupId) {
1830
+ runLlm = ![...this._functionCallsInProgress.values()].some((f) => f !== null &&
1831
+ f.groupId === groupId &&
1832
+ // We are now able to receive "updates", so the current frame
1833
+ // can still be in the in progress list, and we need to ignore it.
1834
+ f.toolCallId !== frame.toolCallId);
1835
+ }
1836
+ else {
1837
+ runLlm = true;
1838
+ }
1839
+ }
1840
+ }
1841
+ if (runLlm) {
1842
+ this._contextPushOwed = true;
1843
+ }
1844
+ if (this._contextPushOwed && !this._userSpeaking) {
1845
+ await this._maybePushContext();
1846
+ }
1847
+ // Call the `onContextUpdated` callback once the function call result is
1848
+ // added to the context. Also, run this in a separate task to make sure we
1849
+ // don't block the pipeline.
1850
+ const onContextUpdated = properties?.onContextUpdated;
1851
+ if (onContextUpdated) {
1852
+ const workerName = `${frame.functionName}:${frame.toolCallId}:on_context_updated`;
1853
+ const task = this.createTask(() => onContextUpdated(), workerName);
1854
+ this._contextUpdatedTasks.add(task);
1855
+ task.addDoneCallback((t) => this._contextUpdatedTaskFinished(t));
1856
+ }
1857
+ }
1858
+ /**
1859
+ * Decide whether to push a context frame after a function call settles or a message is appended.
1860
+ *
1861
+ * Push an `LLMContextFrame` upstream (with care to avoid duplicate pushes
1862
+ * while results or appends are queued or the bot is still speaking).
1863
+ * Cascade LLMs use the context frame to re-run inference with the new tool
1864
+ * result in scope. Realtime LLMs read the new tool result out of the
1865
+ * context the same way — they don't get function results from
1866
+ * `FunctionCallResultFrame` directly — so the same push is load-bearing for
1867
+ * both modes.
1868
+ */
1869
+ async _maybePushContext() {
1870
+ if (this.hasQueuedFrame(FunctionCallResultFrame) ||
1871
+ this.hasQueuedFrame(LLMMessagesAppendFrame)) {
1872
+ // Another result or append is already queued. Defer the context push to
1873
+ // bundle them all into a single LLM call instead of triggering one
1874
+ // inference pass per frame. The push is owed until it happens, so the
1875
+ // last frame in the queue makes it whether or not that frame asks to
1876
+ // run — a burst can end with an intermediate result, or an
1877
+ // LLMMessagesAppendFrame with runLlm=false, that doesn't.
1878
+ logger.debug(`${this}: More results or appends queued — deferring context frame push.`);
1879
+ }
1880
+ else if (this._botSpeaking) {
1881
+ // Defer the context frame push until the bot finishes speaking. If
1882
+ // multiple function call results arrive while the bot is speaking, they
1883
+ // all accumulate in the context and a single push is performed once
1884
+ // speaking stops, preventing the LLM from running multiple times and
1885
+ // producing duplicated responses. This should be an edge case, since it
1886
+ // would require a FunctionCallResultFrame being queued between an LLM
1887
+ // response start and end frame.
1888
+ logger.debug(`${this}: Bot is speaking — deferring context frame push.`);
1889
+ this._pushContextOnBotStoppedSpeaking = true;
1890
+ }
1891
+ else {
1892
+ logger.debug(`${this}: Pushing context frame!`);
1893
+ await this.pushContextFrame(FrameDirection.UPSTREAM);
1894
+ }
1895
+ }
1896
+ /**
1897
+ * Handle an intermediate result for an async function call.
1898
+ *
1899
+ * Injects an intermediate developer message into the context without
1900
+ * removing the call from the in-progress map.
1901
+ */
1902
+ async _handleFunctionCallIntermediateResult(frame, _inProgressFrame) {
1903
+ if (!isTruthy(frame.result)) {
1904
+ logger.warning(`${this} result_callback called with is_final=False but no result!`);
1905
+ return;
1906
+ }
1907
+ const result = jsonDumps(frame.result);
1908
+ this._context.addMessage(asyncToolMessages.buildIntermediateResultMessage(frame.toolCallId, result));
1909
+ }
1910
+ /**
1911
+ * Handle the final result of a function call.
1912
+ *
1913
+ * Removes the call from the in-progress map, updates the context, and
1914
+ * triggers LLM inference when appropriate.
1915
+ */
1916
+ async _handleFunctionCallFinished(frame, inProgressFrame) {
1917
+ const deferred = this._isDeferred(inProgressFrame);
1918
+ this._functionCallsInProgress.delete(frame.toolCallId);
1919
+ const result = isTruthy(frame.result) ? jsonDumps(frame.result) : "COMPLETED";
1920
+ if (deferred) {
1921
+ // The conversation moved on while the call ran, so the LLM is told
1922
+ // about the result in a developer message rather than in the tool
1923
+ // message it has already seen.
1924
+ this._context.addMessage(asyncToolMessages.buildFinalResultMessage(frame.toolCallId, result));
1925
+ }
1926
+ else {
1927
+ this._updateFunctionCallResult(frame.functionName, frame.toolCallId, result);
1928
+ }
1929
+ }
1930
+ async _handleFunctionCallCancel(frame) {
1931
+ logger.debug(`${this} FunctionCallCancelFrame: [${frame.functionName}:${frame.toolCallId}]`);
1932
+ const functionCall = this._functionCallsInProgress.get(frame.toolCallId);
1933
+ if (!functionCall) {
1934
+ return;
1935
+ }
1936
+ // Update context with the function call cancellation. An async call is
1937
+ // settled with a developer message, the same channel its results use,
1938
+ // whether or not the conversation moved on, since the notice says the
1939
+ // call did not complete and asks for the user to be told.
1940
+ if (functionCall.cancelOnInterruption) {
1941
+ this._updateFunctionCallResult(frame.functionName, frame.toolCallId, "CANCELLED");
1942
+ }
1943
+ else {
1944
+ this._context.addMessage(asyncToolMessages.buildCancelledMessage(frame.toolCallId));
1945
+ }
1946
+ const groupId = functionCall.groupId;
1947
+ this._functionCallsInProgress.delete(frame.toolCallId);
1948
+ if (!frame.runLlm || this._userSpeaking) {
1949
+ return;
1950
+ }
1951
+ // Hold off while siblings from the same LLM response are still running:
1952
+ // whichever of them settles last runs inference, with this cancellation
1953
+ // already in the context.
1954
+ if (groupId &&
1955
+ [...this._functionCallsInProgress.values()].some((f) => f !== null && f.groupId === groupId)) {
1956
+ return;
1957
+ }
1958
+ await this._maybePushContext();
1959
+ }
1960
+ async _handleUserImageFrame(frame) {
1961
+ let imageAppended = false;
1962
+ // Check if this image is a result of a function call.
1963
+ if (frame.request?.toolCallId && this._functionCallsInProgress.has(frame.request.toolCallId)) {
1964
+ this._functionCallsImageResults.set(frame.request.toolCallId, frame);
1965
+ // Call the resultCallback if provided. This signals that the image has
1966
+ // been retrieved and the function call can now complete.
1967
+ if (frame.request.resultCallback) {
1968
+ const resultCallback = frame.request.resultCallback;
1969
+ await resultCallback(null);
1970
+ }
1971
+ }
1972
+ else {
1973
+ // Commit any in-progress assistant aggregation before appending, so the
1974
+ // image message lands after it in the context. Context-only: the turn
1975
+ // stays open and its frames fire at the turn's real end.
1976
+ await this._addAggregationToContext();
1977
+ imageAppended = await this._maybeAppendImageToContext(frame);
1978
+ }
1979
+ if (imageAppended && frame.runLlm !== false) {
1980
+ await this.pushContextFrame(FrameDirection.UPSTREAM);
1981
+ }
1982
+ }
1983
+ async _handleUserFileFrame(frame) {
1984
+ // TODO: Should this have a similar function-call check like _handleUserImageFrame?
1985
+ if (!frame.appendToContext) {
1986
+ return;
1987
+ }
1988
+ // Commit any in-progress assistant aggregation before appending, so the
1989
+ // file message lands after it in the context. The order matters beyond
1990
+ // chronology: LLMContext.removeInvalidFileMessage() only considers
1991
+ // messages after the last assistant message, so a file message written
1992
+ // before a later-committed assistant message could never be cleaned up if
1993
+ // the provider rejects it. Context-only: the assistant turn stays open,
1994
+ // any remaining reply text commits at the turn's end, and the turn's
1995
+ // frames fire there.
1996
+ await this._addAggregationToContext();
1997
+ logger.debug(`${this} Appending UserFileRawFrame to LLM context (format: ${frame.format})`);
1998
+ await this._context.addFileFrameMessage({
1999
+ type: frame.type,
2000
+ format: frame.format,
2001
+ text: frame.text,
2002
+ file: frame.file,
2003
+ name: frame.filename,
2004
+ // TODO: pass customOptions through to adapters via the universal message
2005
+ });
2006
+ if (frame.runLlm !== false) {
2007
+ await this.pushContextFrame(FrameDirection.UPSTREAM);
2008
+ }
2009
+ }
2010
+ async _handleAssistantImageFrame(frame) {
2011
+ logger.debug(`${this} Appending AssistantImageRawFrame to LLM context (size: (${frame.size.join(", ")}))`);
2012
+ if (frame.originalData && frame.originalMimeType) {
2013
+ await this._context.addImageFrameMessage({
2014
+ format: frame.originalMimeType,
2015
+ size: frame.size, // Technically doesn't matter, since already encoded
2016
+ image: frame.originalData,
2017
+ role: "assistant",
2018
+ });
2019
+ }
2020
+ else {
2021
+ if (frame.format === null) {
2022
+ throw new Error("AssistantImageRawFrame.format must not be null");
2023
+ }
2024
+ await this._context.addImageFrameMessage({
2025
+ format: frame.format,
2026
+ size: frame.size,
2027
+ image: frame.image,
2028
+ role: "assistant",
2029
+ });
2030
+ }
2031
+ }
2032
+ async _handleLlmStart(_frame) {
2033
+ // Realtime mode treats LLMFullResponseStartFrame as the user turn's end
2034
+ // signal for context-writing purposes — see _realtimeHandleLlmStart.
2035
+ if (this._realtimeServiceMode) {
2036
+ await this._realtimeHandleLlmStart();
2037
+ return;
2038
+ }
2039
+ await this._triggerAssistantTurnStarted();
2040
+ }
2041
+ /**
2042
+ * Realtime mode: commit the paired user message, then start the assistant turn.
2043
+ *
2044
+ * Realtime mode treats `LLMFullResponseStartFrame` as the user turn's end
2045
+ * signal for context-writing purposes (in place of
2046
+ * `UserStoppedSpeakingFrame`). The paired user aggregator may defer the
2047
+ * actual write for one `ttfsP99Latency` window to allow a late-arriving
2048
+ * transcript to land.
2049
+ */
2050
+ async _realtimeHandleLlmStart() {
2051
+ if (this._pairedUserAggregator !== null) {
2052
+ await this._pairedUserAggregator._realtimeHandoffFlush();
2053
+ }
2054
+ await this._triggerAssistantTurnStarted();
2055
+ }
2056
+ async _handleLlmEnd(_frame) {
2057
+ // Realtime mode failsafe: by the time the assistant response fully
2058
+ // completes, even slow transcripts should have landed. Cancel any
2059
+ // still-pending deferred handoff flush and commit the user message now so
2060
+ // it precedes the assistant message in context.
2061
+ if (this._realtimeServiceMode && this._pairedUserAggregator !== null) {
2062
+ await this._pairedUserAggregator._realtimeHandoffFlushImmediate();
2063
+ }
2064
+ await this._triggerAssistantTurnStopped();
2065
+ }
2066
+ async _handleTtsStarted(frame) {
2067
+ // If this TTS output will be written to context and we don't already have
2068
+ // an open assistant turn, open one. This handles the case of
2069
+ // TTSSpeakFrame-driven utterances that have no surrounding LLM response
2070
+ // frames to signal assistant turn boundaries, while rightly deferring to
2071
+ // any earlier LLM-driven turn start.
2072
+ if (frame.appendToContext && !this._assistantTurnStartTimestamp) {
2073
+ await this._triggerAssistantTurnStarted();
2074
+ }
2075
+ }
2076
+ async _handlePushAggregation() {
2077
+ // LLMAssistantPushAggregationFrame is emitted at the end of a
2078
+ // TTSSpeakFrame-driven utterance to commit the spoken text.
2079
+ await this._triggerAssistantTurnStopped();
2080
+ }
2081
+ async _handleText(frame) {
2082
+ // Skip TextFrame types not intended to build the assistant context
2083
+ if (frame instanceof TranscriptionFrame ||
2084
+ frame instanceof TranslationFrame ||
2085
+ frame instanceof InterimTranscriptionFrame ||
2086
+ frame instanceof EagerTranscriptionFrame) {
2087
+ return;
2088
+ }
2089
+ // A backchannel is reported once, by the segment the TTS will speak,
2090
+ // which the output transport releases as its audio starts.
2091
+ if (frame instanceof AggregatedTextFrame &&
2092
+ !(frame instanceof TTSTextFrame) &&
2093
+ frame.textType === TextType.BACKCHANNEL &&
2094
+ frame.willBeSpoken) {
2095
+ const message = new AssistantBackchannelMessage({
2096
+ content: frame.text,
2097
+ timestamp: timeNowIso8601(),
2098
+ });
2099
+ await this._callEventHandler("on_assistant_backchannel", message);
2100
+ }
2101
+ if (!frame.appendToContext) {
2102
+ return;
2103
+ }
2104
+ // Make sure we really have text (spaces count, too!)
2105
+ if (frame.text.length === 0) {
2106
+ return;
2107
+ }
2108
+ const text = frame instanceof AggregatedTextFrame && frame.rawText ? frame.rawText : frame.text;
2109
+ this._aggregation.push(new TextPartForConcatenation({
2110
+ text,
2111
+ includesInterPartSpaces: frame.includesInterFrameSpaces,
2112
+ }));
2113
+ }
2114
+ async _handleMarkerFrame(frame) {
2115
+ this._seenTurnMarkers.add(frame.marker);
2116
+ if (frame.appendToContextImmediately) {
2117
+ // Stand-alone marker: write it to the context now as its own assistant
2118
+ // message. Used when the marker is the entire assistant turn — e.g. the
2119
+ // ◐ / ○ incomplete-turn signals, where the spoken response is
2120
+ // suppressed and the marker is the only artifact.
2121
+ this._context.addMessage({ role: "assistant", content: frame.marker });
2122
+ await this.pushContextFrame();
2123
+ const timestampFrame = new LLMContextAssistantTimestampFrame({
2124
+ timestamp: timeNowIso8601(),
2125
+ });
2126
+ await this.pushFrame(timestampFrame);
2127
+ return;
2128
+ }
2129
+ // Marker is part of an in-progress assistant response. Append it to the
2130
+ // running aggregation so `pushAggregation` writes marker + text as a
2131
+ // single context message — e.g. the ● complete-turn signal that prefixes
2132
+ // the spoken response, producing "● <response>" in context. Markers are
2133
+ // stripped from the transcript via `_maybeStripTurnCompletionMarkers` so
2134
+ // consumers see clean text.
2135
+ this._aggregation.push(new TextPartForConcatenation({ text: frame.marker, includesInterPartSpaces: false }));
2136
+ }
2137
+ async _handleThoughtStart(frame) {
2138
+ await this._resetThoughtAggregation();
2139
+ this._thoughtAppendToContext = frame.appendToContext;
2140
+ // frame.llm is required when frame.appendToContext is true, which is the
2141
+ // only case where we read this value, so we can safely default to an
2142
+ // empty string here for typing purposes.
2143
+ this._thoughtLlm = frame.llm || "";
2144
+ this._thoughtStartTime = timeNowIso8601();
2145
+ }
2146
+ async _handleThoughtText(frame) {
2147
+ // Make sure we really have text (spaces count, too!)
2148
+ if (frame.text.length === 0) {
2149
+ return;
2150
+ }
2151
+ this._thoughtAggregation.push(new TextPartForConcatenation({
2152
+ text: frame.text,
2153
+ includesInterPartSpaces: frame.includesInterFrameSpaces,
2154
+ }));
2155
+ }
2156
+ async _handleThoughtEnd(frame) {
2157
+ const thought = concatenateAggregatedText(this._thoughtAggregation);
2158
+ if (this._thoughtAppendToContext) {
2159
+ const llm = this._thoughtLlm;
2160
+ this._context.addMessage(new LLMSpecificMessage({
2161
+ llm,
2162
+ message: {
2163
+ type: "thought",
2164
+ text: thought,
2165
+ signature: frame.signature,
2166
+ },
2167
+ }));
2168
+ }
2169
+ const message = new AssistantThoughtMessage({
2170
+ content: thought,
2171
+ timestamp: this._thoughtStartTime,
2172
+ });
2173
+ await this._resetThoughtAggregation();
2174
+ await this._callEventHandler("on_assistant_thought", message);
2175
+ }
2176
+ async _maybeAppendImageToContext(frame) {
2177
+ if (!frame.appendToContext) {
2178
+ return false;
2179
+ }
2180
+ logger.debug(`${this} Appending UserImageRawFrame to LLM context (size: (${frame.size.join(", ")}))`);
2181
+ if (frame.format === null) {
2182
+ throw new Error("UserImageRawFrame.format must not be null");
2183
+ }
2184
+ await this._context.addImageFrameMessage({
2185
+ format: frame.format,
2186
+ size: frame.size,
2187
+ image: frame.image,
2188
+ text: frame.text,
2189
+ });
2190
+ return true;
2191
+ }
2192
+ /**
2193
+ * Whether a call's final result must be delivered as a deferred message.
2194
+ *
2195
+ * An async call (`cancelOnInterruption: false`) whose result arrives before
2196
+ * the conversation moved on is indistinguishable from a synchronous one,
2197
+ * and settles in place: its "started" placeholder becomes the tool result,
2198
+ * and the LLM sees an ordinary call. The context alone decides. The result
2199
+ * is deferred when a user or developer message follows the placeholder:
2200
+ * something the user said, new task instructions, or an intermediate update
2201
+ * from this call. Assistant messages are ignored, so filler spoken with a
2202
+ * `TTSSpeakFrame` while the call runs does not defer it, whether the tool
2203
+ * handler or an event handler spoke it. The same holds for text the model
2204
+ * wrote after seeing the placeholder with no user turn in between, as when
2205
+ * an ungrouped sibling's result runs inference; the result still settles in
2206
+ * place. Other calls' placeholders, results, and deferred messages are
2207
+ * ignored too. A placeholder that is no longer in the context, because the
2208
+ * context was rebuilt while the call ran, also defers.
2209
+ *
2210
+ * @param inProgressFrame The call's in-progress frame.
2211
+ * @returns True when the result goes into a developer message.
2212
+ */
2213
+ _isDeferred(inProgressFrame) {
2214
+ if (inProgressFrame.cancelOnInterruption) {
2215
+ return false;
2216
+ }
2217
+ const toolCallId = inProgressFrame.toolCallId;
2218
+ let afterPlaceholder = false;
2219
+ for (const message of this._context.messages) {
2220
+ if (message instanceof LLMSpecificMessage) {
2221
+ continue;
2222
+ }
2223
+ if (!afterPlaceholder) {
2224
+ afterPlaceholder = message.role === "tool" && message.tool_call_id === toolCallId;
2225
+ continue;
2226
+ }
2227
+ const role = message.role;
2228
+ if (role === "tool" || role === "assistant") {
2229
+ // A sibling's placeholder or result, or assistant output.
2230
+ continue;
2231
+ }
2232
+ const payload = asyncToolMessages.parseMessage(message);
2233
+ if (payload !== null && payload.toolCallId !== toolCallId) {
2234
+ // A sibling's deferred update or result.
2235
+ continue;
2236
+ }
2237
+ return true;
2238
+ }
2239
+ return !afterPlaceholder;
2240
+ }
2241
+ _updateFunctionCallResult(_functionName, toolCallId, result) {
2242
+ for (const message of this._context.getMessages()) {
2243
+ if (!(message instanceof LLMSpecificMessage) &&
2244
+ message.role === "tool" &&
2245
+ message.tool_call_id &&
2246
+ message.tool_call_id === toolCallId) {
2247
+ message.content = result;
2248
+ }
2249
+ }
2250
+ }
2251
+ _contextUpdatedTaskFinished(task) {
2252
+ this._contextUpdatedTasks.delete(task);
2253
+ }
2254
+ async _triggerAssistantTurnStarted() {
2255
+ this._assistantTurnStartTimestamp = timeNowIso8601();
2256
+ await this._callEventHandler("on_assistant_turn_started");
2257
+ }
2258
+ async _triggerAssistantTurnStopped(options = {}) {
2259
+ const interrupted = options.interrupted ?? false;
2260
+ if (!this._assistantTurnStartTimestamp) {
2261
+ return;
2262
+ }
2263
+ let aggregation = await this.pushAggregation();
2264
+ if (aggregation) {
2265
+ // Strip turn completion markers from the transcript
2266
+ aggregation = this._maybeStripTurnCompletionMarkers(aggregation);
2267
+ }
2268
+ const message = new AssistantTurnStoppedMessage({
2269
+ content: aggregation,
2270
+ interrupted,
2271
+ timestamp: this._assistantTurnStartTimestamp,
2272
+ });
2273
+ await this._callEventHandler("on_assistant_turn_stopped", message);
2274
+ if (aggregation) {
2275
+ await this.broadcastFrame(LLMContextAssistantTurnFrame, {
2276
+ text: aggregation,
2277
+ timestamp: this._assistantTurnStartTimestamp,
2278
+ });
2279
+ }
2280
+ this._assistantTurnStartTimestamp = "";
2281
+ }
2282
+ /**
2283
+ * Strip turn completion markers from assistant transcript.
2284
+ *
2285
+ * Turn completion markers are used internally for turn completion detection
2286
+ * and shouldn't appear in the final transcript. Both the default markers
2287
+ * and any seen on this turn's marker frames are removed, so a custom marker
2288
+ * set is stripped too.
2289
+ */
2290
+ _maybeStripTurnCompletionMarkers(text) {
2291
+ let markerFound = false;
2292
+ const markers = new Set([
2293
+ USER_TURN_COMPLETE_MARKER,
2294
+ USER_TURN_INCOMPLETE_SHORT_MARKER,
2295
+ USER_TURN_INCOMPLETE_LONG_MARKER,
2296
+ ...this._seenTurnMarkers,
2297
+ ]);
2298
+ for (const marker of markers) {
2299
+ if (text.includes(marker)) {
2300
+ text = text.replaceAll(marker, "");
2301
+ markerFound = true;
2302
+ }
2303
+ }
2304
+ // Only strip whitespace if we removed a marker
2305
+ return markerFound ? text.trim() : text;
2306
+ }
2307
+ /**
2308
+ * Handle summarization request from the summarizer.
2309
+ *
2310
+ * Push the request frame UPSTREAM to the LLM service for processing.
2311
+ *
2312
+ * @param summarizer The summarizer that generated the request.
2313
+ * @param frame The summarization request frame to broadcast.
2314
+ */
2315
+ async _onRequestSummarization(_summarizer, frame) {
2316
+ await this.pushFrame(frame, FrameDirection.UPSTREAM);
2317
+ }
2318
+ /**
2319
+ * Handle summary applied event from the summarizer.
2320
+ *
2321
+ * Forwards the event to any registered `on_summary_applied` handlers.
2322
+ *
2323
+ * @param summarizer The summarizer that applied the summary.
2324
+ * @param event The summary applied event.
2325
+ */
2326
+ async _onSummaryApplied(summarizer, event) {
2327
+ await this._callEventHandler("on_summary_applied", summarizer, event);
2328
+ }
2329
+ }
2330
+ /** Pair of LLM context aggregators for updating context with user and assistant messages. */
2331
+ export class LLMContextAggregatorPair {
2332
+ _user;
2333
+ _assistant;
2334
+ /**
2335
+ * Initialize the LLM context aggregator pair.
2336
+ *
2337
+ * @param context The context to be managed by the aggregators.
2338
+ */
2339
+ constructor(context, options = {}) {
2340
+ const userParams = options.userParams || new LLMUserAggregatorParams();
2341
+ const assistantParams = options.assistantParams || new LLMAssistantAggregatorParams();
2342
+ const addToolChangeMessages = options.addToolChangeMessages ?? null;
2343
+ if (addToolChangeMessages !== null) {
2344
+ userParams.addToolChangeMessages = addToolChangeMessages;
2345
+ assistantParams.addToolChangeMessages = addToolChangeMessages;
2346
+ }
2347
+ const realtimeServiceMode = options.realtimeServiceMode ?? null;
2348
+ this._user = new LLMUserAggregator(context, {
2349
+ params: userParams,
2350
+ _realtimeServiceMode: realtimeServiceMode,
2351
+ });
2352
+ // Wire the assistant→user back-reference unconditionally: realtime mode
2353
+ // may be auto-configured later (realtimeServiceMode=null), so the
2354
+ // reference must already exist when it flips on. Realtime mode treats the
2355
+ // assistant response start as the user turn's end signal — the assistant
2356
+ // half triggers a (possibly deferred) flush of the user half so the user
2357
+ // message lands in context before the assistant turn starts. The user
2358
+ // side has nothing to flush back, and the behavior stays gated on the
2359
+ // resolved realtime flag, so this reference is inert in cascade mode.
2360
+ this._assistant = new LLMAssistantAggregator(context, {
2361
+ params: assistantParams,
2362
+ _realtimeServiceMode: realtimeServiceMode,
2363
+ _pairedUserAggregator: this._user,
2364
+ });
2365
+ }
2366
+ /**
2367
+ * Get the user context aggregator.
2368
+ *
2369
+ * @returns The user context aggregator instance.
2370
+ */
2371
+ user() {
2372
+ return this._user;
2373
+ }
2374
+ /**
2375
+ * Get the assistant context aggregator.
2376
+ *
2377
+ * @returns The assistant context aggregator instance.
2378
+ */
2379
+ assistant() {
2380
+ return this._assistant;
2381
+ }
2382
+ /**
2383
+ * Allow destructuring of the aggregator pair.
2384
+ *
2385
+ * @example
2386
+ * const pair = new LLMContextAggregatorPair(context); // The instance
2387
+ * const [user, assistant] = new LLMContextAggregatorPair(context); // Destructured
2388
+ */
2389
+ *[Symbol.iterator]() {
2390
+ yield this._user;
2391
+ yield this._assistant;
2392
+ }
2393
+ }
2394
+ //# sourceMappingURL=llm_response_universal.js.map