@pipedog/core 0.0.0 → 0.1.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 (698) 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/index.d.ts +2 -0
  42. package/dist/audio/resamplers/index.js +8 -0
  43. package/dist/audio/resamplers/resampy_resampler.d.ts +10 -0
  44. package/dist/audio/resamplers/resampy_resampler.js +15 -0
  45. package/dist/audio/resamplers/soxr_resampler.d.ts +54 -0
  46. package/dist/audio/resamplers/soxr_resampler.js +69 -0
  47. package/dist/audio/resamplers/soxr_stream_resampler.d.ts +75 -0
  48. package/dist/audio/resamplers/soxr_stream_resampler.js +146 -0
  49. package/dist/audio/turn/base_turn_analyzer.d.ts +84 -0
  50. package/dist/audio/turn/base_turn_analyzer.js +61 -0
  51. package/dist/audio/turn/index.d.ts +2 -0
  52. package/dist/audio/turn/index.js +8 -0
  53. package/dist/audio/turn/smart_turn/_whisper_features.d.ts +30 -0
  54. package/dist/audio/turn/smart_turn/_whisper_features.js +225 -0
  55. package/dist/audio/turn/smart_turn/base_smart_turn.d.ts +94 -0
  56. package/dist/audio/turn/smart_turn/base_smart_turn.js +239 -0
  57. package/dist/audio/turn/smart_turn/local_smart_turn_v3.d.ts +76 -0
  58. package/dist/audio/turn/smart_turn/local_smart_turn_v3.js +143 -0
  59. package/dist/audio/utils.d.ts +224 -0
  60. package/dist/audio/utils.js +634 -0
  61. package/dist/audio/vad/data/index.d.ts +2 -0
  62. package/dist/audio/vad/data/index.js +8 -0
  63. package/dist/audio/vad/index.d.ts +2 -0
  64. package/dist/audio/vad/index.js +8 -0
  65. package/dist/audio/vad/vad_analyzer.d.ts +126 -0
  66. package/dist/audio/vad/vad_analyzer.js +211 -0
  67. package/dist/audio/vad/vad_controller.d.ts +143 -0
  68. package/dist/audio/vad/vad_controller.js +228 -0
  69. package/dist/audio/volume.d.ts +38 -0
  70. package/dist/audio/volume.js +75 -0
  71. package/dist/bus/adapters/base.d.ts +35 -0
  72. package/dist/bus/adapters/base.js +19 -0
  73. package/dist/bus/adapters/index.d.ts +10 -0
  74. package/dist/bus/adapters/index.js +15 -0
  75. package/dist/bus/adapters/llm_context_adapter.d.ts +36 -0
  76. package/dist/bus/adapters/llm_context_adapter.js +82 -0
  77. package/dist/bus/adapters/tools_schema_adapter.d.ts +25 -0
  78. package/dist/bus/adapters/tools_schema_adapter.js +70 -0
  79. package/dist/bus/bridge_processor.d.ts +111 -0
  80. package/dist/bus/bridge_processor.js +166 -0
  81. package/dist/bus/bus.d.ts +110 -0
  82. package/dist/bus/bus.js +200 -0
  83. package/dist/bus/index.d.ts +22 -0
  84. package/dist/bus/index.js +27 -0
  85. package/dist/bus/local/async_queue.d.ts +12 -0
  86. package/dist/bus/local/async_queue.js +21 -0
  87. package/dist/bus/local/index.d.ts +3 -0
  88. package/dist/bus/local/index.js +8 -0
  89. package/dist/bus/messages.d.ts +378 -0
  90. package/dist/bus/messages.js +357 -0
  91. package/dist/bus/network/index.d.ts +16 -0
  92. package/dist/bus/network/index.js +21 -0
  93. package/dist/bus/queue.d.ts +27 -0
  94. package/dist/bus/queue.js +51 -0
  95. package/dist/bus/serializers/base.d.ts +25 -0
  96. package/dist/bus/serializers/base.js +14 -0
  97. package/dist/bus/serializers/index.d.ts +9 -0
  98. package/dist/bus/serializers/index.js +14 -0
  99. package/dist/bus/serializers/json.d.ts +77 -0
  100. package/dist/bus/serializers/json.js +367 -0
  101. package/dist/bus/subscriber.d.ts +46 -0
  102. package/dist/bus/subscriber.js +27 -0
  103. package/dist/bus/ui/index.d.ts +3 -0
  104. package/dist/bus/ui/index.js +8 -0
  105. package/dist/bus/ui/messages.d.ts +186 -0
  106. package/dist/bus/ui/messages.js +171 -0
  107. package/dist/classifiers/base_classifier.d.ts +263 -0
  108. package/dist/classifiers/base_classifier.js +346 -0
  109. package/dist/classifiers/cloudflare/clef/classifier.d.ts +70 -0
  110. package/dist/classifiers/cloudflare/clef/classifier.js +204 -0
  111. package/dist/classifiers/cloudflare/clef/client.d.ts +104 -0
  112. package/dist/classifiers/cloudflare/clef/client.js +234 -0
  113. package/dist/classifiers/cloudflare/clef/index.d.ts +2 -0
  114. package/dist/classifiers/cloudflare/clef/index.js +8 -0
  115. package/dist/classifiers/cloudflare/index.d.ts +2 -0
  116. package/dist/classifiers/cloudflare/index.js +8 -0
  117. package/dist/classifiers/http_client.d.ts +70 -0
  118. package/dist/classifiers/http_client.js +161 -0
  119. package/dist/classifiers/index.d.ts +2 -0
  120. package/dist/classifiers/index.js +8 -0
  121. package/dist/classifiers/jev/classifier.d.ts +8 -0
  122. package/dist/classifiers/jev/classifier.js +13 -0
  123. package/dist/classifiers/jev/client.d.ts +8 -0
  124. package/dist/classifiers/jev/client.js +13 -0
  125. package/dist/classifiers/jev/index.d.ts +2 -0
  126. package/dist/classifiers/jev/index.js +8 -0
  127. package/dist/classifiers/llm/classifier.d.ts +72 -0
  128. package/dist/classifiers/llm/classifier.js +304 -0
  129. package/dist/classifiers/llm/index.d.ts +2 -0
  130. package/dist/classifiers/llm/index.js +8 -0
  131. package/dist/classifiers/openai/decisions/classifier.d.ts +77 -0
  132. package/dist/classifiers/openai/decisions/classifier.js +241 -0
  133. package/dist/classifiers/openai/decisions/client.d.ts +108 -0
  134. package/dist/classifiers/openai/decisions/client.js +234 -0
  135. package/dist/classifiers/openai/decisions/index.d.ts +2 -0
  136. package/dist/classifiers/openai/decisions/index.js +8 -0
  137. package/dist/classifiers/openai/index.d.ts +2 -0
  138. package/dist/classifiers/openai/index.js +8 -0
  139. package/dist/classifiers/typesafe/index.d.ts +2 -0
  140. package/dist/classifiers/typesafe/index.js +8 -0
  141. package/dist/classifiers/typesafe/jev/classifier.d.ts +70 -0
  142. package/dist/classifiers/typesafe/jev/classifier.js +220 -0
  143. package/dist/classifiers/typesafe/jev/client.d.ts +105 -0
  144. package/dist/classifiers/typesafe/jev/client.js +233 -0
  145. package/dist/classifiers/typesafe/jev/index.d.ts +2 -0
  146. package/dist/classifiers/typesafe/jev/index.js +8 -0
  147. package/dist/clocks/base_clock.d.ts +13 -0
  148. package/dist/clocks/base_clock.js +14 -0
  149. package/dist/clocks/index.d.ts +2 -0
  150. package/dist/clocks/index.js +8 -0
  151. package/dist/clocks/system_clock.d.ts +15 -0
  152. package/dist/clocks/system_clock.js +25 -0
  153. package/dist/frames/frames.d.ts +2533 -0
  154. package/dist/frames/frames.js +2485 -0
  155. package/dist/frames/index.d.ts +2 -0
  156. package/dist/frames/index.js +8 -0
  157. package/dist/frames/protobufs/frames_pb2.d.ts +229 -0
  158. package/dist/frames/protobufs/frames_pb2.js +466 -0
  159. package/dist/index.d.ts +11 -0
  160. package/dist/index.js +56 -0
  161. package/dist/metrics/index.d.ts +2 -0
  162. package/dist/metrics/index.js +8 -0
  163. package/dist/metrics/metrics.d.ts +276 -0
  164. package/dist/metrics/metrics.js +254 -0
  165. package/dist/observers/base_observer.d.ts +173 -0
  166. package/dist/observers/base_observer.js +151 -0
  167. package/dist/observers/error_observer.d.ts +75 -0
  168. package/dist/observers/error_observer.js +87 -0
  169. package/dist/observers/function_call_observer.d.ts +157 -0
  170. package/dist/observers/function_call_observer.js +222 -0
  171. package/dist/observers/index.d.ts +2 -0
  172. package/dist/observers/index.js +8 -0
  173. package/dist/observers/loggers/debug_log_observer.d.ts +91 -0
  174. package/dist/observers/loggers/debug_log_observer.js +228 -0
  175. package/dist/observers/loggers/index.d.ts +2 -0
  176. package/dist/observers/loggers/index.js +8 -0
  177. package/dist/observers/loggers/llm_log_observer.d.ts +24 -0
  178. package/dist/observers/loggers/llm_log_observer.js +110 -0
  179. package/dist/observers/loggers/metrics_log_observer.d.ts +85 -0
  180. package/dist/observers/loggers/metrics_log_observer.js +186 -0
  181. package/dist/observers/loggers/transcription_log_observer.d.ts +23 -0
  182. package/dist/observers/loggers/transcription_log_observer.js +74 -0
  183. package/dist/observers/service_metrics_observer.d.ts +168 -0
  184. package/dist/observers/service_metrics_observer.js +236 -0
  185. package/dist/observers/speaking_observer.d.ts +104 -0
  186. package/dist/observers/speaking_observer.js +150 -0
  187. package/dist/observers/startup_timing_observer.d.ts +221 -0
  188. package/dist/observers/startup_timing_observer.js +365 -0
  189. package/dist/observers/turn_tracking_observer.d.ts +78 -0
  190. package/dist/observers/turn_tracking_observer.js +189 -0
  191. package/dist/observers/user_bot_latency_observer.d.ts +411 -0
  192. package/dist/observers/user_bot_latency_observer.js +974 -0
  193. package/dist/pipeline/base_pipeline.d.ts +49 -0
  194. package/dist/pipeline/base_pipeline.js +101 -0
  195. package/dist/pipeline/capabilities.d.ts +76 -0
  196. package/dist/pipeline/capabilities.js +97 -0
  197. package/dist/pipeline/index.d.ts +2 -0
  198. package/dist/pipeline/index.js +8 -0
  199. package/dist/pipeline/job_context.d.ts +317 -0
  200. package/dist/pipeline/job_context.js +450 -0
  201. package/dist/pipeline/job_decorator.d.ts +54 -0
  202. package/dist/pipeline/job_decorator.js +92 -0
  203. package/dist/pipeline/llm_switcher.d.ts +84 -0
  204. package/dist/pipeline/llm_switcher.js +107 -0
  205. package/dist/pipeline/llm_with_backend.d.ts +435 -0
  206. package/dist/pipeline/llm_with_backend.js +806 -0
  207. package/dist/pipeline/parallel_pipeline.d.ts +83 -0
  208. package/dist/pipeline/parallel_pipeline.js +200 -0
  209. package/dist/pipeline/pipeline.d.ts +126 -0
  210. package/dist/pipeline/pipeline.js +196 -0
  211. package/dist/pipeline/service_switcher.d.ts +287 -0
  212. package/dist/pipeline/service_switcher.js +509 -0
  213. package/dist/pipeline/sync_parallel_pipeline.d.ts +192 -0
  214. package/dist/pipeline/sync_parallel_pipeline.js +387 -0
  215. package/dist/pipeline/worker.d.ts +814 -0
  216. package/dist/pipeline/worker.js +1639 -0
  217. package/dist/pipeline/worker_observer.d.ts +131 -0
  218. package/dist/pipeline/worker_observer.js +274 -0
  219. package/dist/pipeline/worker_ready_decorator.d.ts +45 -0
  220. package/dist/pipeline/worker_ready_decorator.js +76 -0
  221. package/dist/processors/aggregators/async_tool_messages.d.ts +146 -0
  222. package/dist/processors/aggregators/async_tool_messages.js +291 -0
  223. package/dist/processors/aggregators/dtmf_aggregator.d.ts +61 -0
  224. package/dist/processors/aggregators/dtmf_aggregator.js +144 -0
  225. package/dist/processors/aggregators/gated.d.ts +43 -0
  226. package/dist/processors/aggregators/gated.js +86 -0
  227. package/dist/processors/aggregators/gated_llm_context.d.ts +42 -0
  228. package/dist/processors/aggregators/gated_llm_context.js +86 -0
  229. package/dist/processors/aggregators/index.d.ts +2 -0
  230. package/dist/processors/aggregators/index.js +8 -0
  231. package/dist/processors/aggregators/llm_context.d.ts +370 -0
  232. package/dist/processors/aggregators/llm_context.js +580 -0
  233. package/dist/processors/aggregators/llm_context_summarizer.d.ts +219 -0
  234. package/dist/processors/aggregators/llm_context_summarizer.js +447 -0
  235. package/dist/processors/aggregators/llm_response.d.ts +37 -0
  236. package/dist/processors/aggregators/llm_response.js +71 -0
  237. package/dist/processors/aggregators/llm_response_universal.d.ts +966 -0
  238. package/dist/processors/aggregators/llm_response_universal.js +2394 -0
  239. package/dist/processors/aggregators/llm_text_processor.d.ts +53 -0
  240. package/dist/processors/aggregators/llm_text_processor.js +102 -0
  241. package/dist/processors/aggregators/sentence.d.ts +39 -0
  242. package/dist/processors/aggregators/sentence.js +69 -0
  243. package/dist/processors/async_generator.d.ts +42 -0
  244. package/dist/processors/async_generator.js +76 -0
  245. package/dist/processors/audio/audio_buffer_processor.d.ts +241 -0
  246. package/dist/processors/audio/audio_buffer_processor.js +565 -0
  247. package/dist/processors/audio/index.d.ts +2 -0
  248. package/dist/processors/audio/index.js +8 -0
  249. package/dist/processors/audio/vad_processor.d.ts +67 -0
  250. package/dist/processors/audio/vad_processor.js +103 -0
  251. package/dist/processors/consumer_processor.d.ts +49 -0
  252. package/dist/processors/consumer_processor.js +85 -0
  253. package/dist/processors/filters/frame_filter.d.ts +39 -0
  254. package/dist/processors/filters/frame_filter.js +52 -0
  255. package/dist/processors/filters/function_filter.d.ts +59 -0
  256. package/dist/processors/filters/function_filter.js +78 -0
  257. package/dist/processors/filters/identity_filter.d.ts +33 -0
  258. package/dist/processors/filters/identity_filter.js +37 -0
  259. package/dist/processors/filters/index.d.ts +2 -0
  260. package/dist/processors/filters/index.js +8 -0
  261. package/dist/processors/filters/null_filter.d.ts +34 -0
  262. package/dist/processors/filters/null_filter.js +47 -0
  263. package/dist/processors/filters/wake_notifier_filter.d.ts +48 -0
  264. package/dist/processors/filters/wake_notifier_filter.js +49 -0
  265. package/dist/processors/frame_processor.d.ts +360 -0
  266. package/dist/processors/frame_processor.js +932 -0
  267. package/dist/processors/frameworks/index.d.ts +2 -0
  268. package/dist/processors/frameworks/index.js +8 -0
  269. package/dist/processors/frameworks/rtvi/frames.d.ts +241 -0
  270. package/dist/processors/frameworks/rtvi/frames.js +259 -0
  271. package/dist/processors/frameworks/rtvi/index.d.ts +6 -0
  272. package/dist/processors/frameworks/rtvi/index.js +11 -0
  273. package/dist/processors/frameworks/rtvi/models.d.ts +1741 -0
  274. package/dist/processors/frameworks/rtvi/models.js +1807 -0
  275. package/dist/processors/frameworks/rtvi/observer.d.ts +340 -0
  276. package/dist/processors/frameworks/rtvi/observer.js +1113 -0
  277. package/dist/processors/frameworks/rtvi/processor.d.ts +182 -0
  278. package/dist/processors/frameworks/rtvi/processor.js +676 -0
  279. package/dist/processors/idle_frame_processor.d.ts +44 -0
  280. package/dist/processors/idle_frame_processor.js +86 -0
  281. package/dist/processors/index.d.ts +2 -0
  282. package/dist/processors/index.js +8 -0
  283. package/dist/processors/logger.d.ts +42 -0
  284. package/dist/processors/logger.js +87 -0
  285. package/dist/processors/metrics/frame_processor_metrics.d.ts +172 -0
  286. package/dist/processors/metrics/frame_processor_metrics.js +372 -0
  287. package/dist/processors/metrics/index.d.ts +2 -0
  288. package/dist/processors/metrics/index.js +8 -0
  289. package/dist/processors/producer_processor.d.ts +68 -0
  290. package/dist/processors/producer_processor.js +78 -0
  291. package/dist/processors/text_transformer.d.ts +30 -0
  292. package/dist/processors/text_transformer.js +48 -0
  293. package/dist/registry/index.d.ts +3 -0
  294. package/dist/registry/index.js +8 -0
  295. package/dist/registry/registry.d.ts +69 -0
  296. package/dist/registry/registry.js +149 -0
  297. package/dist/registry/types.d.ts +47 -0
  298. package/dist/registry/types.js +39 -0
  299. package/dist/serializers/base_serializer.d.ts +94 -0
  300. package/dist/serializers/base_serializer.js +82 -0
  301. package/dist/serializers/index.d.ts +2 -0
  302. package/dist/serializers/index.js +8 -0
  303. package/dist/services/ai_service.d.ts +145 -0
  304. package/dist/services/ai_service.js +262 -0
  305. package/dist/services/anthropic/index.d.ts +2 -0
  306. package/dist/services/anthropic/index.js +7 -0
  307. package/dist/services/assemblyai/index.d.ts +2 -0
  308. package/dist/services/assemblyai/index.js +7 -0
  309. package/dist/services/asyncai/index.d.ts +2 -0
  310. package/dist/services/asyncai/index.js +7 -0
  311. package/dist/services/aws/index.d.ts +2 -0
  312. package/dist/services/aws/index.js +7 -0
  313. package/dist/services/azure/index.d.ts +2 -0
  314. package/dist/services/azure/index.js +7 -0
  315. package/dist/services/baseten/index.d.ts +2 -0
  316. package/dist/services/baseten/index.js +7 -0
  317. package/dist/services/bland/index.d.ts +2 -0
  318. package/dist/services/bland/index.js +7 -0
  319. package/dist/services/camb/index.d.ts +2 -0
  320. package/dist/services/camb/index.js +7 -0
  321. package/dist/services/cartesia/index.d.ts +2 -0
  322. package/dist/services/cartesia/index.js +7 -0
  323. package/dist/services/cerebras/index.d.ts +2 -0
  324. package/dist/services/cerebras/index.js +7 -0
  325. package/dist/services/crusoe/index.d.ts +2 -0
  326. package/dist/services/crusoe/index.js +7 -0
  327. package/dist/services/deepgram/index.d.ts +2 -0
  328. package/dist/services/deepgram/index.js +7 -0
  329. package/dist/services/deepseek/index.d.ts +2 -0
  330. package/dist/services/deepseek/index.js +7 -0
  331. package/dist/services/elevenlabs/index.d.ts +2 -0
  332. package/dist/services/elevenlabs/index.js +7 -0
  333. package/dist/services/fal/index.d.ts +2 -0
  334. package/dist/services/fal/index.js +7 -0
  335. package/dist/services/fireworks/index.d.ts +2 -0
  336. package/dist/services/fireworks/index.js +7 -0
  337. package/dist/services/fish/index.d.ts +2 -0
  338. package/dist/services/fish/index.js +7 -0
  339. package/dist/services/funasr/index.d.ts +3 -0
  340. package/dist/services/funasr/index.js +8 -0
  341. package/dist/services/gladia/index.d.ts +2 -0
  342. package/dist/services/gladia/index.js +7 -0
  343. package/dist/services/google/index.d.ts +2 -0
  344. package/dist/services/google/index.js +7 -0
  345. package/dist/services/gradium/index.d.ts +2 -0
  346. package/dist/services/gradium/index.js +7 -0
  347. package/dist/services/grok/index.d.ts +2 -0
  348. package/dist/services/grok/index.js +7 -0
  349. package/dist/services/groq/index.d.ts +2 -0
  350. package/dist/services/groq/index.js +7 -0
  351. package/dist/services/heygen/index.d.ts +2 -0
  352. package/dist/services/heygen/index.js +7 -0
  353. package/dist/services/hume/index.d.ts +2 -0
  354. package/dist/services/hume/index.js +7 -0
  355. package/dist/services/image_service.d.ts +52 -0
  356. package/dist/services/image_service.js +60 -0
  357. package/dist/services/inception/index.d.ts +2 -0
  358. package/dist/services/inception/index.js +7 -0
  359. package/dist/services/index.d.ts +2 -0
  360. package/dist/services/index.js +8 -0
  361. package/dist/services/inworld/index.d.ts +2 -0
  362. package/dist/services/inworld/index.js +7 -0
  363. package/dist/services/keenable/index.d.ts +2 -0
  364. package/dist/services/keenable/index.js +7 -0
  365. package/dist/services/kokoro/index.d.ts +2 -0
  366. package/dist/services/kokoro/index.js +7 -0
  367. package/dist/services/llm_service.d.ts +1106 -0
  368. package/dist/services/llm_service.js +2231 -0
  369. package/dist/services/mem0/index.d.ts +2 -0
  370. package/dist/services/mem0/index.js +7 -0
  371. package/dist/services/meta/index.d.ts +2 -0
  372. package/dist/services/meta/index.js +7 -0
  373. package/dist/services/minimax/index.d.ts +2 -0
  374. package/dist/services/minimax/index.js +7 -0
  375. package/dist/services/mistral/index.d.ts +2 -0
  376. package/dist/services/mistral/index.js +7 -0
  377. package/dist/services/moondream/index.d.ts +2 -0
  378. package/dist/services/moondream/index.js +7 -0
  379. package/dist/services/moonshine/index.d.ts +2 -0
  380. package/dist/services/moonshine/index.js +7 -0
  381. package/dist/services/nebius/index.d.ts +2 -0
  382. package/dist/services/nebius/index.js +7 -0
  383. package/dist/services/neuphonic/index.d.ts +2 -0
  384. package/dist/services/neuphonic/index.js +7 -0
  385. package/dist/services/novita/index.d.ts +2 -0
  386. package/dist/services/novita/index.js +7 -0
  387. package/dist/services/nvidia/index.d.ts +2 -0
  388. package/dist/services/nvidia/index.js +11 -0
  389. package/dist/services/ollama/index.d.ts +2 -0
  390. package/dist/services/ollama/index.js +7 -0
  391. package/dist/services/openai/index.d.ts +2 -0
  392. package/dist/services/openai/index.js +7 -0
  393. package/dist/services/openclaw/index.d.ts +14 -0
  394. package/dist/services/openclaw/index.js +19 -0
  395. package/dist/services/openrouter/index.d.ts +2 -0
  396. package/dist/services/openrouter/index.js +7 -0
  397. package/dist/services/perplexity/index.d.ts +2 -0
  398. package/dist/services/perplexity/index.js +7 -0
  399. package/dist/services/piper/index.d.ts +2 -0
  400. package/dist/services/piper/index.js +7 -0
  401. package/dist/services/pocket_tts/index.d.ts +2 -0
  402. package/dist/services/pocket_tts/index.js +7 -0
  403. package/dist/services/qwen/index.d.ts +2 -0
  404. package/dist/services/qwen/index.js +7 -0
  405. package/dist/services/resembleai/index.d.ts +2 -0
  406. package/dist/services/resembleai/index.js +7 -0
  407. package/dist/services/rime/index.d.ts +2 -0
  408. package/dist/services/rime/index.js +7 -0
  409. package/dist/services/sambanova/index.d.ts +2 -0
  410. package/dist/services/sambanova/index.js +7 -0
  411. package/dist/services/sarvam/index.d.ts +2 -0
  412. package/dist/services/sarvam/index.js +7 -0
  413. package/dist/services/settings.d.ts +356 -0
  414. package/dist/services/settings.js +437 -0
  415. package/dist/services/simli/index.d.ts +2 -0
  416. package/dist/services/simli/index.js +7 -0
  417. package/dist/services/smallest/index.d.ts +2 -0
  418. package/dist/services/smallest/index.js +7 -0
  419. package/dist/services/soniox/index.d.ts +2 -0
  420. package/dist/services/soniox/index.js +7 -0
  421. package/dist/services/speechify/index.d.ts +2 -0
  422. package/dist/services/speechify/index.js +7 -0
  423. package/dist/services/speechmatics/index.d.ts +2 -0
  424. package/dist/services/speechmatics/index.js +7 -0
  425. package/dist/services/stt_latency.d.ts +62 -0
  426. package/dist/services/stt_latency.js +71 -0
  427. package/dist/services/stt_service.d.ts +601 -0
  428. package/dist/services/stt_service.js +1044 -0
  429. package/dist/services/tavus/index.d.ts +2 -0
  430. package/dist/services/tavus/index.js +7 -0
  431. package/dist/services/together/index.d.ts +2 -0
  432. package/dist/services/together/index.js +7 -0
  433. package/dist/services/tts_service.d.ts +909 -0
  434. package/dist/services/tts_service.js +2018 -0
  435. package/dist/services/ultravox/index.d.ts +2 -0
  436. package/dist/services/ultravox/index.js +9 -0
  437. package/dist/services/vision_service.d.ts +56 -0
  438. package/dist/services/vision_service.js +62 -0
  439. package/dist/services/websocket_service.d.ts +213 -0
  440. package/dist/services/websocket_service.js +654 -0
  441. package/dist/services/whisper/index.d.ts +2 -0
  442. package/dist/services/whisper/index.js +9 -0
  443. package/dist/services/xai/index.d.ts +2 -0
  444. package/dist/services/xai/index.js +9 -0
  445. package/dist/services/xtts/index.d.ts +2 -0
  446. package/dist/services/xtts/index.js +9 -0
  447. package/dist/tests/index.d.ts +2 -0
  448. package/dist/tests/index.js +8 -0
  449. package/dist/tests/utils.d.ts +123 -0
  450. package/dist/tests/utils.js +212 -0
  451. package/dist/transcriptions/index.d.ts +2 -0
  452. package/dist/transcriptions/index.js +8 -0
  453. package/dist/transcriptions/language.d.ts +376 -0
  454. package/dist/transcriptions/language.js +543 -0
  455. package/dist/transports/base_input.d.ts +149 -0
  456. package/dist/transports/base_input.js +338 -0
  457. package/dist/transports/base_output.d.ts +432 -0
  458. package/dist/transports/base_output.js +1231 -0
  459. package/dist/transports/base_transport.d.ts +223 -0
  460. package/dist/transports/base_transport.js +167 -0
  461. package/dist/transports/daily/index.d.ts +2 -0
  462. package/dist/transports/daily/index.js +9 -0
  463. package/dist/transports/heygen/index.d.ts +2 -0
  464. package/dist/transports/heygen/index.js +9 -0
  465. package/dist/transports/index.d.ts +2 -0
  466. package/dist/transports/index.js +8 -0
  467. package/dist/transports/lemonslice/index.d.ts +2 -0
  468. package/dist/transports/lemonslice/index.js +9 -0
  469. package/dist/transports/livekit/index.d.ts +2 -0
  470. package/dist/transports/livekit/index.js +9 -0
  471. package/dist/transports/local/index.d.ts +2 -0
  472. package/dist/transports/local/index.js +9 -0
  473. package/dist/transports/moq/index.d.ts +2 -0
  474. package/dist/transports/moq/index.js +9 -0
  475. package/dist/transports/smallwebrtc/index.d.ts +2 -0
  476. package/dist/transports/smallwebrtc/index.js +9 -0
  477. package/dist/transports/tavus/index.d.ts +2 -0
  478. package/dist/transports/tavus/index.js +9 -0
  479. package/dist/transports/video_in_sampler.d.ts +157 -0
  480. package/dist/transports/video_in_sampler.js +258 -0
  481. package/dist/transports/vonage/index.d.ts +2 -0
  482. package/dist/transports/vonage/index.js +9 -0
  483. package/dist/transports/websocket/index.d.ts +2 -0
  484. package/dist/transports/websocket/index.js +9 -0
  485. package/dist/transports/whatsapp/index.d.ts +2 -0
  486. package/dist/transports/whatsapp/index.js +9 -0
  487. package/dist/turns/eager_end_of_turn_mixin.d.ts +70 -0
  488. package/dist/turns/eager_end_of_turn_mixin.js +122 -0
  489. package/dist/turns/empty_user_turn.d.ts +55 -0
  490. package/dist/turns/empty_user_turn.js +53 -0
  491. package/dist/turns/index.d.ts +2 -0
  492. package/dist/turns/index.js +8 -0
  493. package/dist/turns/speculation_gate.d.ts +169 -0
  494. package/dist/turns/speculation_gate.js +294 -0
  495. package/dist/turns/types.d.ts +33 -0
  496. package/dist/turns/types.js +35 -0
  497. package/dist/turns/user_idle_controller.d.ts +94 -0
  498. package/dist/turns/user_idle_controller.js +187 -0
  499. package/dist/turns/user_mute/always_user_mute_strategy.d.ts +15 -0
  500. package/dist/turns/user_mute/always_user_mute_strategy.js +29 -0
  501. package/dist/turns/user_mute/base_user_mute_strategy.d.ts +39 -0
  502. package/dist/turns/user_mute/base_user_mute_strategy.js +45 -0
  503. package/dist/turns/user_mute/first_speech_user_mute_strategy.d.ts +28 -0
  504. package/dist/turns/user_mute/first_speech_user_mute_strategy.js +52 -0
  505. package/dist/turns/user_mute/function_call_user_mute_strategy.d.ts +25 -0
  506. package/dist/turns/user_mute/function_call_user_mute_strategy.js +50 -0
  507. package/dist/turns/user_mute/index.d.ts +6 -0
  508. package/dist/turns/user_mute/index.js +11 -0
  509. package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.d.ts +43 -0
  510. package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.js +76 -0
  511. package/dist/turns/user_start/base_user_turn_start_strategy.d.ts +135 -0
  512. package/dist/turns/user_start/base_user_turn_start_strategy.js +134 -0
  513. package/dist/turns/user_start/external_user_turn_start_strategy.d.ts +45 -0
  514. package/dist/turns/user_start/external_user_turn_start_strategy.js +64 -0
  515. package/dist/turns/user_start/index.d.ts +8 -0
  516. package/dist/turns/user_start/index.js +16 -0
  517. package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.d.ts +133 -0
  518. package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.js +243 -0
  519. package/dist/turns/user_start/min_words_user_turn_start_strategy.d.ts +60 -0
  520. package/dist/turns/user_start/min_words_user_turn_start_strategy.js +102 -0
  521. package/dist/turns/user_start/transcription_user_turn_start_strategy.d.ts +29 -0
  522. package/dist/turns/user_start/transcription_user_turn_start_strategy.js +43 -0
  523. package/dist/turns/user_start/vad_user_turn_start_strategy.d.ts +20 -0
  524. package/dist/turns/user_start/vad_user_turn_start_strategy.js +31 -0
  525. package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.d.ts +141 -0
  526. package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.js +238 -0
  527. package/dist/turns/user_stop/base_user_turn_stop_strategy.d.ts +174 -0
  528. package/dist/turns/user_stop/base_user_turn_stop_strategy.js +170 -0
  529. package/dist/turns/user_stop/deferred_user_turn_stop_strategy.d.ts +81 -0
  530. package/dist/turns/user_stop/deferred_user_turn_stop_strategy.js +107 -0
  531. package/dist/turns/user_stop/eager_match_policy.d.ts +36 -0
  532. package/dist/turns/user_stop/eager_match_policy.js +50 -0
  533. package/dist/turns/user_stop/eager_user_turn_stop_strategy.d.ts +84 -0
  534. package/dist/turns/user_stop/eager_user_turn_stop_strategy.js +154 -0
  535. package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.d.ts +45 -0
  536. package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.js +55 -0
  537. package/dist/turns/user_stop/external_user_turn_stop_strategy.d.ts +115 -0
  538. package/dist/turns/user_stop/external_user_turn_stop_strategy.js +205 -0
  539. package/dist/turns/user_stop/index.d.ts +10 -0
  540. package/dist/turns/user_stop/index.js +15 -0
  541. package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.d.ts +44 -0
  542. package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.js +59 -0
  543. package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.d.ts +140 -0
  544. package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.js +303 -0
  545. package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.d.ts +135 -0
  546. package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.js +332 -0
  547. package/dist/turns/user_turn_completion_mixin.d.ts +147 -0
  548. package/dist/turns/user_turn_completion_mixin.js +591 -0
  549. package/dist/turns/user_turn_controller.d.ts +134 -0
  550. package/dist/turns/user_turn_controller.js +335 -0
  551. package/dist/turns/user_turn_processor.d.ts +87 -0
  552. package/dist/turns/user_turn_processor.js +180 -0
  553. package/dist/turns/user_turn_strategies.d.ts +187 -0
  554. package/dist/turns/user_turn_strategies.js +222 -0
  555. package/dist/utils/async_tool_cancellation.d.ts +35 -0
  556. package/dist/utils/async_tool_cancellation.js +78 -0
  557. package/dist/utils/asyncio/index.d.ts +2 -0
  558. package/dist/utils/asyncio/index.js +7 -0
  559. package/dist/utils/asyncio/task_manager.d.ts +49 -0
  560. package/dist/utils/asyncio/task_manager.js +105 -0
  561. package/dist/utils/base_object.d.ts +76 -0
  562. package/dist/utils/base_object.js +154 -0
  563. package/dist/utils/context/aggregated_frame_sequencer.d.ts +491 -0
  564. package/dist/utils/context/aggregated_frame_sequencer.js +803 -0
  565. package/dist/utils/context/audio_context_manager.d.ts +131 -0
  566. package/dist/utils/context/audio_context_manager.js +233 -0
  567. package/dist/utils/context/index.d.ts +2 -0
  568. package/dist/utils/context/index.js +7 -0
  569. package/dist/utils/context/llm_context_summarization.d.ts +258 -0
  570. package/dist/utils/context/llm_context_summarization.js +511 -0
  571. package/dist/utils/context/text_segment_map.d.ts +611 -0
  572. package/dist/utils/context/text_segment_map.js +1132 -0
  573. package/dist/utils/context/word_completion_tracker.d.ts +273 -0
  574. package/dist/utils/context/word_completion_tracker.js +441 -0
  575. package/dist/utils/context/word_timestamp_tracker.d.ts +72 -0
  576. package/dist/utils/context/word_timestamp_tracker.js +98 -0
  577. package/dist/utils/deprecation.d.ts +28 -0
  578. package/dist/utils/deprecation.js +75 -0
  579. package/dist/utils/env.d.ts +38 -0
  580. package/dist/utils/env.js +62 -0
  581. package/dist/utils/errors.d.ts +75 -0
  582. package/dist/utils/errors.js +202 -0
  583. package/dist/utils/file_resolver.d.ts +126 -0
  584. package/dist/utils/file_resolver.js +314 -0
  585. package/dist/utils/file_storage.d.ts +99 -0
  586. package/dist/utils/file_storage.js +100 -0
  587. package/dist/utils/frame_queue.d.ts +36 -0
  588. package/dist/utils/frame_queue.js +66 -0
  589. package/dist/utils/http.d.ts +39 -0
  590. package/dist/utils/http.js +53 -0
  591. package/dist/utils/image.d.ts +15 -0
  592. package/dist/utils/image.js +30 -0
  593. package/dist/utils/index.d.ts +2 -0
  594. package/dist/utils/index.js +8 -0
  595. package/dist/utils/log_config.d.ts +34 -0
  596. package/dist/utils/log_config.js +76 -0
  597. package/dist/utils/logger.d.ts +40 -0
  598. package/dist/utils/logger.js +91 -0
  599. package/dist/utils/mixin.d.ts +14 -0
  600. package/dist/utils/mixin.js +63 -0
  601. package/dist/utils/network.d.ts +73 -0
  602. package/dist/utils/network.js +90 -0
  603. package/dist/utils/security/allowed_origins.d.ts +16 -0
  604. package/dist/utils/security/allowed_origins.js +34 -0
  605. package/dist/utils/security/index.d.ts +2 -0
  606. package/dist/utils/security/index.js +7 -0
  607. package/dist/utils/security/redaction.d.ts +27 -0
  608. package/dist/utils/security/redaction.js +123 -0
  609. package/dist/utils/security/ssrf.d.ts +79 -0
  610. package/dist/utils/security/ssrf.js +327 -0
  611. package/dist/utils/shared.d.ts +35 -0
  612. package/dist/utils/shared.js +136 -0
  613. package/dist/utils/startup.d.ts +17 -0
  614. package/dist/utils/startup.js +130 -0
  615. package/dist/utils/string.d.ts +116 -0
  616. package/dist/utils/string.js +460 -0
  617. package/dist/utils/sync/base_notifier.d.ts +25 -0
  618. package/dist/utils/sync/base_notifier.js +16 -0
  619. package/dist/utils/sync/event_notifier.d.ts +38 -0
  620. package/dist/utils/sync/event_notifier.js +51 -0
  621. package/dist/utils/sync/index.d.ts +2 -0
  622. package/dist/utils/sync/index.js +7 -0
  623. package/dist/utils/text/alnum_utils.d.ts +109 -0
  624. package/dist/utils/text/alnum_utils.js +262 -0
  625. package/dist/utils/text/base_text_aggregator.d.ts +146 -0
  626. package/dist/utils/text/base_text_aggregator.js +102 -0
  627. package/dist/utils/text/base_text_filter.d.ts +54 -0
  628. package/dist/utils/text/base_text_filter.js +50 -0
  629. package/dist/utils/text/index.d.ts +2 -0
  630. package/dist/utils/text/index.js +7 -0
  631. package/dist/utils/text/markdown_text_filter.d.ts +147 -0
  632. package/dist/utils/text/markdown_text_filter.js +522 -0
  633. package/dist/utils/text/markup_utils.d.ts +54 -0
  634. package/dist/utils/text/markup_utils.js +156 -0
  635. package/dist/utils/text/pattern_pair_aggregator.d.ts +251 -0
  636. package/dist/utils/text/pattern_pair_aggregator.js +478 -0
  637. package/dist/utils/text/phonemes.d.ts +60 -0
  638. package/dist/utils/text/phonemes.js +172 -0
  639. package/dist/utils/text/simple_text_aggregator.d.ts +135 -0
  640. package/dist/utils/text/simple_text_aggregator.js +254 -0
  641. package/dist/utils/text/skip_tags_aggregator.d.ts +85 -0
  642. package/dist/utils/text/skip_tags_aggregator.js +147 -0
  643. package/dist/utils/text/transforms/pronunciations.d.ts +65 -0
  644. package/dist/utils/text/transforms/pronunciations.js +111 -0
  645. package/dist/utils/text/word_timestamp_utils.d.ts +26 -0
  646. package/dist/utils/text/word_timestamp_utils.js +52 -0
  647. package/dist/utils/time.d.ts +13 -0
  648. package/dist/utils/time.js +31 -0
  649. package/dist/utils/tracing/index.d.ts +3 -0
  650. package/dist/utils/tracing/index.js +8 -0
  651. package/dist/utils/tracing/service_attributes.d.ts +198 -0
  652. package/dist/utils/tracing/service_attributes.js +434 -0
  653. package/dist/utils/tracing/service_decorators.d.ts +214 -0
  654. package/dist/utils/tracing/service_decorators.js +1662 -0
  655. package/dist/utils/tracing/setup.d.ts +95 -0
  656. package/dist/utils/tracing/setup.js +110 -0
  657. package/dist/utils/tracing/tracing_context.d.ts +55 -0
  658. package/dist/utils/tracing/tracing_context.js +97 -0
  659. package/dist/utils/tracing/turn_trace_observer.d.ts +90 -0
  660. package/dist/utils/tracing/turn_trace_observer.js +219 -0
  661. package/dist/utils/types.d.ts +80 -0
  662. package/dist/utils/types.js +108 -0
  663. package/dist/utils/utils.d.ts +5 -0
  664. package/dist/utils/utils.js +22 -0
  665. package/dist/utils/yaml.d.ts +64 -0
  666. package/dist/utils/yaml.js +41 -0
  667. package/dist/version.d.ts +3 -0
  668. package/dist/version.js +7 -0
  669. package/dist/workers/base_worker.d.ts +772 -0
  670. package/dist/workers/base_worker.js +1298 -0
  671. package/dist/workers/index.d.ts +2 -0
  672. package/dist/workers/index.js +8 -0
  673. package/dist/workers/llm/backend_llm_worker.d.ts +552 -0
  674. package/dist/workers/llm/backend_llm_worker.js +1055 -0
  675. package/dist/workers/llm/index.d.ts +6 -0
  676. package/dist/workers/llm/index.js +11 -0
  677. package/dist/workers/llm/llm_context_worker.d.ts +66 -0
  678. package/dist/workers/llm/llm_context_worker.js +72 -0
  679. package/dist/workers/llm/llm_worker.d.ts +180 -0
  680. package/dist/workers/llm/llm_worker.js +323 -0
  681. package/dist/workers/llm/tool_decorator.d.ts +68 -0
  682. package/dist/workers/llm/tool_decorator.js +60 -0
  683. package/dist/workers/runner.d.ts +268 -0
  684. package/dist/workers/runner.js +489 -0
  685. package/dist/workers/ui/index.d.ts +22 -0
  686. package/dist/workers/ui/index.js +27 -0
  687. package/dist/workers/ui/ui_event_decorator.d.ts +43 -0
  688. package/dist/workers/ui/ui_event_decorator.js +75 -0
  689. package/dist/workers/ui/ui_job_context.d.ts +12 -0
  690. package/dist/workers/ui/ui_job_context.js +17 -0
  691. package/dist/workers/ui/ui_prompts.d.ts +21 -0
  692. package/dist/workers/ui/ui_prompts.js +41 -0
  693. package/dist/workers/ui/ui_tools.d.ts +32 -0
  694. package/dist/workers/ui/ui_tools.js +112 -0
  695. package/dist/workers/ui/ui_worker.d.ts +571 -0
  696. package/dist/workers/ui/ui_worker.js +1156 -0
  697. package/package.json +46 -3
  698. package/README.md +0 -5
