@livekit/agents 1.0.24 → 1.0.27

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 (184) hide show
  1. package/dist/inference/llm.cjs +1 -2
  2. package/dist/inference/llm.cjs.map +1 -1
  3. package/dist/inference/llm.d.ts.map +1 -1
  4. package/dist/inference/llm.js +1 -2
  5. package/dist/inference/llm.js.map +1 -1
  6. package/dist/inference/stt.cjs +1 -1
  7. package/dist/inference/stt.cjs.map +1 -1
  8. package/dist/inference/stt.d.ts.map +1 -1
  9. package/dist/inference/stt.js +1 -1
  10. package/dist/inference/stt.js.map +1 -1
  11. package/dist/inference/tts.cjs +4 -4
  12. package/dist/inference/tts.cjs.map +1 -1
  13. package/dist/inference/tts.d.cts +0 -1
  14. package/dist/inference/tts.d.ts +0 -1
  15. package/dist/inference/tts.d.ts.map +1 -1
  16. package/dist/inference/tts.js +4 -4
  17. package/dist/inference/tts.js.map +1 -1
  18. package/dist/ipc/job_proc_lazy_main.cjs +1 -1
  19. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
  20. package/dist/ipc/job_proc_lazy_main.js +1 -1
  21. package/dist/ipc/job_proc_lazy_main.js.map +1 -1
  22. package/dist/job.cjs +29 -2
  23. package/dist/job.cjs.map +1 -1
  24. package/dist/job.d.cts +6 -0
  25. package/dist/job.d.ts +6 -0
  26. package/dist/job.d.ts.map +1 -1
  27. package/dist/job.js +19 -2
  28. package/dist/job.js.map +1 -1
  29. package/dist/llm/llm.cjs +2 -1
  30. package/dist/llm/llm.cjs.map +1 -1
  31. package/dist/llm/llm.d.cts +1 -1
  32. package/dist/llm/llm.d.ts +1 -1
  33. package/dist/llm/llm.d.ts.map +1 -1
  34. package/dist/llm/llm.js +2 -1
  35. package/dist/llm/llm.js.map +1 -1
  36. package/dist/stream/deferred_stream.cjs +12 -4
  37. package/dist/stream/deferred_stream.cjs.map +1 -1
  38. package/dist/stream/deferred_stream.d.cts +6 -1
  39. package/dist/stream/deferred_stream.d.ts +6 -1
  40. package/dist/stream/deferred_stream.d.ts.map +1 -1
  41. package/dist/stream/deferred_stream.js +12 -4
  42. package/dist/stream/deferred_stream.js.map +1 -1
  43. package/dist/stream/deferred_stream.test.cjs +2 -2
  44. package/dist/stream/deferred_stream.test.cjs.map +1 -1
  45. package/dist/stream/deferred_stream.test.js +2 -2
  46. package/dist/stream/deferred_stream.test.js.map +1 -1
  47. package/dist/stt/stream_adapter.cjs +15 -8
  48. package/dist/stt/stream_adapter.cjs.map +1 -1
  49. package/dist/stt/stream_adapter.d.cts +7 -3
  50. package/dist/stt/stream_adapter.d.ts +7 -3
  51. package/dist/stt/stream_adapter.d.ts.map +1 -1
  52. package/dist/stt/stream_adapter.js +15 -8
  53. package/dist/stt/stream_adapter.js.map +1 -1
  54. package/dist/stt/stt.cjs +8 -3
  55. package/dist/stt/stt.cjs.map +1 -1
  56. package/dist/stt/stt.d.cts +9 -3
  57. package/dist/stt/stt.d.ts +9 -3
  58. package/dist/stt/stt.d.ts.map +1 -1
  59. package/dist/stt/stt.js +9 -4
  60. package/dist/stt/stt.js.map +1 -1
  61. package/dist/telemetry/traces.cjs +23 -2
  62. package/dist/telemetry/traces.cjs.map +1 -1
  63. package/dist/telemetry/traces.d.ts.map +1 -1
  64. package/dist/telemetry/traces.js +23 -2
  65. package/dist/telemetry/traces.js.map +1 -1
  66. package/dist/tts/stream_adapter.cjs +10 -7
  67. package/dist/tts/stream_adapter.cjs.map +1 -1
  68. package/dist/tts/stream_adapter.d.cts +6 -3
  69. package/dist/tts/stream_adapter.d.ts +6 -3
  70. package/dist/tts/stream_adapter.d.ts.map +1 -1
  71. package/dist/tts/stream_adapter.js +10 -7
  72. package/dist/tts/stream_adapter.js.map +1 -1
  73. package/dist/tts/tts.cjs +27 -16
  74. package/dist/tts/tts.cjs.map +1 -1
  75. package/dist/tts/tts.d.cts +12 -5
  76. package/dist/tts/tts.d.ts +12 -5
  77. package/dist/tts/tts.d.ts.map +1 -1
  78. package/dist/tts/tts.js +28 -17
  79. package/dist/tts/tts.js.map +1 -1
  80. package/dist/types.cjs +21 -32
  81. package/dist/types.cjs.map +1 -1
  82. package/dist/types.d.cts +41 -10
  83. package/dist/types.d.ts +41 -10
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/types.js +18 -30
  86. package/dist/types.js.map +1 -1
  87. package/dist/voice/agent.cjs +54 -19
  88. package/dist/voice/agent.cjs.map +1 -1
  89. package/dist/voice/agent.d.ts.map +1 -1
  90. package/dist/voice/agent.js +54 -19
  91. package/dist/voice/agent.js.map +1 -1
  92. package/dist/voice/agent_activity.cjs +0 -3
  93. package/dist/voice/agent_activity.cjs.map +1 -1
  94. package/dist/voice/agent_activity.d.ts.map +1 -1
  95. package/dist/voice/agent_activity.js +0 -3
  96. package/dist/voice/agent_activity.js.map +1 -1
  97. package/dist/voice/agent_session.cjs +107 -27
  98. package/dist/voice/agent_session.cjs.map +1 -1
  99. package/dist/voice/agent_session.d.cts +16 -2
  100. package/dist/voice/agent_session.d.ts +16 -2
  101. package/dist/voice/agent_session.d.ts.map +1 -1
  102. package/dist/voice/agent_session.js +110 -27
  103. package/dist/voice/agent_session.js.map +1 -1
  104. package/dist/voice/events.cjs.map +1 -1
  105. package/dist/voice/events.d.cts +4 -4
  106. package/dist/voice/events.d.ts +4 -4
  107. package/dist/voice/events.d.ts.map +1 -1
  108. package/dist/voice/events.js.map +1 -1
  109. package/dist/voice/generation.cjs +6 -7
  110. package/dist/voice/generation.cjs.map +1 -1
  111. package/dist/voice/generation.d.ts.map +1 -1
  112. package/dist/voice/generation.js +7 -8
  113. package/dist/voice/generation.js.map +1 -1
  114. package/dist/voice/io.cjs +16 -0
  115. package/dist/voice/io.cjs.map +1 -1
  116. package/dist/voice/io.d.cts +8 -0
  117. package/dist/voice/io.d.ts +8 -0
  118. package/dist/voice/io.d.ts.map +1 -1
  119. package/dist/voice/io.js +16 -0
  120. package/dist/voice/io.js.map +1 -1
  121. package/dist/voice/recorder_io/index.cjs +23 -0
  122. package/dist/voice/recorder_io/index.cjs.map +1 -0
  123. package/dist/voice/recorder_io/index.d.cts +2 -0
  124. package/dist/voice/recorder_io/index.d.ts +2 -0
  125. package/dist/voice/recorder_io/index.d.ts.map +1 -0
  126. package/dist/voice/recorder_io/index.js +2 -0
  127. package/dist/voice/recorder_io/index.js.map +1 -0
  128. package/dist/voice/recorder_io/recorder_io.cjs +542 -0
  129. package/dist/voice/recorder_io/recorder_io.cjs.map +1 -0
  130. package/dist/voice/recorder_io/recorder_io.d.cts +100 -0
  131. package/dist/voice/recorder_io/recorder_io.d.ts +100 -0
  132. package/dist/voice/recorder_io/recorder_io.d.ts.map +1 -0
  133. package/dist/voice/recorder_io/recorder_io.js +508 -0
  134. package/dist/voice/recorder_io/recorder_io.js.map +1 -0
  135. package/dist/voice/report.cjs +7 -2
  136. package/dist/voice/report.cjs.map +1 -1
  137. package/dist/voice/report.d.cts +11 -1
  138. package/dist/voice/report.d.ts +11 -1
  139. package/dist/voice/report.d.ts.map +1 -1
  140. package/dist/voice/report.js +7 -2
  141. package/dist/voice/report.js.map +1 -1
  142. package/dist/voice/room_io/_input.cjs +2 -1
  143. package/dist/voice/room_io/_input.cjs.map +1 -1
  144. package/dist/voice/room_io/_input.d.ts.map +1 -1
  145. package/dist/voice/room_io/_input.js +2 -1
  146. package/dist/voice/room_io/_input.js.map +1 -1
  147. package/dist/voice/room_io/_output.cjs +8 -7
  148. package/dist/voice/room_io/_output.cjs.map +1 -1
  149. package/dist/voice/room_io/_output.d.cts +2 -1
  150. package/dist/voice/room_io/_output.d.ts +2 -1
  151. package/dist/voice/room_io/_output.d.ts.map +1 -1
  152. package/dist/voice/room_io/_output.js +8 -7
  153. package/dist/voice/room_io/_output.js.map +1 -1
  154. package/dist/worker.cjs +4 -3
  155. package/dist/worker.cjs.map +1 -1
  156. package/dist/worker.js +4 -3
  157. package/dist/worker.js.map +1 -1
  158. package/package.json +1 -1
  159. package/src/inference/llm.ts +0 -1
  160. package/src/inference/stt.ts +1 -2
  161. package/src/inference/tts.ts +5 -4
  162. package/src/ipc/job_proc_lazy_main.ts +1 -1
  163. package/src/job.ts +21 -2
  164. package/src/llm/llm.ts +2 -2
  165. package/src/stream/deferred_stream.test.ts +3 -3
  166. package/src/stream/deferred_stream.ts +22 -5
  167. package/src/stt/stream_adapter.ts +18 -8
  168. package/src/stt/stt.ts +19 -6
  169. package/src/telemetry/traces.ts +25 -3
  170. package/src/tts/stream_adapter.ts +15 -7
  171. package/src/tts/tts.ts +46 -21
  172. package/src/types.ts +57 -33
  173. package/src/voice/agent.ts +59 -19
  174. package/src/voice/agent_activity.ts +0 -3
  175. package/src/voice/agent_session.ts +142 -35
  176. package/src/voice/events.ts +6 -3
  177. package/src/voice/generation.ts +10 -8
  178. package/src/voice/io.ts +19 -0
  179. package/src/voice/recorder_io/index.ts +4 -0
  180. package/src/voice/recorder_io/recorder_io.ts +690 -0
  181. package/src/voice/report.ts +20 -3
  182. package/src/voice/room_io/_input.ts +2 -1
  183. package/src/voice/room_io/_output.ts +10 -7
  184. package/src/worker.ts +1 -1
