@livekit/agents 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/dist/_exceptions.cjs.map +1 -1
  2. package/dist/_exceptions.d.ts.map +1 -1
  3. package/dist/_exceptions.js.map +1 -1
  4. package/dist/audio.cjs +10 -0
  5. package/dist/audio.cjs.map +1 -1
  6. package/dist/audio.d.cts +1 -1
  7. package/dist/audio.d.ts +1 -1
  8. package/dist/audio.d.ts.map +1 -1
  9. package/dist/audio.js +10 -0
  10. package/dist/audio.js.map +1 -1
  11. package/dist/beta/workflows/task_group.cjs +7 -4
  12. package/dist/beta/workflows/task_group.cjs.map +1 -1
  13. package/dist/beta/workflows/task_group.d.ts.map +1 -1
  14. package/dist/beta/workflows/task_group.js +7 -4
  15. package/dist/beta/workflows/task_group.js.map +1 -1
  16. package/dist/inference/api_protos.d.cts +26 -26
  17. package/dist/inference/api_protos.d.ts +26 -26
  18. package/dist/inference/interruption/http_transport.cjs.map +1 -1
  19. package/dist/inference/interruption/http_transport.d.cts +3 -1
  20. package/dist/inference/interruption/http_transport.d.ts +3 -1
  21. package/dist/inference/interruption/http_transport.d.ts.map +1 -1
  22. package/dist/inference/interruption/http_transport.js.map +1 -1
  23. package/dist/inference/interruption/ws_transport.cjs +37 -32
  24. package/dist/inference/interruption/ws_transport.cjs.map +1 -1
  25. package/dist/inference/interruption/ws_transport.d.ts.map +1 -1
  26. package/dist/inference/interruption/ws_transport.js +37 -32
  27. package/dist/inference/interruption/ws_transport.js.map +1 -1
  28. package/dist/inference/tts.cjs +14 -1
  29. package/dist/inference/tts.cjs.map +1 -1
  30. package/dist/inference/tts.d.cts +42 -4
  31. package/dist/inference/tts.d.ts +42 -4
  32. package/dist/inference/tts.d.ts.map +1 -1
  33. package/dist/inference/tts.js +24 -3
  34. package/dist/inference/tts.js.map +1 -1
  35. package/dist/inference/tts.test.cjs +72 -0
  36. package/dist/inference/tts.test.cjs.map +1 -1
  37. package/dist/inference/tts.test.js +72 -0
  38. package/dist/inference/tts.test.js.map +1 -1
  39. package/dist/ipc/job_proc_lazy_main.cjs +7 -2
  40. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
  41. package/dist/ipc/job_proc_lazy_main.js +7 -2
  42. package/dist/ipc/job_proc_lazy_main.js.map +1 -1
  43. package/dist/ipc/supervised_proc.cjs +4 -1
  44. package/dist/ipc/supervised_proc.cjs.map +1 -1
  45. package/dist/ipc/supervised_proc.d.ts.map +1 -1
  46. package/dist/ipc/supervised_proc.js +4 -1
  47. package/dist/ipc/supervised_proc.js.map +1 -1
  48. package/dist/ipc/supervised_proc.test.cjs +82 -0
  49. package/dist/ipc/supervised_proc.test.cjs.map +1 -1
  50. package/dist/ipc/supervised_proc.test.js +82 -0
  51. package/dist/ipc/supervised_proc.test.js.map +1 -1
  52. package/dist/job.cjs +2 -1
  53. package/dist/job.cjs.map +1 -1
  54. package/dist/job.d.ts.map +1 -1
  55. package/dist/job.js +2 -1
  56. package/dist/job.js.map +1 -1
  57. package/dist/llm/chat_context.cjs +102 -31
  58. package/dist/llm/chat_context.cjs.map +1 -1
  59. package/dist/llm/chat_context.d.ts.map +1 -1
  60. package/dist/llm/chat_context.js +102 -31
  61. package/dist/llm/chat_context.js.map +1 -1
  62. package/dist/llm/chat_context.test.cjs +123 -5
  63. package/dist/llm/chat_context.test.cjs.map +1 -1
  64. package/dist/llm/chat_context.test.js +123 -5
  65. package/dist/llm/chat_context.test.js.map +1 -1
  66. package/dist/llm/fallback_adapter.cjs +2 -0
  67. package/dist/llm/fallback_adapter.cjs.map +1 -1
  68. package/dist/llm/fallback_adapter.d.ts.map +1 -1
  69. package/dist/llm/fallback_adapter.js +2 -0
  70. package/dist/llm/fallback_adapter.js.map +1 -1
  71. package/dist/llm/index.cjs +2 -0
  72. package/dist/llm/index.cjs.map +1 -1
  73. package/dist/llm/index.d.cts +1 -1
  74. package/dist/llm/index.d.ts +1 -1
  75. package/dist/llm/index.d.ts.map +1 -1
  76. package/dist/llm/index.js +2 -0
  77. package/dist/llm/index.js.map +1 -1
  78. package/dist/llm/utils.cjs +89 -0
  79. package/dist/llm/utils.cjs.map +1 -1
  80. package/dist/llm/utils.d.cts +8 -0
  81. package/dist/llm/utils.d.ts +8 -0
  82. package/dist/llm/utils.d.ts.map +1 -1
  83. package/dist/llm/utils.js +88 -0
  84. package/dist/llm/utils.js.map +1 -1
  85. package/dist/llm/utils.test.cjs +90 -0
  86. package/dist/llm/utils.test.cjs.map +1 -1
  87. package/dist/llm/utils.test.js +98 -2
  88. package/dist/llm/utils.test.js.map +1 -1
  89. package/dist/stt/stt.cjs +8 -0
  90. package/dist/stt/stt.cjs.map +1 -1
  91. package/dist/stt/stt.d.cts +8 -0
  92. package/dist/stt/stt.d.ts +8 -0
  93. package/dist/stt/stt.d.ts.map +1 -1
  94. package/dist/stt/stt.js +8 -0
  95. package/dist/stt/stt.js.map +1 -1
  96. package/dist/tts/fallback_adapter.cjs +6 -0
  97. package/dist/tts/fallback_adapter.cjs.map +1 -1
  98. package/dist/tts/fallback_adapter.d.ts.map +1 -1
  99. package/dist/tts/fallback_adapter.js +6 -0
  100. package/dist/tts/fallback_adapter.js.map +1 -1
  101. package/dist/typed_promise.cjs +48 -0
  102. package/dist/typed_promise.cjs.map +1 -0
  103. package/dist/typed_promise.d.cts +24 -0
  104. package/dist/typed_promise.d.ts +24 -0
  105. package/dist/typed_promise.d.ts.map +1 -0
  106. package/dist/typed_promise.js +28 -0
  107. package/dist/typed_promise.js.map +1 -0
  108. package/dist/utils.cjs +30 -2
  109. package/dist/utils.cjs.map +1 -1
  110. package/dist/utils.d.cts +18 -0
  111. package/dist/utils.d.ts +18 -0
  112. package/dist/utils.d.ts.map +1 -1
  113. package/dist/utils.js +27 -2
  114. package/dist/utils.js.map +1 -1
  115. package/dist/version.cjs +1 -1
  116. package/dist/version.js +1 -1
  117. package/dist/voice/agent_activity.cjs +10 -0
  118. package/dist/voice/agent_activity.cjs.map +1 -1
  119. package/dist/voice/agent_activity.d.ts.map +1 -1
  120. package/dist/voice/agent_activity.js +11 -0
  121. package/dist/voice/agent_activity.js.map +1 -1
  122. package/dist/voice/agent_session.cjs +1 -1
  123. package/dist/voice/agent_session.cjs.map +1 -1
  124. package/dist/voice/agent_session.d.cts +4 -2
  125. package/dist/voice/agent_session.d.ts +4 -2
  126. package/dist/voice/agent_session.d.ts.map +1 -1
  127. package/dist/voice/agent_session.js +1 -1
  128. package/dist/voice/agent_session.js.map +1 -1
  129. package/dist/voice/events.cjs +11 -0
  130. package/dist/voice/events.cjs.map +1 -1
  131. package/dist/voice/events.d.cts +12 -1
  132. package/dist/voice/events.d.ts +12 -1
  133. package/dist/voice/events.d.ts.map +1 -1
  134. package/dist/voice/events.js +10 -0
  135. package/dist/voice/events.js.map +1 -1
  136. package/dist/voice/generation.cjs +23 -4
  137. package/dist/voice/generation.cjs.map +1 -1
  138. package/dist/voice/generation.d.ts.map +1 -1
  139. package/dist/voice/generation.js +32 -5
  140. package/dist/voice/generation.js.map +1 -1
  141. package/dist/voice/generation_tts_timeout.test.cjs +85 -0
  142. package/dist/voice/generation_tts_timeout.test.cjs.map +1 -0
  143. package/dist/voice/generation_tts_timeout.test.js +84 -0
  144. package/dist/voice/generation_tts_timeout.test.js.map +1 -0
  145. package/dist/voice/index.cjs.map +1 -1
  146. package/dist/voice/index.d.cts +1 -1
  147. package/dist/voice/index.d.ts +1 -1
  148. package/dist/voice/index.d.ts.map +1 -1
  149. package/dist/voice/index.js +3 -1
  150. package/dist/voice/index.js.map +1 -1
  151. package/dist/voice/recorder_io/recorder_io.cjs +1 -2
  152. package/dist/voice/recorder_io/recorder_io.cjs.map +1 -1
  153. package/dist/voice/recorder_io/recorder_io.d.ts.map +1 -1
  154. package/dist/voice/recorder_io/recorder_io.js +2 -3
  155. package/dist/voice/recorder_io/recorder_io.js.map +1 -1
  156. package/dist/voice/report.cjs +1 -1
  157. package/dist/voice/report.cjs.map +1 -1
  158. package/dist/voice/report.js +1 -1
  159. package/dist/voice/report.js.map +1 -1
  160. package/dist/voice/report.test.cjs +70 -0
  161. package/dist/voice/report.test.cjs.map +1 -1
  162. package/dist/voice/report.test.js +70 -0
  163. package/dist/voice/report.test.js.map +1 -1
  164. package/dist/voice/room_io/room_io.cjs +5 -1
  165. package/dist/voice/room_io/room_io.cjs.map +1 -1
  166. package/dist/voice/room_io/room_io.d.ts.map +1 -1
  167. package/dist/voice/room_io/room_io.js +5 -1
  168. package/dist/voice/room_io/room_io.js.map +1 -1
  169. package/dist/voice/room_io/room_io.test.cjs +18 -0
  170. package/dist/voice/room_io/room_io.test.cjs.map +1 -0
  171. package/dist/voice/room_io/room_io.test.js +17 -0
  172. package/dist/voice/room_io/room_io.test.js.map +1 -0
  173. package/package.json +4 -2
  174. package/src/_exceptions.ts +5 -0
  175. package/src/audio.ts +12 -1
  176. package/src/beta/workflows/task_group.ts +14 -5
  177. package/src/inference/interruption/http_transport.ts +2 -1
  178. package/src/inference/interruption/ws_transport.ts +44 -34
  179. package/src/inference/tts.test.ts +87 -0
  180. package/src/inference/tts.ts +71 -9
  181. package/src/ipc/job_proc_lazy_main.ts +7 -2
  182. package/src/ipc/supervised_proc.test.ts +96 -0
  183. package/src/ipc/supervised_proc.ts +8 -1
  184. package/src/job.ts +1 -0
  185. package/src/llm/chat_context.test.ts +137 -5
  186. package/src/llm/chat_context.ts +119 -38
  187. package/src/llm/fallback_adapter.ts +5 -2
  188. package/src/llm/index.ts +2 -0
  189. package/src/llm/utils.test.ts +103 -2
  190. package/src/llm/utils.ts +128 -0
  191. package/src/stt/stt.ts +9 -1
  192. package/src/tts/fallback_adapter.ts +9 -2
  193. package/src/typed_promise.ts +67 -0
  194. package/src/utils.ts +45 -2
  195. package/src/voice/agent_activity.ts +11 -0
  196. package/src/voice/agent_session.ts +13 -7
  197. package/src/voice/events.ts +21 -0
  198. package/src/voice/generation.ts +35 -8
  199. package/src/voice/generation_tts_timeout.test.ts +112 -0
  200. package/src/voice/index.ts +6 -1
  201. package/src/voice/recorder_io/recorder_io.ts +2 -7
  202. package/src/voice/report.test.ts +78 -0
  203. package/src/voice/report.ts +1 -1
  204. package/src/voice/room_io/room_io.test.ts +38 -0
  205. package/src/voice/room_io/room_io.ts +7 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_exceptions.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Raised when accepting a job but not receiving an assignment within the specified timeout.\n * The server may have chosen another worker to handle this job.\n */\nexport class AssignmentTimeoutError extends Error {\n constructor(message = 'Assignment timeout occurred') {\n super(message);\n this.name = 'AssignmentTimeoutError';\n Error.captureStackTrace(this, AssignmentTimeoutError);\n }\n}\n\n/**\n * Interface for API error options\n */\ninterface APIErrorOptions {\n body?: object | null;\n retryable?: boolean;\n}\n\nconst API_ERROR_SYMBOL = Symbol('APIError');\n\n/**\n * Raised when an API request failed.\n * This is used on our TTS/STT/LLM plugins.\n */\nexport class APIError extends Error {\n readonly body: object | null;\n readonly retryable: boolean;\n\n constructor(message: string, { body = null, retryable = true }: APIErrorOptions = {}) {\n super(message);\n this.name = 'APIError';\n\n this.body = body;\n this.retryable = retryable;\n Error.captureStackTrace(this, APIError);\n Object.defineProperty(this, API_ERROR_SYMBOL, {\n value: true,\n writable: false,\n enumerable: false,\n configurable: false,\n });\n }\n\n toString(): string {\n return `${this.message} (body=${JSON.stringify(this.body)}, retryable=${this.retryable})`;\n }\n}\n\n/**\n * Interface for API status error options\n */\ninterface APIStatusErrorOptions extends APIErrorOptions {\n statusCode?: number;\n requestId?: string | null;\n}\n\n/**\n * Raised when an API response has a status code of 4xx or 5xx.\n */\nexport class APIStatusError extends APIError {\n readonly statusCode: number;\n readonly requestId: string | null;\n\n constructor({\n message = 'API error.',\n options = {},\n }: {\n message?: string;\n options?: APIStatusErrorOptions;\n }) {\n const statusCode = options.statusCode ?? -1;\n // 4xx errors are not retryable\n const isRetryable = options.retryable ?? !(statusCode >= 400 && statusCode < 500);\n\n super(message, { body: options.body, retryable: isRetryable });\n this.name = 'APIStatusError';\n\n this.statusCode = statusCode;\n this.requestId = options.requestId ?? null;\n Error.captureStackTrace(this, APIStatusError);\n }\n\n toString(): string {\n return (\n `${this.message} ` +\n `(statusCode=${this.statusCode}, ` +\n `requestId=${this.requestId}, ` +\n `body=${JSON.stringify(this.body)}, ` +\n `retryable=${this.retryable})`\n );\n }\n}\n\n/**\n * Raised when an API request failed due to a connection error.\n */\nexport class APIConnectionError extends APIError {\n constructor({\n message = 'Connection error.',\n options = {},\n }: {\n message?: string;\n options?: APIErrorOptions;\n }) {\n super(message, { body: null, retryable: options.retryable ?? true });\n this.name = 'APIConnectionError';\n Error.captureStackTrace(this, APIConnectionError);\n }\n}\n\n/**\n * Raised when an API request timed out.\n */\nexport class APITimeoutError extends APIConnectionError {\n constructor({\n message = 'Request timed out.',\n options = {},\n }: {\n message?: string;\n options?: APIErrorOptions;\n }) {\n const retryable = options?.retryable ?? true;\n\n super({ message, options: { retryable } });\n this.name = 'APITimeoutError';\n Error.captureStackTrace(this, APITimeoutError);\n }\n}\n\nexport function isAPIError(error: unknown): error is APIError {\n return error !== null && typeof error === 'object' && API_ERROR_SYMBOL in error;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,MAAM,+BAA+B,MAAM;AAAA,EAChD,YAAY,UAAU,+BAA+B;AACnD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,sBAAsB;AAAA,EACtD;AACF;AAUA,MAAM,mBAAmB,OAAO,UAAU;AAMnC,MAAM,iBAAiB,MAAM;AAAA,EACzB;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,EAAE,OAAO,MAAM,YAAY,KAAK,IAAqB,CAAC,GAAG;AACpF,UAAM,OAAO;AACb,SAAK,OAAO;AAEZ,SAAK,OAAO;AACZ,SAAK,YAAY;AACjB,UAAM,kBAAkB,MAAM,QAAQ;AACtC,WAAO,eAAe,MAAM,kBAAkB;AAAA,MAC5C,OAAO;AAAA,MACP,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,cAAc;AAAA,IAChB,CAAC;AAAA,EACH;AAAA,EAEA,WAAmB;AACjB,WAAO,GAAG,KAAK,OAAO,UAAU,KAAK,UAAU,KAAK,IAAI,CAAC,eAAe,KAAK,SAAS;AAAA,EACxF;AACF;AAaO,MAAM,uBAAuB,SAAS;AAAA,EAClC;AAAA,EACA;AAAA,EAET,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,aAAa,QAAQ,cAAc;AAEzC,UAAM,cAAc,QAAQ,aAAa,EAAE,cAAc,OAAO,aAAa;AAE7E,UAAM,SAAS,EAAE,MAAM,QAAQ,MAAM,WAAW,YAAY,CAAC;AAC7D,SAAK,OAAO;AAEZ,SAAK,aAAa;AAClB,SAAK,YAAY,QAAQ,aAAa;AACtC,UAAM,kBAAkB,MAAM,cAAc;AAAA,EAC9C;AAAA,EAEA,WAAmB;AACjB,WACE,GAAG,KAAK,OAAO,gBACA,KAAK,UAAU,eACjB,KAAK,SAAS,UACnB,KAAK,UAAU,KAAK,IAAI,CAAC,eACpB,KAAK,SAAS;AAAA,EAE/B;AACF;AAKO,MAAM,2BAA2B,SAAS;AAAA,EAC/C,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,SAAS,EAAE,MAAM,MAAM,WAAW,QAAQ,aAAa,KAAK,CAAC;AACnE,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,kBAAkB;AAAA,EAClD;AACF;AAKO,MAAM,wBAAwB,mBAAmB;AAAA,EACtD,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,aAAY,mCAAS,cAAa;AAExC,UAAM,EAAE,SAAS,SAAS,EAAE,UAAU,EAAE,CAAC;AACzC,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,eAAe;AAAA,EAC/C;AACF;AAEO,SAAS,WAAW,OAAmC;AAC5D,SAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,oBAAoB;AAC5E;","names":[]}
1
+ {"version":3,"sources":["../src/_exceptions.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n// FIXME(@livekit/agents): Consider migrating error signaling away from Error subclasses\n// toward an AbortSignal-based pattern for cancellation/error propagation. Error subclasses\n// work well for synchronous throw/catch, but AbortSignal integrates better with async\n// streams, fetch, and the broader Web API ecosystem.\n\n/**\n * Raised when accepting a job but not receiving an assignment within the specified timeout.\n * The server may have chosen another worker to handle this job.\n */\nexport class AssignmentTimeoutError extends Error {\n constructor(message = 'Assignment timeout occurred') {\n super(message);\n this.name = 'AssignmentTimeoutError';\n Error.captureStackTrace(this, AssignmentTimeoutError);\n }\n}\n\n/**\n * Interface for API error options\n */\ninterface APIErrorOptions {\n body?: object | null;\n retryable?: boolean;\n}\n\nconst API_ERROR_SYMBOL = Symbol('APIError');\n\n/**\n * Raised when an API request failed.\n * This is used on our TTS/STT/LLM plugins.\n */\nexport class APIError extends Error {\n readonly body: object | null;\n readonly retryable: boolean;\n\n constructor(message: string, { body = null, retryable = true }: APIErrorOptions = {}) {\n super(message);\n this.name = 'APIError';\n\n this.body = body;\n this.retryable = retryable;\n Error.captureStackTrace(this, APIError);\n Object.defineProperty(this, API_ERROR_SYMBOL, {\n value: true,\n writable: false,\n enumerable: false,\n configurable: false,\n });\n }\n\n toString(): string {\n return `${this.message} (body=${JSON.stringify(this.body)}, retryable=${this.retryable})`;\n }\n}\n\n/**\n * Interface for API status error options\n */\ninterface APIStatusErrorOptions extends APIErrorOptions {\n statusCode?: number;\n requestId?: string | null;\n}\n\n/**\n * Raised when an API response has a status code of 4xx or 5xx.\n */\nexport class APIStatusError extends APIError {\n readonly statusCode: number;\n readonly requestId: string | null;\n\n constructor({\n message = 'API error.',\n options = {},\n }: {\n message?: string;\n options?: APIStatusErrorOptions;\n }) {\n const statusCode = options.statusCode ?? -1;\n // 4xx errors are not retryable\n const isRetryable = options.retryable ?? !(statusCode >= 400 && statusCode < 500);\n\n super(message, { body: options.body, retryable: isRetryable });\n this.name = 'APIStatusError';\n\n this.statusCode = statusCode;\n this.requestId = options.requestId ?? null;\n Error.captureStackTrace(this, APIStatusError);\n }\n\n toString(): string {\n return (\n `${this.message} ` +\n `(statusCode=${this.statusCode}, ` +\n `requestId=${this.requestId}, ` +\n `body=${JSON.stringify(this.body)}, ` +\n `retryable=${this.retryable})`\n );\n }\n}\n\n/**\n * Raised when an API request failed due to a connection error.\n */\nexport class APIConnectionError extends APIError {\n constructor({\n message = 'Connection error.',\n options = {},\n }: {\n message?: string;\n options?: APIErrorOptions;\n }) {\n super(message, { body: null, retryable: options.retryable ?? true });\n this.name = 'APIConnectionError';\n Error.captureStackTrace(this, APIConnectionError);\n }\n}\n\n/**\n * Raised when an API request timed out.\n */\nexport class APITimeoutError extends APIConnectionError {\n constructor({\n message = 'Request timed out.',\n options = {},\n }: {\n message?: string;\n options?: APIErrorOptions;\n }) {\n const retryable = options?.retryable ?? true;\n\n super({ message, options: { retryable } });\n this.name = 'APITimeoutError';\n Error.captureStackTrace(this, APITimeoutError);\n }\n}\n\nexport function isAPIError(error: unknown): error is APIError {\n return error !== null && typeof error === 'object' && API_ERROR_SYMBOL in error;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYO,MAAM,+BAA+B,MAAM;AAAA,EAChD,YAAY,UAAU,+BAA+B;AACnD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,sBAAsB;AAAA,EACtD;AACF;AAUA,MAAM,mBAAmB,OAAO,UAAU;AAMnC,MAAM,iBAAiB,MAAM;AAAA,EACzB;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,EAAE,OAAO,MAAM,YAAY,KAAK,IAAqB,CAAC,GAAG;AACpF,UAAM,OAAO;AACb,SAAK,OAAO;AAEZ,SAAK,OAAO;AACZ,SAAK,YAAY;AACjB,UAAM,kBAAkB,MAAM,QAAQ;AACtC,WAAO,eAAe,MAAM,kBAAkB;AAAA,MAC5C,OAAO;AAAA,MACP,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,cAAc;AAAA,IAChB,CAAC;AAAA,EACH;AAAA,EAEA,WAAmB;AACjB,WAAO,GAAG,KAAK,OAAO,UAAU,KAAK,UAAU,KAAK,IAAI,CAAC,eAAe,KAAK,SAAS;AAAA,EACxF;AACF;AAaO,MAAM,uBAAuB,SAAS;AAAA,EAClC;AAAA,EACA;AAAA,EAET,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,aAAa,QAAQ,cAAc;AAEzC,UAAM,cAAc,QAAQ,aAAa,EAAE,cAAc,OAAO,aAAa;AAE7E,UAAM,SAAS,EAAE,MAAM,QAAQ,MAAM,WAAW,YAAY,CAAC;AAC7D,SAAK,OAAO;AAEZ,SAAK,aAAa;AAClB,SAAK,YAAY,QAAQ,aAAa;AACtC,UAAM,kBAAkB,MAAM,cAAc;AAAA,EAC9C;AAAA,EAEA,WAAmB;AACjB,WACE,GAAG,KAAK,OAAO,gBACA,KAAK,UAAU,eACjB,KAAK,SAAS,UACnB,KAAK,UAAU,KAAK,IAAI,CAAC,eACpB,KAAK,SAAS;AAAA,EAE/B;AACF;AAKO,MAAM,2BAA2B,SAAS;AAAA,EAC/C,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,SAAS,EAAE,MAAM,MAAM,WAAW,QAAQ,aAAa,KAAK,CAAC;AACnE,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,kBAAkB;AAAA,EAClD;AACF;AAKO,MAAM,wBAAwB,mBAAmB;AAAA,EACtD,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,aAAY,mCAAS,cAAa;AAExC,UAAM,EAAE,SAAS,SAAS,EAAE,UAAU,EAAE,CAAC;AACzC,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,eAAe;AAAA,EAC/C;AACF;AAEO,SAAS,WAAW,OAAmC;AAC5D,SAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,oBAAoB;AAC5E;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"_exceptions.d.ts","sourceRoot":"","sources":["../src/_exceptions.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,SAAgC;CAKpD;AAED;;GAEG;AACH,UAAU,eAAe;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAID;;;GAGG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;gBAEhB,OAAO,EAAE,MAAM,EAAE,EAAE,IAAW,EAAE,SAAgB,EAAE,GAAE,eAAoB;IAepF,QAAQ,IAAI,MAAM;CAGnB;AAED;;GAEG;AACH,UAAU,qBAAsB,SAAQ,eAAe;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEtB,EACV,OAAsB,EACtB,OAAY,GACb,EAAE;QACD,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,qBAAqB,CAAC;KACjC;IAaD,QAAQ,IAAI,MAAM;CASnB;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,QAAQ;gBAClC,EACV,OAA6B,EAC7B,OAAY,GACb,EAAE;QACD,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,eAAe,CAAC;KAC3B;CAKF;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,kBAAkB;gBACzC,EACV,OAA8B,EAC9B,OAAY,GACb,EAAE;QACD,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,eAAe,CAAC;KAC3B;CAOF;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAE5D"}
1
+ {"version":3,"file":"_exceptions.d.ts","sourceRoot":"","sources":["../src/_exceptions.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,SAAgC;CAKpD;AAED;;GAEG;AACH,UAAU,eAAe;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAID;;;GAGG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;gBAEhB,OAAO,EAAE,MAAM,EAAE,EAAE,IAAW,EAAE,SAAgB,EAAE,GAAE,eAAoB;IAepF,QAAQ,IAAI,MAAM;CAGnB;AAED;;GAEG;AACH,UAAU,qBAAsB,SAAQ,eAAe;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEtB,EACV,OAAsB,EACtB,OAAY,GACb,EAAE;QACD,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,qBAAqB,CAAC;KACjC;IAaD,QAAQ,IAAI,MAAM;CASnB;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,QAAQ;gBAClC,EACV,OAA6B,EAC7B,OAAY,GACb,EAAE;QACD,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,eAAe,CAAC;KAC3B;CAKF;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,kBAAkB;gBACzC,EACV,OAA8B,EAC9B,OAAY,GACb,EAAE;QACD,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,eAAe,CAAC;KAC3B;CAOF;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAE5D"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_exceptions.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Raised when accepting a job but not receiving an assignment within the specified timeout.\n * The server may have chosen another worker to handle this job.\n */\nexport class AssignmentTimeoutError extends Error {\n constructor(message = 'Assignment timeout occurred') {\n super(message);\n this.name = 'AssignmentTimeoutError';\n Error.captureStackTrace(this, AssignmentTimeoutError);\n }\n}\n\n/**\n * Interface for API error options\n */\ninterface APIErrorOptions {\n body?: object | null;\n retryable?: boolean;\n}\n\nconst API_ERROR_SYMBOL = Symbol('APIError');\n\n/**\n * Raised when an API request failed.\n * This is used on our TTS/STT/LLM plugins.\n */\nexport class APIError extends Error {\n readonly body: object | null;\n readonly retryable: boolean;\n\n constructor(message: string, { body = null, retryable = true }: APIErrorOptions = {}) {\n super(message);\n this.name = 'APIError';\n\n this.body = body;\n this.retryable = retryable;\n Error.captureStackTrace(this, APIError);\n Object.defineProperty(this, API_ERROR_SYMBOL, {\n value: true,\n writable: false,\n enumerable: false,\n configurable: false,\n });\n }\n\n toString(): string {\n return `${this.message} (body=${JSON.stringify(this.body)}, retryable=${this.retryable})`;\n }\n}\n\n/**\n * Interface for API status error options\n */\ninterface APIStatusErrorOptions extends APIErrorOptions {\n statusCode?: number;\n requestId?: string | null;\n}\n\n/**\n * Raised when an API response has a status code of 4xx or 5xx.\n */\nexport class APIStatusError extends APIError {\n readonly statusCode: number;\n readonly requestId: string | null;\n\n constructor({\n message = 'API error.',\n options = {},\n }: {\n message?: string;\n options?: APIStatusErrorOptions;\n }) {\n const statusCode = options.statusCode ?? -1;\n // 4xx errors are not retryable\n const isRetryable = options.retryable ?? !(statusCode >= 400 && statusCode < 500);\n\n super(message, { body: options.body, retryable: isRetryable });\n this.name = 'APIStatusError';\n\n this.statusCode = statusCode;\n this.requestId = options.requestId ?? null;\n Error.captureStackTrace(this, APIStatusError);\n }\n\n toString(): string {\n return (\n `${this.message} ` +\n `(statusCode=${this.statusCode}, ` +\n `requestId=${this.requestId}, ` +\n `body=${JSON.stringify(this.body)}, ` +\n `retryable=${this.retryable})`\n );\n }\n}\n\n/**\n * Raised when an API request failed due to a connection error.\n */\nexport class APIConnectionError extends APIError {\n constructor({\n message = 'Connection error.',\n options = {},\n }: {\n message?: string;\n options?: APIErrorOptions;\n }) {\n super(message, { body: null, retryable: options.retryable ?? true });\n this.name = 'APIConnectionError';\n Error.captureStackTrace(this, APIConnectionError);\n }\n}\n\n/**\n * Raised when an API request timed out.\n */\nexport class APITimeoutError extends APIConnectionError {\n constructor({\n message = 'Request timed out.',\n options = {},\n }: {\n message?: string;\n options?: APIErrorOptions;\n }) {\n const retryable = options?.retryable ?? true;\n\n super({ message, options: { retryable } });\n this.name = 'APITimeoutError';\n Error.captureStackTrace(this, APITimeoutError);\n }\n}\n\nexport function isAPIError(error: unknown): error is APIError {\n return error !== null && typeof error === 'object' && API_ERROR_SYMBOL in error;\n}\n"],"mappings":"AAOO,MAAM,+BAA+B,MAAM;AAAA,EAChD,YAAY,UAAU,+BAA+B;AACnD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,sBAAsB;AAAA,EACtD;AACF;AAUA,MAAM,mBAAmB,OAAO,UAAU;AAMnC,MAAM,iBAAiB,MAAM;AAAA,EACzB;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,EAAE,OAAO,MAAM,YAAY,KAAK,IAAqB,CAAC,GAAG;AACpF,UAAM,OAAO;AACb,SAAK,OAAO;AAEZ,SAAK,OAAO;AACZ,SAAK,YAAY;AACjB,UAAM,kBAAkB,MAAM,QAAQ;AACtC,WAAO,eAAe,MAAM,kBAAkB;AAAA,MAC5C,OAAO;AAAA,MACP,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,cAAc;AAAA,IAChB,CAAC;AAAA,EACH;AAAA,EAEA,WAAmB;AACjB,WAAO,GAAG,KAAK,OAAO,UAAU,KAAK,UAAU,KAAK,IAAI,CAAC,eAAe,KAAK,SAAS;AAAA,EACxF;AACF;AAaO,MAAM,uBAAuB,SAAS;AAAA,EAClC;AAAA,EACA;AAAA,EAET,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,aAAa,QAAQ,cAAc;AAEzC,UAAM,cAAc,QAAQ,aAAa,EAAE,cAAc,OAAO,aAAa;AAE7E,UAAM,SAAS,EAAE,MAAM,QAAQ,MAAM,WAAW,YAAY,CAAC;AAC7D,SAAK,OAAO;AAEZ,SAAK,aAAa;AAClB,SAAK,YAAY,QAAQ,aAAa;AACtC,UAAM,kBAAkB,MAAM,cAAc;AAAA,EAC9C;AAAA,EAEA,WAAmB;AACjB,WACE,GAAG,KAAK,OAAO,gBACA,KAAK,UAAU,eACjB,KAAK,SAAS,UACnB,KAAK,UAAU,KAAK,IAAI,CAAC,eACpB,KAAK,SAAS;AAAA,EAE/B;AACF;AAKO,MAAM,2BAA2B,SAAS;AAAA,EAC/C,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,SAAS,EAAE,MAAM,MAAM,WAAW,QAAQ,aAAa,KAAK,CAAC;AACnE,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,kBAAkB;AAAA,EAClD;AACF;AAKO,MAAM,wBAAwB,mBAAmB;AAAA,EACtD,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,aAAY,mCAAS,cAAa;AAExC,UAAM,EAAE,SAAS,SAAS,EAAE,UAAU,EAAE,CAAC;AACzC,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,eAAe;AAAA,EAC/C;AACF;AAEO,SAAS,WAAW,OAAmC;AAC5D,SAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,oBAAoB;AAC5E;","names":[]}
1
+ {"version":3,"sources":["../src/_exceptions.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n// FIXME(@livekit/agents): Consider migrating error signaling away from Error subclasses\n// toward an AbortSignal-based pattern for cancellation/error propagation. Error subclasses\n// work well for synchronous throw/catch, but AbortSignal integrates better with async\n// streams, fetch, and the broader Web API ecosystem.\n\n/**\n * Raised when accepting a job but not receiving an assignment within the specified timeout.\n * The server may have chosen another worker to handle this job.\n */\nexport class AssignmentTimeoutError extends Error {\n constructor(message = 'Assignment timeout occurred') {\n super(message);\n this.name = 'AssignmentTimeoutError';\n Error.captureStackTrace(this, AssignmentTimeoutError);\n }\n}\n\n/**\n * Interface for API error options\n */\ninterface APIErrorOptions {\n body?: object | null;\n retryable?: boolean;\n}\n\nconst API_ERROR_SYMBOL = Symbol('APIError');\n\n/**\n * Raised when an API request failed.\n * This is used on our TTS/STT/LLM plugins.\n */\nexport class APIError extends Error {\n readonly body: object | null;\n readonly retryable: boolean;\n\n constructor(message: string, { body = null, retryable = true }: APIErrorOptions = {}) {\n super(message);\n this.name = 'APIError';\n\n this.body = body;\n this.retryable = retryable;\n Error.captureStackTrace(this, APIError);\n Object.defineProperty(this, API_ERROR_SYMBOL, {\n value: true,\n writable: false,\n enumerable: false,\n configurable: false,\n });\n }\n\n toString(): string {\n return `${this.message} (body=${JSON.stringify(this.body)}, retryable=${this.retryable})`;\n }\n}\n\n/**\n * Interface for API status error options\n */\ninterface APIStatusErrorOptions extends APIErrorOptions {\n statusCode?: number;\n requestId?: string | null;\n}\n\n/**\n * Raised when an API response has a status code of 4xx or 5xx.\n */\nexport class APIStatusError extends APIError {\n readonly statusCode: number;\n readonly requestId: string | null;\n\n constructor({\n message = 'API error.',\n options = {},\n }: {\n message?: string;\n options?: APIStatusErrorOptions;\n }) {\n const statusCode = options.statusCode ?? -1;\n // 4xx errors are not retryable\n const isRetryable = options.retryable ?? !(statusCode >= 400 && statusCode < 500);\n\n super(message, { body: options.body, retryable: isRetryable });\n this.name = 'APIStatusError';\n\n this.statusCode = statusCode;\n this.requestId = options.requestId ?? null;\n Error.captureStackTrace(this, APIStatusError);\n }\n\n toString(): string {\n return (\n `${this.message} ` +\n `(statusCode=${this.statusCode}, ` +\n `requestId=${this.requestId}, ` +\n `body=${JSON.stringify(this.body)}, ` +\n `retryable=${this.retryable})`\n );\n }\n}\n\n/**\n * Raised when an API request failed due to a connection error.\n */\nexport class APIConnectionError extends APIError {\n constructor({\n message = 'Connection error.',\n options = {},\n }: {\n message?: string;\n options?: APIErrorOptions;\n }) {\n super(message, { body: null, retryable: options.retryable ?? true });\n this.name = 'APIConnectionError';\n Error.captureStackTrace(this, APIConnectionError);\n }\n}\n\n/**\n * Raised when an API request timed out.\n */\nexport class APITimeoutError extends APIConnectionError {\n constructor({\n message = 'Request timed out.',\n options = {},\n }: {\n message?: string;\n options?: APIErrorOptions;\n }) {\n const retryable = options?.retryable ?? true;\n\n super({ message, options: { retryable } });\n this.name = 'APITimeoutError';\n Error.captureStackTrace(this, APITimeoutError);\n }\n}\n\nexport function isAPIError(error: unknown): error is APIError {\n return error !== null && typeof error === 'object' && API_ERROR_SYMBOL in error;\n}\n"],"mappings":"AAYO,MAAM,+BAA+B,MAAM;AAAA,EAChD,YAAY,UAAU,+BAA+B;AACnD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,sBAAsB;AAAA,EACtD;AACF;AAUA,MAAM,mBAAmB,OAAO,UAAU;AAMnC,MAAM,iBAAiB,MAAM;AAAA,EACzB;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,EAAE,OAAO,MAAM,YAAY,KAAK,IAAqB,CAAC,GAAG;AACpF,UAAM,OAAO;AACb,SAAK,OAAO;AAEZ,SAAK,OAAO;AACZ,SAAK,YAAY;AACjB,UAAM,kBAAkB,MAAM,QAAQ;AACtC,WAAO,eAAe,MAAM,kBAAkB;AAAA,MAC5C,OAAO;AAAA,MACP,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,cAAc;AAAA,IAChB,CAAC;AAAA,EACH;AAAA,EAEA,WAAmB;AACjB,WAAO,GAAG,KAAK,OAAO,UAAU,KAAK,UAAU,KAAK,IAAI,CAAC,eAAe,KAAK,SAAS;AAAA,EACxF;AACF;AAaO,MAAM,uBAAuB,SAAS;AAAA,EAClC;AAAA,EACA;AAAA,EAET,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,aAAa,QAAQ,cAAc;AAEzC,UAAM,cAAc,QAAQ,aAAa,EAAE,cAAc,OAAO,aAAa;AAE7E,UAAM,SAAS,EAAE,MAAM,QAAQ,MAAM,WAAW,YAAY,CAAC;AAC7D,SAAK,OAAO;AAEZ,SAAK,aAAa;AAClB,SAAK,YAAY,QAAQ,aAAa;AACtC,UAAM,kBAAkB,MAAM,cAAc;AAAA,EAC9C;AAAA,EAEA,WAAmB;AACjB,WACE,GAAG,KAAK,OAAO,gBACA,KAAK,UAAU,eACjB,KAAK,SAAS,UACnB,KAAK,UAAU,KAAK,IAAI,CAAC,eACpB,KAAK,SAAS;AAAA,EAE/B;AACF;AAKO,MAAM,2BAA2B,SAAS;AAAA,EAC/C,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,SAAS,EAAE,MAAM,MAAM,WAAW,QAAQ,aAAa,KAAK,CAAC;AACnE,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,kBAAkB;AAAA,EAClD;AACF;AAKO,MAAM,wBAAwB,mBAAmB;AAAA,EACtD,YAAY;AAAA,IACV,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,GAGG;AACD,UAAM,aAAY,mCAAS,cAAa;AAExC,UAAM,EAAE,SAAS,SAAS,EAAE,UAAU,EAAE,CAAC;AACzC,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,eAAe;AAAA,EAC/C;AACF;AAEO,SAAS,WAAW,OAAmC;AAC5D,SAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,oBAAoB;AAC5E;","names":[]}
package/dist/audio.cjs CHANGED
@@ -39,6 +39,7 @@ var import_rtc_node = require("@livekit/rtc-node");
39
39
  var import_fluent_ffmpeg = __toESM(require("fluent-ffmpeg"), 1);
40
40
  var import_log = require("./log.cjs");
41
41
  var import_stream_channel = require("./stream/stream_channel.cjs");
42
+ var import_utils = require("./utils.cjs");
42
43
  import_fluent_ffmpeg.default.setFfmpegPath(import_ffmpeg.default.path);
43
44
  function calculateAudioDurationSeconds(frame) {
44
45
  return Array.isArray(frame) ? frame.reduce((sum, a) => sum + a.samplesPerChannel / a.sampleRate, 0) : frame.samplesPerChannel / frame.sampleRate;
@@ -118,6 +119,15 @@ function audioFramesFromFile(filePath, options = {}) {
118
119
  command.kill("SIGKILL");
119
120
  }
120
121
  };
122
+ command.on("error", (err) => {
123
+ if ((0, import_utils.isFfmpegTeardownError)(err)) {
124
+ logger.debug("FFmpeg command ended during shutdown");
125
+ } else {
126
+ logger.error(err, "FFmpeg command error");
127
+ }
128
+ commandRunning = false;
129
+ onClose();
130
+ });
121
131
  const outputStream = command.pipe();
122
132
  (_a = options.abortSignal) == null ? void 0 : _a.addEventListener("abort", onClose, { once: true });
123
133
  outputStream.on("data", (chunk) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/audio.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport ffmpegInstaller from '@ffmpeg-installer/ffmpeg';\nimport { AudioFrame } from '@livekit/rtc-node';\nimport ffmpeg from 'fluent-ffmpeg';\nimport type { ReadableStream } from 'node:stream/web';\nimport { log } from './log.js';\nimport { createStreamChannel } from './stream/stream_channel.js';\nimport type { AudioBuffer } from './utils.js';\n\nffmpeg.setFfmpegPath(ffmpegInstaller.path);\n\nexport interface AudioDecodeOptions {\n sampleRate?: number;\n numChannels?: number;\n /**\n * Audio format hint (e.g., 'mp3', 'ogg', 'wav', 'opus')\n * If not provided, FFmpeg will auto-detect\n */\n format?: string;\n abortSignal?: AbortSignal;\n}\n\nexport function calculateAudioDurationSeconds(frame: AudioBuffer) {\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n return Array.isArray(frame)\n ? frame.reduce((sum, a) => sum + a.samplesPerChannel / a.sampleRate, 0)\n : frame.samplesPerChannel / frame.sampleRate;\n}\n\n/** AudioByteStream translates between LiveKit AudioFrame packets and raw byte data. */\nexport class AudioByteStream {\n #sampleRate: number;\n #numChannels: number;\n #bytesPerFrame: number;\n #buf: Int8Array;\n #logger = log();\n\n constructor(sampleRate: number, numChannels: number, samplesPerChannel: number | null = null) {\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n\n if (samplesPerChannel === null) {\n samplesPerChannel = Math.floor(sampleRate / 10); // 100ms by default\n }\n\n this.#bytesPerFrame = numChannels * samplesPerChannel * 2; // 2 bytes per sample (Int16)\n this.#buf = new Int8Array();\n }\n\n write(data: ArrayBuffer): AudioFrame[] {\n this.#buf = new Int8Array([...this.#buf, ...new Int8Array(data)]);\n\n const frames: AudioFrame[] = [];\n while (this.#buf.length >= this.#bytesPerFrame) {\n const frameData = this.#buf.slice(0, this.#bytesPerFrame);\n this.#buf = this.#buf.slice(this.#bytesPerFrame);\n\n frames.push(\n new AudioFrame(\n new Int16Array(frameData.buffer),\n this.#sampleRate,\n this.#numChannels,\n frameData.length / 2,\n ),\n );\n }\n\n return frames;\n }\n\n flush(): AudioFrame[] {\n if (this.#buf.length % (2 * this.#numChannels) !== 0) {\n this.#logger.warn('AudioByteStream: incomplete frame during flush, dropping');\n return [];\n }\n\n const frames = [\n new AudioFrame(\n new Int16Array(this.#buf.buffer),\n this.#sampleRate,\n this.#numChannels,\n this.#buf.length / 2,\n ),\n ];\n\n this.#buf = new Int8Array(); // Clear buffer after flushing\n return frames;\n }\n}\n\n/**\n * Decode an audio file into AudioFrame instances\n *\n * @param filePath - Path to the audio file\n * @param options - Decoding options\n * @returns AsyncGenerator that yields AudioFrame objects\n *\n * @example\n * ```typescript\n * for await (const frame of audioFramesFromFile('audio.ogg', { sampleRate: 48000 })) {\n * console.log('Frame:', frame.samplesPerChannel, 'samples');\n * }\n * ```\n */\nexport function audioFramesFromFile(\n filePath: string,\n options: AudioDecodeOptions = {},\n): ReadableStream<AudioFrame> {\n const sampleRate = options.sampleRate ?? 48000;\n const numChannels = options.numChannels ?? 1;\n\n const audioStream = new AudioByteStream(sampleRate, numChannels);\n const channel = createStreamChannel<AudioFrame>();\n const logger = log();\n\n // TODO (Brian): decode WAV using a custom decoder instead of FFmpeg\n const command = ffmpeg(filePath)\n .inputOptions([\n '-probesize',\n '32',\n '-analyzeduration',\n '0',\n '-fflags',\n '+nobuffer+flush_packets',\n '-flags',\n 'low_delay',\n ])\n .format('s16le') // signed 16-bit little-endian PCM to be consistent cross-platform\n .audioChannels(numChannels)\n .audioFrequency(sampleRate);\n\n let commandRunning = true;\n\n const onClose = () => {\n logger.debug('Audio file playback aborted');\n\n channel.close();\n if (commandRunning) {\n commandRunning = false;\n command.kill('SIGKILL');\n }\n };\n\n const outputStream = command.pipe();\n options.abortSignal?.addEventListener('abort', onClose, { once: true });\n\n outputStream.on('data', (chunk: Buffer) => {\n const arrayBuffer = chunk.buffer.slice(\n chunk.byteOffset,\n chunk.byteOffset + chunk.byteLength,\n ) as ArrayBuffer;\n\n const frames = audioStream.write(arrayBuffer);\n for (const frame of frames) {\n channel.write(frame);\n }\n });\n\n outputStream.on('end', () => {\n const frames = audioStream.flush();\n for (const frame of frames) {\n channel.write(frame);\n }\n commandRunning = false;\n channel.close();\n });\n\n outputStream.on('error', (err: Error) => {\n logger.error(err);\n commandRunning = false;\n onClose();\n });\n\n return channel.stream();\n}\n\n/**\n * Loop audio frames from a file indefinitely\n *\n * @param filePath - Path to the audio file\n * @param options - Decoding options\n * @returns AsyncGenerator that yields AudioFrame objects in an infinite loop\n */\nexport async function* loopAudioFramesFromFile(\n filePath: string,\n options: AudioDecodeOptions = {},\n): AsyncGenerator<AudioFrame, void, unknown> {\n const frames: AudioFrame[] = [];\n const logger = log();\n\n for await (const frame of audioFramesFromFile(filePath, options)) {\n frames.push(frame);\n yield frame;\n }\n\n while (!options.abortSignal?.aborted) {\n for (const frame of frames) {\n yield frame;\n }\n }\n\n logger.debug('Audio file playback loop finished');\n}\n"],"mappings":"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","names":["ffmpeg","ffmpegInstaller"]}
1
+ {"version":3,"sources":["../src/audio.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport ffmpegInstaller from '@ffmpeg-installer/ffmpeg';\nimport { AudioFrame } from '@livekit/rtc-node';\nimport ffmpeg from 'fluent-ffmpeg';\nimport type { ReadableStream } from 'node:stream/web';\nimport { log } from './log.js';\nimport { createStreamChannel } from './stream/stream_channel.js';\nimport { type AudioBuffer, isFfmpegTeardownError } from './utils.js';\n\nffmpeg.setFfmpegPath(ffmpegInstaller.path);\n\nexport interface AudioDecodeOptions {\n sampleRate?: number;\n numChannels?: number;\n /**\n * Audio format hint (e.g., 'mp3', 'ogg', 'wav', 'opus')\n * If not provided, FFmpeg will auto-detect\n */\n format?: string;\n abortSignal?: AbortSignal;\n}\n\nexport function calculateAudioDurationSeconds(frame: AudioBuffer) {\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n return Array.isArray(frame)\n ? frame.reduce((sum, a) => sum + a.samplesPerChannel / a.sampleRate, 0)\n : frame.samplesPerChannel / frame.sampleRate;\n}\n\n/** AudioByteStream translates between LiveKit AudioFrame packets and raw byte data. */\nexport class AudioByteStream {\n #sampleRate: number;\n #numChannels: number;\n #bytesPerFrame: number;\n #buf: Int8Array;\n #logger = log();\n\n constructor(sampleRate: number, numChannels: number, samplesPerChannel: number | null = null) {\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n\n if (samplesPerChannel === null) {\n samplesPerChannel = Math.floor(sampleRate / 10); // 100ms by default\n }\n\n this.#bytesPerFrame = numChannels * samplesPerChannel * 2; // 2 bytes per sample (Int16)\n this.#buf = new Int8Array();\n }\n\n write(data: ArrayBuffer): AudioFrame[] {\n this.#buf = new Int8Array([...this.#buf, ...new Int8Array(data)]);\n\n const frames: AudioFrame[] = [];\n while (this.#buf.length >= this.#bytesPerFrame) {\n const frameData = this.#buf.slice(0, this.#bytesPerFrame);\n this.#buf = this.#buf.slice(this.#bytesPerFrame);\n\n frames.push(\n new AudioFrame(\n new Int16Array(frameData.buffer),\n this.#sampleRate,\n this.#numChannels,\n frameData.length / 2,\n ),\n );\n }\n\n return frames;\n }\n\n flush(): AudioFrame[] {\n if (this.#buf.length % (2 * this.#numChannels) !== 0) {\n this.#logger.warn('AudioByteStream: incomplete frame during flush, dropping');\n return [];\n }\n\n const frames = [\n new AudioFrame(\n new Int16Array(this.#buf.buffer),\n this.#sampleRate,\n this.#numChannels,\n this.#buf.length / 2,\n ),\n ];\n\n this.#buf = new Int8Array(); // Clear buffer after flushing\n return frames;\n }\n}\n\n/**\n * Decode an audio file into AudioFrame instances\n *\n * @param filePath - Path to the audio file\n * @param options - Decoding options\n * @returns AsyncGenerator that yields AudioFrame objects\n *\n * @example\n * ```typescript\n * for await (const frame of audioFramesFromFile('audio.ogg', { sampleRate: 48000 })) {\n * console.log('Frame:', frame.samplesPerChannel, 'samples');\n * }\n * ```\n */\nexport function audioFramesFromFile(\n filePath: string,\n options: AudioDecodeOptions = {},\n): ReadableStream<AudioFrame> {\n const sampleRate = options.sampleRate ?? 48000;\n const numChannels = options.numChannels ?? 1;\n\n const audioStream = new AudioByteStream(sampleRate, numChannels);\n const channel = createStreamChannel<AudioFrame>();\n const logger = log();\n\n // TODO (Brian): decode WAV using a custom decoder instead of FFmpeg\n const command = ffmpeg(filePath)\n .inputOptions([\n '-probesize',\n '32',\n '-analyzeduration',\n '0',\n '-fflags',\n '+nobuffer+flush_packets',\n '-flags',\n 'low_delay',\n ])\n .format('s16le') // signed 16-bit little-endian PCM to be consistent cross-platform\n .audioChannels(numChannels)\n .audioFrequency(sampleRate);\n\n let commandRunning = true;\n\n const onClose = () => {\n logger.debug('Audio file playback aborted');\n\n channel.close();\n if (commandRunning) {\n commandRunning = false;\n command.kill('SIGKILL');\n }\n };\n\n command.on('error', (err: Error) => {\n if (isFfmpegTeardownError(err)) {\n // Expected during teardown — not an error\n logger.debug('FFmpeg command ended during shutdown');\n } else {\n logger.error(err, 'FFmpeg command error');\n }\n commandRunning = false;\n onClose();\n });\n\n const outputStream = command.pipe();\n options.abortSignal?.addEventListener('abort', onClose, { once: true });\n\n outputStream.on('data', (chunk: Buffer) => {\n const arrayBuffer = chunk.buffer.slice(\n chunk.byteOffset,\n chunk.byteOffset + chunk.byteLength,\n ) as ArrayBuffer;\n\n const frames = audioStream.write(arrayBuffer);\n for (const frame of frames) {\n channel.write(frame);\n }\n });\n\n outputStream.on('end', () => {\n const frames = audioStream.flush();\n for (const frame of frames) {\n channel.write(frame);\n }\n commandRunning = false;\n channel.close();\n });\n\n outputStream.on('error', (err: Error) => {\n logger.error(err);\n commandRunning = false;\n onClose();\n });\n\n return channel.stream();\n}\n\n/**\n * Loop audio frames from a file indefinitely\n *\n * @param filePath - Path to the audio file\n * @param options - Decoding options\n * @returns AsyncGenerator that yields AudioFrame objects in an infinite loop\n */\nexport async function* loopAudioFramesFromFile(\n filePath: string,\n options: AudioDecodeOptions = {},\n): AsyncGenerator<AudioFrame, void, unknown> {\n const frames: AudioFrame[] = [];\n const logger = log();\n\n for await (const frame of audioFramesFromFile(filePath, options)) {\n frames.push(frame);\n yield frame;\n }\n\n while (!options.abortSignal?.aborted) {\n for (const frame of frames) {\n yield frame;\n }\n }\n\n logger.debug('Audio file playback loop finished');\n}\n"],"mappings":"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","names":["ffmpeg","ffmpegInstaller"]}
package/dist/audio.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  import { AudioFrame } from '@livekit/rtc-node';
3
3
  import type { ReadableStream } from 'node:stream/web';
4
- import type { AudioBuffer } from './utils.js';
4
+ import { type AudioBuffer } from './utils.js';
5
5
  export interface AudioDecodeOptions {
6
6
  sampleRate?: number;
7
7
  numChannels?: number;
package/dist/audio.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  import { AudioFrame } from '@livekit/rtc-node';
3
3
  import type { ReadableStream } from 'node:stream/web';
4
- import type { AudioBuffer } from './utils.js';
4
+ import { type AudioBuffer } from './utils.js';
5
5
  export interface AudioDecodeOptions {
6
6
  sampleRate?: number;
7
7
  numChannels?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,WAAW,UAK/D;AAED,uFAAuF;AACvF,qBAAa,eAAe;;gBAOd,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,GAAE,MAAM,GAAG,IAAW;IAY5F,KAAK,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,EAAE;IAqBtC,KAAK,IAAI,UAAU,EAAE;CAkBtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,kBAAuB,GAC/B,cAAc,CAAC,UAAU,CAAC,CAmE5B;AAED;;;;;;GAMG;AACH,wBAAuB,uBAAuB,CAC5C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,kBAAuB,GAC/B,cAAc,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,CAAC,CAgB3C"}
1
+ {"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGtD,OAAO,EAAE,KAAK,WAAW,EAAyB,MAAM,YAAY,CAAC;AAIrE,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,WAAW,UAK/D;AAED,uFAAuF;AACvF,qBAAa,eAAe;;gBAOd,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,GAAE,MAAM,GAAG,IAAW;IAY5F,KAAK,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,EAAE;IAqBtC,KAAK,IAAI,UAAU,EAAE;CAkBtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,kBAAuB,GAC/B,cAAc,CAAC,UAAU,CAAC,CA8E5B;AAED;;;;;;GAMG;AACH,wBAAuB,uBAAuB,CAC5C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,kBAAuB,GAC/B,cAAc,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,CAAC,CAgB3C"}
package/dist/audio.js CHANGED
@@ -3,6 +3,7 @@ import { AudioFrame } from "@livekit/rtc-node";
3
3
  import ffmpeg from "fluent-ffmpeg";
4
4
  import { log } from "./log.js";
5
5
  import { createStreamChannel } from "./stream/stream_channel.js";
6
+ import { isFfmpegTeardownError } from "./utils.js";
6
7
  ffmpeg.setFfmpegPath(ffmpegInstaller.path);
7
8
  function calculateAudioDurationSeconds(frame) {
8
9
  return Array.isArray(frame) ? frame.reduce((sum, a) => sum + a.samplesPerChannel / a.sampleRate, 0) : frame.samplesPerChannel / frame.sampleRate;
@@ -82,6 +83,15 @@ function audioFramesFromFile(filePath, options = {}) {
82
83
  command.kill("SIGKILL");
83
84
  }
84
85
  };
86
+ command.on("error", (err) => {
87
+ if (isFfmpegTeardownError(err)) {
88
+ logger.debug("FFmpeg command ended during shutdown");
89
+ } else {
90
+ logger.error(err, "FFmpeg command error");
91
+ }
92
+ commandRunning = false;
93
+ onClose();
94
+ });
85
95
  const outputStream = command.pipe();
86
96
  (_a = options.abortSignal) == null ? void 0 : _a.addEventListener("abort", onClose, { once: true });
87
97
  outputStream.on("data", (chunk) => {
package/dist/audio.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/audio.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport ffmpegInstaller from '@ffmpeg-installer/ffmpeg';\nimport { AudioFrame } from '@livekit/rtc-node';\nimport ffmpeg from 'fluent-ffmpeg';\nimport type { ReadableStream } from 'node:stream/web';\nimport { log } from './log.js';\nimport { createStreamChannel } from './stream/stream_channel.js';\nimport type { AudioBuffer } from './utils.js';\n\nffmpeg.setFfmpegPath(ffmpegInstaller.path);\n\nexport interface AudioDecodeOptions {\n sampleRate?: number;\n numChannels?: number;\n /**\n * Audio format hint (e.g., 'mp3', 'ogg', 'wav', 'opus')\n * If not provided, FFmpeg will auto-detect\n */\n format?: string;\n abortSignal?: AbortSignal;\n}\n\nexport function calculateAudioDurationSeconds(frame: AudioBuffer) {\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n return Array.isArray(frame)\n ? frame.reduce((sum, a) => sum + a.samplesPerChannel / a.sampleRate, 0)\n : frame.samplesPerChannel / frame.sampleRate;\n}\n\n/** AudioByteStream translates between LiveKit AudioFrame packets and raw byte data. */\nexport class AudioByteStream {\n #sampleRate: number;\n #numChannels: number;\n #bytesPerFrame: number;\n #buf: Int8Array;\n #logger = log();\n\n constructor(sampleRate: number, numChannels: number, samplesPerChannel: number | null = null) {\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n\n if (samplesPerChannel === null) {\n samplesPerChannel = Math.floor(sampleRate / 10); // 100ms by default\n }\n\n this.#bytesPerFrame = numChannels * samplesPerChannel * 2; // 2 bytes per sample (Int16)\n this.#buf = new Int8Array();\n }\n\n write(data: ArrayBuffer): AudioFrame[] {\n this.#buf = new Int8Array([...this.#buf, ...new Int8Array(data)]);\n\n const frames: AudioFrame[] = [];\n while (this.#buf.length >= this.#bytesPerFrame) {\n const frameData = this.#buf.slice(0, this.#bytesPerFrame);\n this.#buf = this.#buf.slice(this.#bytesPerFrame);\n\n frames.push(\n new AudioFrame(\n new Int16Array(frameData.buffer),\n this.#sampleRate,\n this.#numChannels,\n frameData.length / 2,\n ),\n );\n }\n\n return frames;\n }\n\n flush(): AudioFrame[] {\n if (this.#buf.length % (2 * this.#numChannels) !== 0) {\n this.#logger.warn('AudioByteStream: incomplete frame during flush, dropping');\n return [];\n }\n\n const frames = [\n new AudioFrame(\n new Int16Array(this.#buf.buffer),\n this.#sampleRate,\n this.#numChannels,\n this.#buf.length / 2,\n ),\n ];\n\n this.#buf = new Int8Array(); // Clear buffer after flushing\n return frames;\n }\n}\n\n/**\n * Decode an audio file into AudioFrame instances\n *\n * @param filePath - Path to the audio file\n * @param options - Decoding options\n * @returns AsyncGenerator that yields AudioFrame objects\n *\n * @example\n * ```typescript\n * for await (const frame of audioFramesFromFile('audio.ogg', { sampleRate: 48000 })) {\n * console.log('Frame:', frame.samplesPerChannel, 'samples');\n * }\n * ```\n */\nexport function audioFramesFromFile(\n filePath: string,\n options: AudioDecodeOptions = {},\n): ReadableStream<AudioFrame> {\n const sampleRate = options.sampleRate ?? 48000;\n const numChannels = options.numChannels ?? 1;\n\n const audioStream = new AudioByteStream(sampleRate, numChannels);\n const channel = createStreamChannel<AudioFrame>();\n const logger = log();\n\n // TODO (Brian): decode WAV using a custom decoder instead of FFmpeg\n const command = ffmpeg(filePath)\n .inputOptions([\n '-probesize',\n '32',\n '-analyzeduration',\n '0',\n '-fflags',\n '+nobuffer+flush_packets',\n '-flags',\n 'low_delay',\n ])\n .format('s16le') // signed 16-bit little-endian PCM to be consistent cross-platform\n .audioChannels(numChannels)\n .audioFrequency(sampleRate);\n\n let commandRunning = true;\n\n const onClose = () => {\n logger.debug('Audio file playback aborted');\n\n channel.close();\n if (commandRunning) {\n commandRunning = false;\n command.kill('SIGKILL');\n }\n };\n\n const outputStream = command.pipe();\n options.abortSignal?.addEventListener('abort', onClose, { once: true });\n\n outputStream.on('data', (chunk: Buffer) => {\n const arrayBuffer = chunk.buffer.slice(\n chunk.byteOffset,\n chunk.byteOffset + chunk.byteLength,\n ) as ArrayBuffer;\n\n const frames = audioStream.write(arrayBuffer);\n for (const frame of frames) {\n channel.write(frame);\n }\n });\n\n outputStream.on('end', () => {\n const frames = audioStream.flush();\n for (const frame of frames) {\n channel.write(frame);\n }\n commandRunning = false;\n channel.close();\n });\n\n outputStream.on('error', (err: Error) => {\n logger.error(err);\n commandRunning = false;\n onClose();\n });\n\n return channel.stream();\n}\n\n/**\n * Loop audio frames from a file indefinitely\n *\n * @param filePath - Path to the audio file\n * @param options - Decoding options\n * @returns AsyncGenerator that yields AudioFrame objects in an infinite loop\n */\nexport async function* loopAudioFramesFromFile(\n filePath: string,\n options: AudioDecodeOptions = {},\n): AsyncGenerator<AudioFrame, void, unknown> {\n const frames: AudioFrame[] = [];\n const logger = log();\n\n for await (const frame of audioFramesFromFile(filePath, options)) {\n frames.push(frame);\n yield frame;\n }\n\n while (!options.abortSignal?.aborted) {\n for (const frame of frames) {\n yield frame;\n }\n }\n\n logger.debug('Audio file playback loop finished');\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/audio.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport ffmpegInstaller from '@ffmpeg-installer/ffmpeg';\nimport { AudioFrame } from '@livekit/rtc-node';\nimport ffmpeg from 'fluent-ffmpeg';\nimport type { ReadableStream } from 'node:stream/web';\nimport { log } from './log.js';\nimport { createStreamChannel } from './stream/stream_channel.js';\nimport { type AudioBuffer, isFfmpegTeardownError } from './utils.js';\n\nffmpeg.setFfmpegPath(ffmpegInstaller.path);\n\nexport interface AudioDecodeOptions {\n sampleRate?: number;\n numChannels?: number;\n /**\n * Audio format hint (e.g., 'mp3', 'ogg', 'wav', 'opus')\n * If not provided, FFmpeg will auto-detect\n */\n format?: string;\n abortSignal?: AbortSignal;\n}\n\nexport function calculateAudioDurationSeconds(frame: AudioBuffer) {\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n return Array.isArray(frame)\n ? frame.reduce((sum, a) => sum + a.samplesPerChannel / a.sampleRate, 0)\n : frame.samplesPerChannel / frame.sampleRate;\n}\n\n/** AudioByteStream translates between LiveKit AudioFrame packets and raw byte data. */\nexport class AudioByteStream {\n #sampleRate: number;\n #numChannels: number;\n #bytesPerFrame: number;\n #buf: Int8Array;\n #logger = log();\n\n constructor(sampleRate: number, numChannels: number, samplesPerChannel: number | null = null) {\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n\n if (samplesPerChannel === null) {\n samplesPerChannel = Math.floor(sampleRate / 10); // 100ms by default\n }\n\n this.#bytesPerFrame = numChannels * samplesPerChannel * 2; // 2 bytes per sample (Int16)\n this.#buf = new Int8Array();\n }\n\n write(data: ArrayBuffer): AudioFrame[] {\n this.#buf = new Int8Array([...this.#buf, ...new Int8Array(data)]);\n\n const frames: AudioFrame[] = [];\n while (this.#buf.length >= this.#bytesPerFrame) {\n const frameData = this.#buf.slice(0, this.#bytesPerFrame);\n this.#buf = this.#buf.slice(this.#bytesPerFrame);\n\n frames.push(\n new AudioFrame(\n new Int16Array(frameData.buffer),\n this.#sampleRate,\n this.#numChannels,\n frameData.length / 2,\n ),\n );\n }\n\n return frames;\n }\n\n flush(): AudioFrame[] {\n if (this.#buf.length % (2 * this.#numChannels) !== 0) {\n this.#logger.warn('AudioByteStream: incomplete frame during flush, dropping');\n return [];\n }\n\n const frames = [\n new AudioFrame(\n new Int16Array(this.#buf.buffer),\n this.#sampleRate,\n this.#numChannels,\n this.#buf.length / 2,\n ),\n ];\n\n this.#buf = new Int8Array(); // Clear buffer after flushing\n return frames;\n }\n}\n\n/**\n * Decode an audio file into AudioFrame instances\n *\n * @param filePath - Path to the audio file\n * @param options - Decoding options\n * @returns AsyncGenerator that yields AudioFrame objects\n *\n * @example\n * ```typescript\n * for await (const frame of audioFramesFromFile('audio.ogg', { sampleRate: 48000 })) {\n * console.log('Frame:', frame.samplesPerChannel, 'samples');\n * }\n * ```\n */\nexport function audioFramesFromFile(\n filePath: string,\n options: AudioDecodeOptions = {},\n): ReadableStream<AudioFrame> {\n const sampleRate = options.sampleRate ?? 48000;\n const numChannels = options.numChannels ?? 1;\n\n const audioStream = new AudioByteStream(sampleRate, numChannels);\n const channel = createStreamChannel<AudioFrame>();\n const logger = log();\n\n // TODO (Brian): decode WAV using a custom decoder instead of FFmpeg\n const command = ffmpeg(filePath)\n .inputOptions([\n '-probesize',\n '32',\n '-analyzeduration',\n '0',\n '-fflags',\n '+nobuffer+flush_packets',\n '-flags',\n 'low_delay',\n ])\n .format('s16le') // signed 16-bit little-endian PCM to be consistent cross-platform\n .audioChannels(numChannels)\n .audioFrequency(sampleRate);\n\n let commandRunning = true;\n\n const onClose = () => {\n logger.debug('Audio file playback aborted');\n\n channel.close();\n if (commandRunning) {\n commandRunning = false;\n command.kill('SIGKILL');\n }\n };\n\n command.on('error', (err: Error) => {\n if (isFfmpegTeardownError(err)) {\n // Expected during teardown — not an error\n logger.debug('FFmpeg command ended during shutdown');\n } else {\n logger.error(err, 'FFmpeg command error');\n }\n commandRunning = false;\n onClose();\n });\n\n const outputStream = command.pipe();\n options.abortSignal?.addEventListener('abort', onClose, { once: true });\n\n outputStream.on('data', (chunk: Buffer) => {\n const arrayBuffer = chunk.buffer.slice(\n chunk.byteOffset,\n chunk.byteOffset + chunk.byteLength,\n ) as ArrayBuffer;\n\n const frames = audioStream.write(arrayBuffer);\n for (const frame of frames) {\n channel.write(frame);\n }\n });\n\n outputStream.on('end', () => {\n const frames = audioStream.flush();\n for (const frame of frames) {\n channel.write(frame);\n }\n commandRunning = false;\n channel.close();\n });\n\n outputStream.on('error', (err: Error) => {\n logger.error(err);\n commandRunning = false;\n onClose();\n });\n\n return channel.stream();\n}\n\n/**\n * Loop audio frames from a file indefinitely\n *\n * @param filePath - Path to the audio file\n * @param options - Decoding options\n * @returns AsyncGenerator that yields AudioFrame objects in an infinite loop\n */\nexport async function* loopAudioFramesFromFile(\n filePath: string,\n options: AudioDecodeOptions = {},\n): AsyncGenerator<AudioFrame, void, unknown> {\n const frames: AudioFrame[] = [];\n const logger = log();\n\n for await (const frame of audioFramesFromFile(filePath, options)) {\n frames.push(frame);\n yield frame;\n }\n\n while (!options.abortSignal?.aborted) {\n for (const frame of frames) {\n yield frame;\n }\n }\n\n logger.debug('Audio file playback loop finished');\n}\n"],"mappings":"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","names":[]}
@@ -68,6 +68,9 @@ class TaskGroup extends import_agent.AgentTask {
68
68
  try {
69
69
  this._visitedTasks.add(taskId);
70
70
  const res = await this._currentTask.run();
71
+ this._chatCtx.merge(this._currentTask.chatCtx.copy(), {
72
+ excludeInstructions: true
73
+ });
71
74
  taskResults[taskId] = res;
72
75
  if (this._taskCompletedCallback) {
73
76
  await this._taskCompletedCallback({
@@ -100,10 +103,10 @@ class TaskGroup extends import_agent.AgentTask {
100
103
  throw new Error("summarizeChatCtx requires a standard LLM on the session");
101
104
  }
102
105
  const ctxToSummarize = this._chatCtx.copy({
103
- excludeInstructions: true,
104
- excludeHandoff: true,
105
- excludeEmptyMessage: true,
106
- excludeFunctionCall: true
106
+ excludeInstructions: false,
107
+ excludeFunctionCall: false,
108
+ excludeEmptyMessage: false,
109
+ excludeHandoff: false
107
110
  });
108
111
  const summarizedChatCtx = await ctxToSummarize._summarize(sessionLlm, {
109
112
  keepLastTurns: 0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/beta/workflows/task_group.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { z } from 'zod';\nimport type { ChatContext } from '../../llm/chat_context.js';\nimport { LLM, ToolError, ToolFlag, tool } from '../../llm/index.js';\nimport { AgentTask } from '../../voice/agent.js';\n\ninterface FactoryInfo {\n taskFactory: () => AgentTask;\n id: string;\n description: string;\n}\n\nexport interface TaskGroupResult {\n taskResults: Record<string, unknown>;\n}\n\nexport interface TaskCompletedEvent {\n agentTask: AgentTask;\n taskId: string;\n result: unknown;\n}\n\nclass OutOfScopeError extends ToolError {\n readonly targetTaskIds: string[];\n\n constructor(targetTaskIds: string[]) {\n super('out_of_scope');\n this.targetTaskIds = targetTaskIds;\n }\n}\n\nexport interface TaskGroupOptions {\n summarizeChatCtx?: boolean;\n returnExceptions?: boolean;\n chatCtx?: ChatContext;\n onTaskCompleted?: (event: TaskCompletedEvent) => Promise<void>;\n}\n\nexport class TaskGroup extends AgentTask<TaskGroupResult> {\n private _summarizeChatCtx: boolean;\n private _returnExceptions: boolean;\n private _visitedTasks = new Set<string>();\n private _registeredFactories = new Map<string, FactoryInfo>();\n private _taskCompletedCallback?: (event: TaskCompletedEvent) => Promise<void>;\n private _currentTask?: AgentTask;\n\n constructor(options: TaskGroupOptions = {}) {\n const { summarizeChatCtx = true, returnExceptions = false, chatCtx, onTaskCompleted } = options;\n\n super({ instructions: '*empty*', chatCtx });\n\n this._summarizeChatCtx = summarizeChatCtx;\n this._returnExceptions = returnExceptions;\n this._taskCompletedCallback = onTaskCompleted;\n }\n\n add(task: () => AgentTask, { id, description }: { id: string; description: string }): this {\n this._registeredFactories.set(id, { taskFactory: task, id, description });\n return this;\n }\n\n async onEnter(): Promise<void> {\n const taskStack = [...this._registeredFactories.keys()];\n const taskResults: Record<string, unknown> = {};\n\n while (taskStack.length > 0) {\n const taskId = taskStack.shift()!;\n const factoryInfo = this._registeredFactories.get(taskId)!;\n\n this._currentTask = factoryInfo.taskFactory();\n\n const sharedChatCtx = this._chatCtx.copy();\n await this._currentTask.updateChatCtx(sharedChatCtx);\n\n const outOfScopeTool = this.buildOutOfScopeTool(taskId);\n if (outOfScopeTool) {\n await this._currentTask.updateTools({\n ...this._currentTask.toolCtx,\n out_of_scope: outOfScopeTool,\n });\n }\n\n try {\n this._visitedTasks.add(taskId);\n const res = await this._currentTask.run();\n taskResults[taskId] = res;\n\n if (this._taskCompletedCallback) {\n await this._taskCompletedCallback({\n agentTask: this._currentTask,\n taskId,\n result: res,\n });\n }\n } catch (e) {\n if (e instanceof OutOfScopeError) {\n taskStack.unshift(taskId);\n for (let i = e.targetTaskIds.length - 1; i >= 0; i--) {\n taskStack.unshift(e.targetTaskIds[i]!);\n }\n continue;\n }\n\n if (this._returnExceptions) {\n taskResults[taskId] = e;\n continue;\n } else {\n this.complete(e instanceof Error ? e : new Error(String(e)));\n return;\n }\n }\n }\n\n try {\n if (this._summarizeChatCtx) {\n const sessionLlm = this.session.llm;\n if (!(sessionLlm instanceof LLM)) {\n throw new Error('summarizeChatCtx requires a standard LLM on the session');\n }\n\n // TODO(parity): Add excludeConfigUpdate when AgentConfigUpdate is ported\n const ctxToSummarize = this._chatCtx.copy({\n excludeInstructions: true,\n excludeHandoff: true,\n excludeEmptyMessage: true,\n excludeFunctionCall: true,\n });\n\n const summarizedChatCtx = await ctxToSummarize._summarize(sessionLlm, {\n keepLastTurns: 0,\n });\n await this.updateChatCtx(summarizedChatCtx);\n }\n } catch (e) {\n this.complete(new Error(`failed to summarize the chat_ctx: ${e}`));\n return;\n }\n\n this.complete({ taskResults });\n }\n\n private buildOutOfScopeTool(activeTaskId: string) {\n if (this._visitedTasks.size === 0) {\n return undefined;\n }\n\n const regressionTaskIds = new Set(this._visitedTasks);\n regressionTaskIds.delete(activeTaskId);\n\n if (regressionTaskIds.size === 0) {\n return undefined;\n }\n\n const taskRepr: Record<string, string> = {};\n for (const [id, info] of this._registeredFactories) {\n if (regressionTaskIds.has(id)) {\n taskRepr[id] = info.description;\n }\n }\n\n const taskIdValues = [...regressionTaskIds] as [string, ...string[]];\n\n const description =\n 'Call to regress to other tasks according to what the user requested to modify, return the corresponding task ids. ' +\n 'For example, if the user wants to change their email and there is a task with id \"email_task\" with a description of \"Collect the user\\'s email\", return the id (\"get_email_task\"). ' +\n 'If the user requests to regress to multiple tasks, such as changing their phone number and email, return both task ids in the order they were requested. ' +\n `The following are the IDs and their corresponding task description. ${JSON.stringify(taskRepr)}`;\n\n const currentTask = this._currentTask;\n const registeredFactories = this._registeredFactories;\n const visitedTasks = this._visitedTasks;\n\n return tool({\n description,\n flags: ToolFlag.IGNORE_ON_ENTER,\n parameters: z.object({\n task_ids: z.array(z.enum(taskIdValues)).describe('The IDs of the tasks requested'),\n }),\n execute: async ({ task_ids }: { task_ids: string[] }) => {\n for (const tid of task_ids) {\n if (!registeredFactories.has(tid) || !visitedTasks.has(tid)) {\n throw new ToolError(`Unable to regress, invalid task id ${tid}`);\n }\n }\n\n if (currentTask && !currentTask.done) {\n currentTask.complete(new OutOfScopeError(task_ids));\n }\n },\n });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/beta/workflows/task_group.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { z } from 'zod';\nimport type { ChatContext } from '../../llm/chat_context.js';\nimport { LLM, ToolError, ToolFlag, tool } from '../../llm/index.js';\nimport { AgentTask } from '../../voice/agent.js';\n\ninterface FactoryInfo {\n taskFactory: () => AgentTask;\n id: string;\n description: string;\n}\n\nexport interface TaskGroupResult {\n taskResults: Record<string, unknown>;\n}\n\nexport interface TaskCompletedEvent {\n agentTask: AgentTask;\n taskId: string;\n result: unknown;\n}\n\nclass OutOfScopeError extends ToolError {\n readonly targetTaskIds: string[];\n\n constructor(targetTaskIds: string[]) {\n super('out_of_scope');\n this.targetTaskIds = targetTaskIds;\n }\n}\n\nexport interface TaskGroupOptions {\n summarizeChatCtx?: boolean;\n returnExceptions?: boolean;\n chatCtx?: ChatContext;\n onTaskCompleted?: (event: TaskCompletedEvent) => Promise<void>;\n}\n\nexport class TaskGroup extends AgentTask<TaskGroupResult> {\n private _summarizeChatCtx: boolean;\n private _returnExceptions: boolean;\n private _visitedTasks = new Set<string>();\n private _registeredFactories = new Map<string, FactoryInfo>();\n private _taskCompletedCallback?: (event: TaskCompletedEvent) => Promise<void>;\n private _currentTask?: AgentTask;\n\n constructor(options: TaskGroupOptions = {}) {\n const { summarizeChatCtx = true, returnExceptions = false, chatCtx, onTaskCompleted } = options;\n\n super({ instructions: '*empty*', chatCtx });\n\n this._summarizeChatCtx = summarizeChatCtx;\n this._returnExceptions = returnExceptions;\n this._taskCompletedCallback = onTaskCompleted;\n }\n\n add(task: () => AgentTask, { id, description }: { id: string; description: string }): this {\n this._registeredFactories.set(id, { taskFactory: task, id, description });\n return this;\n }\n\n async onEnter(): Promise<void> {\n const taskStack = [...this._registeredFactories.keys()];\n const taskResults: Record<string, unknown> = {};\n\n while (taskStack.length > 0) {\n const taskId = taskStack.shift()!;\n const factoryInfo = this._registeredFactories.get(taskId)!;\n\n this._currentTask = factoryInfo.taskFactory();\n\n const sharedChatCtx = this._chatCtx.copy();\n await this._currentTask.updateChatCtx(sharedChatCtx);\n\n const outOfScopeTool = this.buildOutOfScopeTool(taskId);\n if (outOfScopeTool) {\n await this._currentTask.updateTools({\n ...this._currentTask.toolCtx,\n out_of_scope: outOfScopeTool,\n });\n }\n\n try {\n this._visitedTasks.add(taskId);\n const res = await this._currentTask.run();\n\n // AgentTask handoff merges omit function calls. Re-merge the completed\n // task context so task-group summarization can incorporate tool results.\n this._chatCtx.merge(this._currentTask.chatCtx.copy(), {\n excludeInstructions: true,\n });\n\n taskResults[taskId] = res;\n\n if (this._taskCompletedCallback) {\n await this._taskCompletedCallback({\n agentTask: this._currentTask,\n taskId,\n result: res,\n });\n }\n } catch (e) {\n if (e instanceof OutOfScopeError) {\n taskStack.unshift(taskId);\n for (let i = e.targetTaskIds.length - 1; i >= 0; i--) {\n taskStack.unshift(e.targetTaskIds[i]!);\n }\n continue;\n }\n\n if (this._returnExceptions) {\n taskResults[taskId] = e;\n continue;\n } else {\n this.complete(e instanceof Error ? e : new Error(String(e)));\n return;\n }\n }\n }\n\n try {\n if (this._summarizeChatCtx) {\n const sessionLlm = this.session.llm;\n if (!(sessionLlm instanceof LLM)) {\n throw new Error('summarizeChatCtx requires a standard LLM on the session');\n }\n\n // Keep the full item stream so summarization can distill tool results\n // into the history summary instead of dropping them up front.\n const ctxToSummarize = this._chatCtx.copy({\n excludeInstructions: false,\n excludeFunctionCall: false,\n excludeEmptyMessage: false,\n excludeHandoff: false,\n });\n\n const summarizedChatCtx = await ctxToSummarize._summarize(sessionLlm, {\n keepLastTurns: 0,\n });\n\n await this.updateChatCtx(summarizedChatCtx);\n }\n } catch (e) {\n this.complete(new Error(`failed to summarize the chat_ctx: ${e}`));\n return;\n }\n\n this.complete({ taskResults });\n }\n\n private buildOutOfScopeTool(activeTaskId: string) {\n if (this._visitedTasks.size === 0) {\n return undefined;\n }\n\n const regressionTaskIds = new Set(this._visitedTasks);\n regressionTaskIds.delete(activeTaskId);\n\n if (regressionTaskIds.size === 0) {\n return undefined;\n }\n\n const taskRepr: Record<string, string> = {};\n for (const [id, info] of this._registeredFactories) {\n if (regressionTaskIds.has(id)) {\n taskRepr[id] = info.description;\n }\n }\n\n const taskIdValues = [...regressionTaskIds] as [string, ...string[]];\n\n const description =\n 'Call to regress to other tasks according to what the user requested to modify, return the corresponding task ids. ' +\n 'For example, if the user wants to change their email and there is a task with id \"email_task\" with a description of \"Collect the user\\'s email\", return the id (\"get_email_task\"). ' +\n 'If the user requests to regress to multiple tasks, such as changing their phone number and email, return both task ids in the order they were requested. ' +\n `The following are the IDs and their corresponding task description. ${JSON.stringify(taskRepr)}`;\n\n const currentTask = this._currentTask;\n const registeredFactories = this._registeredFactories;\n const visitedTasks = this._visitedTasks;\n\n return tool({\n description,\n flags: ToolFlag.IGNORE_ON_ENTER,\n parameters: z.object({\n task_ids: z.array(z.enum(taskIdValues)).describe('The IDs of the tasks requested'),\n }),\n execute: async ({ task_ids }: { task_ids: string[] }) => {\n for (const tid of task_ids) {\n if (!registeredFactories.has(tid) || !visitedTasks.has(tid)) {\n throw new ToolError(`Unable to regress, invalid task id ${tid}`);\n }\n }\n\n if (currentTask && !currentTask.done) {\n currentTask.complete(new OutOfScopeError(task_ids));\n }\n },\n });\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"task_group.d.ts","sourceRoot":"","sources":["../../../src/beta/workflows/task_group.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAQjD,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAWD,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChE;AAED,qBAAa,SAAU,SAAQ,SAAS,CAAC,eAAe,CAAC;IACvD,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,oBAAoB,CAAkC;IAC9D,OAAO,CAAC,sBAAsB,CAAC,CAA+C;IAC9E,OAAO,CAAC,YAAY,CAAC,CAAY;gBAErB,OAAO,GAAE,gBAAqB;IAU1C,GAAG,CAAC,IAAI,EAAE,MAAM,SAAS,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAKpF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgF9B,OAAO,CAAC,mBAAmB;CAkD5B"}
1
+ {"version":3,"file":"task_group.d.ts","sourceRoot":"","sources":["../../../src/beta/workflows/task_group.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAQjD,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAWD,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChE;AAED,qBAAa,SAAU,SAAQ,SAAS,CAAC,eAAe,CAAC;IACvD,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,oBAAoB,CAAkC;IAC9D,OAAO,CAAC,sBAAsB,CAAC,CAA+C;IAC9E,OAAO,CAAC,YAAY,CAAC,CAAY;gBAErB,OAAO,GAAE,gBAAqB;IAU1C,GAAG,CAAC,IAAI,EAAE,MAAM,SAAS,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAKpF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAyF9B,OAAO,CAAC,mBAAmB;CAkD5B"}
@@ -45,6 +45,9 @@ class TaskGroup extends AgentTask {
45
45
  try {
46
46
  this._visitedTasks.add(taskId);
47
47
  const res = await this._currentTask.run();
48
+ this._chatCtx.merge(this._currentTask.chatCtx.copy(), {
49
+ excludeInstructions: true
50
+ });
48
51
  taskResults[taskId] = res;
49
52
  if (this._taskCompletedCallback) {
50
53
  await this._taskCompletedCallback({
@@ -77,10 +80,10 @@ class TaskGroup extends AgentTask {
77
80
  throw new Error("summarizeChatCtx requires a standard LLM on the session");
78
81
  }
79
82
  const ctxToSummarize = this._chatCtx.copy({
80
- excludeInstructions: true,
81
- excludeHandoff: true,
82
- excludeEmptyMessage: true,
83
- excludeFunctionCall: true
83
+ excludeInstructions: false,
84
+ excludeFunctionCall: false,
85
+ excludeEmptyMessage: false,
86
+ excludeHandoff: false
84
87
  });
85
88
  const summarizedChatCtx = await ctxToSummarize._summarize(sessionLlm, {
86
89
  keepLastTurns: 0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/beta/workflows/task_group.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { z } from 'zod';\nimport type { ChatContext } from '../../llm/chat_context.js';\nimport { LLM, ToolError, ToolFlag, tool } from '../../llm/index.js';\nimport { AgentTask } from '../../voice/agent.js';\n\ninterface FactoryInfo {\n taskFactory: () => AgentTask;\n id: string;\n description: string;\n}\n\nexport interface TaskGroupResult {\n taskResults: Record<string, unknown>;\n}\n\nexport interface TaskCompletedEvent {\n agentTask: AgentTask;\n taskId: string;\n result: unknown;\n}\n\nclass OutOfScopeError extends ToolError {\n readonly targetTaskIds: string[];\n\n constructor(targetTaskIds: string[]) {\n super('out_of_scope');\n this.targetTaskIds = targetTaskIds;\n }\n}\n\nexport interface TaskGroupOptions {\n summarizeChatCtx?: boolean;\n returnExceptions?: boolean;\n chatCtx?: ChatContext;\n onTaskCompleted?: (event: TaskCompletedEvent) => Promise<void>;\n}\n\nexport class TaskGroup extends AgentTask<TaskGroupResult> {\n private _summarizeChatCtx: boolean;\n private _returnExceptions: boolean;\n private _visitedTasks = new Set<string>();\n private _registeredFactories = new Map<string, FactoryInfo>();\n private _taskCompletedCallback?: (event: TaskCompletedEvent) => Promise<void>;\n private _currentTask?: AgentTask;\n\n constructor(options: TaskGroupOptions = {}) {\n const { summarizeChatCtx = true, returnExceptions = false, chatCtx, onTaskCompleted } = options;\n\n super({ instructions: '*empty*', chatCtx });\n\n this._summarizeChatCtx = summarizeChatCtx;\n this._returnExceptions = returnExceptions;\n this._taskCompletedCallback = onTaskCompleted;\n }\n\n add(task: () => AgentTask, { id, description }: { id: string; description: string }): this {\n this._registeredFactories.set(id, { taskFactory: task, id, description });\n return this;\n }\n\n async onEnter(): Promise<void> {\n const taskStack = [...this._registeredFactories.keys()];\n const taskResults: Record<string, unknown> = {};\n\n while (taskStack.length > 0) {\n const taskId = taskStack.shift()!;\n const factoryInfo = this._registeredFactories.get(taskId)!;\n\n this._currentTask = factoryInfo.taskFactory();\n\n const sharedChatCtx = this._chatCtx.copy();\n await this._currentTask.updateChatCtx(sharedChatCtx);\n\n const outOfScopeTool = this.buildOutOfScopeTool(taskId);\n if (outOfScopeTool) {\n await this._currentTask.updateTools({\n ...this._currentTask.toolCtx,\n out_of_scope: outOfScopeTool,\n });\n }\n\n try {\n this._visitedTasks.add(taskId);\n const res = await this._currentTask.run();\n taskResults[taskId] = res;\n\n if (this._taskCompletedCallback) {\n await this._taskCompletedCallback({\n agentTask: this._currentTask,\n taskId,\n result: res,\n });\n }\n } catch (e) {\n if (e instanceof OutOfScopeError) {\n taskStack.unshift(taskId);\n for (let i = e.targetTaskIds.length - 1; i >= 0; i--) {\n taskStack.unshift(e.targetTaskIds[i]!);\n }\n continue;\n }\n\n if (this._returnExceptions) {\n taskResults[taskId] = e;\n continue;\n } else {\n this.complete(e instanceof Error ? e : new Error(String(e)));\n return;\n }\n }\n }\n\n try {\n if (this._summarizeChatCtx) {\n const sessionLlm = this.session.llm;\n if (!(sessionLlm instanceof LLM)) {\n throw new Error('summarizeChatCtx requires a standard LLM on the session');\n }\n\n // TODO(parity): Add excludeConfigUpdate when AgentConfigUpdate is ported\n const ctxToSummarize = this._chatCtx.copy({\n excludeInstructions: true,\n excludeHandoff: true,\n excludeEmptyMessage: true,\n excludeFunctionCall: true,\n });\n\n const summarizedChatCtx = await ctxToSummarize._summarize(sessionLlm, {\n keepLastTurns: 0,\n });\n await this.updateChatCtx(summarizedChatCtx);\n }\n } catch (e) {\n this.complete(new Error(`failed to summarize the chat_ctx: ${e}`));\n return;\n }\n\n this.complete({ taskResults });\n }\n\n private buildOutOfScopeTool(activeTaskId: string) {\n if (this._visitedTasks.size === 0) {\n return undefined;\n }\n\n const regressionTaskIds = new Set(this._visitedTasks);\n regressionTaskIds.delete(activeTaskId);\n\n if (regressionTaskIds.size === 0) {\n return undefined;\n }\n\n const taskRepr: Record<string, string> = {};\n for (const [id, info] of this._registeredFactories) {\n if (regressionTaskIds.has(id)) {\n taskRepr[id] = info.description;\n }\n }\n\n const taskIdValues = [...regressionTaskIds] as [string, ...string[]];\n\n const description =\n 'Call to regress to other tasks according to what the user requested to modify, return the corresponding task ids. ' +\n 'For example, if the user wants to change their email and there is a task with id \"email_task\" with a description of \"Collect the user\\'s email\", return the id (\"get_email_task\"). ' +\n 'If the user requests to regress to multiple tasks, such as changing their phone number and email, return both task ids in the order they were requested. ' +\n `The following are the IDs and their corresponding task description. ${JSON.stringify(taskRepr)}`;\n\n const currentTask = this._currentTask;\n const registeredFactories = this._registeredFactories;\n const visitedTasks = this._visitedTasks;\n\n return tool({\n description,\n flags: ToolFlag.IGNORE_ON_ENTER,\n parameters: z.object({\n task_ids: z.array(z.enum(taskIdValues)).describe('The IDs of the tasks requested'),\n }),\n execute: async ({ task_ids }: { task_ids: string[] }) => {\n for (const tid of task_ids) {\n if (!registeredFactories.has(tid) || !visitedTasks.has(tid)) {\n throw new ToolError(`Unable to regress, invalid task id ${tid}`);\n }\n }\n\n if (currentTask && !currentTask.done) {\n currentTask.complete(new OutOfScopeError(task_ids));\n }\n },\n });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/beta/workflows/task_group.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { z } from 'zod';\nimport type { ChatContext } from '../../llm/chat_context.js';\nimport { LLM, ToolError, ToolFlag, tool } from '../../llm/index.js';\nimport { AgentTask } from '../../voice/agent.js';\n\ninterface FactoryInfo {\n taskFactory: () => AgentTask;\n id: string;\n description: string;\n}\n\nexport interface TaskGroupResult {\n taskResults: Record<string, unknown>;\n}\n\nexport interface TaskCompletedEvent {\n agentTask: AgentTask;\n taskId: string;\n result: unknown;\n}\n\nclass OutOfScopeError extends ToolError {\n readonly targetTaskIds: string[];\n\n constructor(targetTaskIds: string[]) {\n super('out_of_scope');\n this.targetTaskIds = targetTaskIds;\n }\n}\n\nexport interface TaskGroupOptions {\n summarizeChatCtx?: boolean;\n returnExceptions?: boolean;\n chatCtx?: ChatContext;\n onTaskCompleted?: (event: TaskCompletedEvent) => Promise<void>;\n}\n\nexport class TaskGroup extends AgentTask<TaskGroupResult> {\n private _summarizeChatCtx: boolean;\n private _returnExceptions: boolean;\n private _visitedTasks = new Set<string>();\n private _registeredFactories = new Map<string, FactoryInfo>();\n private _taskCompletedCallback?: (event: TaskCompletedEvent) => Promise<void>;\n private _currentTask?: AgentTask;\n\n constructor(options: TaskGroupOptions = {}) {\n const { summarizeChatCtx = true, returnExceptions = false, chatCtx, onTaskCompleted } = options;\n\n super({ instructions: '*empty*', chatCtx });\n\n this._summarizeChatCtx = summarizeChatCtx;\n this._returnExceptions = returnExceptions;\n this._taskCompletedCallback = onTaskCompleted;\n }\n\n add(task: () => AgentTask, { id, description }: { id: string; description: string }): this {\n this._registeredFactories.set(id, { taskFactory: task, id, description });\n return this;\n }\n\n async onEnter(): Promise<void> {\n const taskStack = [...this._registeredFactories.keys()];\n const taskResults: Record<string, unknown> = {};\n\n while (taskStack.length > 0) {\n const taskId = taskStack.shift()!;\n const factoryInfo = this._registeredFactories.get(taskId)!;\n\n this._currentTask = factoryInfo.taskFactory();\n\n const sharedChatCtx = this._chatCtx.copy();\n await this._currentTask.updateChatCtx(sharedChatCtx);\n\n const outOfScopeTool = this.buildOutOfScopeTool(taskId);\n if (outOfScopeTool) {\n await this._currentTask.updateTools({\n ...this._currentTask.toolCtx,\n out_of_scope: outOfScopeTool,\n });\n }\n\n try {\n this._visitedTasks.add(taskId);\n const res = await this._currentTask.run();\n\n // AgentTask handoff merges omit function calls. Re-merge the completed\n // task context so task-group summarization can incorporate tool results.\n this._chatCtx.merge(this._currentTask.chatCtx.copy(), {\n excludeInstructions: true,\n });\n\n taskResults[taskId] = res;\n\n if (this._taskCompletedCallback) {\n await this._taskCompletedCallback({\n agentTask: this._currentTask,\n taskId,\n result: res,\n });\n }\n } catch (e) {\n if (e instanceof OutOfScopeError) {\n taskStack.unshift(taskId);\n for (let i = e.targetTaskIds.length - 1; i >= 0; i--) {\n taskStack.unshift(e.targetTaskIds[i]!);\n }\n continue;\n }\n\n if (this._returnExceptions) {\n taskResults[taskId] = e;\n continue;\n } else {\n this.complete(e instanceof Error ? e : new Error(String(e)));\n return;\n }\n }\n }\n\n try {\n if (this._summarizeChatCtx) {\n const sessionLlm = this.session.llm;\n if (!(sessionLlm instanceof LLM)) {\n throw new Error('summarizeChatCtx requires a standard LLM on the session');\n }\n\n // Keep the full item stream so summarization can distill tool results\n // into the history summary instead of dropping them up front.\n const ctxToSummarize = this._chatCtx.copy({\n excludeInstructions: false,\n excludeFunctionCall: false,\n excludeEmptyMessage: false,\n excludeHandoff: false,\n });\n\n const summarizedChatCtx = await ctxToSummarize._summarize(sessionLlm, {\n keepLastTurns: 0,\n });\n\n await this.updateChatCtx(summarizedChatCtx);\n }\n } catch (e) {\n this.complete(new Error(`failed to summarize the chat_ctx: ${e}`));\n return;\n }\n\n this.complete({ taskResults });\n }\n\n private buildOutOfScopeTool(activeTaskId: string) {\n if (this._visitedTasks.size === 0) {\n return undefined;\n }\n\n const regressionTaskIds = new Set(this._visitedTasks);\n regressionTaskIds.delete(activeTaskId);\n\n if (regressionTaskIds.size === 0) {\n return undefined;\n }\n\n const taskRepr: Record<string, string> = {};\n for (const [id, info] of this._registeredFactories) {\n if (regressionTaskIds.has(id)) {\n taskRepr[id] = info.description;\n }\n }\n\n const taskIdValues = [...regressionTaskIds] as [string, ...string[]];\n\n const description =\n 'Call to regress to other tasks according to what the user requested to modify, return the corresponding task ids. ' +\n 'For example, if the user wants to change their email and there is a task with id \"email_task\" with a description of \"Collect the user\\'s email\", return the id (\"get_email_task\"). ' +\n 'If the user requests to regress to multiple tasks, such as changing their phone number and email, return both task ids in the order they were requested. ' +\n `The following are the IDs and their corresponding task description. ${JSON.stringify(taskRepr)}`;\n\n const currentTask = this._currentTask;\n const registeredFactories = this._registeredFactories;\n const visitedTasks = this._visitedTasks;\n\n return tool({\n description,\n flags: ToolFlag.IGNORE_ON_ENTER,\n parameters: z.object({\n task_ids: z.array(z.enum(taskIdValues)).describe('The IDs of the tasks requested'),\n }),\n execute: async ({ task_ids }: { task_ids: string[] }) => {\n for (const tid of task_ids) {\n if (!registeredFactories.has(tid) || !visitedTasks.has(tid)) {\n throw new ToolError(`Unable to regress, invalid task id ${tid}`);\n }\n }\n\n if (currentTask && !currentTask.done) {\n currentTask.complete(new OutOfScopeError(task_ids));\n }\n },\n });\n }\n}\n"],"mappings":"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","names":[]}