@@ -0,0 +1,2231 @@
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/services/llm_service.py
6
+ /**
7
+ * Base classes for Large Language Model services with function calling support.
8
+ *
9
+ * Porting notes:
10
+ * - Cancelling a function call cancels the task running its handler. In
11
+ * JavaScript cancellation is delivered at cancellation points (see
12
+ * `@pipedog/asyncio`), so a handler awaiting foreign work (`fetch`, an
13
+ * SDK) should pass `currentSignal()` to it, or wrap the await in
14
+ * `abortable()`, to stop as promptly as the Python handler would.
15
+ * - Deprecated upstream APIs are not ported: `FunctionCallParams.tool_resources`,
16
+ * `register_direct_function()`, `unregister_direct_function()`, the
17
+ * `enable_async_tool_cancellation` option, the warnings for application-set
18
+ * turn-completion settings, and the legacy `LLMUpdateSettingsFrame(settings={...})`
19
+ * dict path.
20
+ */
21
+ import { CancelledError, Queue, rethrowIfCancelled, sleep, TimeoutError, wait, waitFor, } from "@pipedog/asyncio";
22
+ import { DirectFunctionWrapper } from "../adapters/schemas/direct_function.js";
23
+ import { ToolsSchema } from "../adapters/schemas/tools_schema.js";
24
+ import { OpenAILLMAdapter } from "../adapters/services/open_ai_adapter.js";
25
+ import { EagerEndOfTurnCancelFrame, FunctionCallCancelFrame, FunctionCallFromLLM, FunctionCallInProgressFrame, FunctionCallResultFrame, FunctionCallResultProperties, FunctionCallsStartedFrame, InterruptionFrame, LLMConfigureOutputFrame, LLMContextFrame, LLMContextSummaryRequestFrame, LLMContextSummaryResultFrame, LLMFullResponseEndFrame, LLMFullResponseStartFrame, LLMServiceMetadataFrame, LLMTextFrame, LLMUpdateSettingsFrame, } from "../frames/frames.js";
26
+ import { ASYNC_TOOL_INSTRUCTIONS } from "../processors/aggregators/async_tool_messages.js";
27
+ import { LLMContext } from "../processors/aggregators/llm_context.js";
28
+ import { FrameDirection } from "../processors/frame_processor.js";
29
+ import { SpeculationGate } from "../turns/speculation_gate.js";
30
+ import { UserTurnCompletionLLMServiceMixin } from "../turns/user_turn_completion_mixin.js";
31
+ import { ASYNC_TOOL_CANCELLATION_INSTRUCTIONS, buildCancelToolSchema, CANCEL_TOOL_PREFIX, cancelToolName, } from "../utils/async_tool_cancellation.js";
32
+ import { DEFAULT_SUMMARIZATION_TIMEOUT, LLMContextSummarizationUtil, } from "../utils/context/llm_context_summarization.js";
33
+ import { ErrorCategory } from "../utils/errors.js";
34
+ import { FileResolver } from "../utils/file_resolver.js";
35
+ import { _getLlmContextLogMode, _LLMContextLogMode } from "../utils/log_config.js";
36
+ import { logger } from "../utils/logger.js";
37
+ import { assertGiven, isGiven, NOT_GIVEN } from "../utils/types.js";
38
+ import { AIService } from "./ai_service.js";
39
+ import { LLMSettings } from "./settings.js";
40
+ import { ConnectionClosed, State, WebsocketService, } from "./websocket_service.js";
41
+ /**
42
+ * Raised by a method a service doesn't implement (Python's
43
+ * `NotImplementedError`), e.g. `LLMService.runInference()`.
44
+ */
45
+ export class NotImplementedError extends Error {
46
+ name = "NotImplementedError";
47
+ }
48
+ /**
49
+ * Parameters for a function call.
50
+ *
51
+ * - `functionName`: the name of the function being called.
52
+ * - `toolCallId`: a unique identifier for the function call.
53
+ * - `arguments`: the arguments for the function.
54
+ * - `llm`: the LLMService instance being used.
55
+ * - `pipelineWorker`: the worker running the pipeline this call was made from.
56
+ * Carries worker-scoped state, most notably `pipelineWorker.appResources`.
57
+ * - `context`: the LLM context.
58
+ * - `resultCallback`: callback to deliver the result of the function call.
59
+ * For async function calls (`cancelOnInterruption: false`), call it with
60
+ * `properties: new FunctionCallResultProperties({ isFinal: false })` to push
61
+ * intermediate updates before the final result.
62
+ * - `appResources`: the application-defined resources passed to
63
+ * `new PipelineWorker(..., { appResources })`. Same object — passed by
64
+ * reference, not a copy. Use it to share DB handles, clients, state, feature
65
+ * flags, etc. across all of a session's tool handlers.
66
+ * - `workerRunner`: the runner hosting `pipelineWorker`. Use it to reach
67
+ * another worker on the runner by name, e.g.
68
+ * `params.workerRunner.getWorker("ui-jobs")`. Always set for a call made
69
+ * from a running pipeline; null only when these params were built by hand.
70
+ *
71
+ * A handler runs in its own task and is cancelled on interruption, timeout or
72
+ * an LLM cancel request. Pass `currentSignal()` to `fetch` and other foreign
73
+ * awaits so they stop too.
74
+ */
75
+ export class FunctionCallParams {
76
+ functionName;
77
+ toolCallId;
78
+ arguments;
79
+ llm;
80
+ pipelineWorker;
81
+ context;
82
+ resultCallback;
83
+ appResources;
84
+ workerRunner;
85
+ constructor(options) {
86
+ this.functionName = options.functionName;
87
+ this.toolCallId = options.toolCallId;
88
+ this.arguments = options.arguments;
89
+ this.llm = options.llm;
90
+ this.pipelineWorker = options.pipelineWorker;
91
+ this.context = options.context;
92
+ this.resultCallback = options.resultCallback;
93
+ this.appResources = options.appResources ?? null;
94
+ this.workerRunner = options.workerRunner ?? null;
95
+ }
96
+ }
97
+ /**
98
+ * Internal record of a registered function-call handler.
99
+ *
100
+ * Created by the service when a function is registered — directly via
101
+ * `registerFunction`, or automatically from a direct function advertised in
102
+ * an `LLMContext` / `LLMSetToolsFrame`. Application code doesn't construct
103
+ * these.
104
+ *
105
+ * - `functionName`: the name of the function (null for catch-all handler).
106
+ * - `handler`: the handler for processing function call parameters.
107
+ * - `cancelOnInterruption`: whether to cancel the call on interruption. When
108
+ * false the call is treated as asynchronous: the LLM continues the
109
+ * conversation immediately without waiting for the result, and the result
110
+ * is injected later via a developer message.
111
+ * - `timeoutSecs`: optional per-tool timeout in seconds. Overrides the global
112
+ * `functionCallTimeoutSecs` for this specific function. A call that runs
113
+ * past it is cancelled.
114
+ * - `cancellableByLlm`: whether the LLM may cancel this call while it runs.
115
+ * Only meaningful on an async call (`cancelOnInterruption: false`), since a
116
+ * synchronous one holds the LLM until it returns.
117
+ * - `autoRegistered`: true only for a direct function that was auto-registered
118
+ * from an advertised tool set (listed in an `LLMContext` or
119
+ * `LLMSetToolsFrame`). False for every explicitly registered handler —
120
+ * direct or non-direct — and for the catch-all and built-in handlers.
121
+ */
122
+ export class FunctionCallRegistryItem {
123
+ functionName;
124
+ handler;
125
+ cancelOnInterruption;
126
+ timeoutSecs;
127
+ cancellableByLlm;
128
+ autoRegistered;
129
+ constructor(options) {
130
+ this.functionName = options.functionName;
131
+ this.handler = options.handler;
132
+ this.cancelOnInterruption = options.cancelOnInterruption;
133
+ this.timeoutSecs = options.timeoutSecs ?? null;
134
+ this.cancellableByLlm = options.cancellableByLlm ?? false;
135
+ this.autoRegistered = options.autoRegistered ?? false;
136
+ }
137
+ }
138
+ /**
139
+ * Internal function call entry for the function call runner.
140
+ *
141
+ * The runner executes function calls in order.
142
+ *
143
+ * - `registryItem`: the registry item containing handler information.
144
+ * - `functionName`: the name of the function.
145
+ * - `toolCallId`: a unique identifier for the function call.
146
+ * - `arguments`: the arguments for the function.
147
+ * - `context`: the LLM context.
148
+ * - `runLlm`: optional flag to control LLM execution after function call.
149
+ * - `groupId`: shared identifier for all function calls from the same LLM
150
+ * response batch. Used to trigger the LLM exactly once when the last call in
151
+ * the group completes.
152
+ * - `settled`: whether the call has already reached a terminal state — a final
153
+ * result, a timeout, or a cancellation. Results reported after that are
154
+ * rejected, since the rest of the pipeline has stopped tracking the call.
155
+ */
156
+ export class FunctionCallRunnerItem {
157
+ registryItem;
158
+ functionName;
159
+ toolCallId;
160
+ arguments;
161
+ context;
162
+ runLlm;
163
+ groupId;
164
+ settled;
165
+ constructor(options) {
166
+ this.registryItem = options.registryItem;
167
+ this.functionName = options.functionName;
168
+ this.toolCallId = options.toolCallId;
169
+ this.arguments = options.arguments;
170
+ this.context = options.context;
171
+ this.runLlm = options.runLlm ?? null;
172
+ this.groupId = options.groupId ?? null;
173
+ this.settled = options.settled ?? false;
174
+ }
175
+ }
176
+ /**
177
+ * Attributes `toolOptions()` sets on a handler (upstream `@tool_options`),
178
+ * with the option names the log lines use.
179
+ */
180
+ const TOOL_OPTION_NAMES = {
181
+ _pipecatCancelOnInterruption: "cancel_on_interruption",
182
+ _pipecatTimeoutSecs: "timeout_secs",
183
+ _pipecatCancellableByLlm: "cancellable_by_llm",
184
+ };
185
+ /** Python's `repr()` closely enough for log lines. */
186
+ function repr(value) {
187
+ if (typeof value === "string") {
188
+ return value.includes("'") && !value.includes('"') ? `"${value}"` : `'${value}'`;
189
+ }
190
+ if (value === null || value === undefined)
191
+ return "None";
192
+ if (value === true)
193
+ return "True";
194
+ if (value === false)
195
+ return "False";
196
+ if (Array.isArray(value))
197
+ return `[${value.map(repr).join(", ")}]`;
198
+ if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
199
+ const entries = Object.entries(value);
200
+ return `{${entries.map(([k, v]) => `${repr(k)}: ${repr(v)}`).join(", ")}}`;
201
+ }
202
+ return String(value);
203
+ }
204
+ /** Python's `f"{e}"` for an exception. */
205
+ function errorText(e) {
206
+ return e instanceof Error ? e.message : String(e);
207
+ }
208
+ /** A string attribute set on a handler function, if any. */
209
+ function handlerAttr(handler, attr) {
210
+ if (handler === null || handler === undefined)
211
+ return null;
212
+ return handler[attr] ?? null;
213
+ }
214
+ /**
215
+ * Base class for all LLM services.
216
+ *
217
+ * Handles function calling registration and execution with support for both
218
+ * parallel and sequential execution modes. Provides event handlers for
219
+ * completion timeouts and function call lifecycle events.
220
+ *
221
+ * The service supports the following event handlers:
222
+ *
223
+ * - on_completion_timeout: called when an LLM completion timeout occurs
224
+ * - on_function_calls_started: called when function calls are received and
225
+ * execution is about to start. Built-in tools (e.g. a `cancel_<name>` tool)
226
+ * are excluded from this event.
227
+ * - on_function_calls_cancelled: called after one or more async tool calls are
228
+ * cancelled.
229
+ *
230
+ * @example
231
+ * llm.eventHandler("on_completion_timeout", async (service) => {
232
+ * logger.warning("LLM completion timed out");
233
+ * });
234
+ * llm.eventHandler("on_function_calls_started", async (service, functionCalls: FunctionCallFromLLM[]) => {
235
+ * logger.info(`Starting ${functionCalls.length} function calls`);
236
+ * });
237
+ * llm.eventHandler("on_function_calls_cancelled", async (service, functionCalls: FunctionCallFromLLM[]) => {
238
+ * logger.info(`Cancelled ${functionCalls.length} function calls`);
239
+ * });
240
+ *
241
+ * @throws InvalidEnvVarValueError (from the constructor) if
242
+ * `PIPECAT_LOG_LLM_CONTEXT` is set to an unknown mode.
243
+ */
244
+ export class LLMService extends UserTurnCompletionLLMServiceMixin(AIService) {
245
+ _adapter;
246
+ /**
247
+ * Subclasses should set this to a more specific adapter when necessary.
248
+ * `null` selects OpenAILLMAdapter, which aligns with most LLM
249
+ * implementations.
250
+ */
251
+ static adapterClass = null;
252
+ /**
253
+ * Returned to the LLM as the tool result when an unavailable function is
254
+ * called. Deliberately neutral about future availability so the LLM can
255
+ * pick the function up again if it returns (e.g. via the
256
+ * `addToolChangeMessages` activation message, or silently on a later
257
+ * inference). `{function_name}` is substituted at runtime.
258
+ */
259
+ static MISSING_FUNCTION_CALL_MESSAGE_TEMPLATE = "The function `{function_name}` is not currently available.";
260
+ /**
261
+ * Returned to the LLM as the tool result when a handler raises. It names
262
+ * the function as the thing that failed. The exception itself is
263
+ * deliberately left out.
264
+ */
265
+ static FUNCTION_CALL_ERROR_MESSAGE_TEMPLATE = "The function `{function_name}` failed and returned no result.";
266
+ /**
267
+ * Whether the provider can enforce a response schema in `runInference()`.
268
+ *
269
+ * Services whose provider can set this to true. When only some of its
270
+ * models can, they also override `modelSupportsResponseSchema()`.
271
+ */
272
+ static supportsResponseSchema = false;
273
+ _runInParallel;
274
+ _groupParallelTools;
275
+ _functionCallTimeoutSecs;
276
+ _fileResolver;
277
+ _reportsTtfat = null;
278
+ // Turn completion is owned by LLMTurnCompletionUserTurnStopStrategy, which
279
+ // enables it over an LLMUpdateSettingsFrame once the pipeline starts.
280
+ _filterIncompleteUserTurns = false;
281
+ _loggedThinkingDefaultModel = null;
282
+ // Holds a speculative response until its turn is confirmed. Frames are
283
+ // routed through it on the way out, in `pushFrame`.
284
+ _speculationGate;
285
+ // The per-tool cancel tools currently advertised, by name.
286
+ _cancelToolNames = new Set();
287
+ // Cancel-tool names that collide with a user's own tool, warned about once.
288
+ _warnedCancelCollisions = new Set();
289
+ // The user's base system instruction, without composed addons. Captured
290
+ // here and refreshed when the user changes `systemInstruction` at runtime;
291
+ // `_composeSystemInstruction` always rebuilds the effective instruction
292
+ // from this plus any appended / addon instructions.
293
+ _baseSystemInstruction;
294
+ _appendedSystemInstructions = [];
295
+ // The instruction as last composed, so a recomposition that changes nothing
296
+ // (every tool sync recomposes) is not logged again.
297
+ _composedSystemInstruction = null;
298
+ _functions = new Map();
299
+ // Cleanup callables carried by registered tool handlers, awaited at service
300
+ // teardown (see _recordToolCleanup).
301
+ _toolCleanups = [];
302
+ // Names we've already warned about for a redundant manual registration (an
303
+ // explicit registerFunction call for a tool whose advertised FunctionSchema
304
+ // already carries a handler), so the warning fires once rather than on
305
+ // every context frame.
306
+ _redundantRegistrationWarned = new Set();
307
+ // Names explicitly unregistered (via unregisterFunction) that
308
+ // auto-registration must not re-register while they're still advertised —
309
+ // otherwise a standalone unregister would be undone by the next context
310
+ // frame. Cleared when the name is registered again or stops being
311
+ // advertised (see _syncRegisteredToolHandlers).
312
+ _explicitlyUnregisteredFunctionNames = new Set();
313
+ _functionCallTasks = new Map();
314
+ _sequentialRunnerTask = null;
315
+ _sequentialRunnerQueue = new Queue();
316
+ _skipTts = null;
317
+ _summaryTask = null;
318
+ // Whether the one-time realtime-service "no turn frames" warning has fired
319
+ // (see _warnIfRealtimeServiceEmitsNoTurnFrames).
320
+ _warnedRealtimeServiceNoTurnFrames = false;
321
+ // Bound handlers, so they compare equal wherever they are stored (Python
322
+ // bound methods compare equal; JavaScript closures don't).
323
+ #functionCallTaskDone = (task) => this._functionCallTaskFinished(task);
324
+ #missingFunctionCallHandler = (params) => this._missingFunctionCallHandler(params);
325
+ #cancelToolHandler = (params) => this._cancelToolHandler(params);
326
+ constructor(options = {}) {
327
+ super({
328
+ ...options,
329
+ // Here in case subclass doesn't implement more specific settings (which
330
+ // hopefully should be rare)
331
+ settings: options.settings ?? new LLMSettings(),
332
+ });
333
+ // Filled in here rather than in every service's default settings, since
334
+ // only the services with thinking defaults read it.
335
+ if (!isGiven(this._settings.useProviderThinkingDefault)) {
336
+ this._settings.useProviderThinkingDefault = false;
337
+ }
338
+ // Validated here so a bad value stops the bot at startup; otherwise it
339
+ // would surface on the first inference, and only with DEBUG enabled.
340
+ _getLlmContextLogMode();
341
+ this._runInParallel = options.runInParallel ?? true;
342
+ this._groupParallelTools = options.groupParallelTools ?? true;
343
+ this._functionCallTimeoutSecs = options.functionCallTimeoutSecs ?? null;
344
+ this._fileResolver = options.fileResolver ?? new FileResolver();
345
+ this._speculationGate = new SpeculationGate({ name: `${this}::SpeculationGate` });
346
+ const baseSi = this._settings.systemInstruction;
347
+ this._baseSystemInstruction = typeof baseSi === "string" ? baseSi : null;
348
+ const adapterClass = this.constructor._resolveAdapterClass();
349
+ this._adapter = new adapterClass();
350
+ // The adapter reads the resolver's caches during every conversion —
351
+ // invocation params and logging alike.
352
+ this._adapter.fileResolver = this._fileResolver;
353
+ this._registerEventHandler("on_function_calls_started");
354
+ this._registerEventHandler("on_function_calls_cancelled");
355
+ this._registerEventHandler("on_completion_timeout");
356
+ }
357
+ /** Return the adapter class to instantiate for this service. */
358
+ static _resolveAdapterClass() {
359
+ if (LLMService.adapterClass !== null) {
360
+ return LLMService.adapterClass;
361
+ }
362
+ return OpenAILLMAdapter;
363
+ }
364
+ /**
365
+ * Get the LLM adapter instance.
366
+ *
367
+ * @returns The adapter instance used for LLM communication.
368
+ */
369
+ getLlmAdapter() {
370
+ return this._adapter;
371
+ }
372
+ /**
373
+ * Create an LLM-specific message (as opposed to a standard message) for use
374
+ * in an LLMContext.
375
+ *
376
+ * @param message The message content.
377
+ * @returns A LLMSpecificMessage instance.
378
+ */
379
+ createLlmSpecificMessage(message) {
380
+ return this.getLlmAdapter().createLlmSpecificMessage(message);
381
+ }
382
+ /**
383
+ * Log that the service is generating a response from the given context.
384
+ *
385
+ * The context is included unless the LLM context log mode is `off`.
386
+ *
387
+ * @param context The context sent with the request.
388
+ */
389
+ _logLlmResponse(context) {
390
+ this._logLlmEvent("Generating LLM response", context);
391
+ }
392
+ /**
393
+ * Log that a realtime service is seeding its server-side conversation.
394
+ *
395
+ * Realtime services upload the context once per session (and again after a
396
+ * reset), then send only new items. The context is included unless the LLM
397
+ * context log mode is `off`.
398
+ *
399
+ * @param context The context being uploaded.
400
+ */
401
+ _logLlmConversationSetup(context) {
402
+ this._logLlmEvent("Setting up LLM conversation", context);
403
+ }
404
+ _logLlmEvent(event, context) {
405
+ // Built only if a sink accepts DEBUG.
406
+ if (!logger.isEnabledFor("DEBUG"))
407
+ return;
408
+ if (_getLlmContextLogMode() === _LLMContextLogMode.OFF) {
409
+ logger.debug(`${this}: ${event}`);
410
+ return;
411
+ }
412
+ const messages = this.getLlmAdapter().getMessagesForLogging(context);
413
+ logger.debug(`${this}: ${event} ${JSON.stringify(messages)}`);
414
+ }
415
+ /**
416
+ * Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the
417
+ * given LLM context.
418
+ *
419
+ * Must be implemented by subclasses.
420
+ *
421
+ * @param context The LLM context containing conversation history.
422
+ * @param options.maxTokens Optional maximum number of tokens to generate. If
423
+ * provided, overrides the service's default max_tokens/max_completion_tokens
424
+ * setting.
425
+ * @param options.systemInstruction Optional system instruction to use for
426
+ * this inference. If provided, overrides any system instruction in the
427
+ * context.
428
+ * @param options.responseSchema Optional JSON schema the reply must follow.
429
+ * Services that can have the provider enforce it return JSON text matching
430
+ * the schema. When the service or its current model cannot enforce one
431
+ * (see `supportsResponseSchema`), the schema is ignored with a warning.
432
+ * The schema must satisfy the strictest provider in use: every object
433
+ * lists all its properties as required and sets `additionalProperties` to
434
+ * false.
435
+ * @returns The LLM's response as a string, or null if no response is generated.
436
+ */
437
+ async runInference(_context, _options = {}) {
438
+ throw new NotImplementedError(`run_inference() not supported by ${this.constructor.name}`);
439
+ }
440
+ /** The schema to send, or null with a warning if it cannot be enforced. */
441
+ _checkResponseSchema(responseSchema) {
442
+ if (responseSchema === null || responseSchema === undefined) {
443
+ return null;
444
+ }
445
+ if (!this.supportsResponseSchema) {
446
+ logger.warning(`${this}: response_schema is not supported and is ignored`);
447
+ return null;
448
+ }
449
+ const model = this._settings.model;
450
+ const cls = this.constructor;
451
+ if (!cls.modelSupportsResponseSchema(isGiven(model) && model ? model : "")) {
452
+ logger.warning(`${this}: response_schema is not supported by model ${model ?? "None"} and is ignored`);
453
+ return null;
454
+ }
455
+ return responseSchema;
456
+ }
457
+ /** Whether this service's provider can enforce a response schema. */
458
+ get supportsResponseSchema() {
459
+ return this.constructor.supportsResponseSchema;
460
+ }
461
+ /**
462
+ * Whether a model can enforce a response schema, on a provider that can.
463
+ *
464
+ * @param model The model name.
465
+ */
466
+ static modelSupportsResponseSchema(_model) {
467
+ return true;
468
+ }
469
+ /**
470
+ * Whether this service reports time-to-first-answer-token.
471
+ *
472
+ * Speech-to-speech services answer in audio, which has no answer token to
473
+ * measure to, so only text-answering services report it. Derived from
474
+ * `serviceMetadataFrame()` and cached, since it is read once per streamed
475
+ * token.
476
+ */
477
+ get reportsTtfat() {
478
+ if (this._reportsTtfat === null) {
479
+ this._reportsTtfat = !this.serviceMetadataFrame().isRealtimeService;
480
+ }
481
+ return this._reportsTtfat;
482
+ }
483
+ /**
484
+ * Whether this service delivers a tool's intermediate results to the model.
485
+ *
486
+ * An intermediate result is one reported with
487
+ * `new FunctionCallResultProperties({ isFinal: false })` while the call keeps
488
+ * running. A text service passes them on as context messages. A
489
+ * speech-to-speech service has to map them onto whatever second channel its
490
+ * provider offers, and not all of them have one.
491
+ */
492
+ get acceptsIntermediateFunctionCallResults() {
493
+ return true;
494
+ }
495
+ /**
496
+ * Why this service can't take the given role in an `LLMWithBackend`, if it
497
+ * can't.
498
+ *
499
+ * The composite raises with what this returns, so the objection says what
500
+ * to do instead. Services that work in both roles, which is nearly all of
501
+ * them, say nothing.
502
+ *
503
+ * @param role The role the service is being asked to take.
504
+ * @returns The reason, or null when the service can take the role.
505
+ */
506
+ llmWithBackendRoleObjection(_role) {
507
+ return null;
508
+ }
509
+ /**
510
+ * Send a model the thinking default its service lists, and log it once per
511
+ * model.
512
+ *
513
+ * Services that send each model its lowest thinking setting call this when
514
+ * the request configures no thinking of its own. With
515
+ * `useProviderThinkingDefault` set, the service sends no default. The first
516
+ * request for each model logs the setting sent, or that the model runs at
517
+ * the provider's default, warning when it has no default.
518
+ *
519
+ * @param params The request params, updated in place. Each field of
520
+ * `defaultParams` is merged into the dict already there, if any.
521
+ * @param model The model the request is for.
522
+ * @param defaultParams The request fields to send, or null if the service
523
+ * lists no default for the model.
524
+ * @param options.canThink Whether the model can think, which decides
525
+ * whether a model with no default is warned about.
526
+ * @param options.provider The provider's name, for the log lines.
527
+ * @param options.settingsFields The settings fields that configure thinking,
528
+ * as the log lines name them, such as "`thinking`".
529
+ */
530
+ _applyThinkingDefault(params, model, defaultParams, options) {
531
+ const { canThink, provider, settingsFields } = options;
532
+ const providerDefault = assertGiven(this._settings.useProviderThinkingDefault);
533
+ if (defaultParams !== null && !providerDefault) {
534
+ for (const [field, value] of Object.entries(defaultParams)) {
535
+ params[field] = { ...(params[field] ?? {}), ...value };
536
+ }
537
+ }
538
+ if (model === this._loggedThinkingDefaultModel) {
539
+ return;
540
+ }
541
+ this._loggedThinkingDefaultModel = model;
542
+ const settingsClass = `${this.constructor.name}.Settings`;
543
+ if (providerDefault) {
544
+ if (canThink) {
545
+ logger.debug(`${this}: use_provider_thinking_default is set, so ${model} runs at ${provider}'s ` +
546
+ `default thinking. To change it, set ${settingsFields} in ${settingsClass}.`);
547
+ }
548
+ }
549
+ else if (defaultParams !== null) {
550
+ const setting = Object.entries(defaultParams)
551
+ .map(([field, value]) => `${field}=${repr(value)}`)
552
+ .join(", ");
553
+ logger.info(`${this}: sending ${setting} to ${model}, its lowest thinking setting, for the ` +
554
+ "fastest responses. If your bot needs multistep reasoning, set " +
555
+ `${settingsFields} in ${settingsClass}.`);
556
+ }
557
+ else if (canThink) {
558
+ logger.warning(`${this}: Pipecat has no thinking default for ${model}, so it runs at ` +
559
+ `${provider}'s default, which can think before replying and slow responses. ` +
560
+ `For the fastest responses, set ${settingsFields} in ${settingsClass} to the ` +
561
+ `lowest setting ${model} supports.`);
562
+ }
563
+ }
564
+ /**
565
+ * The metadata frame this LLM service broadcasts at start.
566
+ *
567
+ * The base returns a plain (non-realtime) frame; realtime (speech-to-speech)
568
+ * subclasses override this to set `isRealtimeService: true` and, when their
569
+ * turns are provider-driven, recommend `ExternalUserTurnStrategies` via
570
+ * `userTurnStrategies`.
571
+ *
572
+ * Mostly here for conceptual consistency — today only realtime services
573
+ * need to override it — but it's a natural placeholder for future
574
+ * LLM-service metadata.
575
+ */
576
+ serviceMetadataFrame() {
577
+ return new LLMServiceMetadataFrame({ serviceName: this.name });
578
+ }
579
+ /**
580
+ * Warn (once) when a realtime service won't emit its own turn frames.
581
+ *
582
+ * Realtime services call this from `serviceMetadataFrame()` at the point
583
+ * they determine whether their turns are provider-driven. When they aren't,
584
+ * downstream processors that expect `UserStarted/StoppedSpeakingFrame`
585
+ * (e.g. RTVI) may need local VAD/turn detection.
586
+ */
587
+ _warnIfRealtimeServiceEmitsNoTurnFrames(emitsTurnFrames) {
588
+ if (emitsTurnFrames || this._warnedRealtimeServiceNoTurnFrames) {
589
+ return;
590
+ }
591
+ this._warnedRealtimeServiceNoTurnFrames = true;
592
+ logger.warning(`${this} is not emitting turn frames ` +
593
+ "(UserStartedSpeakingFrame/UserStoppedSpeakingFrame) — this service " +
594
+ "either doesn't support them or isn't configured to emit them. A " +
595
+ "couple of things to keep in mind:\n" +
596
+ " - Other processors in the pipeline (e.g. RTVI) may expect " +
597
+ "turn frames. You can enable local VAD/turn detection by " +
598
+ "setting a vad_analyzer in LLMUserAggregatorParams.\n" +
599
+ " - Be aware that local turns may NOT perfectly align with " +
600
+ 'the "ground truth" of server-decided turns, so they should ' +
601
+ "be thought of as APPROXIMATE (unless, of course, you've " +
602
+ "also configured local turn detection to *drive* the " +
603
+ "realtime service's turns, e.g. by setting " +
604
+ "vad=GeminiVADParams(disabled=True)).");
605
+ }
606
+ /**
607
+ * Start the LLM service.
608
+ *
609
+ * @param frame The start frame.
610
+ */
611
+ async start(frame) {
612
+ await super.start(frame);
613
+ if (!this._runInParallel) {
614
+ await this._createSequentialRunnerTask();
615
+ }
616
+ // A realtime service can run a tool it was configured with before the
617
+ // first context frame reaches it, so the handlers of its own tools are
618
+ // registered up front. Context frames re-sync them from then on.
619
+ this._syncRegisteredToolHandlers(null);
620
+ }
621
+ /**
622
+ * Stop the LLM service.
623
+ *
624
+ * @param frame The end frame.
625
+ */
626
+ async stop(frame) {
627
+ await super.stop(frame);
628
+ if (!this._runInParallel) {
629
+ await this._cancelSequentialRunnerTask();
630
+ }
631
+ await this._cancelSummaryTask();
632
+ }
633
+ /**
634
+ * Cancel the LLM service.
635
+ *
636
+ * @param frame The cancel frame.
637
+ */
638
+ async cancel(frame) {
639
+ await super.cancel(frame);
640
+ if (!this._runInParallel) {
641
+ await this._cancelSequentialRunnerTask();
642
+ }
643
+ await this._cancelSummaryTask();
644
+ }
645
+ /**
646
+ * Release LLM service resources at teardown.
647
+ *
648
+ * Besides this service's own tasks, runs any cleanup callables carried by
649
+ * registered tool handlers (e.g. closing an MCP client's connection).
650
+ */
651
+ async cleanup() {
652
+ await super.cleanup();
653
+ if (!this._runInParallel) {
654
+ await this._cancelSequentialRunnerTask();
655
+ }
656
+ await this._cancelSummaryTask();
657
+ await this._cancelAllFunctionCallTasks();
658
+ await this._runToolCleanups();
659
+ }
660
+ /**
661
+ * Append durable text to the system instruction, preserving the user's
662
+ * prompt.
663
+ *
664
+ * The text is composed onto the end of the system instruction (joined with
665
+ * a blank line) and re-applied on every inference, so it survives
666
+ * context-message resets (e.g. `new LLMMessagesUpdateFrame({ messages: [] })`).
667
+ * Intended for framework components that own an LLM and need to add
668
+ * standard guidance to a user-provided prompt — for example, `UIWorker`
669
+ * appends the UI wire-format guide. Appended instructions compose after the
670
+ * user's base prompt and alongside the turn-completion and
671
+ * async-tool-cancellation instructions.
672
+ *
673
+ * @param instruction The instruction text to append.
674
+ */
675
+ appendSystemInstruction(instruction) {
676
+ this._appendedSystemInstructions.push(instruction);
677
+ this._composeSystemInstruction();
678
+ }
679
+ /**
680
+ * Whether any registered tool runs asynchronously.
681
+ *
682
+ * @returns True when a tool is registered with `cancelOnInterruption: false`,
683
+ * which is what makes the async-tool guidance worth composing in. The
684
+ * built-in cancellation tool is registered as a synchronous one, so it
685
+ * never counts on its own.
686
+ */
687
+ _hasAsyncTools() {
688
+ for (const item of this._functions.values()) {
689
+ if (!item.cancelOnInterruption)
690
+ return true;
691
+ }
692
+ return false;
693
+ }
694
+ /**
695
+ * Rebuild `systemInstruction` from the base prompt and all active addons.
696
+ *
697
+ * Joins the user's base system instruction (the single source of truth,
698
+ * captured at construction and refreshed on runtime `systemInstruction`
699
+ * updates) with any appended instructions (e.g. the `UIWorker` prompt
700
+ * guide), turn completion instructions (when enabled), and async tool
701
+ * cancellation instructions (when enabled). Safe to call repeatedly — it
702
+ * always rebuilds from the base, so it never compounds, and it logs the
703
+ * result only when it differs from the previous composition.
704
+ */
705
+ _composeSystemInstruction() {
706
+ const base = this._baseSystemInstruction;
707
+ const parts = base ? [base] : [];
708
+ parts.push(...this._appendedSystemInstructions);
709
+ if (this._filterIncompleteUserTurns) {
710
+ parts.push(this._userTurnCompletionConfig.completionInstructions);
711
+ }
712
+ if (this._cancelToolNames.size > 0) {
713
+ parts.push(ASYNC_TOOL_CANCELLATION_INSTRUCTIONS);
714
+ }
715
+ if (this._hasAsyncTools()) {
716
+ parts.push(ASYNC_TOOL_INSTRUCTIONS);
717
+ }
718
+ const composed = parts.filter((p) => p).join("\n\n") || null;
719
+ this._settings.systemInstruction = composed;
720
+ if (composed !== this._composedSystemInstruction) {
721
+ this._composedSystemInstruction = composed;
722
+ logger.debug(`${this}: System instruction composed: ${composed}`);
723
+ }
724
+ }
725
+ /**
726
+ * Apply a settings delta, handling turn-completion fields.
727
+ *
728
+ * @param delta An LLM settings delta.
729
+ * @returns Dict mapping changed field names to their previous values.
730
+ */
731
+ async _updateSettings(delta) {
732
+ const changed = await super._updateSettings(delta);
733
+ if ("filterIncompleteUserTurns" in changed) {
734
+ const value = this._settings.filterIncompleteUserTurns;
735
+ this._filterIncompleteUserTurns = isGiven(value) && Boolean(value);
736
+ logger.info(`${this}: Incomplete turn filtering ` +
737
+ `${this._filterIncompleteUserTurns ? "enabled" : "disabled"}`);
738
+ }
739
+ if ("systemInstruction" in changed) {
740
+ // The user replaced the base prompt; re-snapshot it so composition
741
+ // rebuilds the effective instruction from the new value.
742
+ const baseSi = this._settings.systemInstruction;
743
+ this._baseSystemInstruction = typeof baseSi === "string" ? baseSi : null;
744
+ }
745
+ if ("userTurnCompletionConfig" in changed && this._filterIncompleteUserTurns) {
746
+ this.setUserTurnCompletionConfig(assertGiven(this._settings.userTurnCompletionConfig));
747
+ }
748
+ // Any of these fields changes the composed instruction; rebuild it.
749
+ if ("filterIncompleteUserTurns" in changed ||
750
+ "systemInstruction" in changed ||
751
+ "userTurnCompletionConfig" in changed) {
752
+ this._composeSystemInstruction();
753
+ }
754
+ return changed;
755
+ }
756
+ /**
757
+ * Process a frame.
758
+ *
759
+ * @param frame The frame to process.
760
+ * @param direction The direction of frame processing.
761
+ */
762
+ async processFrame(frame, direction) {
763
+ await super.processFrame(frame, direction);
764
+ if (frame instanceof InterruptionFrame) {
765
+ await this._handleInterruptions(frame);
766
+ }
767
+ else if (frame instanceof EagerEndOfTurnCancelFrame) {
768
+ await this._handleEagerEndOfTurnCancel(frame);
769
+ }
770
+ else if (frame instanceof LLMConfigureOutputFrame) {
771
+ this._skipTts = frame.skipTts;
772
+ }
773
+ else if (frame instanceof LLMUpdateSettingsFrame) {
774
+ if (frame.service !== null && frame.service !== undefined && frame.service !== this) {
775
+ await this.pushFrame(frame, direction);
776
+ }
777
+ else if (frame.delta !== null && frame.delta !== undefined) {
778
+ await this._updateSettings(frame.delta);
779
+ }
780
+ }
781
+ else if (frame instanceof LLMContextSummaryRequestFrame) {
782
+ await this._handleSummaryRequest(frame);
783
+ }
784
+ if (frame instanceof LLMContextFrame) {
785
+ // Runs before the subclass starts the completion, so the gate knows
786
+ // what this inference answers before any of its frames arrive.
787
+ this._speculationGate.beginSpeculation(frame.speculation);
788
+ // Sync the registered handlers with the tools advertised in the
789
+ // context: register any newly advertised handler, drop the ones we
790
+ // auto-registered that are no longer advertised. The context carries
791
+ // the current tool set on every inference, so this is the single place
792
+ // tool changes take effect for text LLMs.
793
+ //
794
+ // Realtime (speech-to-speech) services run continuously and don't get a
795
+ // fresh context frame per turn, so they additionally call
796
+ // _syncRegisteredToolHandlers on their own LLMSetToolsFrame handling.
797
+ this._syncRegisteredToolHandlers(frame.context.tools);
798
+ }
799
+ }
800
+ /**
801
+ * Pushes a frame.
802
+ *
803
+ * @param frame The frame to push.
804
+ * @param direction The direction of frame pushing.
805
+ */
806
+ async pushFrame(frame, direction = FrameDirection.DOWNSTREAM) {
807
+ if (frame instanceof LLMTextFrame ||
808
+ frame instanceof LLMFullResponseStartFrame ||
809
+ frame instanceof LLMFullResponseEndFrame) {
810
+ if (this._skipTts !== null) {
811
+ frame.skipTts = this._skipTts;
812
+ }
813
+ }
814
+ // The gate decides synchronously, so its verdict can't be torn by another
815
+ // task pushing at the same time. Everything it hands back is pushed past
816
+ // it — routing that back through here would re-gate it.
817
+ const emitted = this._speculationGate.process(frame, direction);
818
+ for (const [gatedFrame, gatedDirection] of emitted) {
819
+ await super.pushFrame(gatedFrame, gatedDirection);
820
+ }
821
+ }
822
+ /**
823
+ * Push LLM text, using turn completion detection if enabled.
824
+ *
825
+ * This helper method simplifies text pushing in LLM implementations by
826
+ * handling the conditional logic for turn completion internally.
827
+ *
828
+ * @param text The text content from the LLM to push.
829
+ */
830
+ async _pushLlmText(text) {
831
+ // Measured before turn-completion filtering, which can hold text back or
832
+ // drop it entirely — neither says anything about how fast the model
833
+ // answered.
834
+ if (this.reportsTtfat) {
835
+ await this.stopTtfatMetrics();
836
+ }
837
+ if (this._filterIncompleteUserTurns) {
838
+ await this._pushTurnText(text);
839
+ }
840
+ else {
841
+ await this.pushFrame(new LLMTextFrame({ text }));
842
+ }
843
+ }
844
+ async _handleInterruptions(_frame) {
845
+ for (const [functionName, entry] of [...this._functions.entries()]) {
846
+ if (entry.cancelOnInterruption) {
847
+ await this._cancelFunctionCall(functionName);
848
+ }
849
+ }
850
+ }
851
+ /**
852
+ * Stop generating a response whose user turn turned out to be unfinished.
853
+ *
854
+ * The tokens are wasted either way; stopping keeps us from paying for the
855
+ * rest of them. Unlike an interruption this leaves the turn open — the bot
856
+ * never spoke, and the user is still mid-turn.
857
+ */
858
+ async _handleEagerEndOfTurnCancel(_frame) {
859
+ // Runs before the frame reaches the gate, which is what clears the
860
+ // speculation, so this still sees the one being withdrawn.
861
+ if (!this._speculationGate.isSpeculating) {
862
+ return;
863
+ }
864
+ logger.debug(`${this}: eager end of turn withdrawn, stopping the speculative inference`);
865
+ await this._startInterruption();
866
+ await this.stopAllMetrics();
867
+ }
868
+ /**
869
+ * Handle context summarization request from aggregator.
870
+ *
871
+ * Processes a summarization request by generating a compressed summary of
872
+ * conversation history. Uses the adapter to format the summary according
873
+ * to the provider's requirements. Broadcasts the result back to the
874
+ * aggregator for context reconstruction.
875
+ *
876
+ * @param frame The summary request frame containing context and parameters.
877
+ */
878
+ async _handleSummaryRequest(frame) {
879
+ logger.debug(`${this}: Processing summarization request ${frame.requestId}`);
880
+ // Create a background task to generate the summary without blocking
881
+ this._summaryTask = this.createTask(() => this._generateSummaryTask(frame), "_generate_summary_task");
882
+ }
883
+ /**
884
+ * Background task to generate summary without blocking the pipeline.
885
+ *
886
+ * @param frame The summary request frame containing context and parameters.
887
+ */
888
+ async _generateSummaryTask(frame) {
889
+ let summary = "";
890
+ let lastIndex = -1;
891
+ let error = null;
892
+ // Python `or`: a 0 timeout also falls back to the default.
893
+ const timeout = frame.summarizationTimeout || DEFAULT_SUMMARIZATION_TIMEOUT;
894
+ try {
895
+ [summary, lastIndex] = await waitFor(() => this._generateSummary(frame), timeout);
896
+ }
897
+ catch (e) {
898
+ rethrowIfCancelled(e);
899
+ if (e instanceof TimeoutError) {
900
+ await this.pushError(`Context summarization timed out after ${timeout}s`);
901
+ }
902
+ else {
903
+ error = `Error generating context summary: ${errorText(e)}`;
904
+ await this.pushError(error, { exception: e });
905
+ }
906
+ }
907
+ await this.broadcastFrame(LLMContextSummaryResultFrame, {
908
+ requestId: frame.requestId,
909
+ summary,
910
+ lastSummarizedIndex: lastIndex,
911
+ error,
912
+ });
913
+ this._summaryTask = null;
914
+ }
915
+ /**
916
+ * Generate a compressed summary of conversation context.
917
+ *
918
+ * Uses the message selection logic to identify which messages to
919
+ * summarize, formats them as a transcript, and invokes the LLM to generate
920
+ * a concise summary. The summary is formatted according to the LLM
921
+ * provider's requirements using the adapter.
922
+ *
923
+ * Requires the service to implement `runInference()` for one-shot LLM calls.
924
+ *
925
+ * @param frame The summary request frame containing context and configuration.
926
+ * @returns Tuple of (formatted summary message, lastSummarizedIndex).
927
+ * @throws Error if there are no messages to summarize, the service doesn't
928
+ * support runInference(), or the LLM returns an empty summary.
929
+ */
930
+ async _generateSummary(frame) {
931
+ // Get messages to summarize using utility method
932
+ const result = LLMContextSummarizationUtil.getMessagesToSummarize(frame.context, frame.minMessagesToKeep);
933
+ if (result.messages.length === 0) {
934
+ logger.debug(`${this}: No messages to summarize`);
935
+ throw new Error("No messages to summarize");
936
+ }
937
+ logger.debug(`${this}: Generating summary for ${result.messages.length} messages ` +
938
+ `(index 0 to ${result.lastSummarizedIndex}), ` +
939
+ `target_context_tokens=${frame.targetContextTokens}`);
940
+ // Create summary context
941
+ const transcript = LLMContextSummarizationUtil.formatMessagesForSummary(result.messages);
942
+ const summaryContext = new LLMContext({
943
+ messages: [{ role: "user", content: `Conversation history:\n${transcript}` }],
944
+ });
945
+ // Generate summary using runInference. This will be overridden by each
946
+ // LLM service implementation.
947
+ let summaryText;
948
+ try {
949
+ summaryText = await this.runInference(summaryContext, {
950
+ maxTokens: frame.targetContextTokens,
951
+ systemInstruction: frame.summarizationPrompt,
952
+ });
953
+ }
954
+ catch (e) {
955
+ if (e instanceof NotImplementedError) {
956
+ throw new Error(`LLM service ${this.constructor.name} does not implement run_inference`);
957
+ }
958
+ throw e;
959
+ }
960
+ if (!summaryText) {
961
+ throw new Error("LLM returned empty summary");
962
+ }
963
+ summaryText = summaryText.trim();
964
+ logger.info(`${this}: Generated summary of ${summaryText.length} characters ` +
965
+ `for ${result.messages.length} messages`);
966
+ return [summaryText, result.lastSummarizedIndex];
967
+ }
968
+ /**
969
+ * Register a function handler for LLM function calls.
970
+ *
971
+ * Call options resolve with the precedence **explicit argument >
972
+ * `toolOptions()` attributes > default**. `null`/`undefined` (the default)
973
+ * means "not provided" — the option falls back to the `toolOptions()` value
974
+ * on the handler, then to the documented default.
975
+ *
976
+ * @param functionName The name of the function to handle. Use null to handle
977
+ * all function calls with a catch-all handler.
978
+ * @param handler The function handler. Should accept a single
979
+ * FunctionCallParams parameter.
980
+ * @param options.cancelOnInterruption Whether to cancel this function call
981
+ * when an interruption occurs. When false the call is treated as
982
+ * asynchronous: the LLM continues the conversation immediately without
983
+ * waiting for the result, and the result is injected later via a
984
+ * developer message. Defaults to null (fall back to the `toolOptions()`
985
+ * value, then to true). A speech-to-speech service takes intermediate
986
+ * results (reported via `new FunctionCallResultProperties({ isFinal: false })`)
987
+ * on whatever second channel its provider offers; one that can't says so
988
+ * with `acceptsIntermediateFunctionCallResults` and drops them with a
989
+ * warning.
990
+ * @param options.timeoutSecs Optional per-tool timeout in seconds,
991
+ * overriding the global `functionCallTimeoutSecs`. A call that runs past it
992
+ * is cancelled: the handler is cancelled, the call is settled as
993
+ * cancelled, and inference runs so the LLM can report that it didn't
994
+ * complete. The deadline covers the handler's execution as a whole, so
995
+ * reporting an intermediate result neither clears it nor restarts it.
996
+ * Defaults to null (fall back to the `toolOptions()` value, then to the
997
+ * global timeout).
998
+ * @param options.cancellableByLlm Whether the LLM may cancel this call while
999
+ * it runs, through the `cancel_<name>` tool advertised alongside it. Pair
1000
+ * it with `cancelOnInterruption: false`; a synchronous call holds the LLM
1001
+ * until it returns, so there is no moment at which it could ask for the
1002
+ * call to stop. Defaults to null (fall back to the `toolOptions()` value,
1003
+ * then to false).
1004
+ */
1005
+ registerFunction(functionName, handler, options = {}) {
1006
+ if (functionName !== null && this._cancelToolNames.has(functionName)) {
1007
+ throw new Error(`'${functionName}' is a reserved built-in tool name and cannot be ` +
1008
+ "registered by user code.");
1009
+ }
1010
+ // Explicitly registering a handler clears any standalone-unregister
1011
+ // suppression for its name.
1012
+ this._explicitlyUnregisteredFunctionNames.delete(functionName);
1013
+ // Registering a function with the functionName set to null will run that
1014
+ // handler for all functions
1015
+ const resolvedCancelOnInterruption = this._resolveToolOption(functionName, options.cancelOnInterruption, handler, "_pipecatCancelOnInterruption", { default: true });
1016
+ this._functions.set(functionName, new FunctionCallRegistryItem({
1017
+ functionName,
1018
+ handler,
1019
+ cancelOnInterruption: resolvedCancelOnInterruption,
1020
+ timeoutSecs: this._resolveToolOption(functionName, options.timeoutSecs, handler, "_pipecatTimeoutSecs", { default: null }),
1021
+ cancellableByLlm: this._resolveCancellableByLlm(functionName, options.cancellableByLlm, handler, resolvedCancelOnInterruption),
1022
+ }));
1023
+ this._recordToolCleanup(handler);
1024
+ }
1025
+ /**
1026
+ * Track cleanup work a registered tool handler asks to run at teardown.
1027
+ *
1028
+ * A handler backed by a resource that outlives the call — an MCP server
1029
+ * connection, for instance — can carry a zero-argument async callable under
1030
+ * `_pipecatCleanup`. It is awaited when this service is cleaned up, the hook
1031
+ * the pipeline guarantees to run at teardown, so the resource is released
1032
+ * without the developer wiring teardown manually. Handlers sharing a
1033
+ * resource carry the same callable, recorded once.
1034
+ *
1035
+ * Cleanups are kept for the service's lifetime even if their handlers are
1036
+ * later unregistered: the resource may outlive any one tool's
1037
+ * advertisement, and cleanups are expected to be idempotent.
1038
+ */
1039
+ _recordToolCleanup(handler) {
1040
+ const cleanup = handlerAttr(handler, "_pipecatCleanup");
1041
+ if (cleanup === null || this._toolCleanups.includes(cleanup)) {
1042
+ return;
1043
+ }
1044
+ this._toolCleanups.push(cleanup);
1045
+ }
1046
+ /** Run recorded tool cleanups; errors are logged, not raised. */
1047
+ async _runToolCleanups() {
1048
+ const cleanups = this._toolCleanups;
1049
+ this._toolCleanups = [];
1050
+ for (const cleanup of cleanups) {
1051
+ try {
1052
+ await cleanup();
1053
+ }
1054
+ catch (e) {
1055
+ rethrowIfCancelled(e);
1056
+ logger.error(`${this} error running tool cleanup ${cleanup.name}: ${errorText(e)}`);
1057
+ }
1058
+ }
1059
+ }
1060
+ /**
1061
+ * Register a direct function handler.
1062
+ *
1063
+ * Shared core behind automatic registration (from an `LLMContext` or
1064
+ * `LLMSetToolsFrame`). Call options resolve by precedence: explicit
1065
+ * argument > `toolOptions()` attributes > default.
1066
+ *
1067
+ * @param handler The direct function to register.
1068
+ * @param options.cancelOnInterruption Explicit override, or null to use the
1069
+ * handler's value (then the true default).
1070
+ * @param options.timeoutSecs Explicit override, or null to use the handler's
1071
+ * value (then the null default, i.e. the global timeout).
1072
+ * @param options.cancellableByLlm Explicit override, or null to use the
1073
+ * handler's value (then the false default).
1074
+ */
1075
+ _registerDirectFunction(handler, options = {}) {
1076
+ const wrapper = new DirectFunctionWrapper(handler);
1077
+ if (this._cancelToolNames.has(wrapper.name)) {
1078
+ throw new Error(`'${wrapper.name}' is a reserved built-in tool name and cannot be ` +
1079
+ "registered by user code.");
1080
+ }
1081
+ const cancelOnInterruption = this._resolveToolOption(wrapper.name, options.cancelOnInterruption, handler, "_pipecatCancelOnInterruption", { default: true });
1082
+ const timeoutSecs = this._resolveToolOption(wrapper.name, options.timeoutSecs, handler, "_pipecatTimeoutSecs", { default: null });
1083
+ // Explicitly registering a handler clears any standalone-unregister
1084
+ // suppression for its name. (Auto-registration skips suppressed names
1085
+ // before reaching this method, so this only fires for explicit calls.)
1086
+ this._explicitlyUnregisteredFunctionNames.delete(wrapper.name);
1087
+ // The new entry defaults to autoRegistered=false, so a handler registered
1088
+ // through this method is treated as explicit and is never auto-pruned.
1089
+ this._functions.set(wrapper.name, new FunctionCallRegistryItem({
1090
+ functionName: wrapper.name,
1091
+ handler: wrapper,
1092
+ cancelOnInterruption,
1093
+ timeoutSecs,
1094
+ cancellableByLlm: this._resolveCancellableByLlm(wrapper.name, options.cancellableByLlm, handler, cancelOnInterruption),
1095
+ }));
1096
+ }
1097
+ /**
1098
+ * Resolve whether the LLM may cancel a tool's calls.
1099
+ *
1100
+ * @param functionName The tool's name, for the warning below.
1101
+ * @param explicit The value passed to the register call, or null if omitted.
1102
+ * @param handler The handler, which may carry a `toolOptions()` value.
1103
+ * @param cancelOnInterruption The tool's interruption behaviour, already
1104
+ * resolved by the caller — resolving it again here would read past
1105
+ * whatever the caller was told explicitly.
1106
+ * @returns Whether the LLM may cancel this tool's calls.
1107
+ */
1108
+ _resolveCancellableByLlm(functionName, explicit, handler, cancelOnInterruption) {
1109
+ const resolved = this._resolveToolOption(functionName, explicit, handler, "_pipecatCancellableByLlm", { default: false });
1110
+ if (!resolved) {
1111
+ return false;
1112
+ }
1113
+ if (cancelOnInterruption) {
1114
+ // A synchronous call holds the LLM until it returns, so it is never
1115
+ // running at a moment the LLM could ask to cancel it.
1116
+ logger.warning(`${this}: '${functionName}' sets cancellable_by_llm=True with ` +
1117
+ "cancel_on_interruption=True. The LLM cannot cancel a synchronous " +
1118
+ "call; pair it with cancel_on_interruption=False.");
1119
+ return false;
1120
+ }
1121
+ return true;
1122
+ }
1123
+ /**
1124
+ * Resolve a tool call option by precedence: explicit > handler attribute >
1125
+ * default.
1126
+ *
1127
+ * An explicit `null` is treated as "not provided" and falls back to the
1128
+ * handler's value, then the default — so `null` can't be passed to force a
1129
+ * default past a handler value (an intentionally unsupported niche; set
1130
+ * `functionCallTimeoutSecs` on the service for that).
1131
+ *
1132
+ * @param functionName The tool's name, for logging.
1133
+ * @param explicit The value passed to the register call, or null if the
1134
+ * caller omitted it.
1135
+ * @param handler The handler, which may carry `toolOptions()` attributes.
1136
+ * @param attr The attribute to read (e.g. `_pipecatTimeoutSecs`).
1137
+ * @param options.default The value to use when neither an explicit argument
1138
+ * nor a handler value is present.
1139
+ * @returns The resolved option value.
1140
+ */
1141
+ _resolveToolOption(functionName, explicit, handler, attr, options) {
1142
+ const decorated = handlerAttr(handler, attr);
1143
+ if (explicit !== null && explicit !== undefined) {
1144
+ if (decorated !== null && decorated !== explicit) {
1145
+ const optionName = TOOL_OPTION_NAMES[attr] ?? attr.replace(/^_pipecat/, "");
1146
+ logger.debug(`${this}: '${functionName}' registered with explicit ` +
1147
+ `${optionName}=${repr(explicit)}, overriding the ` +
1148
+ `decorator value ${repr(decorated)}.`);
1149
+ }
1150
+ return explicit;
1151
+ }
1152
+ return decorated !== null ? decorated : options.default;
1153
+ }
1154
+ /**
1155
+ * Whether an advertised handler differs from the one already registered.
1156
+ *
1157
+ * Normalizes a `DirectFunctionWrapper` to its underlying `.function` on both
1158
+ * sides, then compares by identity. Flows reuses one closure per node across
1159
+ * repeated context frames (no change) but builds a fresh closure on
1160
+ * transition (a real change), so identity is the right test.
1161
+ *
1162
+ * @param item The currently registered entry.
1163
+ * @param newHandler The advertised handler — a raw callable or a
1164
+ * `DirectFunctionWrapper`.
1165
+ * @returns True if `newHandler` is a different callable than `item` holds.
1166
+ */
1167
+ _advertisedHandlerChanged(item, newHandler) {
1168
+ const current = item.handler instanceof DirectFunctionWrapper ? item.handler.function : item.handler;
1169
+ const next = newHandler instanceof DirectFunctionWrapper ? newHandler.function : newHandler;
1170
+ return current !== next;
1171
+ }
1172
+ /**
1173
+ * Register handlers for any tools in the given set that carry one.
1174
+ *
1175
+ * A tool carries a handler when it's advertised as a direct function or as
1176
+ * a `FunctionSchema` with a `handler` set; either way the handler is
1177
+ * registered automatically, so no `registerFunction` call is needed. A
1178
+ * `FunctionSchema` without a handler is advertise-only and registers
1179
+ * nothing.
1180
+ *
1181
+ * Accepts whatever `LLMContext` accepts for tools — a `ToolsSchema`, a plain
1182
+ * list of direct functions / `FunctionSchema` objects, or `NOT_GIVEN` —
1183
+ * normalizing as needed.
1184
+ *
1185
+ * Explicit `registerFunction` registrations are always left untouched, so
1186
+ * explicit registration wins. An auto-registered entry (from a previous
1187
+ * advertised set) is rebound when the new set carries a *different* handler
1188
+ * for the same name — so a re-declared per-node handler takes effect — and
1189
+ * left untouched when the handler is unchanged, so repeated frames don't
1190
+ * churn.
1191
+ *
1192
+ * @param tools The tools to scan for handlers.
1193
+ */
1194
+ _registerAdvertisedToolHandlers(tools) {
1195
+ // A context's `tools` may be null (rather than `NOT_GIVEN`) — e.g. from
1196
+ // realtime services or stand-in contexts in tests. isGiven(null) is true
1197
+ // and the normalizer rejects null, so guard it explicitly.
1198
+ if (tools === null || tools === undefined) {
1199
+ return;
1200
+ }
1201
+ const normalized = LLMContext._normalizeAndValidateTools(tools);
1202
+ if (!isGiven(normalized)) {
1203
+ return;
1204
+ }
1205
+ // Register direct functions.
1206
+ for (const wrapper of normalized.directFunctions) {
1207
+ const existing = this._functions.get(wrapper.name);
1208
+ if (existing !== undefined) {
1209
+ // An auto-registered entry whose advertised handler changed is
1210
+ // rebound to the new handler. Explicit registrations and unchanged
1211
+ // handlers are left untouched, so explicit wins and repeated frames
1212
+ // don't churn.
1213
+ if (existing.autoRegistered && this._advertisedHandlerChanged(existing, wrapper.function)) {
1214
+ this._registerDirectFunction(wrapper.function);
1215
+ this._markAutoRegistered(wrapper.name);
1216
+ logger.debug(`${this}: rebound advertised direct function '${wrapper.name}' to its new handler`);
1217
+ }
1218
+ continue;
1219
+ }
1220
+ if (this._explicitlyUnregisteredFunctionNames.has(wrapper.name)) {
1221
+ // Explicitly unregistered while still advertised — leave it gone so
1222
+ // calls hit the missing-handler recovery path.
1223
+ continue;
1224
+ }
1225
+ this._registerDirectFunction(wrapper.function);
1226
+ // Mark the entry as advertised-tool-set-managed so it can be pruned on a
1227
+ // later sync that stops advertising it. Explicit registrations are
1228
+ // handled above and keep their default autoRegistered=false, so they
1229
+ // are never pruned.
1230
+ this._markAutoRegistered(wrapper.name);
1231
+ logger.debug(`${this}: auto-registered handler for advertised direct function '${wrapper.name}'`);
1232
+ }
1233
+ // Register the handlers that FunctionSchemas carry. A schema handler
1234
+ // registers like any classic handler — registerFunction reads its
1235
+ // toolOptions() attributes off the handler — only marked autoRegistered
1236
+ // so it's pruned when no longer advertised.
1237
+ for (const schema of normalized.standardTools) {
1238
+ if (schema.handler === null || schema.handler === undefined) {
1239
+ continue;
1240
+ }
1241
+ const existing = this._functions.get(schema.name);
1242
+ if (existing !== undefined) {
1243
+ // Rebind an auto-registered entry whose advertised handler changed;
1244
+ // otherwise leave it (explicit wins, unchanged handlers don't churn)
1245
+ // and warn if a manual registration shadows a handler-carrying schema.
1246
+ if (existing.autoRegistered && this._advertisedHandlerChanged(existing, schema.handler)) {
1247
+ this.registerFunction(schema.name, schema.handler);
1248
+ this._markAutoRegistered(schema.name);
1249
+ logger.debug(`${this}: rebound advertised FunctionSchema '${schema.name}' to its new handler`);
1250
+ }
1251
+ else {
1252
+ this._warnIfRedundantManualRegistration(schema.name);
1253
+ }
1254
+ continue;
1255
+ }
1256
+ if (this._explicitlyUnregisteredFunctionNames.has(schema.name)) {
1257
+ continue;
1258
+ }
1259
+ this.registerFunction(schema.name, schema.handler);
1260
+ this._markAutoRegistered(schema.name);
1261
+ logger.debug(`${this}: auto-registered handler for advertised FunctionSchema '${schema.name}'`);
1262
+ }
1263
+ }
1264
+ /** Mark a registry entry as managed by the advertised tool set. */
1265
+ _markAutoRegistered(name) {
1266
+ const item = this._functions.get(name);
1267
+ if (item)
1268
+ item.autoRegistered = true;
1269
+ }
1270
+ /**
1271
+ * Warn that a manual registration is unnecessary, once per function.
1272
+ *
1273
+ * Fires when a tool is registered explicitly via `registerFunction` yet its
1274
+ * advertised `FunctionSchema` already carries a handler — the schema alone
1275
+ * would register it. An auto-registered entry (e.g. from a same-named
1276
+ * direct function) is not a manual registration and is left silent.
1277
+ *
1278
+ * @param functionName The name shared by the manual registration and the
1279
+ * handler-carrying schema. Must already be registered.
1280
+ */
1281
+ _warnIfRedundantManualRegistration(functionName) {
1282
+ if (this._functions.get(functionName)?.autoRegistered) {
1283
+ return;
1284
+ }
1285
+ if (this._redundantRegistrationWarned.has(functionName)) {
1286
+ return;
1287
+ }
1288
+ this._redundantRegistrationWarned.add(functionName);
1289
+ logger.warning(`${this}: '${functionName}' is registered with register_function() but its ` +
1290
+ "advertised FunctionSchema already carries a handler. The manual registration " +
1291
+ "step is unnecessary when the handler is on the FunctionSchema.");
1292
+ }
1293
+ /**
1294
+ * Return the service's own configured tools, if any.
1295
+ *
1296
+ * Used by the auto-registration mechanism. Tools normally reach a service
1297
+ * through the `LLMContext` on each inference. Some services (the realtime
1298
+ * services) also accept tools configured directly on the service (e.g. via
1299
+ * a constructor option) as an alternative; they override this to return
1300
+ * those tools verbatim, in whatever form they hold them — a `ToolsSchema` or
1301
+ * a provider-native tool list. When the context advertises no tools,
1302
+ * `_syncRegisteredToolHandlers` normalizes the result and registers handlers
1303
+ * for any standard tools it contains; provider-native tools carry no
1304
+ * handlers, so they contribute nothing to register.
1305
+ *
1306
+ * A service that overrides this **must follow the auto-registration
1307
+ * mechanism's preference rules: context tools, then service-configured
1308
+ * tools**.
1309
+ *
1310
+ * @returns The service's configured tools verbatim (e.g. a `ToolsSchema` or
1311
+ * a provider-native tool list), or null when there are none (the default).
1312
+ */
1313
+ _serviceTools() {
1314
+ return null;
1315
+ }
1316
+ /**
1317
+ * Sync the registered handlers with the handlers advertised in `tools`.
1318
+ *
1319
+ * Registers handlers for any tools the set advertises with one (direct
1320
+ * functions, or `FunctionSchema` objects carrying a `handler`), then drops
1321
+ * the handlers we auto-registered for tools it no longer advertises — so
1322
+ * the registry matches what the LLM can see.
1323
+ *
1324
+ * This is the single path for keeping handlers in step with the advertised
1325
+ * tools. The base service runs it on every `LLMContextFrame` (the context
1326
+ * carries the current tool set on each inference). Realtime services that
1327
+ * support runtime tool changes also call it from their own
1328
+ * `LLMSetToolsFrame` handling, since they run continuously and don't get a
1329
+ * context frame per turn. When the context advertises no tools, handlers
1330
+ * fall back to `_serviceTools()` (the service's own configured tools).
1331
+ *
1332
+ * Explicit registrations (`registerFunction`), the catch-all handler, and
1333
+ * built-in tools are never pruned.
1334
+ *
1335
+ * @param tools The advertised tool set (a `ToolsSchema`, a plain list, or
1336
+ * null / `NOT_GIVEN` for "no tools").
1337
+ */
1338
+ _syncRegisteredToolHandlers(tools) {
1339
+ // null and an empty list both mean "no tools advertised" (the normalizer
1340
+ // collapses an empty set to NOT_GIVEN), in which case every
1341
+ // auto-registered handler is pruned.
1342
+ let normalized = tools !== null && tools !== undefined
1343
+ ? LLMContext._normalizeAndValidateTools(tools)
1344
+ : NOT_GIVEN;
1345
+ // No context tools? Fall back to the service's own configured tools.
1346
+ // Those may be provider-native (already formatted, carrying no handlers);
1347
+ // only standard tools (gathered into a ToolsSchema) contribute handlers.
1348
+ if (!isGiven(normalized)) {
1349
+ const serviceTools = this._serviceTools();
1350
+ if (serviceTools !== null) {
1351
+ const normalizedServiceTools = LLMContext._normalizeAndValidateTools(serviceTools, {
1352
+ allowProviderTools: true,
1353
+ });
1354
+ if (normalizedServiceTools instanceof ToolsSchema) {
1355
+ normalized = normalizedServiceTools;
1356
+ }
1357
+ }
1358
+ }
1359
+ this._registerAdvertisedToolHandlers(normalized);
1360
+ const advertised = new Set();
1361
+ if (normalized instanceof ToolsSchema) {
1362
+ for (const wrapper of normalized.directFunctions)
1363
+ advertised.add(wrapper.name);
1364
+ for (const s of normalized.standardTools) {
1365
+ if (s.handler !== null && s.handler !== undefined)
1366
+ advertised.add(s.name);
1367
+ }
1368
+ }
1369
+ this._unregisterUnadvertisedToolHandlers(advertised);
1370
+ // A standalone unregister only suppresses re-registration while the tool
1371
+ // is still advertised. Once it leaves the advertised set, drop the
1372
+ // suppression so re-advertising it (a later tool-set change) registers it
1373
+ // afresh.
1374
+ for (const name of [...this._explicitlyUnregisteredFunctionNames]) {
1375
+ if (!advertised.has(name))
1376
+ this._explicitlyUnregisteredFunctionNames.delete(name);
1377
+ }
1378
+ // What the registry holds decides both whether the async-tool guidance
1379
+ // belongs in the system instruction and whether the cancellation tools
1380
+ // do, and it is settled only here: tools advertised on a context frame
1381
+ // register during this sync, a manual registration never goes through
1382
+ // that path at all, and a context can advertise nothing.
1383
+ this._syncCancelTools();
1384
+ this._composeSystemInstruction();
1385
+ }
1386
+ /**
1387
+ * Drop auto-registered handlers for tools no longer advertised.
1388
+ *
1389
+ * Only entries with `autoRegistered: true` are eligible; explicit
1390
+ * registrations, the catch-all handler, and built-in tools are untouched.
1391
+ *
1392
+ * @param advertised Names of handler-carrying tools in the new advertised set.
1393
+ */
1394
+ _unregisterUnadvertisedToolHandlers(advertised) {
1395
+ const stale = [...this._functions.entries()]
1396
+ .filter(([name, item]) => item.autoRegistered && !advertised.has(name))
1397
+ .map(([name]) => name);
1398
+ for (const name of stale) {
1399
+ this._functions.delete(name);
1400
+ }
1401
+ }
1402
+ /**
1403
+ * Remove a registered function handler.
1404
+ *
1405
+ * This removes the handler but does not stop advertising the tool to the
1406
+ * LLM. To remove a tool cleanly, prefer an `LLMSetToolsFrame` with the
1407
+ * updated tool set — that both stops advertising it and avoids the LLM
1408
+ * trying to call a tool that's no longer there.
1409
+ *
1410
+ * @param functionName The name of the function handler to remove.
1411
+ * @throws Error if no handler is registered under that name.
1412
+ */
1413
+ unregisterFunction(functionName) {
1414
+ if (!this._functions.delete(functionName)) {
1415
+ throw new Error(`KeyError: ${repr(functionName)}`);
1416
+ }
1417
+ // Remember the explicit removal so auto-registration doesn't bring the
1418
+ // handler back on the next context frame while the tool is still
1419
+ // advertised.
1420
+ this._explicitlyUnregisteredFunctionNames.add(functionName);
1421
+ }
1422
+ /**
1423
+ * Remove a registered direct function handler (internal; no warning).
1424
+ *
1425
+ * @param handler The direct function handler to remove.
1426
+ * @throws Error if no handler is registered under its name.
1427
+ */
1428
+ _unregisterDirectFunction(handler) {
1429
+ const wrapper = new DirectFunctionWrapper(handler);
1430
+ if (!this._functions.delete(wrapper.name)) {
1431
+ throw new Error(`KeyError: ${repr(wrapper.name)}`);
1432
+ }
1433
+ // Remember the explicit removal so auto-registration doesn't bring the
1434
+ // handler back on the next context frame while the tool is still advertised.
1435
+ this._explicitlyUnregisteredFunctionNames.add(wrapper.name);
1436
+ // Note: no need to remove start callback here, as direct functions don't support start callbacks.
1437
+ }
1438
+ /**
1439
+ * Check if a function handler is registered.
1440
+ *
1441
+ * @param functionName The name of the function to check.
1442
+ * @returns True if the function is registered or if a catch-all handler
1443
+ * (null) is registered.
1444
+ */
1445
+ hasFunction(functionName) {
1446
+ if (this._functions.has(null)) {
1447
+ return true;
1448
+ }
1449
+ return this._functions.has(functionName);
1450
+ }
1451
+ /**
1452
+ * Whether the named function was registered with cancelOnInterruption=false.
1453
+ *
1454
+ * Mirrors the registry-lookup pattern in `runFunctionCalls()`: a
1455
+ * name-specific entry takes precedence; if there isn't one, fall back to
1456
+ * the null-keyed catch-all entry. Returns false if no entry matches.
1457
+ */
1458
+ _functionIsAsync(functionName) {
1459
+ const item = this._functions.get(functionName) ?? this._functions.get(null);
1460
+ return item !== undefined && !item.cancelOnInterruption;
1461
+ }
1462
+ /**
1463
+ * Execute a sequence of function calls from the LLM.
1464
+ *
1465
+ * Triggers the on_function_calls_started event and executes functions
1466
+ * either in parallel or sequentially based on the runInParallel setting.
1467
+ *
1468
+ * @param functionCalls The function calls to execute.
1469
+ */
1470
+ async runFunctionCalls(functionCalls) {
1471
+ if (functionCalls.length === 0) {
1472
+ return;
1473
+ }
1474
+ if (this._speculationGate.isSpeculating) {
1475
+ // Tools run inside the service, so no downstream gate can undo their
1476
+ // side effects if the speculation is discarded. Withdraw it instead;
1477
+ // the inference that follows the committed transcript runs the call. A
1478
+ // turn confirmed before the call was reached leaves nothing pending,
1479
+ // and the call runs as an ordinary one.
1480
+ logger.debug(`${this}: speculative inference wants a tool call, cancelling it`);
1481
+ await this.broadcastFrame(EagerEndOfTurnCancelFrame);
1482
+ return;
1483
+ }
1484
+ // Exclude the built-in cancel tool — it's an internal mechanism and
1485
+ // should not be surfaced to user-facing event handlers or frames.
1486
+ const userVisibleCalls = functionCalls.filter((fc) => !this._cancelToolNames.has(fc.functionName));
1487
+ if (userVisibleCalls.length > 0) {
1488
+ await this._callEventHandler("on_function_calls_started", userVisibleCalls);
1489
+ await this.broadcastFrame(FunctionCallsStartedFrame, { functionCalls: userVisibleCalls });
1490
+ }
1491
+ // When groupParallelTools is true all calls share a groupId so the
1492
+ // aggregator triggers the LLM exactly once after the last one completes.
1493
+ // When false, groupId is null and each result triggers inference
1494
+ // independently.
1495
+ const groupId = this._groupParallelTools ? crypto.randomUUID() : null;
1496
+ const runnerItems = [];
1497
+ for (const functionCall of functionCalls) {
1498
+ let item = this._functions.get(functionCall.functionName) ?? this._functions.get(null);
1499
+ if (item === undefined) {
1500
+ this._logMissingFunctionCall(functionCall.functionName, functionCall.context);
1501
+ item = this._buildMissingFunctionCallRegistryItem(functionCall.functionName);
1502
+ }
1503
+ runnerItems.push(new FunctionCallRunnerItem({
1504
+ registryItem: item,
1505
+ functionName: functionCall.functionName,
1506
+ toolCallId: functionCall.toolCallId,
1507
+ arguments: functionCall.arguments,
1508
+ context: functionCall.context,
1509
+ groupId,
1510
+ }));
1511
+ }
1512
+ if (this._runInParallel) {
1513
+ await this._runParallelFunctionCalls(runnerItems);
1514
+ }
1515
+ else {
1516
+ await this._runSequentialFunctionCalls(runnerItems);
1517
+ }
1518
+ }
1519
+ async _createSequentialRunnerTask() {
1520
+ if (!this._sequentialRunnerTask) {
1521
+ this._sequentialRunnerQueue = new Queue();
1522
+ this._sequentialRunnerTask = this.createTask(() => this._sequentialRunnerHandler(), "_sequential_runner_handler");
1523
+ }
1524
+ }
1525
+ async _cancelSequentialRunnerTask() {
1526
+ if (this._sequentialRunnerTask) {
1527
+ await this.cancelTask(this._sequentialRunnerTask);
1528
+ this._sequentialRunnerTask = null;
1529
+ }
1530
+ }
1531
+ async _cancelSummaryTask() {
1532
+ if (this._summaryTask) {
1533
+ await this.cancelTask(this._summaryTask);
1534
+ this._summaryTask = null;
1535
+ }
1536
+ }
1537
+ async _cancelAllFunctionCallTasks() {
1538
+ // Snapshot first: cancelTask awaits, during which done callbacks may
1539
+ // mutate _functionCallTasks.
1540
+ for (const task of [...this._functionCallTasks.keys()]) {
1541
+ if (task) {
1542
+ task.removeDoneCallback(this.#functionCallTaskDone);
1543
+ await this.cancelTask(task);
1544
+ }
1545
+ }
1546
+ this._functionCallTasks.clear();
1547
+ }
1548
+ async _sequentialRunnerHandler() {
1549
+ while (true) {
1550
+ const runnerItem = await this._sequentialRunnerQueue.get();
1551
+ const task = this.createTask(() => this._runFunctionCall(runnerItem), "_run_function_call");
1552
+ this._functionCallTasks.set(task, runnerItem);
1553
+ // Since we run tasks sequentially we don't need to add the
1554
+ // _functionCallTaskFinished done callback.
1555
+ try {
1556
+ // Wait on the task rather than awaiting it directly: a function call
1557
+ // cancelled from elsewhere (interruption, timeout) must not tear down
1558
+ // the runner along with it.
1559
+ await wait([task]);
1560
+ }
1561
+ finally {
1562
+ // Waiting doesn't hand cancellation down the way awaiting the task
1563
+ // would, so a runner going away has to cancel the call it was running
1564
+ // itself.
1565
+ if (!task.done()) {
1566
+ await this.cancelTask(task);
1567
+ }
1568
+ // The canceller may already have dropped the entry.
1569
+ this._functionCallTasks.delete(task);
1570
+ }
1571
+ }
1572
+ }
1573
+ async _runParallelFunctionCalls(runnerItems) {
1574
+ for (const runnerItem of runnerItems) {
1575
+ const task = this.createTask(() => this._runFunctionCall(runnerItem), "_run_function_call");
1576
+ this._functionCallTasks.set(task, runnerItem);
1577
+ task.addDoneCallback(this.#functionCallTaskDone);
1578
+ }
1579
+ }
1580
+ async _runSequentialFunctionCalls(runnerItems) {
1581
+ // Enqueue all function calls for background execution.
1582
+ for (const runnerItem of runnerItems) {
1583
+ await this._sequentialRunnerQueue.put(runnerItem);
1584
+ }
1585
+ }
1586
+ async _runFunctionCall(runnerItem) {
1587
+ // Re-resolve the registry item at execution time. The function may have
1588
+ // been unregistered between queuing and execution, in which case we fall
1589
+ // back to the missing-function handler so the call still terminates with
1590
+ // a normal tool result.
1591
+ let item;
1592
+ const named = this._functions.get(runnerItem.functionName);
1593
+ const catchAll = this._functions.get(null);
1594
+ if (named !== undefined) {
1595
+ item = named;
1596
+ }
1597
+ else if (catchAll !== undefined) {
1598
+ item = catchAll;
1599
+ }
1600
+ else if (runnerItem.registryItem.handler === this.#missingFunctionCallHandler) {
1601
+ item = runnerItem.registryItem;
1602
+ }
1603
+ else {
1604
+ // Function was unregistered between queue and execution; the
1605
+ // registry-item-handler check above already covered the
1606
+ // missing-from-the-start case.
1607
+ logger.warning(`${this}: '${runnerItem.functionName}' was just unregistered ` +
1608
+ "between queueing and execution.");
1609
+ item = this._buildMissingFunctionCallRegistryItem(runnerItem.functionName);
1610
+ }
1611
+ logger.debug(`${this} Calling function [${runnerItem.functionName}:${runnerItem.toolCallId}] with arguments ${JSON.stringify(runnerItem.arguments)}`);
1612
+ // Broadcast function call in-progress. This frame will let our assistant
1613
+ // context aggregator know that we are in the middle of a function call.
1614
+ // Some contexts/aggregators may not need this. But some definitely do
1615
+ // (Anthropic, for example).
1616
+ await this.broadcastFrame(FunctionCallInProgressFrame, {
1617
+ functionName: runnerItem.functionName,
1618
+ toolCallId: runnerItem.toolCallId,
1619
+ arguments: runnerItem.arguments,
1620
+ cancelOnInterruption: item.cancelOnInterruption,
1621
+ groupId: runnerItem.groupId,
1622
+ });
1623
+ let timeoutTask = null;
1624
+ // Set when the handler raises, so the result settling the call on its
1625
+ // behalf can say what went wrong.
1626
+ let callError = null;
1627
+ // Single callback for both intermediate updates and final results. Pass
1628
+ // properties: new FunctionCallResultProperties({ isFinal: false }) for updates.
1629
+ const functionCallResultCallback = async (result, options = {}) => {
1630
+ const properties = options.properties ?? null;
1631
+ const isFinal = properties ? properties.isFinal : true;
1632
+ if (!isFinal && item.cancelOnInterruption) {
1633
+ logger.warning(`${this} result_callback called with is_final=False on sync function call` +
1634
+ ` [${runnerItem.functionName}:${runnerItem.toolCallId}].` +
1635
+ " Intermediate updates are only valid for async function calls" +
1636
+ " (cancel_on_interruption=False).");
1637
+ return;
1638
+ }
1639
+ // A handler can outlive its call and still hold this callback, so late
1640
+ // results are rejected here rather than broadcast for the aggregator to
1641
+ // drop.
1642
+ if (runnerItem.settled) {
1643
+ logger.warning(`${this} Ignoring result for function call` +
1644
+ ` [${runnerItem.functionName}:${runnerItem.toolCallId}]: the call has` +
1645
+ " already finished (it reported a result, timed out, or was cancelled).");
1646
+ return;
1647
+ }
1648
+ if (isFinal) {
1649
+ runnerItem.settled = true;
1650
+ // Only a final result settles the call, so only a final result clears
1651
+ // the deadline. An intermediate update must leave it armed —
1652
+ // otherwise a progress report permanently unbounds a hanging async
1653
+ // tool.
1654
+ if (timeoutTask && !timeoutTask.done()) {
1655
+ await this.cancelTask(timeoutTask);
1656
+ }
1657
+ }
1658
+ await this.broadcastFrame(FunctionCallResultFrame, {
1659
+ functionName: runnerItem.functionName,
1660
+ toolCallId: runnerItem.toolCallId,
1661
+ arguments: runnerItem.arguments,
1662
+ result,
1663
+ runLlm: runnerItem.runLlm,
1664
+ properties,
1665
+ error: callError,
1666
+ });
1667
+ };
1668
+ // Python `or`: a 0 per-tool timeout falls back to the global one.
1669
+ const effectiveTimeout = item.timeoutSecs || this._functionCallTimeoutSecs;
1670
+ // Start a timeout task for deferred function calls
1671
+ const timeoutHandler = async () => {
1672
+ // This task is only started when one of the two is set.
1673
+ const timeout = effectiveTimeout;
1674
+ await sleep(timeout);
1675
+ logger.warning(`${this} Function call [${runnerItem.functionName}:${runnerItem.toolCallId}] timed out after ${timeout} seconds and is being cancelled.` +
1676
+ " You can increase this timeout by passing `timeout_secs` to `register_function()`," +
1677
+ " or set a global default via `function_call_timeout_secs` on the LLM constructor.");
1678
+ // Settle the call before cancelling, so a result racing in while the
1679
+ // handler unwinds is rejected instead of broadcast.
1680
+ runnerItem.settled = true;
1681
+ // Cancelling goes through a detached task because it awaits the
1682
+ // function call task this handler runs inside of.
1683
+ this.createTask(() => this._timeoutFunctionCall(runnerItem), "_timeout_function_call");
1684
+ };
1685
+ if (effectiveTimeout) {
1686
+ timeoutTask = this.createTask(timeoutHandler, "timeout_handler");
1687
+ }
1688
+ try {
1689
+ const params = new FunctionCallParams({
1690
+ functionName: runnerItem.functionName,
1691
+ toolCallId: runnerItem.toolCallId,
1692
+ arguments: runnerItem.arguments,
1693
+ llm: this,
1694
+ pipelineWorker: this.pipelineWorker,
1695
+ context: runnerItem.context,
1696
+ resultCallback: functionCallResultCallback,
1697
+ appResources: this.pipelineWorker.appResources,
1698
+ workerRunner: this.pipelineWorker.workerRunner,
1699
+ });
1700
+ if (item.handler instanceof DirectFunctionWrapper) {
1701
+ // Handler is a DirectFunctionWrapper
1702
+ await item.handler.invoke(runnerItem.arguments, params);
1703
+ }
1704
+ else {
1705
+ // Handler is a FunctionCallHandler
1706
+ await item.handler(params);
1707
+ }
1708
+ }
1709
+ catch (e) {
1710
+ if (e instanceof CancelledError) {
1711
+ // Backstop for cancellation that doesn't run through
1712
+ // _cancelFunctionCallTasks, e.g. teardown.
1713
+ runnerItem.settled = true;
1714
+ throw e;
1715
+ }
1716
+ const errorMessage = `Error executing function call [${runnerItem.functionName}]: ${errorText(e)}`;
1717
+ // Pushing the error with category "APPLICATION" since the failure came
1718
+ // from the application's function handler code.
1719
+ await this.pushError(errorMessage, { exception: e, category: ErrorCategory.APPLICATION });
1720
+ // A handler that raised will never report, so settle the call on its
1721
+ // behalf.
1722
+ callError = e instanceof Error ? `${e.name}: ${e.message}` : `Error: ${String(e)}`;
1723
+ const cls = this.constructor;
1724
+ await functionCallResultCallback(cls.FUNCTION_CALL_ERROR_MESSAGE_TEMPLATE.replaceAll("{function_name}", runnerItem.functionName));
1725
+ }
1726
+ finally {
1727
+ if (timeoutTask && !timeoutTask.done()) {
1728
+ await this.cancelTask(timeoutTask);
1729
+ }
1730
+ }
1731
+ }
1732
+ /** Build a registry item that routes to the missing-function handler. */
1733
+ _buildMissingFunctionCallRegistryItem(functionName) {
1734
+ return new FunctionCallRegistryItem({
1735
+ functionName,
1736
+ handler: this.#missingFunctionCallHandler,
1737
+ cancelOnInterruption: true,
1738
+ });
1739
+ }
1740
+ /** Return a terminal tool result when the LLM calls an unknown function. */
1741
+ async _missingFunctionCallHandler(params) {
1742
+ const cls = this.constructor;
1743
+ await params.resultCallback(cls.MISSING_FUNCTION_CALL_MESSAGE_TEMPLATE.replaceAll("{function_name}", params.functionName));
1744
+ }
1745
+ /**
1746
+ * Return the set of standard-tool names currently advertised to the LLM.
1747
+ *
1748
+ * Custom (LLM-specific) tools are not included, since they have no
1749
+ * consistent name field across adapters.
1750
+ */
1751
+ static _advertisedToolNames(context) {
1752
+ const tools = context !== null && context !== undefined ? context.tools : null;
1753
+ if (tools === null || tools === undefined || !isGiven(tools)) {
1754
+ return new Set();
1755
+ }
1756
+ return new Set(tools.standardTools.map((t) => t.name));
1757
+ }
1758
+ /**
1759
+ * Log an appropriate message when a tool is called with no handler.
1760
+ *
1761
+ * Distinguishes two cases:
1762
+ *
1763
+ * - **Developer error:** the tool is advertised to the LLM but no handler
1764
+ * was wired up (a `FunctionSchema` must've been provided with neither
1765
+ * `handler` nor accompanying `registerFunction` call). Logged at error
1766
+ * level since this almost always indicates a bug.
1767
+ * - **Hallucination:** the tool is not in the currently advertised tool
1768
+ * set. Logged at warning level since this is model behavior the
1769
+ * application can do little about beyond returning a terminal result.
1770
+ */
1771
+ _logMissingFunctionCall(functionName, context) {
1772
+ if (LLMService._advertisedToolNames(context).has(functionName)) {
1773
+ logger.error(`${this}: tool '${functionName}' is advertised to the LLM but has ` +
1774
+ "no handler — set FunctionSchema.handler (recommended) or call " +
1775
+ "register_function().");
1776
+ }
1777
+ else {
1778
+ logger.warning(`${this}: LLM called '${functionName}', which is not in the ` +
1779
+ "currently advertised tool set.");
1780
+ }
1781
+ }
1782
+ /**
1783
+ * Whether the LLM may cancel calls of a registered tool.
1784
+ *
1785
+ * The one place that decides it, so what gets advertised and what a cancel
1786
+ * tool will actually stop can't drift apart.
1787
+ *
1788
+ * @param item The registered tool to judge.
1789
+ * @returns True if the tool opted in with `cancellableByLlm`.
1790
+ */
1791
+ _isCancellable(item) {
1792
+ return item.cancellableByLlm;
1793
+ }
1794
+ /**
1795
+ * Names of the registered tools the LLM may cancel.
1796
+ *
1797
+ * @returns The tools that opted in with `cancellableByLlm: true`.
1798
+ */
1799
+ _cancellableToolNames() {
1800
+ const names = new Set();
1801
+ for (const [name, item] of this._functions) {
1802
+ if (name && this._isCancellable(item))
1803
+ names.add(name);
1804
+ }
1805
+ return names;
1806
+ }
1807
+ /**
1808
+ * Match the advertised cancel tools to the tools currently registered.
1809
+ *
1810
+ * One cancel tool per cancellable tool, named for it, reconciled from a
1811
+ * single reading of the registry so the advertised set can never drift from
1812
+ * what is actually cancellable. A tool that never opted in has no cancel
1813
+ * tool at all, so there is nothing for the LLM to call against it.
1814
+ */
1815
+ _syncCancelTools() {
1816
+ const wanted = new Map();
1817
+ for (const name of this._cancellableToolNames()) {
1818
+ wanted.set(cancelToolName(name), name);
1819
+ }
1820
+ for (const stale of [...this._cancelToolNames]) {
1821
+ if (wanted.has(stale))
1822
+ continue;
1823
+ delete this._adapter.builtinTools[stale];
1824
+ this._functions.delete(stale);
1825
+ this._cancelToolNames.delete(stale);
1826
+ }
1827
+ for (const [name, target] of wanted) {
1828
+ if (this._cancelToolNames.has(name)) {
1829
+ continue;
1830
+ }
1831
+ if (this._functions.has(name)) {
1832
+ // Once per name: this runs on every context frame, and the collision
1833
+ // persists for as long as both tools are registered.
1834
+ if (!this._warnedCancelCollisions.has(name)) {
1835
+ this._warnedCancelCollisions.add(name);
1836
+ logger.warning(`${this}: '${target}' is cancellable but its cancel tool can't be ` +
1837
+ `advertised — '${name}' is already registered as a tool of its own. ` +
1838
+ `Rename one of them, or the LLM has no way to stop ${target}.`);
1839
+ }
1840
+ continue;
1841
+ }
1842
+ this._adapter.builtinTools[name] = buildCancelToolSchema(target);
1843
+ this._functions.set(name, new FunctionCallRegistryItem({
1844
+ functionName: name,
1845
+ handler: this.#cancelToolHandler,
1846
+ cancelOnInterruption: true,
1847
+ }));
1848
+ this._cancelToolNames.add(name);
1849
+ }
1850
+ }
1851
+ /**
1852
+ * Handle a `cancel_<tool>` invocation from the LLM.
1853
+ *
1854
+ * Which work to stop comes from the tool the model called, so the common
1855
+ * case takes no arguments at all. `tool_call_id` only has to be given to
1856
+ * choose between several running calls of the same tool.
1857
+ *
1858
+ * @param params Function call parameters, optionally carrying `tool_call_id`.
1859
+ */
1860
+ async _cancelToolHandler(params) {
1861
+ const target = (params.functionName || "").slice(CANCEL_TOOL_PREFIX.length);
1862
+ const toolCallId = params.arguments.tool_call_id ?? null;
1863
+ const refuse = async (reason, extra = {}) => {
1864
+ logger.debug(`${this}: declining to cancel ${target} — ${reason}`);
1865
+ await params.resultCallback({ cancelled: null, reason, ...extra }, { properties: new FunctionCallResultProperties({ runLlm: true }) });
1866
+ };
1867
+ const candidates = [...this._functionCallTasks.values()].filter((item) => item.functionName === target && this._isCancellable(item.registryItem));
1868
+ if (candidates.length === 0) {
1869
+ await refuse(`no ${target} call is running`);
1870
+ return;
1871
+ }
1872
+ let match;
1873
+ if (toolCallId) {
1874
+ const found = candidates.find((i) => i.toolCallId === toolCallId);
1875
+ if (found === undefined) {
1876
+ await refuse(`no running ${target} call has that tool_call_id`);
1877
+ return;
1878
+ }
1879
+ match = found;
1880
+ }
1881
+ else if (candidates.length > 1) {
1882
+ // Several calls of the one tool: the model has to say which, so the
1883
+ // refusal carries the ids to say it with.
1884
+ // Spell the choices out in the reason itself: a model reading a refusal
1885
+ // acts on what it says far more readily than on a field beside it.
1886
+ const choices = candidates
1887
+ .map((i) => `${i.toolCallId} (called with ${JSON.stringify(i.arguments, (_k, v) => typeof v === "bigint" ? String(v) : v)})`)
1888
+ .join("; ");
1889
+ await refuse(`${candidates.length} ${target} calls are running: ${choices}. Call ` +
1890
+ `${params.functionName} again with tool_call_id set to the one to stop.`, {
1891
+ running: candidates.map((i) => ({ tool_call_id: i.toolCallId, arguments: i.arguments })),
1892
+ });
1893
+ return;
1894
+ }
1895
+ else {
1896
+ match = candidates[0];
1897
+ }
1898
+ logger.debug(`${this}: cancelling [${match.functionName}:${match.toolCallId}]`);
1899
+ await this._cancelFunctionCallsByToolCallId(match.toolCallId);
1900
+ await params.resultCallback({ cancelled: match.toolCallId, function_name: match.functionName }, { properties: new FunctionCallResultProperties({ runLlm: true }) });
1901
+ }
1902
+ /**
1903
+ * Cancel the in-flight function calls selected by a predicate.
1904
+ *
1905
+ * Cancelling a call cancels its handler's task (delivering a
1906
+ * `CancelledError`) so cancel-aware handlers run their cleanup, broadcasts a
1907
+ * `FunctionCallCancelFrame` so the rest of the pipeline can settle the
1908
+ * call, and notifies application code via `on_function_calls_cancelled`.
1909
+ *
1910
+ * @param predicate Selects which in-flight calls to cancel.
1911
+ * @param options.reason Short description of what triggered the
1912
+ * cancellation, included in the log messages.
1913
+ * @param options.runLlm Whether inference should run once the cancelled
1914
+ * calls are settled in the context.
1915
+ * @returns The function calls that were cancelled.
1916
+ */
1917
+ async _cancelFunctionCallTasks(predicate, options) {
1918
+ const runLlm = options.runLlm ?? false;
1919
+ const cancelledTasks = new Set();
1920
+ const cancelledItems = [];
1921
+ // Snapshot first: cancelTask awaits, during which done callbacks may
1922
+ // mutate _functionCallTasks.
1923
+ for (const [task, runnerItem] of [...this._functionCallTasks.entries()]) {
1924
+ if (!predicate(runnerItem)) {
1925
+ continue;
1926
+ }
1927
+ const name = runnerItem.functionName;
1928
+ const toolCallId = runnerItem.toolCallId;
1929
+ logger.debug(`${this} Cancelling function call [${name}:${toolCallId}] (${options.reason})...`);
1930
+ // Settle before cancelling: a handler that catches its CancelledError
1931
+ // and reports a result while unwinding must not be able to reopen a
1932
+ // call the pipeline has stopped tracking.
1933
+ runnerItem.settled = true;
1934
+ if (task) {
1935
+ // We remove the callback because we are going to cancel the task
1936
+ // next, otherwise we will be removing it from the map while we are
1937
+ // iterating.
1938
+ task.removeDoneCallback(this.#functionCallTaskDone);
1939
+ await this.cancelTask(task);
1940
+ cancelledTasks.add(task);
1941
+ }
1942
+ cancelledItems.push(await this._broadcastFunctionCallCancelled(runnerItem, { runLlm }));
1943
+ logger.debug(`${this} Function call [${name}:${toolCallId}] has been cancelled`);
1944
+ }
1945
+ // Remove all cancelled tasks from our map.
1946
+ for (const task of cancelledTasks) {
1947
+ this._functionCallTaskFinished(task);
1948
+ }
1949
+ if (cancelledItems.length > 0) {
1950
+ await this._callEventHandler("on_function_calls_cancelled", cancelledItems);
1951
+ }
1952
+ return cancelledItems;
1953
+ }
1954
+ /**
1955
+ * Settle a cancelled function call downstream.
1956
+ *
1957
+ * @param runnerItem The function call that was cancelled.
1958
+ * @param options.runLlm Whether inference should run once the call is
1959
+ * settled in the context.
1960
+ * @returns The cancelled call, described for `on_function_calls_cancelled`.
1961
+ */
1962
+ async _broadcastFunctionCallCancelled(runnerItem, options = {}) {
1963
+ await this.broadcastFrame(FunctionCallCancelFrame, {
1964
+ functionName: runnerItem.functionName,
1965
+ toolCallId: runnerItem.toolCallId,
1966
+ runLlm: options.runLlm ?? false,
1967
+ });
1968
+ return new FunctionCallFromLLM({
1969
+ functionName: runnerItem.functionName,
1970
+ toolCallId: runnerItem.toolCallId,
1971
+ arguments: runnerItem.arguments,
1972
+ context: runnerItem.context,
1973
+ });
1974
+ }
1975
+ /**
1976
+ * Cancel a function call that ran past its deadline.
1977
+ *
1978
+ * The handler usually still owns the call and is cancelled outright. If it
1979
+ * happened to finish while the timeout was being processed its task is
1980
+ * already gone and there is nothing left to cancel, but the deadline has
1981
+ * settled the call, so its result is rejected — the pipeline is told the
1982
+ * call is over regardless, or it waits on a result that can never arrive.
1983
+ *
1984
+ * A deadline is the one cancellation nothing else follows up on, so it asks
1985
+ * for inference: without it the LLM never learns the call is over and the
1986
+ * user is left waiting on an answer that never comes.
1987
+ *
1988
+ * @param runnerItem The function call that timed out.
1989
+ */
1990
+ async _timeoutFunctionCall(runnerItem) {
1991
+ const cancelled = await this._cancelFunctionCallTasks((item) => item.toolCallId === runnerItem.toolCallId, { reason: "timeout", runLlm: true });
1992
+ if (cancelled.length > 0) {
1993
+ return;
1994
+ }
1995
+ const item = await this._broadcastFunctionCallCancelled(runnerItem, { runLlm: true });
1996
+ await this._callEventHandler("on_function_calls_cancelled", [item]);
1997
+ }
1998
+ /**
1999
+ * Cancel every function call in flight, whether or not it survives
2000
+ * interruptions.
2001
+ *
2002
+ * Each handler is cancelled, the call is settled downstream as cancelled,
2003
+ * and `on_function_calls_cancelled` fires. No inference runs for the
2004
+ * cancellations.
2005
+ *
2006
+ * @param options.reason What triggered the cancellation, for the logs.
2007
+ * @returns The calls that were cancelled.
2008
+ */
2009
+ async cancelFunctionCalls(options = {}) {
2010
+ return await this._cancelFunctionCallTasks(() => true, {
2011
+ reason: options.reason ?? "cancelled",
2012
+ });
2013
+ }
2014
+ /**
2015
+ * Cancel in-progress function call tasks by their toolCallId.
2016
+ *
2017
+ * @param toolCallId toolCallId to cancel.
2018
+ */
2019
+ async _cancelFunctionCallsByToolCallId(toolCallId) {
2020
+ await this._cancelFunctionCallTasks((item) => item.toolCallId === toolCallId, {
2021
+ reason: "User request",
2022
+ });
2023
+ }
2024
+ async _cancelFunctionCall(functionName) {
2025
+ await this._cancelFunctionCallTasks((item) => item.registryItem.functionName === functionName, {
2026
+ reason: "interruption",
2027
+ });
2028
+ }
2029
+ _functionCallTaskFinished(task) {
2030
+ this._functionCallTasks.delete(task);
2031
+ }
2032
+ }
2033
+ // ---------------------------------------------------------------------------
2034
+ // WebSocket LLM service base
2035
+ // ---------------------------------------------------------------------------
2036
+ /**
2037
+ * Raised by `_wsSend`/`_wsRecv` after a transparent reconnection.
2038
+ *
2039
+ * Signals that the WebSocket connection was lost and automatically
2040
+ * re-established. The current inference should be restarted — any
2041
+ * connection-local state on the server (e.g. cached responses) is gone.
2042
+ */
2043
+ export class WebsocketReconnectedError extends Error {
2044
+ name = "WebsocketReconnectedError";
2045
+ }
2046
+ /**
2047
+ * Base class for websocket-based LLM services.
2048
+ *
2049
+ * Each LLM inference is a discrete request/response exchange: send one
2050
+ * request, receive events inline until a terminal event, then wait for the
2051
+ * next frame to trigger an inference. This contrasts with
2052
+ * `WebsocketTTSService` / `WebsocketSTTService` which stream data
2053
+ * continuously via a background receive loop (`_receiveTaskHandler`). This
2054
+ * class does **not** start a background receive loop.
2055
+ *
2056
+ * Provides connection lifecycle management (connect on setup, disconnect on
2057
+ * stop/cancel), automatic reconnection with exponential backoff, and three
2058
+ * helpers for running each inference:
2059
+ *
2060
+ * 1. `_ensureConnected()` — verify the websocket is alive, reconnect with
2061
+ * exponential backoff if not.
2062
+ * 2. `_wsSend(message)` — send the inference request as JSON.
2063
+ * 3. `_wsRecv()` — receive and parse response events one at a time until the
2064
+ * caller sees a terminal event.
2065
+ *
2066
+ * `_wsSend` and `_wsRecv` catch `ConnectionClosed` transparently,
2067
+ * auto-reconnect via `_tryReconnect`, and raise `WebsocketReconnectedError`
2068
+ * so callers know the inference must be restarted. If reconnection fails,
2069
+ * the original `ConnectionClosed` propagates.
2070
+ *
2071
+ * Subclasses must implement:
2072
+ * - `_connectWebsocket()`: establish the websocket connection.
2073
+ * - `_disconnectWebsocket()`: close the websocket and clean up.
2074
+ *
2075
+ * Event handlers:
2076
+ * - on_connection_error: called when a websocket connection error occurs.
2077
+ *
2078
+ * @example
2079
+ * llm.eventHandler("on_connection_error", async (llm: LLMService, error: string) => {
2080
+ * logger.error(`LLM connection error: ${error}`);
2081
+ * });
2082
+ */
2083
+ export class WebsocketLLMService extends WebsocketService(LLMService) {
2084
+ constructor(options = {}) {
2085
+ super(options);
2086
+ this._registerEventHandler("on_connection_error");
2087
+ }
2088
+ // -- lifecycle ------------------------------------------------------------
2089
+ /** Connect: reset flags and establish the websocket. */
2090
+ async _connect() {
2091
+ await super._connect();
2092
+ await this._connectWebsocket();
2093
+ }
2094
+ /** Disconnect: set flags and close the websocket. */
2095
+ async _disconnect() {
2096
+ await super._disconnect();
2097
+ await this._disconnectWebsocket();
2098
+ }
2099
+ /**
2100
+ * Set up the service and connect.
2101
+ *
2102
+ * @param setup Configuration object containing setup parameters.
2103
+ */
2104
+ async setup(setup) {
2105
+ await super.setup(setup);
2106
+ await this._connect();
2107
+ }
2108
+ /**
2109
+ * Stop the service and close WebSocket connection.
2110
+ *
2111
+ * @param frame The end frame triggering service shutdown.
2112
+ */
2113
+ async stop(frame) {
2114
+ await super.stop(frame);
2115
+ await this._disconnect();
2116
+ }
2117
+ /**
2118
+ * Cancel the service and close WebSocket connection.
2119
+ *
2120
+ * @param frame The cancel frame triggering service cancellation.
2121
+ */
2122
+ async cancel(frame) {
2123
+ await super.cancel(frame);
2124
+ await this._disconnect();
2125
+ }
2126
+ // -- per-inference helpers ------------------------------------------------
2127
+ /**
2128
+ * Send a JSON message over the websocket.
2129
+ *
2130
+ * Guards against sends during intentional disconnect. If the send fails
2131
+ * with `ConnectionClosed`, attempts to reconnect and raises
2132
+ * `WebsocketReconnectedError` on success so the caller can restart the
2133
+ * inference. If reconnection fails, the original `ConnectionClosed`
2134
+ * propagates.
2135
+ *
2136
+ * @param message The message dict to serialize and send.
2137
+ */
2138
+ async _wsSend(message) {
2139
+ if (this._disconnecting || !this._websocket) {
2140
+ return;
2141
+ }
2142
+ try {
2143
+ await this._websocket.send(JSON.stringify(message));
2144
+ }
2145
+ catch (e) {
2146
+ if (!(e instanceof ConnectionClosed))
2147
+ throw e;
2148
+ if (this._disconnecting) {
2149
+ return;
2150
+ }
2151
+ const success = await this._tryReconnect({
2152
+ reportError: (error, options) => this._reportError(error, options),
2153
+ });
2154
+ if (success) {
2155
+ throw new WebsocketReconnectedError();
2156
+ }
2157
+ throw e;
2158
+ }
2159
+ }
2160
+ /**
2161
+ * Receive and parse a JSON message from the websocket.
2162
+ *
2163
+ * If the receive fails with `ConnectionClosed`, attempts to reconnect and
2164
+ * raises `WebsocketReconnectedError` on success. If reconnection fails, the
2165
+ * original `ConnectionClosed` propagates.
2166
+ *
2167
+ * @returns The parsed JSON message as a dict.
2168
+ */
2169
+ async _wsRecv() {
2170
+ // Should never happen — `_ensureConnected` (which callers must invoke
2171
+ // first) raises if it can't establish a websocket. Match that contract
2172
+ // here.
2173
+ if (this._websocket === null) {
2174
+ throw new Error(`${this} _ws_recv called without a websocket`);
2175
+ }
2176
+ try {
2177
+ const raw = await this._websocket.recv();
2178
+ return JSON.parse(typeof raw === "string" ? raw : new TextDecoder().decode(raw));
2179
+ }
2180
+ catch (e) {
2181
+ if (!(e instanceof ConnectionClosed))
2182
+ throw e;
2183
+ if (this._disconnecting) {
2184
+ throw e;
2185
+ }
2186
+ const success = await this._tryReconnect({
2187
+ reportError: (error, options) => this._reportError(error, options),
2188
+ });
2189
+ if (success) {
2190
+ throw new WebsocketReconnectedError();
2191
+ }
2192
+ throw e;
2193
+ }
2194
+ }
2195
+ /**
2196
+ * Ensure the websocket is connected, reconnecting if needed.
2197
+ *
2198
+ * Uses `_tryReconnect` with exponential backoff.
2199
+ *
2200
+ * @throws Error if the connection could not be established.
2201
+ */
2202
+ async _ensureConnected() {
2203
+ if (this._websocket && this._websocket.state !== State.CLOSED) {
2204
+ return;
2205
+ }
2206
+ const success = await this._tryReconnect({
2207
+ reportError: (error, options) => this._reportError(error, options),
2208
+ });
2209
+ if (!success) {
2210
+ throw new Error(`${this} failed to establish WebSocket connection`);
2211
+ }
2212
+ }
2213
+ // -- WebsocketService interface -------------------------------------------
2214
+ /**
2215
+ * Not used — messages are received inline during each inference.
2216
+ *
2217
+ * This satisfies the `WebsocketService` abstract method but is never called
2218
+ * because `_receiveTaskHandler` is never started.
2219
+ */
2220
+ async _receiveMessages() {
2221
+ throw new NotImplementedError("WebsocketLLMService receives messages inline during inference, " +
2222
+ "not via a continuous background loop");
2223
+ }
2224
+ async _reportError(error, options = {}) {
2225
+ await this._callEventHandler("on_connection_error", error.error);
2226
+ await this.pushErrorFrame(error, {
2227
+ forceTreatAsPermanent: options.forceTreatAsPermanent ?? false,
2228
+ });
2229
+ }
2230
+ }
2231
+ //# sourceMappingURL=llm_service.js.map