package/dist/job.d.cts CHANGED
@@ -52,6 +52,8 @@ export declare class JobContext {
52
52
  shutdownCallbacks: (() => Promise<void>)[];
53
53
  /** @internal */
54
54
  _primaryAgentSession?: AgentSession;
55
+ /** @internal */
56
+ _sessionDirectory: string;
55
57
  private connected;
56
58
  constructor(proc: JobProcess, info: RunningJobInfo, room: Room, onConnect: () => void, onShutdown: (s: string) => void, inferenceExecutor: InferenceExecutor);
57
59
  get proc(): JobProcess;
@@ -64,6 +66,10 @@ export declare class JobContext {
64
66
  get agent(): LocalParticipant | undefined;
65
67
  /** @returns The global inference executor */
66
68
  get inferenceExecutor(): InferenceExecutor;
69
+ /**
70
+ * @returns The session directory for storing recordings and session data.
71
+ */
72
+ get sessionDirectory(): string;
67
73
  /** Adds a promise to be awaited when {@link JobContext.shutdown | shutdown} is called. */
68
74
  addShutdownCallback(callback: () => Promise<void>): void;
69
75
  waitForParticipant(identity?: string): Promise<RemoteParticipant>;
package/dist/job.d.ts CHANGED
@@ -52,6 +52,8 @@ export declare class JobContext {
52
52
  shutdownCallbacks: (() => Promise<void>)[];
53
53
  /** @internal */
54
54
  _primaryAgentSession?: AgentSession;
55
+ /** @internal */
56
+ _sessionDirectory: string;
55
57
  private connected;
56
58
  constructor(proc: JobProcess, info: RunningJobInfo, room: Room, onConnect: () => void, onShutdown: (s: string) => void, inferenceExecutor: InferenceExecutor);
57
59
  get proc(): JobProcess;
@@ -64,6 +66,10 @@ export declare class JobContext {
64
66
  get agent(): LocalParticipant | undefined;
65
67
  /** @returns The global inference executor */
66
68
  get inferenceExecutor(): InferenceExecutor;
69
+ /**
70
+ * @returns The session directory for storing recordings and session data.
71
+ */
72
+ get sessionDirectory(): string;
67
73
  /** Adds a promise to be awaited when {@link JobContext.shutdown | shutdown} is called. */
68
74
  addShutdownCallback(callback: () => Promise<void>): void;
69
75
  waitForParticipant(identity?: string): Promise<RemoteParticipant>;
package/dist/job.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"job.d.ts","sourceRoot":"","sources":["../src/job.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"job.d.ts","sourceRoot":"","sources":["../src/job.ts"],"names":[],"mappings":"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"}
package/dist/job.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { ParticipantKind, RoomEvent, TrackKind } from "@livekit/rtc-node";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
3
5
  import { log } from "./log.js";
4
6
  import { flushOtelLogs, setupCloudTracer, uploadSessionReport } from "./telemetry/index.js";
5
7
  import { isCloud } from "./utils.js";
@@ -45,6 +47,8 @@ class JobContext {
45
47
  #inferenceExecutor;
46
48
  /** @internal */
47
49
  _primaryAgentSession;
50
+ /** @internal */
51
+ _sessionDirectory;
48
52
  connected = false;
49
53
  constructor(proc, info, room, onConnect, onShutdown, inferenceExecutor) {
50
54
  this.#proc = proc;
@@ -56,6 +60,7 @@ class JobContext {
56
60
  this.#room.on(RoomEvent.ParticipantConnected, this.onParticipantConnected);
57
61
  this.#logger = log().child({ info: this.#info });
58
62
  this.#inferenceExecutor = inferenceExecutor;
63
+ this._sessionDirectory = path.join(os.tmpdir(), "livekit-agents", `job-${this.#info.job.id}`);
59
64
  }
60
65
  get proc() {
61
66
  return this.#proc;
@@ -81,6 +86,12 @@ class JobContext {
81
86
  get inferenceExecutor() {
82
87
  return this.#inferenceExecutor;
83
88
  }
89
+ /**
90
+ * @returns The session directory for storing recordings and session data.
91
+ */
92
+ get sessionDirectory() {
93
+ return this._sessionDirectory;
94
+ }
84
95
  /** Adds a promise to be awaited when {@link JobContext.shutdown | shutdown} is called. */
85
96
  addShutdownCallback(callback) {
86
97
  this.shutdownCallbacks.push(callback);
@@ -153,15 +164,21 @@ class JobContext {
153
164
  if (!targetSession) {
154
165
  throw new Error("Cannot prepare report, no AgentSession was found");
155
166
  }
167
+ const recorderIO = targetSession._recorderIO;
168
+ if (recorderIO && recorderIO.recording) {
169
+ throw new Error("Cannot create the AgentSession report, the RecorderIO is still recording");
170
+ }
156
171
  return createSessionReport({
157
172
  jobId: this.job.id,
158
173
  roomId: ((_a = this.job.room) == null ? void 0 : _a.sid) || "",
159
174
  room: ((_b = this.job.room) == null ? void 0 : _b.name) || "",
160
175
  options: targetSession.options,
161
176
  events: targetSession._recordedEvents,
162
- enableUserDataTraining: true,
177
+ enableRecording: targetSession._enableRecording,
163
178
  chatHistory: targetSession.history.copy(),
164
- startedAt: targetSession._startedAt
179
+ startedAt: targetSession._startedAt,
180
+ audioRecordingPath: recorderIO == null ? void 0 : recorderIO.outputPath,
181
+ audioRecordingStartedAt: recorderIO == null ? void 0 : recorderIO.recordingStartedAt
165
182
  });
166
183
  }
167
184
  async _onSessionEnd() {
package/dist/job.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/job.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type * as proto from '@livekit/protocol';\nimport type {\n E2EEOptions,\n LocalParticipant,\n RemoteParticipant,\n Room,\n RtcConfiguration,\n} from '@livekit/rtc-node';\nimport { ParticipantKind, RoomEvent, TrackKind } from '@livekit/rtc-node';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { Logger } from 'pino';\nimport type { InferenceExecutor } from './ipc/inference_executor.js';\nimport { log } from './log.js';\nimport { flushOtelLogs, setupCloudTracer, uploadSessionReport } from './telemetry/index.js';\nimport { isCloud } from './utils.js';\nimport type { AgentSession } from './voice/agent_session.js';\nimport { type SessionReport, createSessionReport } from './voice/report.js';\n\n// AsyncLocalStorage for job context, similar to Python's contextvars\nconst jobContextStorage = new AsyncLocalStorage<JobContext>();\n\n/**\n * Returns the current job context.\n *\n * @throws {Error} if no job context is found\n */\nexport function getJobContext(): JobContext {\n const ctx = jobContextStorage.getStore();\n if (!ctx) {\n throw new Error('no job context found, are you running this code inside a job entrypoint?');\n }\n return ctx;\n}\n\n/**\n * Runs a function within a job context, similar to Python's contextvars.\n * @internal\n */\nexport function runWithJobContext<T>(context: JobContext, fn: () => T): T {\n return jobContextStorage.run(context, fn);\n}\n\n/**\n * Runs an async function within a job context, similar to Python's contextvars.\n * @internal\n */\nexport function runWithJobContextAsync<T>(context: JobContext, fn: () => Promise<T>): Promise<T> {\n return jobContextStorage.run(context, fn);\n}\n\n/** Which tracks, if any, should the agent automatically subscribe to? */\nexport enum AutoSubscribe {\n SUBSCRIBE_ALL,\n SUBSCRIBE_NONE,\n VIDEO_ONLY,\n AUDIO_ONLY,\n}\n\nexport type JobAcceptArguments = {\n name: string;\n identity: string;\n metadata: string;\n attributes?: { [key: string]: string };\n};\n\nexport type RunningJobInfo = {\n acceptArguments: JobAcceptArguments;\n job: proto.Job;\n url: string;\n token: string;\n workerId: string;\n};\n\n/** Attempted to add a function callback, but the function already exists. */\nexport class FunctionExistsError extends Error {\n constructor(msg?: string) {\n super(msg);\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** The job and environment context as seen by the agent, accessible by the entrypoint function. */\nexport class JobContext {\n #proc: JobProcess;\n #info: RunningJobInfo;\n #room: Room;\n #onConnect: () => void;\n #onShutdown: (s: string) => void;\n /** @internal */\n shutdownCallbacks: (() => Promise<void>)[] = [];\n #participantEntrypoints: ((job: JobContext, p: RemoteParticipant) => Promise<void>)[] = [];\n #participantTasks: {\n [id: string]: {\n callback: (job: JobContext, p: RemoteParticipant) => Promise<void>;\n result: Promise<void>;\n };\n } = {};\n #logger: Logger;\n #inferenceExecutor: InferenceExecutor;\n\n /** @internal */\n _primaryAgentSession?: AgentSession;\n\n private connected: boolean = false;\n\n constructor(\n proc: JobProcess,\n info: RunningJobInfo,\n room: Room,\n onConnect: () => void,\n onShutdown: (s: string) => void,\n inferenceExecutor: InferenceExecutor,\n ) {\n this.#proc = proc;\n this.#info = info;\n this.#room = room;\n this.#onConnect = onConnect;\n this.#onShutdown = onShutdown;\n this.onParticipantConnected = this.onParticipantConnected.bind(this);\n this.#room.on(RoomEvent.ParticipantConnected, this.onParticipantConnected);\n this.#logger = log().child({ info: this.#info });\n this.#inferenceExecutor = inferenceExecutor;\n }\n\n get proc(): JobProcess {\n return this.#proc;\n }\n\n get job(): proto.Job {\n return this.#info.job;\n }\n\n get workerId(): string {\n return this.#info.workerId;\n }\n\n /** @returns The room the agent was called into */\n get room(): Room {\n return this.#room;\n }\n\n get info(): RunningJobInfo {\n return this.#info;\n }\n\n /** @returns The agent's participant if connected to the room, otherwise `undefined` */\n get agent(): LocalParticipant | undefined {\n return this.#room.localParticipant;\n }\n\n /** @returns The global inference executor */\n get inferenceExecutor(): InferenceExecutor {\n return this.#inferenceExecutor;\n }\n\n /** Adds a promise to be awaited when {@link JobContext.shutdown | shutdown} is called. */\n addShutdownCallback(callback: () => Promise<void>) {\n this.shutdownCallbacks.push(callback);\n }\n\n async waitForParticipant(identity?: string): Promise<RemoteParticipant> {\n if (!this.#room.isConnected) {\n throw new Error('room is not connected');\n }\n\n for (const p of this.#room.remoteParticipants.values()) {\n if ((!identity || p.identity === identity) && p.info.kind != ParticipantKind.AGENT) {\n return p;\n }\n }\n\n return new Promise((resolve, reject) => {\n const onParticipantConnected = (participant: RemoteParticipant) => {\n if (\n (!identity || participant.identity === identity) &&\n participant.info.kind != ParticipantKind.AGENT\n ) {\n clearHandlers();\n resolve(participant);\n }\n };\n const onDisconnected = () => {\n clearHandlers();\n reject(new Error('Room disconnected while waiting for participant'));\n };\n\n const clearHandlers = () => {\n this.#room.off(RoomEvent.ParticipantConnected, onParticipantConnected);\n this.#room.off(RoomEvent.Disconnected, onDisconnected);\n };\n\n this.#room.on(RoomEvent.ParticipantConnected, onParticipantConnected);\n this.#room.on(RoomEvent.Disconnected, onDisconnected);\n });\n }\n\n /**\n * Connects the agent to the room.\n *\n * @remarks\n * It is recommended to run this command as early in the function as possible, as executing it\n * later may cause noticeable delay between user and agent joins.\n *\n * @see {@link https://github.com/livekit/node-sdks/tree/main/packages/livekit-rtc#readme |\n * @livekit/rtc-node} for more information about the parameters.\n */\n async connect(\n e2ee?: E2EEOptions,\n autoSubscribe: AutoSubscribe = AutoSubscribe.SUBSCRIBE_ALL,\n rtcConfig?: RtcConfiguration,\n ) {\n if (this.connected) {\n return;\n }\n\n const opts = {\n e2ee,\n autoSubscribe: autoSubscribe == AutoSubscribe.SUBSCRIBE_ALL,\n rtcConfig,\n dynacast: false,\n };\n\n await this.#room.connect(this.#info.url, this.#info.token, opts);\n this.#onConnect();\n\n this.#room.remoteParticipants.forEach(this.onParticipantConnected);\n\n if ([AutoSubscribe.AUDIO_ONLY, AutoSubscribe.VIDEO_ONLY].includes(autoSubscribe)) {\n this.#room.remoteParticipants.forEach((p) => {\n p.trackPublications.forEach((pub) => {\n if (\n (autoSubscribe === AutoSubscribe.AUDIO_ONLY && pub.kind === TrackKind.KIND_AUDIO) ||\n (autoSubscribe === AutoSubscribe.VIDEO_ONLY && pub.kind === TrackKind.KIND_VIDEO)\n ) {\n pub.setSubscribed(true);\n }\n });\n });\n }\n this.connected = true;\n }\n\n makeSessionReport(session?: AgentSession): SessionReport {\n const targetSession = session || this._primaryAgentSession;\n\n if (!targetSession) {\n throw new Error('Cannot prepare report, no AgentSession was found');\n }\n\n // TODO(brian): implement and check recorder io\n\n return createSessionReport({\n jobId: this.job.id,\n roomId: this.job.room?.sid || '',\n room: this.job.room?.name || '',\n options: targetSession.options,\n events: targetSession._recordedEvents,\n enableUserDataTraining: true,\n chatHistory: targetSession.history.copy(),\n startedAt: targetSession._startedAt,\n });\n }\n\n async _onSessionEnd(): Promise<void> {\n const session = this._primaryAgentSession;\n if (!session) {\n return;\n }\n\n const report = this.makeSessionReport(session);\n\n // TODO(brian): Implement CLI/console\n\n // Upload session report to LiveKit Cloud if enabled\n const url = new URL(this.#info.url);\n\n if (report.enableRecording && isCloud(url)) {\n try {\n await uploadSessionReport({\n agentName: this.job.agentName,\n cloudHostname: url.hostname,\n report,\n });\n this.#logger.info(\n {\n jobId: report.jobId,\n roomId: report.roomId,\n },\n 'Session report uploaded to LiveKit Cloud',\n );\n } catch (error) {\n this.#logger.error({ error }, 'Failed to upload session report');\n }\n }\n\n this.#logger.debug(\n {\n jobId: report.jobId,\n roomId: report.roomId,\n eventsCount: report.events.length,\n },\n 'Session ended, report generated',\n );\n\n // Explicitly clear the recorded events to avoid leaking memory\n session._recordedEvents = [];\n\n try {\n await flushOtelLogs();\n } catch (error) {\n this.#logger.error({ error }, 'Failed to flush OTEL logs');\n }\n }\n\n /**\n * Gracefully shuts down the job, and runs all shutdown promises.\n *\n * @param reason - Optional reason for shutdown\n */\n shutdown(reason = '') {\n this.#onShutdown(reason);\n }\n\n /** @internal */\n onParticipantConnected(p: RemoteParticipant) {\n for (const callback of this.#participantEntrypoints) {\n if (this.#participantTasks[p.identity!]?.callback == callback) {\n this.#logger.warn(\n 'a participant has joined before a prior prticipant task matching the same identity has finished:',\n p.identity,\n );\n }\n const result = callback(this, p);\n result.finally(() => delete this.#participantTasks[p.identity!]);\n this.#participantTasks[p.identity!] = { callback, result };\n }\n }\n\n /**\n * Adds a promise to be awaited whenever a new participant joins the room.\n *\n * @throws {@link FunctionExistsError} if an entrypoint already exists\n */\n addParticipantEntrypoint(callback: (job: JobContext, p: RemoteParticipant) => Promise<void>) {\n if (this.#participantEntrypoints.includes(callback)) {\n throw new FunctionExistsError('entrypoints cannot be added more than once');\n }\n\n this.#participantEntrypoints.push(callback);\n }\n\n async initRecording() {\n const url = new URL(this.#info.url);\n if (!isCloud(url)) {\n return;\n }\n\n this.#logger.debug({ hostname: url.hostname }, 'Configuring session recording (cloud tracer)');\n await setupCloudTracer({\n roomId: this.job.room!.sid,\n jobId: this.job.id,\n cloudHostname: url.hostname,\n });\n }\n}\n\nexport class JobProcess {\n #pid = process.pid;\n userData: { [id: string]: unknown } = {};\n\n get pid(): number {\n return this.#pid;\n }\n}\n\n/**\n * A request sent by the server to spawn a new agent job.\n *\n * @remarks\n * For most applications, this is best left to the default, which simply accepts the job and\n * handles the logic inside the entrypoint function. This class is useful for vetting which\n * requests should fill idle processes and which should be outright rejected.\n */\nexport class JobRequest {\n #job: proto.Job;\n #onReject: () => Promise<void>;\n #onAccept: (args: JobAcceptArguments) => Promise<void>;\n\n /** @internal */\n constructor(\n job: proto.Job,\n onReject: () => Promise<void>,\n onAccept: (args: JobAcceptArguments) => Promise<void>,\n ) {\n this.#job = job;\n this.#onReject = onReject;\n this.#onAccept = onAccept;\n }\n\n /** @returns The ID of the job, set by the LiveKit server */\n get id(): string {\n return this.#job.id;\n }\n\n /** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */\n get job(): proto.Job {\n return this.#job;\n }\n\n /** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */\n get room(): proto.Room | undefined {\n return this.#job.room;\n }\n\n /** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */\n get publisher(): proto.ParticipantInfo | undefined {\n return this.#job.participant;\n }\n\n /** @returns The agent's name, as set in {@link WorkerOptions} */\n get agentName(): string {\n return this.#job.agentName;\n }\n\n /** Rejects the job. */\n async reject() {\n await this.#onReject();\n }\n\n /** Accepts the job, launching it on an idle child process. */\n async accept(name = '', identity = '', metadata = '', attributes?: { [key: string]: string }) {\n if (identity === '') identity = 'agent-' + this.id;\n\n this.#onAccept({ name, identity, metadata, attributes });\n }\n}\n"],"mappings":"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","names":["AutoSubscribe"]}
1
+ {"version":3,"sources":["../src/job.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type * as proto from '@livekit/protocol';\nimport type {\n E2EEOptions,\n LocalParticipant,\n RemoteParticipant,\n Room,\n RtcConfiguration,\n} from '@livekit/rtc-node';\nimport { ParticipantKind, RoomEvent, TrackKind } from '@livekit/rtc-node';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport type { Logger } from 'pino';\nimport type { InferenceExecutor } from './ipc/inference_executor.js';\nimport { log } from './log.js';\nimport { flushOtelLogs, setupCloudTracer, uploadSessionReport } from './telemetry/index.js';\nimport { isCloud } from './utils.js';\nimport type { AgentSession } from './voice/agent_session.js';\nimport { type SessionReport, createSessionReport } from './voice/report.js';\n\n// AsyncLocalStorage for job context, similar to Python's contextvars\nconst jobContextStorage = new AsyncLocalStorage<JobContext>();\n\n/**\n * Returns the current job context.\n *\n * @throws {Error} if no job context is found\n */\nexport function getJobContext(): JobContext {\n const ctx = jobContextStorage.getStore();\n if (!ctx) {\n throw new Error('no job context found, are you running this code inside a job entrypoint?');\n }\n return ctx;\n}\n\n/**\n * Runs a function within a job context, similar to Python's contextvars.\n * @internal\n */\nexport function runWithJobContext<T>(context: JobContext, fn: () => T): T {\n return jobContextStorage.run(context, fn);\n}\n\n/**\n * Runs an async function within a job context, similar to Python's contextvars.\n * @internal\n */\nexport function runWithJobContextAsync<T>(context: JobContext, fn: () => Promise<T>): Promise<T> {\n return jobContextStorage.run(context, fn);\n}\n\n/** Which tracks, if any, should the agent automatically subscribe to? */\nexport enum AutoSubscribe {\n SUBSCRIBE_ALL,\n SUBSCRIBE_NONE,\n VIDEO_ONLY,\n AUDIO_ONLY,\n}\n\nexport type JobAcceptArguments = {\n name: string;\n identity: string;\n metadata: string;\n attributes?: { [key: string]: string };\n};\n\nexport type RunningJobInfo = {\n acceptArguments: JobAcceptArguments;\n job: proto.Job;\n url: string;\n token: string;\n workerId: string;\n};\n\n/** Attempted to add a function callback, but the function already exists. */\nexport class FunctionExistsError extends Error {\n constructor(msg?: string) {\n super(msg);\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** The job and environment context as seen by the agent, accessible by the entrypoint function. */\nexport class JobContext {\n #proc: JobProcess;\n #info: RunningJobInfo;\n #room: Room;\n #onConnect: () => void;\n #onShutdown: (s: string) => void;\n /** @internal */\n shutdownCallbacks: (() => Promise<void>)[] = [];\n #participantEntrypoints: ((job: JobContext, p: RemoteParticipant) => Promise<void>)[] = [];\n #participantTasks: {\n [id: string]: {\n callback: (job: JobContext, p: RemoteParticipant) => Promise<void>;\n result: Promise<void>;\n };\n } = {};\n #logger: Logger;\n #inferenceExecutor: InferenceExecutor;\n\n /** @internal */\n _primaryAgentSession?: AgentSession;\n\n /** @internal */\n _sessionDirectory: string;\n\n private connected: boolean = false;\n\n constructor(\n proc: JobProcess,\n info: RunningJobInfo,\n room: Room,\n onConnect: () => void,\n onShutdown: (s: string) => void,\n inferenceExecutor: InferenceExecutor,\n ) {\n this.#proc = proc;\n this.#info = info;\n this.#room = room;\n this.#onConnect = onConnect;\n this.#onShutdown = onShutdown;\n this.onParticipantConnected = this.onParticipantConnected.bind(this);\n this.#room.on(RoomEvent.ParticipantConnected, this.onParticipantConnected);\n this.#logger = log().child({ info: this.#info });\n this.#inferenceExecutor = inferenceExecutor;\n this._sessionDirectory = path.join(os.tmpdir(), 'livekit-agents', `job-${this.#info.job.id}`);\n }\n\n get proc(): JobProcess {\n return this.#proc;\n }\n\n get job(): proto.Job {\n return this.#info.job;\n }\n\n get workerId(): string {\n return this.#info.workerId;\n }\n\n /** @returns The room the agent was called into */\n get room(): Room {\n return this.#room;\n }\n\n get info(): RunningJobInfo {\n return this.#info;\n }\n\n /** @returns The agent's participant if connected to the room, otherwise `undefined` */\n get agent(): LocalParticipant | undefined {\n return this.#room.localParticipant;\n }\n\n /** @returns The global inference executor */\n get inferenceExecutor(): InferenceExecutor {\n return this.#inferenceExecutor;\n }\n\n /**\n * @returns The session directory for storing recordings and session data.\n */\n get sessionDirectory(): string {\n return this._sessionDirectory;\n }\n\n /** Adds a promise to be awaited when {@link JobContext.shutdown | shutdown} is called. */\n addShutdownCallback(callback: () => Promise<void>) {\n this.shutdownCallbacks.push(callback);\n }\n\n async waitForParticipant(identity?: string): Promise<RemoteParticipant> {\n if (!this.#room.isConnected) {\n throw new Error('room is not connected');\n }\n\n for (const p of this.#room.remoteParticipants.values()) {\n if ((!identity || p.identity === identity) && p.info.kind != ParticipantKind.AGENT) {\n return p;\n }\n }\n\n return new Promise((resolve, reject) => {\n const onParticipantConnected = (participant: RemoteParticipant) => {\n if (\n (!identity || participant.identity === identity) &&\n participant.info.kind != ParticipantKind.AGENT\n ) {\n clearHandlers();\n resolve(participant);\n }\n };\n const onDisconnected = () => {\n clearHandlers();\n reject(new Error('Room disconnected while waiting for participant'));\n };\n\n const clearHandlers = () => {\n this.#room.off(RoomEvent.ParticipantConnected, onParticipantConnected);\n this.#room.off(RoomEvent.Disconnected, onDisconnected);\n };\n\n this.#room.on(RoomEvent.ParticipantConnected, onParticipantConnected);\n this.#room.on(RoomEvent.Disconnected, onDisconnected);\n });\n }\n\n /**\n * Connects the agent to the room.\n *\n * @remarks\n * It is recommended to run this command as early in the function as possible, as executing it\n * later may cause noticeable delay between user and agent joins.\n *\n * @see {@link https://github.com/livekit/node-sdks/tree/main/packages/livekit-rtc#readme |\n * @livekit/rtc-node} for more information about the parameters.\n */\n async connect(\n e2ee?: E2EEOptions,\n autoSubscribe: AutoSubscribe = AutoSubscribe.SUBSCRIBE_ALL,\n rtcConfig?: RtcConfiguration,\n ) {\n if (this.connected) {\n return;\n }\n\n const opts = {\n e2ee,\n autoSubscribe: autoSubscribe == AutoSubscribe.SUBSCRIBE_ALL,\n rtcConfig,\n dynacast: false,\n };\n\n await this.#room.connect(this.#info.url, this.#info.token, opts);\n this.#onConnect();\n\n this.#room.remoteParticipants.forEach(this.onParticipantConnected);\n\n if ([AutoSubscribe.AUDIO_ONLY, AutoSubscribe.VIDEO_ONLY].includes(autoSubscribe)) {\n this.#room.remoteParticipants.forEach((p) => {\n p.trackPublications.forEach((pub) => {\n if (\n (autoSubscribe === AutoSubscribe.AUDIO_ONLY && pub.kind === TrackKind.KIND_AUDIO) ||\n (autoSubscribe === AutoSubscribe.VIDEO_ONLY && pub.kind === TrackKind.KIND_VIDEO)\n ) {\n pub.setSubscribed(true);\n }\n });\n });\n }\n this.connected = true;\n }\n\n makeSessionReport(session?: AgentSession): SessionReport {\n const targetSession = session || this._primaryAgentSession;\n\n if (!targetSession) {\n throw new Error('Cannot prepare report, no AgentSession was found');\n }\n\n const recorderIO = targetSession._recorderIO;\n\n if (recorderIO && recorderIO.recording) {\n throw new Error('Cannot create the AgentSession report, the RecorderIO is still recording');\n }\n\n return createSessionReport({\n jobId: this.job.id,\n roomId: this.job.room?.sid || '',\n room: this.job.room?.name || '',\n options: targetSession.options,\n events: targetSession._recordedEvents,\n enableRecording: targetSession._enableRecording,\n chatHistory: targetSession.history.copy(),\n startedAt: targetSession._startedAt,\n audioRecordingPath: recorderIO?.outputPath,\n audioRecordingStartedAt: recorderIO?.recordingStartedAt,\n });\n }\n\n async _onSessionEnd(): Promise<void> {\n const session = this._primaryAgentSession;\n if (!session) {\n return;\n }\n\n const report = this.makeSessionReport(session);\n\n // TODO(brian): Implement CLI/console\n\n // Upload session report to LiveKit Cloud if enabled\n const url = new URL(this.#info.url);\n\n if (report.enableRecording && isCloud(url)) {\n try {\n await uploadSessionReport({\n agentName: this.job.agentName,\n cloudHostname: url.hostname,\n report,\n });\n this.#logger.info(\n {\n jobId: report.jobId,\n roomId: report.roomId,\n },\n 'Session report uploaded to LiveKit Cloud',\n );\n } catch (error) {\n this.#logger.error({ error }, 'Failed to upload session report');\n }\n }\n\n this.#logger.debug(\n {\n jobId: report.jobId,\n roomId: report.roomId,\n eventsCount: report.events.length,\n },\n 'Session ended, report generated',\n );\n\n // Explicitly clear the recorded events to avoid leaking memory\n session._recordedEvents = [];\n\n try {\n await flushOtelLogs();\n } catch (error) {\n this.#logger.error({ error }, 'Failed to flush OTEL logs');\n }\n }\n\n /**\n * Gracefully shuts down the job, and runs all shutdown promises.\n *\n * @param reason - Optional reason for shutdown\n */\n shutdown(reason = '') {\n this.#onShutdown(reason);\n }\n\n /** @internal */\n onParticipantConnected(p: RemoteParticipant) {\n for (const callback of this.#participantEntrypoints) {\n if (this.#participantTasks[p.identity!]?.callback == callback) {\n this.#logger.warn(\n 'a participant has joined before a prior prticipant task matching the same identity has finished:',\n p.identity,\n );\n }\n const result = callback(this, p);\n result.finally(() => delete this.#participantTasks[p.identity!]);\n this.#participantTasks[p.identity!] = { callback, result };\n }\n }\n\n /**\n * Adds a promise to be awaited whenever a new participant joins the room.\n *\n * @throws {@link FunctionExistsError} if an entrypoint already exists\n */\n addParticipantEntrypoint(callback: (job: JobContext, p: RemoteParticipant) => Promise<void>) {\n if (this.#participantEntrypoints.includes(callback)) {\n throw new FunctionExistsError('entrypoints cannot be added more than once');\n }\n\n this.#participantEntrypoints.push(callback);\n }\n\n async initRecording() {\n const url = new URL(this.#info.url);\n if (!isCloud(url)) {\n return;\n }\n\n this.#logger.debug({ hostname: url.hostname }, 'Configuring session recording (cloud tracer)');\n await setupCloudTracer({\n roomId: this.job.room!.sid,\n jobId: this.job.id,\n cloudHostname: url.hostname,\n });\n }\n}\n\nexport class JobProcess {\n #pid = process.pid;\n userData: { [id: string]: unknown } = {};\n\n get pid(): number {\n return this.#pid;\n }\n}\n\n/**\n * A request sent by the server to spawn a new agent job.\n *\n * @remarks\n * For most applications, this is best left to the default, which simply accepts the job and\n * handles the logic inside the entrypoint function. This class is useful for vetting which\n * requests should fill idle processes and which should be outright rejected.\n */\nexport class JobRequest {\n #job: proto.Job;\n #onReject: () => Promise<void>;\n #onAccept: (args: JobAcceptArguments) => Promise<void>;\n\n /** @internal */\n constructor(\n job: proto.Job,\n onReject: () => Promise<void>,\n onAccept: (args: JobAcceptArguments) => Promise<void>,\n ) {\n this.#job = job;\n this.#onReject = onReject;\n this.#onAccept = onAccept;\n }\n\n /** @returns The ID of the job, set by the LiveKit server */\n get id(): string {\n return this.#job.id;\n }\n\n /** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */\n get job(): proto.Job {\n return this.#job;\n }\n\n /** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */\n get room(): proto.Room | undefined {\n return this.#job.room;\n }\n\n /** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */\n get publisher(): proto.ParticipantInfo | undefined {\n return this.#job.participant;\n }\n\n /** @returns The agent's name, as set in {@link WorkerOptions} */\n get agentName(): string {\n return this.#job.agentName;\n }\n\n /** Rejects the job. */\n async reject() {\n await this.#onReject();\n }\n\n /** Accepts the job, launching it on an idle child process. */\n async accept(name = '', identity = '', metadata = '', attributes?: { [key: string]: string }) {\n if (identity === '') identity = 'agent-' + this.id;\n\n this.#onAccept({ name, identity, metadata, attributes });\n }\n}\n"],"mappings":"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","names":["AutoSubscribe"]}
package/dist/llm/llm.cjs CHANGED
@@ -26,6 +26,7 @@ var import_node_events = require("node:events");
26
26
  var import_exceptions = require("../_exceptions.cjs");
27
27
  var import_log = require("../log.cjs");
28
28
  var import_telemetry = require("../telemetry/index.cjs");
29
+ var import_types = require("../types.cjs");
29
30
  var import_utils = require("../utils.cjs");
30
31
  var import_chat_context = require("./chat_context.cjs");
31
32
  class LLM extends import_node_events.EventEmitter {
@@ -97,7 +98,7 @@ class LLMStream {
97
98
  );
98
99
  } catch (error) {
99
100
  if (error instanceof import_exceptions.APIError) {
100
- const retryInterval = this._connOptions._intervalForRetry(i);
101
+ const retryInterval = (0, import_types.intervalForRetry)(this._connOptions, i);
101
102
  if (this._connOptions.maxRetry === 0 || !error.retryable) {
102
103
  this.emitError({ error, recoverable: false });
103
104
  throw error;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/llm/llm.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport type { Span } from '@opentelemetry/api';\nimport { EventEmitter } from 'node:events';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { log } from '../log.js';\nimport type { LLMMetrics } from '../metrics/base.js';\nimport { recordException, traceTypes, tracer } from '../telemetry/index.js';\nimport type { APIConnectOptions } from '../types.js';\nimport { AsyncIterableQueue, delay, startSoon, toError } from '../utils.js';\nimport { type ChatContext, type ChatRole, type FunctionCall } from './chat_context.js';\nimport type { ToolChoice, ToolContext } from './tool_context.js';\n\nexport interface ChoiceDelta {\n role: ChatRole;\n content?: string;\n toolCalls?: FunctionCall[];\n}\n\nexport interface CompletionUsage {\n completionTokens: number;\n promptTokens: number;\n promptCachedTokens: number;\n totalTokens: number;\n}\n\nexport interface ChatChunk {\n id: string;\n delta?: ChoiceDelta;\n usage?: CompletionUsage;\n}\n\nexport interface LLMError {\n type: 'llm_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type LLMCallbacks = {\n ['metrics_collected']: (metrics: LLMMetrics) => void;\n ['error']: (error: LLMError) => void;\n};\n\nexport abstract class LLM extends (EventEmitter as new () => TypedEmitter<LLMCallbacks>) {\n constructor() {\n super();\n }\n\n abstract label(): string;\n\n /**\n * Get the model name/identifier for this LLM instance.\n *\n * @returns The model name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their model information.\n */\n get model(): string {\n return 'unknown';\n }\n\n /**\n * Returns a {@link LLMStream} that can be used to push text and receive LLM responses.\n */\n abstract chat({\n chatCtx,\n toolCtx,\n connOptions,\n parallelToolCalls,\n toolChoice,\n extraKwargs,\n }: {\n chatCtx: ChatContext;\n toolCtx?: ToolContext;\n connOptions?: APIConnectOptions;\n parallelToolCalls?: boolean;\n toolChoice?: ToolChoice;\n extraKwargs?: Record<string, unknown>;\n }): LLMStream;\n\n /**\n * Pre-warm connection to the LLM service\n */\n prewarm(): void {\n // Default implementation - subclasses can override\n }\n\n async aclose(): Promise<void> {\n // Default implementation - subclasses can override\n }\n}\n\nexport abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {\n protected output = new AsyncIterableQueue<ChatChunk>();\n protected queue = new AsyncIterableQueue<ChatChunk>();\n protected closed = false;\n protected abortController = new AbortController();\n protected _connOptions: APIConnectOptions;\n protected logger = log();\n\n #llm: LLM;\n #chatCtx: ChatContext;\n #toolCtx?: ToolContext;\n #llmRequestSpan?: Span;\n\n constructor(\n llm: LLM,\n {\n chatCtx,\n toolCtx,\n connOptions,\n }: {\n chatCtx: ChatContext;\n toolCtx?: ToolContext;\n connOptions: APIConnectOptions;\n },\n ) {\n this.#llm = llm;\n this.#chatCtx = chatCtx;\n this.#toolCtx = toolCtx;\n this._connOptions = connOptions;\n this.monitorMetrics();\n this.abortController.signal.addEventListener('abort', () => {\n // TODO (AJS-37) clean this up when we refactor with streams\n this.output.close();\n this.closed = true;\n });\n\n // this is a hack to immitate asyncio.create_task so that mainTask\n // is run **after** the constructor has finished. Otherwise we get\n // runtime error when trying to access class variables in the\n // `run` method.\n startSoon(() => this.mainTask().then(() => this.queue.close()));\n }\n\n private _mainTaskImpl = async (span: Span) => {\n this.#llmRequestSpan = span;\n span.setAttribute(traceTypes.ATTR_GEN_AI_REQUEST_MODEL, this.#llm.model);\n\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'llm_request_run' },\n );\n } catch (error) {\n if (error instanceof APIError) {\n const retryInterval = this._connOptions._intervalForRetry(i);\n\n if (this._connOptions.maxRetry === 0 || !error.retryable) {\n this.emitError({ error, recoverable: false });\n throw error;\n } else if (i === this._connOptions.maxRetry) {\n this.emitError({ error, recoverable: false });\n throw new APIConnectionError({\n message: `failed to generate LLM completion after ${this._connOptions.maxRetry + 1} attempts`,\n options: { retryable: false },\n });\n } else {\n this.emitError({ error, recoverable: true });\n this.logger.warn(\n { llm: this.#llm.label(), attempt: i + 1, error },\n `failed to generate LLM completion, retrying in ${retryInterval}s`,\n );\n }\n\n if (retryInterval > 0) {\n await delay(retryInterval);\n }\n } else {\n this.emitError({ error: toError(error), recoverable: false });\n throw error;\n }\n }\n }\n };\n\n private mainTask = async () =>\n tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'llm_request',\n endOnExit: false,\n });\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#llm.emit('error', {\n type: 'llm_error',\n timestamp: Date.now(),\n label: this.#llm.label(),\n error,\n recoverable,\n });\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let ttft: bigint = BigInt(-1);\n let requestId = '';\n let usage: CompletionUsage | undefined;\n let completionStartTime: string | undefined;\n\n for await (const ev of this.queue) {\n if (this.abortController.signal.aborted) {\n break;\n }\n this.output.put(ev);\n requestId = ev.id;\n if (ttft === BigInt(-1)) {\n ttft = process.hrtime.bigint() - startTime;\n completionStartTime = new Date().toISOString();\n }\n if (ev.usage) {\n usage = ev.usage;\n }\n }\n this.output.close();\n\n const duration = process.hrtime.bigint() - startTime;\n const durationMs = Math.trunc(Number(duration / BigInt(1000000)));\n const metrics: LLMMetrics = {\n type: 'llm_metrics',\n timestamp: Date.now(),\n requestId,\n ttftMs: ttft === BigInt(-1) ? -1 : Math.trunc(Number(ttft / BigInt(1000000))),\n durationMs,\n cancelled: this.abortController.signal.aborted,\n label: this.#llm.label(),\n completionTokens: usage?.completionTokens || 0,\n promptTokens: usage?.promptTokens || 0,\n promptCachedTokens: usage?.promptCachedTokens || 0,\n totalTokens: usage?.totalTokens || 0,\n tokensPerSecond: (() => {\n if (durationMs <= 0) {\n return 0;\n }\n return (usage?.completionTokens || 0) / (durationMs / 1000);\n })(),\n };\n\n if (this.#llmRequestSpan) {\n this.#llmRequestSpan.setAttribute(traceTypes.ATTR_LLM_METRICS, JSON.stringify(metrics));\n\n this.#llmRequestSpan.setAttributes({\n [traceTypes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: metrics.promptTokens,\n [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: metrics.completionTokens,\n });\n\n if (completionStartTime) {\n this.#llmRequestSpan.setAttribute(\n traceTypes.ATTR_LANGFUSE_COMPLETION_START_TIME,\n completionStartTime,\n );\n }\n\n // End the span now that metrics are collected\n this.#llmRequestSpan.end();\n }\n\n this.#llm.emit('metrics_collected', metrics);\n }\n\n protected abstract run(): Promise<void>;\n\n /** The function context of this stream. */\n get toolCtx(): ToolContext | undefined {\n return this.#toolCtx;\n }\n\n /** The initial chat context of this stream. */\n get chatCtx(): ChatContext {\n return this.#chatCtx;\n }\n\n /** The connection options for this stream. */\n get connOptions(): APIConnectOptions {\n return this._connOptions;\n }\n\n next(): Promise<IteratorResult<ChatChunk>> {\n return this.output.next();\n }\n\n close() {\n this.abortController.abort();\n }\n\n [Symbol.asyncIterator](): LLMStream {\n return this;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/llm/llm.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport type { Span } from '@opentelemetry/api';\nimport { EventEmitter } from 'node:events';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { log } from '../log.js';\nimport type { LLMMetrics } from '../metrics/base.js';\nimport { recordException, traceTypes, tracer } from '../telemetry/index.js';\nimport { type APIConnectOptions, intervalForRetry } from '../types.js';\nimport { AsyncIterableQueue, delay, startSoon, toError } from '../utils.js';\nimport { type ChatContext, type ChatRole, type FunctionCall } from './chat_context.js';\nimport type { ToolChoice, ToolContext } from './tool_context.js';\n\nexport interface ChoiceDelta {\n role: ChatRole;\n content?: string;\n toolCalls?: FunctionCall[];\n}\n\nexport interface CompletionUsage {\n completionTokens: number;\n promptTokens: number;\n promptCachedTokens: number;\n totalTokens: number;\n}\n\nexport interface ChatChunk {\n id: string;\n delta?: ChoiceDelta;\n usage?: CompletionUsage;\n}\n\nexport interface LLMError {\n type: 'llm_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type LLMCallbacks = {\n ['metrics_collected']: (metrics: LLMMetrics) => void;\n ['error']: (error: LLMError) => void;\n};\n\nexport abstract class LLM extends (EventEmitter as new () => TypedEmitter<LLMCallbacks>) {\n constructor() {\n super();\n }\n\n abstract label(): string;\n\n /**\n * Get the model name/identifier for this LLM instance.\n *\n * @returns The model name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their model information.\n */\n get model(): string {\n return 'unknown';\n }\n\n /**\n * Returns a {@link LLMStream} that can be used to push text and receive LLM responses.\n */\n abstract chat({\n chatCtx,\n toolCtx,\n connOptions,\n parallelToolCalls,\n toolChoice,\n extraKwargs,\n }: {\n chatCtx: ChatContext;\n toolCtx?: ToolContext;\n connOptions?: APIConnectOptions;\n parallelToolCalls?: boolean;\n toolChoice?: ToolChoice;\n extraKwargs?: Record<string, unknown>;\n }): LLMStream;\n\n /**\n * Pre-warm connection to the LLM service\n */\n prewarm(): void {\n // Default implementation - subclasses can override\n }\n\n async aclose(): Promise<void> {\n // Default implementation - subclasses can override\n }\n}\n\nexport abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {\n protected output = new AsyncIterableQueue<ChatChunk>();\n protected queue = new AsyncIterableQueue<ChatChunk>();\n protected closed = false;\n protected abortController = new AbortController();\n protected _connOptions: APIConnectOptions;\n protected logger = log();\n\n #llm: LLM;\n #chatCtx: ChatContext;\n #toolCtx?: ToolContext;\n #llmRequestSpan?: Span;\n\n constructor(\n llm: LLM,\n {\n chatCtx,\n toolCtx,\n connOptions,\n }: {\n chatCtx: ChatContext;\n toolCtx?: ToolContext;\n connOptions: APIConnectOptions;\n },\n ) {\n this.#llm = llm;\n this.#chatCtx = chatCtx;\n this.#toolCtx = toolCtx;\n this._connOptions = connOptions;\n this.monitorMetrics();\n this.abortController.signal.addEventListener('abort', () => {\n // TODO (AJS-37) clean this up when we refactor with streams\n this.output.close();\n this.closed = true;\n });\n\n // this is a hack to immitate asyncio.create_task so that mainTask\n // is run **after** the constructor has finished. Otherwise we get\n // runtime error when trying to access class variables in the\n // `run` method.\n startSoon(() => this.mainTask().then(() => this.queue.close()));\n }\n\n private _mainTaskImpl = async (span: Span) => {\n this.#llmRequestSpan = span;\n span.setAttribute(traceTypes.ATTR_GEN_AI_REQUEST_MODEL, this.#llm.model);\n\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'llm_request_run' },\n );\n } catch (error) {\n if (error instanceof APIError) {\n const retryInterval = intervalForRetry(this._connOptions, i);\n\n if (this._connOptions.maxRetry === 0 || !error.retryable) {\n this.emitError({ error, recoverable: false });\n throw error;\n } else if (i === this._connOptions.maxRetry) {\n this.emitError({ error, recoverable: false });\n throw new APIConnectionError({\n message: `failed to generate LLM completion after ${this._connOptions.maxRetry + 1} attempts`,\n options: { retryable: false },\n });\n } else {\n this.emitError({ error, recoverable: true });\n this.logger.warn(\n { llm: this.#llm.label(), attempt: i + 1, error },\n `failed to generate LLM completion, retrying in ${retryInterval}s`,\n );\n }\n\n if (retryInterval > 0) {\n await delay(retryInterval);\n }\n } else {\n this.emitError({ error: toError(error), recoverable: false });\n throw error;\n }\n }\n }\n };\n\n private mainTask = async () =>\n tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'llm_request',\n endOnExit: false,\n });\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#llm.emit('error', {\n type: 'llm_error',\n timestamp: Date.now(),\n label: this.#llm.label(),\n error,\n recoverable,\n });\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let ttft: bigint = BigInt(-1);\n let requestId = '';\n let usage: CompletionUsage | undefined;\n let completionStartTime: string | undefined;\n\n for await (const ev of this.queue) {\n if (this.abortController.signal.aborted) {\n break;\n }\n this.output.put(ev);\n requestId = ev.id;\n if (ttft === BigInt(-1)) {\n ttft = process.hrtime.bigint() - startTime;\n completionStartTime = new Date().toISOString();\n }\n if (ev.usage) {\n usage = ev.usage;\n }\n }\n this.output.close();\n\n const duration = process.hrtime.bigint() - startTime;\n const durationMs = Math.trunc(Number(duration / BigInt(1000000)));\n const metrics: LLMMetrics = {\n type: 'llm_metrics',\n timestamp: Date.now(),\n requestId,\n ttftMs: ttft === BigInt(-1) ? -1 : Math.trunc(Number(ttft / BigInt(1000000))),\n durationMs,\n cancelled: this.abortController.signal.aborted,\n label: this.#llm.label(),\n completionTokens: usage?.completionTokens || 0,\n promptTokens: usage?.promptTokens || 0,\n promptCachedTokens: usage?.promptCachedTokens || 0,\n totalTokens: usage?.totalTokens || 0,\n tokensPerSecond: (() => {\n if (durationMs <= 0) {\n return 0;\n }\n return (usage?.completionTokens || 0) / (durationMs / 1000);\n })(),\n };\n\n if (this.#llmRequestSpan) {\n this.#llmRequestSpan.setAttribute(traceTypes.ATTR_LLM_METRICS, JSON.stringify(metrics));\n\n this.#llmRequestSpan.setAttributes({\n [traceTypes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: metrics.promptTokens,\n [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: metrics.completionTokens,\n });\n\n if (completionStartTime) {\n this.#llmRequestSpan.setAttribute(\n traceTypes.ATTR_LANGFUSE_COMPLETION_START_TIME,\n completionStartTime,\n );\n }\n\n // End the span now that metrics are collected\n this.#llmRequestSpan.end();\n }\n\n this.#llm.emit('metrics_collected', metrics);\n }\n\n protected abstract run(): Promise<void>;\n\n /** The function context of this stream. */\n get toolCtx(): ToolContext | undefined {\n return this.#toolCtx;\n }\n\n /** The initial chat context of this stream. */\n get chatCtx(): ChatContext {\n return this.#chatCtx;\n }\n\n /** The connection options for this stream. */\n get connOptions(): APIConnectOptions {\n return this._connOptions;\n }\n\n next(): Promise<IteratorResult<ChatChunk>> {\n return this.output.next();\n }\n\n close() {\n this.abortController.abort();\n }\n\n [Symbol.asyncIterator](): LLMStream {\n return this;\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';
2
2
  import type { LLMMetrics } from '../metrics/base.js';
3
- import type { APIConnectOptions } from '../types.js';
3
+ import { type APIConnectOptions } from '../types.js';
4
4
  import { AsyncIterableQueue } from '../utils.js';
5
5
  import { type ChatContext, type ChatRole, type FunctionCall } from './chat_context.js';
6
6
  import type { ToolChoice, ToolContext } from './tool_context.js';
package/dist/llm/llm.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';
2
2
  import type { LLMMetrics } from '../metrics/base.js';
3
- import type { APIConnectOptions } from '../types.js';
3
+ import { type APIConnectOptions } from '../types.js';
4
4
  import { AsyncIterableQueue } from '../utils.js';
5
5
  import { type ChatContext, type ChatRole, type FunctionCall } from './chat_context.js';
6
6
  import type { ToolChoice, ToolContext } from './tool_context.js';
@@ -1 +1 @@
1
- {"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../../src/llm/llm.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../../src/llm/llm.ts"],"names":[],"mappings":"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"}
package/dist/llm/llm.js CHANGED
@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
2
2
  import { APIConnectionError, APIError } from "../_exceptions.js";
3
3
  import { log } from "../log.js";
4
4
  import { recordException, traceTypes, tracer } from "../telemetry/index.js";
5
+ import { intervalForRetry } from "../types.js";
5
6
  import { AsyncIterableQueue, delay, startSoon, toError } from "../utils.js";
6
7
  import {} from "./chat_context.js";
7
8
  class LLM extends EventEmitter {
@@ -73,7 +74,7 @@ class LLMStream {
73
74
  );
74
75
  } catch (error) {
75
76
  if (error instanceof APIError) {
76
- const retryInterval = this._connOptions._intervalForRetry(i);
77
+ const retryInterval = intervalForRetry(this._connOptions, i);
77
78
  if (this._connOptions.maxRetry === 0 || !error.retryable) {
78
79
  this.emitError({ error, recoverable: false });
79
80
  throw error;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/llm/llm.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport type { Span } from '@opentelemetry/api';\nimport { EventEmitter } from 'node:events';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { log } from '../log.js';\nimport type { LLMMetrics } from '../metrics/base.js';\nimport { recordException, traceTypes, tracer } from '../telemetry/index.js';\nimport type { APIConnectOptions } from '../types.js';\nimport { AsyncIterableQueue, delay, startSoon, toError } from '../utils.js';\nimport { type ChatContext, type ChatRole, type FunctionCall } from './chat_context.js';\nimport type { ToolChoice, ToolContext } from './tool_context.js';\n\nexport interface ChoiceDelta {\n role: ChatRole;\n content?: string;\n toolCalls?: FunctionCall[];\n}\n\nexport interface CompletionUsage {\n completionTokens: number;\n promptTokens: number;\n promptCachedTokens: number;\n totalTokens: number;\n}\n\nexport interface ChatChunk {\n id: string;\n delta?: ChoiceDelta;\n usage?: CompletionUsage;\n}\n\nexport interface LLMError {\n type: 'llm_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type LLMCallbacks = {\n ['metrics_collected']: (metrics: LLMMetrics) => void;\n ['error']: (error: LLMError) => void;\n};\n\nexport abstract class LLM extends (EventEmitter as new () => TypedEmitter<LLMCallbacks>) {\n constructor() {\n super();\n }\n\n abstract label(): string;\n\n /**\n * Get the model name/identifier for this LLM instance.\n *\n * @returns The model name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their model information.\n */\n get model(): string {\n return 'unknown';\n }\n\n /**\n * Returns a {@link LLMStream} that can be used to push text and receive LLM responses.\n */\n abstract chat({\n chatCtx,\n toolCtx,\n connOptions,\n parallelToolCalls,\n toolChoice,\n extraKwargs,\n }: {\n chatCtx: ChatContext;\n toolCtx?: ToolContext;\n connOptions?: APIConnectOptions;\n parallelToolCalls?: boolean;\n toolChoice?: ToolChoice;\n extraKwargs?: Record<string, unknown>;\n }): LLMStream;\n\n /**\n * Pre-warm connection to the LLM service\n */\n prewarm(): void {\n // Default implementation - subclasses can override\n }\n\n async aclose(): Promise<void> {\n // Default implementation - subclasses can override\n }\n}\n\nexport abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {\n protected output = new AsyncIterableQueue<ChatChunk>();\n protected queue = new AsyncIterableQueue<ChatChunk>();\n protected closed = false;\n protected abortController = new AbortController();\n protected _connOptions: APIConnectOptions;\n protected logger = log();\n\n #llm: LLM;\n #chatCtx: ChatContext;\n #toolCtx?: ToolContext;\n #llmRequestSpan?: Span;\n\n constructor(\n llm: LLM,\n {\n chatCtx,\n toolCtx,\n connOptions,\n }: {\n chatCtx: ChatContext;\n toolCtx?: ToolContext;\n connOptions: APIConnectOptions;\n },\n ) {\n this.#llm = llm;\n this.#chatCtx = chatCtx;\n this.#toolCtx = toolCtx;\n this._connOptions = connOptions;\n this.monitorMetrics();\n this.abortController.signal.addEventListener('abort', () => {\n // TODO (AJS-37) clean this up when we refactor with streams\n this.output.close();\n this.closed = true;\n });\n\n // this is a hack to immitate asyncio.create_task so that mainTask\n // is run **after** the constructor has finished. Otherwise we get\n // runtime error when trying to access class variables in the\n // `run` method.\n startSoon(() => this.mainTask().then(() => this.queue.close()));\n }\n\n private _mainTaskImpl = async (span: Span) => {\n this.#llmRequestSpan = span;\n span.setAttribute(traceTypes.ATTR_GEN_AI_REQUEST_MODEL, this.#llm.model);\n\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'llm_request_run' },\n );\n } catch (error) {\n if (error instanceof APIError) {\n const retryInterval = this._connOptions._intervalForRetry(i);\n\n if (this._connOptions.maxRetry === 0 || !error.retryable) {\n this.emitError({ error, recoverable: false });\n throw error;\n } else if (i === this._connOptions.maxRetry) {\n this.emitError({ error, recoverable: false });\n throw new APIConnectionError({\n message: `failed to generate LLM completion after ${this._connOptions.maxRetry + 1} attempts`,\n options: { retryable: false },\n });\n } else {\n this.emitError({ error, recoverable: true });\n this.logger.warn(\n { llm: this.#llm.label(), attempt: i + 1, error },\n `failed to generate LLM completion, retrying in ${retryInterval}s`,\n );\n }\n\n if (retryInterval > 0) {\n await delay(retryInterval);\n }\n } else {\n this.emitError({ error: toError(error), recoverable: false });\n throw error;\n }\n }\n }\n };\n\n private mainTask = async () =>\n tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'llm_request',\n endOnExit: false,\n });\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#llm.emit('error', {\n type: 'llm_error',\n timestamp: Date.now(),\n label: this.#llm.label(),\n error,\n recoverable,\n });\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let ttft: bigint = BigInt(-1);\n let requestId = '';\n let usage: CompletionUsage | undefined;\n let completionStartTime: string | undefined;\n\n for await (const ev of this.queue) {\n if (this.abortController.signal.aborted) {\n break;\n }\n this.output.put(ev);\n requestId = ev.id;\n if (ttft === BigInt(-1)) {\n ttft = process.hrtime.bigint() - startTime;\n completionStartTime = new Date().toISOString();\n }\n if (ev.usage) {\n usage = ev.usage;\n }\n }\n this.output.close();\n\n const duration = process.hrtime.bigint() - startTime;\n const durationMs = Math.trunc(Number(duration / BigInt(1000000)));\n const metrics: LLMMetrics = {\n type: 'llm_metrics',\n timestamp: Date.now(),\n requestId,\n ttftMs: ttft === BigInt(-1) ? -1 : Math.trunc(Number(ttft / BigInt(1000000))),\n durationMs,\n cancelled: this.abortController.signal.aborted,\n label: this.#llm.label(),\n completionTokens: usage?.completionTokens || 0,\n promptTokens: usage?.promptTokens || 0,\n promptCachedTokens: usage?.promptCachedTokens || 0,\n totalTokens: usage?.totalTokens || 0,\n tokensPerSecond: (() => {\n if (durationMs <= 0) {\n return 0;\n }\n return (usage?.completionTokens || 0) / (durationMs / 1000);\n })(),\n };\n\n if (this.#llmRequestSpan) {\n this.#llmRequestSpan.setAttribute(traceTypes.ATTR_LLM_METRICS, JSON.stringify(metrics));\n\n this.#llmRequestSpan.setAttributes({\n [traceTypes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: metrics.promptTokens,\n [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: metrics.completionTokens,\n });\n\n if (completionStartTime) {\n this.#llmRequestSpan.setAttribute(\n traceTypes.ATTR_LANGFUSE_COMPLETION_START_TIME,\n completionStartTime,\n );\n }\n\n // End the span now that metrics are collected\n this.#llmRequestSpan.end();\n }\n\n this.#llm.emit('metrics_collected', metrics);\n }\n\n protected abstract run(): Promise<void>;\n\n /** The function context of this stream. */\n get toolCtx(): ToolContext | undefined {\n return this.#toolCtx;\n }\n\n /** The initial chat context of this stream. */\n get chatCtx(): ChatContext {\n return this.#chatCtx;\n }\n\n /** The connection options for this stream. */\n get connOptions(): APIConnectOptions {\n return this._connOptions;\n }\n\n next(): Promise<IteratorResult<ChatChunk>> {\n return this.output.next();\n }\n\n close() {\n this.abortController.abort();\n }\n\n [Symbol.asyncIterator](): LLMStream {\n return this;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/llm/llm.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport type { Span } from '@opentelemetry/api';\nimport { EventEmitter } from 'node:events';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { log } from '../log.js';\nimport type { LLMMetrics } from '../metrics/base.js';\nimport { recordException, traceTypes, tracer } from '../telemetry/index.js';\nimport { type APIConnectOptions, intervalForRetry } from '../types.js';\nimport { AsyncIterableQueue, delay, startSoon, toError } from '../utils.js';\nimport { type ChatContext, type ChatRole, type FunctionCall } from './chat_context.js';\nimport type { ToolChoice, ToolContext } from './tool_context.js';\n\nexport interface ChoiceDelta {\n role: ChatRole;\n content?: string;\n toolCalls?: FunctionCall[];\n}\n\nexport interface CompletionUsage {\n completionTokens: number;\n promptTokens: number;\n promptCachedTokens: number;\n totalTokens: number;\n}\n\nexport interface ChatChunk {\n id: string;\n delta?: ChoiceDelta;\n usage?: CompletionUsage;\n}\n\nexport interface LLMError {\n type: 'llm_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type LLMCallbacks = {\n ['metrics_collected']: (metrics: LLMMetrics) => void;\n ['error']: (error: LLMError) => void;\n};\n\nexport abstract class LLM extends (EventEmitter as new () => TypedEmitter<LLMCallbacks>) {\n constructor() {\n super();\n }\n\n abstract label(): string;\n\n /**\n * Get the model name/identifier for this LLM instance.\n *\n * @returns The model name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their model information.\n */\n get model(): string {\n return 'unknown';\n }\n\n /**\n * Returns a {@link LLMStream} that can be used to push text and receive LLM responses.\n */\n abstract chat({\n chatCtx,\n toolCtx,\n connOptions,\n parallelToolCalls,\n toolChoice,\n extraKwargs,\n }: {\n chatCtx: ChatContext;\n toolCtx?: ToolContext;\n connOptions?: APIConnectOptions;\n parallelToolCalls?: boolean;\n toolChoice?: ToolChoice;\n extraKwargs?: Record<string, unknown>;\n }): LLMStream;\n\n /**\n * Pre-warm connection to the LLM service\n */\n prewarm(): void {\n // Default implementation - subclasses can override\n }\n\n async aclose(): Promise<void> {\n // Default implementation - subclasses can override\n }\n}\n\nexport abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {\n protected output = new AsyncIterableQueue<ChatChunk>();\n protected queue = new AsyncIterableQueue<ChatChunk>();\n protected closed = false;\n protected abortController = new AbortController();\n protected _connOptions: APIConnectOptions;\n protected logger = log();\n\n #llm: LLM;\n #chatCtx: ChatContext;\n #toolCtx?: ToolContext;\n #llmRequestSpan?: Span;\n\n constructor(\n llm: LLM,\n {\n chatCtx,\n toolCtx,\n connOptions,\n }: {\n chatCtx: ChatContext;\n toolCtx?: ToolContext;\n connOptions: APIConnectOptions;\n },\n ) {\n this.#llm = llm;\n this.#chatCtx = chatCtx;\n this.#toolCtx = toolCtx;\n this._connOptions = connOptions;\n this.monitorMetrics();\n this.abortController.signal.addEventListener('abort', () => {\n // TODO (AJS-37) clean this up when we refactor with streams\n this.output.close();\n this.closed = true;\n });\n\n // this is a hack to immitate asyncio.create_task so that mainTask\n // is run **after** the constructor has finished. Otherwise we get\n // runtime error when trying to access class variables in the\n // `run` method.\n startSoon(() => this.mainTask().then(() => this.queue.close()));\n }\n\n private _mainTaskImpl = async (span: Span) => {\n this.#llmRequestSpan = span;\n span.setAttribute(traceTypes.ATTR_GEN_AI_REQUEST_MODEL, this.#llm.model);\n\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'llm_request_run' },\n );\n } catch (error) {\n if (error instanceof APIError) {\n const retryInterval = intervalForRetry(this._connOptions, i);\n\n if (this._connOptions.maxRetry === 0 || !error.retryable) {\n this.emitError({ error, recoverable: false });\n throw error;\n } else if (i === this._connOptions.maxRetry) {\n this.emitError({ error, recoverable: false });\n throw new APIConnectionError({\n message: `failed to generate LLM completion after ${this._connOptions.maxRetry + 1} attempts`,\n options: { retryable: false },\n });\n } else {\n this.emitError({ error, recoverable: true });\n this.logger.warn(\n { llm: this.#llm.label(), attempt: i + 1, error },\n `failed to generate LLM completion, retrying in ${retryInterval}s`,\n );\n }\n\n if (retryInterval > 0) {\n await delay(retryInterval);\n }\n } else {\n this.emitError({ error: toError(error), recoverable: false });\n throw error;\n }\n }\n }\n };\n\n private mainTask = async () =>\n tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'llm_request',\n endOnExit: false,\n });\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#llm.emit('error', {\n type: 'llm_error',\n timestamp: Date.now(),\n label: this.#llm.label(),\n error,\n recoverable,\n });\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let ttft: bigint = BigInt(-1);\n let requestId = '';\n let usage: CompletionUsage | undefined;\n let completionStartTime: string | undefined;\n\n for await (const ev of this.queue) {\n if (this.abortController.signal.aborted) {\n break;\n }\n this.output.put(ev);\n requestId = ev.id;\n if (ttft === BigInt(-1)) {\n ttft = process.hrtime.bigint() - startTime;\n completionStartTime = new Date().toISOString();\n }\n if (ev.usage) {\n usage = ev.usage;\n }\n }\n this.output.close();\n\n const duration = process.hrtime.bigint() - startTime;\n const durationMs = Math.trunc(Number(duration / BigInt(1000000)));\n const metrics: LLMMetrics = {\n type: 'llm_metrics',\n timestamp: Date.now(),\n requestId,\n ttftMs: ttft === BigInt(-1) ? -1 : Math.trunc(Number(ttft / BigInt(1000000))),\n durationMs,\n cancelled: this.abortController.signal.aborted,\n label: this.#llm.label(),\n completionTokens: usage?.completionTokens || 0,\n promptTokens: usage?.promptTokens || 0,\n promptCachedTokens: usage?.promptCachedTokens || 0,\n totalTokens: usage?.totalTokens || 0,\n tokensPerSecond: (() => {\n if (durationMs <= 0) {\n return 0;\n }\n return (usage?.completionTokens || 0) / (durationMs / 1000);\n })(),\n };\n\n if (this.#llmRequestSpan) {\n this.#llmRequestSpan.setAttribute(traceTypes.ATTR_LLM_METRICS, JSON.stringify(metrics));\n\n this.#llmRequestSpan.setAttributes({\n [traceTypes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: metrics.promptTokens,\n [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: metrics.completionTokens,\n });\n\n if (completionStartTime) {\n this.#llmRequestSpan.setAttribute(\n traceTypes.ATTR_LANGFUSE_COMPLETION_START_TIME,\n completionStartTime,\n );\n }\n\n // End the span now that metrics are collected\n this.#llmRequestSpan.end();\n }\n\n this.#llm.emit('metrics_collected', metrics);\n }\n\n protected abstract run(): Promise<void>;\n\n /** The function context of this stream. */\n get toolCtx(): ToolContext | undefined {\n return this.#toolCtx;\n }\n\n /** The initial chat context of this stream. */\n get chatCtx(): ChatContext {\n return this.#chatCtx;\n }\n\n /** The connection options for this stream. */\n get connOptions(): APIConnectOptions {\n return this._connOptions;\n }\n\n next(): Promise<IteratorResult<ChatChunk>> {\n return this.output.next();\n }\n\n close() {\n this.abortController.abort();\n }\n\n [Symbol.asyncIterator](): LLMStream {\n return this;\n }\n}\n"],"mappings":"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","names":[]}
@@ -26,7 +26,9 @@ var import_identity_transform = require("./identity_transform.cjs");
26
26
  function isStreamReaderReleaseError(e) {
27
27
  const allowedMessages = [
28
28
  "Invalid state: Releasing reader",
29
- "Invalid state: The reader is not attached to a stream"
29
+ "Invalid state: The reader is not attached to a stream",
30
+ "Controller is already closed",
31
+ "WritableStream is closed"
30
32
  ];
31
33
  if (e instanceof TypeError) {
32
34
  return allowedMessages.some((message) => e.message.includes(message));
@@ -70,10 +72,16 @@ class DeferredReadableStream {
70
72
  sourceError = e;
71
73
  } finally {
72
74
  if (sourceError) {
73
- this.writer.abort(sourceError);
75
+ try {
76
+ this.writer.abort(sourceError);
77
+ } catch (e) {
78
+ }
74
79
  return;
75
80
  }
76
- this.writer.releaseLock();
81
+ try {
82
+ this.writer.releaseLock();
83
+ } catch (e) {
84
+ }
77
85
  try {
78
86
  await this.transform.writable.close();
79
87
  } catch (e) {
@@ -85,7 +93,7 @@ class DeferredReadableStream {
85
93
  */
86
94
  async detachSource() {
87
95
  if (!this.isSourceSet) {
88
- throw new Error("Source not set");
96
+ return;
89
97
  }
90
98
  this.sourceReader.releaseLock();
91
99
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stream/deferred_stream.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type {\n ReadableStream,\n ReadableStreamDefaultReader,\n WritableStreamDefaultWriter,\n} from 'node:stream/web';\nimport { IdentityTransform } from './identity_transform.js';\n\n/**\n * Check if error is related to reader.read after release lock\n *\n * Invalid state: Releasing reader\n * Invalid state: The reader is not attached to a stream\n */\nexport function isStreamReaderReleaseError(e: unknown) {\n const allowedMessages = [\n 'Invalid state: Releasing reader',\n 'Invalid state: The reader is not attached to a stream',\n ];\n\n if (e instanceof TypeError) {\n return allowedMessages.some((message) => e.message.includes(message));\n }\n\n return false;\n}\nexport class DeferredReadableStream<T> {\n private transform: IdentityTransform<T>;\n private writer: WritableStreamDefaultWriter<T>;\n private sourceReader?: ReadableStreamDefaultReader<T>;\n\n constructor() {\n this.transform = new IdentityTransform<T>();\n this.writer = this.transform.writable.getWriter();\n }\n\n get stream() {\n return this.transform.readable;\n }\n\n get isSourceSet() {\n return !!this.sourceReader;\n }\n\n /**\n * Call once the actual source is ready.\n */\n setSource(source: ReadableStream<T>) {\n if (this.isSourceSet) {\n throw new Error('Stream source already set');\n }\n\n this.sourceReader = source.getReader();\n this.pump();\n }\n\n private async pump() {\n let sourceError: unknown;\n\n try {\n while (true) {\n const { done, value } = await this.sourceReader!.read();\n if (done) break;\n await this.writer.write(value);\n }\n } catch (e) {\n // skip source detach related errors\n if (isStreamReaderReleaseError(e)) return;\n sourceError = e;\n } finally {\n // any other error from source will be propagated to the consumer\n if (sourceError) {\n this.writer.abort(sourceError);\n return;\n }\n\n // release lock so this.stream.getReader().read() will terminate with done: true\n this.writer.releaseLock();\n\n // we only close the writable stream after done\n try {\n await this.transform.writable.close();\n // NOTE: we do not cancel this.transform.readable as there might be access to\n // this.transform.readable.getReader() outside that blocks this cancellation\n // hence, user is responsible for canceling reader on their own\n } catch (e) {\n // ignore TypeError: Invalid state: WritableStream is closed\n // in case stream reader is already closed, this will throw\n // but we ignore it as we are closing the stream anyway\n }\n }\n }\n\n /**\n * Detach the source stream and clean up resources.\n */\n async detachSource() {\n if (!this.isSourceSet) {\n throw new Error('Source not set');\n }\n\n // release lock will make any pending read() throw TypeError\n // which are expected, and we intentionally catch those error\n // using isStreamReaderReleaseError\n // this will unblock any pending read() inside the async for loop\n this.sourceReader!.releaseLock();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,gCAAkC;AAQ3B,SAAS,2BAA2B,GAAY;AACrD,QAAM,kBAAkB;AAAA,IACtB;AAAA,IACA;AAAA,EACF;AAEA,MAAI,aAAa,WAAW;AAC1B,WAAO,gBAAgB,KAAK,CAAC,YAAY,EAAE,QAAQ,SAAS,OAAO,CAAC;AAAA,EACtE;AAEA,SAAO;AACT;AACO,MAAM,uBAA0B;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EAER,cAAc;AACZ,SAAK,YAAY,IAAI,4CAAqB;AAC1C,SAAK,SAAS,KAAK,UAAU,SAAS,UAAU;AAAA,EAClD;AAAA,EAEA,IAAI,SAAS;AACX,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA,EAEA,IAAI,cAAc;AAChB,WAAO,CAAC,CAAC,KAAK;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,QAA2B;AACnC,QAAI,KAAK,aAAa;AACpB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,MAAc,OAAO;AACnB,QAAI;AAEJ,QAAI;AACF,aAAO,MAAM;AACX,cAAM,EAAE,MAAM,MAAM,IAAI,MAAM,KAAK,aAAc,KAAK;AACtD,YAAI,KAAM;AACV,cAAM,KAAK,OAAO,MAAM,KAAK;AAAA,MAC/B;AAAA,IACF,SAAS,GAAG;AAEV,UAAI,2BAA2B,CAAC,EAAG;AACnC,oBAAc;AAAA,IAChB,UAAE;AAEA,UAAI,aAAa;AACf,aAAK,OAAO,MAAM,WAAW;AAC7B;AAAA,MACF;AAGA,WAAK,OAAO,YAAY;AAGxB,UAAI;AACF,cAAM,KAAK,UAAU,SAAS,MAAM;AAAA,MAItC,SAAS,GAAG;AAAA,MAIZ;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe;AACnB,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,gBAAgB;AAAA,IAClC;AAMA,SAAK,aAAc,YAAY;AAAA,EACjC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/stream/deferred_stream.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type {\n ReadableStream,\n ReadableStreamDefaultReader,\n WritableStreamDefaultWriter,\n} from 'node:stream/web';\nimport { IdentityTransform } from './identity_transform.js';\n\n/**\n * Check if error is related to stream cleanup operations.\n *\n * These errors are expected when calling reader.read() after releaseLock()\n * or when writing to already closed streams during cleanup:\n *\n * Invalid state: Releasing reader\n * Invalid state: The reader is not attached to a stream\n * Invalid state: Controller is already closed\n * Invalid state: WritableStream is closed\n */\nexport function isStreamReaderReleaseError(e: unknown) {\n const allowedMessages = [\n 'Invalid state: Releasing reader',\n 'Invalid state: The reader is not attached to a stream',\n 'Controller is already closed',\n 'WritableStream is closed',\n ];\n\n if (e instanceof TypeError) {\n return allowedMessages.some((message) => e.message.includes(message));\n }\n\n return false;\n}\nexport class DeferredReadableStream<T> {\n private transform: IdentityTransform<T>;\n private writer: WritableStreamDefaultWriter<T>;\n private sourceReader?: ReadableStreamDefaultReader<T>;\n\n constructor() {\n this.transform = new IdentityTransform<T>();\n this.writer = this.transform.writable.getWriter();\n }\n\n get stream() {\n return this.transform.readable;\n }\n\n get isSourceSet() {\n return !!this.sourceReader;\n }\n\n /**\n * Call once the actual source is ready.\n */\n setSource(source: ReadableStream<T>) {\n if (this.isSourceSet) {\n throw new Error('Stream source already set');\n }\n\n this.sourceReader = source.getReader();\n this.pump();\n }\n\n private async pump() {\n let sourceError: unknown;\n\n try {\n while (true) {\n const { done, value } = await this.sourceReader!.read();\n if (done) break;\n await this.writer.write(value);\n }\n } catch (e) {\n // skip stream cleanup related errors\n if (isStreamReaderReleaseError(e)) return;\n\n sourceError = e;\n } finally {\n // any other error from source will be propagated to the consumer\n if (sourceError) {\n try {\n this.writer.abort(sourceError);\n } catch (e) {\n // ignore if writer is already closed\n }\n return;\n }\n\n // release lock so this.stream.getReader().read() will terminate with done: true\n try {\n this.writer.releaseLock();\n } catch (e) {\n // ignore if writer lock is already released\n }\n\n // we only close the writable stream after done\n try {\n await this.transform.writable.close();\n // NOTE: we do not cancel this.transform.readable as there might be access to\n // this.transform.readable.getReader() outside that blocks this cancellation\n // hence, user is responsible for canceling reader on their own\n } catch (e) {\n // ignore TypeError: Invalid state: WritableStream is closed\n // in case stream reader is already closed, this will throw\n // but we ignore it as we are closing the stream anyway\n }\n }\n }\n\n /**\n * Detach the source stream and clean up resources.\n */\n async detachSource() {\n if (!this.isSourceSet) {\n // No-op if source was never set - this is a common case during cleanup\n return;\n }\n\n // release lock will make any pending read() throw TypeError\n // which are expected, and we intentionally catch those error\n // using isStreamReaderReleaseError\n // this will unblock any pending read() inside the async for loop\n this.sourceReader!.releaseLock();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,gCAAkC;AAa3B,SAAS,2BAA2B,GAAY;AACrD,QAAM,kBAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,MAAI,aAAa,WAAW;AAC1B,WAAO,gBAAgB,KAAK,CAAC,YAAY,EAAE,QAAQ,SAAS,OAAO,CAAC;AAAA,EACtE;AAEA,SAAO;AACT;AACO,MAAM,uBAA0B;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EAER,cAAc;AACZ,SAAK,YAAY,IAAI,4CAAqB;AAC1C,SAAK,SAAS,KAAK,UAAU,SAAS,UAAU;AAAA,EAClD;AAAA,EAEA,IAAI,SAAS;AACX,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA,EAEA,IAAI,cAAc;AAChB,WAAO,CAAC,CAAC,KAAK;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,QAA2B;AACnC,QAAI,KAAK,aAAa;AACpB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,MAAc,OAAO;AACnB,QAAI;AAEJ,QAAI;AACF,aAAO,MAAM;AACX,cAAM,EAAE,MAAM,MAAM,IAAI,MAAM,KAAK,aAAc,KAAK;AACtD,YAAI,KAAM;AACV,cAAM,KAAK,OAAO,MAAM,KAAK;AAAA,MAC/B;AAAA,IACF,SAAS,GAAG;AAEV,UAAI,2BAA2B,CAAC,EAAG;AAEnC,oBAAc;AAAA,IAChB,UAAE;AAEA,UAAI,aAAa;AACf,YAAI;AACF,eAAK,OAAO,MAAM,WAAW;AAAA,QAC/B,SAAS,GAAG;AAAA,QAEZ;AACA;AAAA,MACF;AAGA,UAAI;AACF,aAAK,OAAO,YAAY;AAAA,MAC1B,SAAS,GAAG;AAAA,MAEZ;AAGA,UAAI;AACF,cAAM,KAAK,UAAU,SAAS,MAAM;AAAA,MAItC,SAAS,GAAG;AAAA,MAIZ;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe;AACnB,QAAI,CAAC,KAAK,aAAa;AAErB;AAAA,IACF;AAMA,SAAK,aAAc,YAAY;AAAA,EACjC;AACF;","names":[]}
@@ -1,10 +1,15 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  import type { ReadableStream } from 'node:stream/web';
3
3
  /**
4
- * Check if error is related to reader.read after release lock
4
+ * Check if error is related to stream cleanup operations.
5
+ *
6
+ * These errors are expected when calling reader.read() after releaseLock()
7
+ * or when writing to already closed streams during cleanup:
5
8
  *
6
9
  * Invalid state: Releasing reader
7
10
  * Invalid state: The reader is not attached to a stream
11
+ * Invalid state: Controller is already closed
12
+ * Invalid state: WritableStream is closed
8
13
  */
9
14
  export declare function isStreamReaderReleaseError(e: unknown): boolean;
10
15
  export declare class DeferredReadableStream<T> {
@@ -1,10 +1,15 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  import type { ReadableStream } from 'node:stream/web';
3
3
  /**
4
- * Check if error is related to reader.read after release lock
4
+ * Check if error is related to stream cleanup operations.
5
+ *
6
+ * These errors are expected when calling reader.read() after releaseLock()
7
+ * or when writing to already closed streams during cleanup:
5
8
  *
6
9
  * Invalid state: Releasing reader
7
10
  * Invalid state: The reader is not attached to a stream
11
+ * Invalid state: Controller is already closed
12
+ * Invalid state: WritableStream is closed
8
13
  */
9
14
  export declare function isStreamReaderReleaseError(e: unknown): boolean;
10
15
  export declare class DeferredReadableStream<T> {
@@ -1 +1 @@
1
- {"version":3,"file":"deferred_stream.d.ts","sourceRoot":"","sources":["../../src/stream/deferred_stream.ts"],"names":[],"mappings":";AAGA,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,iBAAiB,CAAC;AAGzB;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,OAAO,WAWpD;AACD,qBAAa,sBAAsB,CAAC,CAAC;IACnC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,YAAY,CAAC,CAAiC;;IAOtD,IAAI,MAAM,sBAET;IAED,IAAI,WAAW,YAEd;IAED;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;YASrB,IAAI;IAqClB;;OAEG;IACG,YAAY;CAWnB"}
1
+ {"version":3,"file":"deferred_stream.d.ts","sourceRoot":"","sources":["../../src/stream/deferred_stream.ts"],"names":[],"mappings":";AAGA,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,iBAAiB,CAAC;AAGzB;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,OAAO,WAapD;AACD,qBAAa,sBAAsB,CAAC,CAAC;IACnC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,YAAY,CAAC,CAAiC;;IAOtD,IAAI,MAAM,sBAET;IAED,IAAI,WAAW,YAEd;IAED;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;YASrB,IAAI;IA8ClB;;OAEG;IACG,YAAY;CAYnB"}
@@ -2,7 +2,9 @@ import { IdentityTransform } from "./identity_transform.js";
2
2
  function isStreamReaderReleaseError(e) {
3
3
  const allowedMessages = [
4
4
  "Invalid state: Releasing reader",
5
- "Invalid state: The reader is not attached to a stream"
5
+ "Invalid state: The reader is not attached to a stream",
6
+ "Controller is already closed",
7
+ "WritableStream is closed"
6
8
  ];
7
9
  if (e instanceof TypeError) {
8
10
  return allowedMessages.some((message) => e.message.includes(message));
@@ -46,10 +48,16 @@ class DeferredReadableStream {
46
48
  sourceError = e;
47
49
  } finally {
48
50
  if (sourceError) {
49
- this.writer.abort(sourceError);
51
+ try {
52
+ this.writer.abort(sourceError);
53
+ } catch (e) {
54
+ }
50
55
  return;
51
56
  }
52
- this.writer.releaseLock();
57
+ try {
58
+ this.writer.releaseLock();
59
+ } catch (e) {
60
+ }
53
61
  try {
54
62
  await this.transform.writable.close();
55
63
  } catch (e) {
@@ -61,7 +69,7 @@ class DeferredReadableStream {
61
69
  */
62
70
  async detachSource() {
63
71
  if (!this.isSourceSet) {
64
- throw new Error("Source not set");
72
+ return;
65
73
  }
66
74
  this.sourceReader.releaseLock();
67
75
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stream/deferred_stream.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type {\n ReadableStream,\n ReadableStreamDefaultReader,\n WritableStreamDefaultWriter,\n} from 'node:stream/web';\nimport { IdentityTransform } from './identity_transform.js';\n\n/**\n * Check if error is related to reader.read after release lock\n *\n * Invalid state: Releasing reader\n * Invalid state: The reader is not attached to a stream\n */\nexport function isStreamReaderReleaseError(e: unknown) {\n const allowedMessages = [\n 'Invalid state: Releasing reader',\n 'Invalid state: The reader is not attached to a stream',\n ];\n\n if (e instanceof TypeError) {\n return allowedMessages.some((message) => e.message.includes(message));\n }\n\n return false;\n}\nexport class DeferredReadableStream<T> {\n private transform: IdentityTransform<T>;\n private writer: WritableStreamDefaultWriter<T>;\n private sourceReader?: ReadableStreamDefaultReader<T>;\n\n constructor() {\n this.transform = new IdentityTransform<T>();\n this.writer = this.transform.writable.getWriter();\n }\n\n get stream() {\n return this.transform.readable;\n }\n\n get isSourceSet() {\n return !!this.sourceReader;\n }\n\n /**\n * Call once the actual source is ready.\n */\n setSource(source: ReadableStream<T>) {\n if (this.isSourceSet) {\n throw new Error('Stream source already set');\n }\n\n this.sourceReader = source.getReader();\n this.pump();\n }\n\n private async pump() {\n let sourceError: unknown;\n\n try {\n while (true) {\n const { done, value } = await this.sourceReader!.read();\n if (done) break;\n await this.writer.write(value);\n }\n } catch (e) {\n // skip source detach related errors\n if (isStreamReaderReleaseError(e)) return;\n sourceError = e;\n } finally {\n // any other error from source will be propagated to the consumer\n if (sourceError) {\n this.writer.abort(sourceError);\n return;\n }\n\n // release lock so this.stream.getReader().read() will terminate with done: true\n this.writer.releaseLock();\n\n // we only close the writable stream after done\n try {\n await this.transform.writable.close();\n // NOTE: we do not cancel this.transform.readable as there might be access to\n // this.transform.readable.getReader() outside that blocks this cancellation\n // hence, user is responsible for canceling reader on their own\n } catch (e) {\n // ignore TypeError: Invalid state: WritableStream is closed\n // in case stream reader is already closed, this will throw\n // but we ignore it as we are closing the stream anyway\n }\n }\n }\n\n /**\n * Detach the source stream and clean up resources.\n */\n async detachSource() {\n if (!this.isSourceSet) {\n throw new Error('Source not set');\n }\n\n // release lock will make any pending read() throw TypeError\n // which are expected, and we intentionally catch those error\n // using isStreamReaderReleaseError\n // this will unblock any pending read() inside the async for loop\n this.sourceReader!.releaseLock();\n }\n}\n"],"mappings":"AAQA,SAAS,yBAAyB;AAQ3B,SAAS,2BAA2B,GAAY;AACrD,QAAM,kBAAkB;AAAA,IACtB;AAAA,IACA;AAAA,EACF;AAEA,MAAI,aAAa,WAAW;AAC1B,WAAO,gBAAgB,KAAK,CAAC,YAAY,EAAE,QAAQ,SAAS,OAAO,CAAC;AAAA,EACtE;AAEA,SAAO;AACT;AACO,MAAM,uBAA0B;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EAER,cAAc;AACZ,SAAK,YAAY,IAAI,kBAAqB;AAC1C,SAAK,SAAS,KAAK,UAAU,SAAS,UAAU;AAAA,EAClD;AAAA,EAEA,IAAI,SAAS;AACX,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA,EAEA,IAAI,cAAc;AAChB,WAAO,CAAC,CAAC,KAAK;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,QAA2B;AACnC,QAAI,KAAK,aAAa;AACpB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,MAAc,OAAO;AACnB,QAAI;AAEJ,QAAI;AACF,aAAO,MAAM;AACX,cAAM,EAAE,MAAM,MAAM,IAAI,MAAM,KAAK,aAAc,KAAK;AACtD,YAAI,KAAM;AACV,cAAM,KAAK,OAAO,MAAM,KAAK;AAAA,MAC/B;AAAA,IACF,SAAS,GAAG;AAEV,UAAI,2BAA2B,CAAC,EAAG;AACnC,oBAAc;AAAA,IAChB,UAAE;AAEA,UAAI,aAAa;AACf,aAAK,OAAO,MAAM,WAAW;AAC7B;AAAA,MACF;AAGA,WAAK,OAAO,YAAY;AAGxB,UAAI;AACF,cAAM,KAAK,UAAU,SAAS,MAAM;AAAA,MAItC,SAAS,GAAG;AAAA,MAIZ;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe;AACnB,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,gBAAgB;AAAA,IAClC;AAMA,SAAK,aAAc,YAAY;AAAA,EACjC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/stream/deferred_stream.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type {\n ReadableStream,\n ReadableStreamDefaultReader,\n WritableStreamDefaultWriter,\n} from 'node:stream/web';\nimport { IdentityTransform } from './identity_transform.js';\n\n/**\n * Check if error is related to stream cleanup operations.\n *\n * These errors are expected when calling reader.read() after releaseLock()\n * or when writing to already closed streams during cleanup:\n *\n * Invalid state: Releasing reader\n * Invalid state: The reader is not attached to a stream\n * Invalid state: Controller is already closed\n * Invalid state: WritableStream is closed\n */\nexport function isStreamReaderReleaseError(e: unknown) {\n const allowedMessages = [\n 'Invalid state: Releasing reader',\n 'Invalid state: The reader is not attached to a stream',\n 'Controller is already closed',\n 'WritableStream is closed',\n ];\n\n if (e instanceof TypeError) {\n return allowedMessages.some((message) => e.message.includes(message));\n }\n\n return false;\n}\nexport class DeferredReadableStream<T> {\n private transform: IdentityTransform<T>;\n private writer: WritableStreamDefaultWriter<T>;\n private sourceReader?: ReadableStreamDefaultReader<T>;\n\n constructor() {\n this.transform = new IdentityTransform<T>();\n this.writer = this.transform.writable.getWriter();\n }\n\n get stream() {\n return this.transform.readable;\n }\n\n get isSourceSet() {\n return !!this.sourceReader;\n }\n\n /**\n * Call once the actual source is ready.\n */\n setSource(source: ReadableStream<T>) {\n if (this.isSourceSet) {\n throw new Error('Stream source already set');\n }\n\n this.sourceReader = source.getReader();\n this.pump();\n }\n\n private async pump() {\n let sourceError: unknown;\n\n try {\n while (true) {\n const { done, value } = await this.sourceReader!.read();\n if (done) break;\n await this.writer.write(value);\n }\n } catch (e) {\n // skip stream cleanup related errors\n if (isStreamReaderReleaseError(e)) return;\n\n sourceError = e;\n } finally {\n // any other error from source will be propagated to the consumer\n if (sourceError) {\n try {\n this.writer.abort(sourceError);\n } catch (e) {\n // ignore if writer is already closed\n }\n return;\n }\n\n // release lock so this.stream.getReader().read() will terminate with done: true\n try {\n this.writer.releaseLock();\n } catch (e) {\n // ignore if writer lock is already released\n }\n\n // we only close the writable stream after done\n try {\n await this.transform.writable.close();\n // NOTE: we do not cancel this.transform.readable as there might be access to\n // this.transform.readable.getReader() outside that blocks this cancellation\n // hence, user is responsible for canceling reader on their own\n } catch (e) {\n // ignore TypeError: Invalid state: WritableStream is closed\n // in case stream reader is already closed, this will throw\n // but we ignore it as we are closing the stream anyway\n }\n }\n }\n\n /**\n * Detach the source stream and clean up resources.\n */\n async detachSource() {\n if (!this.isSourceSet) {\n // No-op if source was never set - this is a common case during cleanup\n return;\n }\n\n // release lock will make any pending read() throw TypeError\n // which are expected, and we intentionally catch those error\n // using isStreamReaderReleaseError\n // this will unblock any pending read() inside the async for loop\n this.sourceReader!.releaseLock();\n }\n}\n"],"mappings":"AAQA,SAAS,yBAAyB;AAa3B,SAAS,2BAA2B,GAAY;AACrD,QAAM,kBAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,MAAI,aAAa,WAAW;AAC1B,WAAO,gBAAgB,KAAK,CAAC,YAAY,EAAE,QAAQ,SAAS,OAAO,CAAC;AAAA,EACtE;AAEA,SAAO;AACT;AACO,MAAM,uBAA0B;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EAER,cAAc;AACZ,SAAK,YAAY,IAAI,kBAAqB;AAC1C,SAAK,SAAS,KAAK,UAAU,SAAS,UAAU;AAAA,EAClD;AAAA,EAEA,IAAI,SAAS;AACX,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA,EAEA,IAAI,cAAc;AAChB,WAAO,CAAC,CAAC,KAAK;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,QAA2B;AACnC,QAAI,KAAK,aAAa;AACpB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,MAAc,OAAO;AACnB,QAAI;AAEJ,QAAI;AACF,aAAO,MAAM;AACX,cAAM,EAAE,MAAM,MAAM,IAAI,MAAM,KAAK,aAAc,KAAK;AACtD,YAAI,KAAM;AACV,cAAM,KAAK,OAAO,MAAM,KAAK;AAAA,MAC/B;AAAA,IACF,SAAS,GAAG;AAEV,UAAI,2BAA2B,CAAC,EAAG;AAEnC,oBAAc;AAAA,IAChB,UAAE;AAEA,UAAI,aAAa;AACf,YAAI;AACF,eAAK,OAAO,MAAM,WAAW;AAAA,QAC/B,SAAS,GAAG;AAAA,QAEZ;AACA;AAAA,MACF;AAGA,UAAI;AACF,aAAK,OAAO,YAAY;AAAA,MAC1B,SAAS,GAAG;AAAA,MAEZ;AAGA,UAAI;AACF,cAAM,KAAK,UAAU,SAAS,MAAM;AAAA,MAItC,SAAS,GAAG;AAAA,MAIZ;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe;AACnB,QAAI,CAAC,KAAK,aAAa;AAErB;AAAA,IACF;AAMA,SAAK,aAAc,YAAY;AAAA,EACjC;AACF;","names":[]}
@@ -360,9 +360,9 @@ var import_deferred_stream = require("./deferred_stream.cjs");
360
360
  deferred.setSource(source1);
361
361
  (0, import_vitest.expect)(() => deferred.setSource(source2)).toThrow("Stream source already set");
362
362
  });
363
- (0, import_vitest.it)("should throw error when trying to detach source before setting it", async () => {
363
+ (0, import_vitest.it)("should safely detach source before setting it (no-op)", async () => {
364
364
  const deferred = new import_deferred_stream.DeferredReadableStream();
365
- await (0, import_vitest.expect)(deferred.detachSource()).rejects.toThrow("Source not set");
365
+ await (0, import_vitest.expect)(deferred.detachSource()).resolves.toBeUndefined();
366
366
  });
367
367
  (0, import_vitest.it)("read returns undefined as soon as reader is cancelled", async () => {
368
368
  const deferred = new import_deferred_stream.DeferredReadableStream();