@copilotkit/aimock 1.46.0 → 1.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +12 -1
  4. package/README.md +4 -1
  5. package/dist/aws-event-stream.cjs +20 -23
  6. package/dist/aws-event-stream.cjs.map +1 -1
  7. package/dist/aws-event-stream.d.cts +0 -1
  8. package/dist/aws-event-stream.d.ts +0 -1
  9. package/dist/aws-event-stream.js +20 -24
  10. package/dist/aws-event-stream.js.map +1 -1
  11. package/dist/bedrock-converse.cjs +79 -82
  12. package/dist/bedrock-converse.cjs.map +1 -1
  13. package/dist/bedrock-converse.d.cts.map +1 -1
  14. package/dist/bedrock-converse.d.ts.map +1 -1
  15. package/dist/bedrock-converse.js +80 -83
  16. package/dist/bedrock-converse.js.map +1 -1
  17. package/dist/bedrock.cjs +112 -121
  18. package/dist/bedrock.cjs.map +1 -1
  19. package/dist/bedrock.d.cts.map +1 -1
  20. package/dist/bedrock.d.ts.map +1 -1
  21. package/dist/bedrock.js +113 -122
  22. package/dist/bedrock.js.map +1 -1
  23. package/dist/cohere.cjs +58 -106
  24. package/dist/cohere.cjs.map +1 -1
  25. package/dist/cohere.d.cts.map +1 -1
  26. package/dist/cohere.d.ts.map +1 -1
  27. package/dist/cohere.js +60 -108
  28. package/dist/cohere.js.map +1 -1
  29. package/dist/config-loader.d.cts.map +1 -1
  30. package/dist/config-loader.d.ts.map +1 -1
  31. package/dist/fixture-loader.cjs +103 -2
  32. package/dist/fixture-loader.cjs.map +1 -1
  33. package/dist/fixture-loader.d.cts.map +1 -1
  34. package/dist/fixture-loader.d.ts.map +1 -1
  35. package/dist/fixture-loader.js +104 -4
  36. package/dist/fixture-loader.js.map +1 -1
  37. package/dist/gemini-interactions.cjs +63 -121
  38. package/dist/gemini-interactions.cjs.map +1 -1
  39. package/dist/gemini-interactions.d.cts.map +1 -1
  40. package/dist/gemini-interactions.d.ts.map +1 -1
  41. package/dist/gemini-interactions.js +65 -123
  42. package/dist/gemini-interactions.js.map +1 -1
  43. package/dist/gemini.cjs +242 -155
  44. package/dist/gemini.cjs.map +1 -1
  45. package/dist/gemini.d.cts.map +1 -1
  46. package/dist/gemini.d.ts.map +1 -1
  47. package/dist/gemini.js +244 -157
  48. package/dist/gemini.js.map +1 -1
  49. package/dist/helpers.cjs +400 -7
  50. package/dist/helpers.cjs.map +1 -1
  51. package/dist/helpers.d.cts +51 -1
  52. package/dist/helpers.d.cts.map +1 -1
  53. package/dist/helpers.d.ts +51 -1
  54. package/dist/helpers.d.ts.map +1 -1
  55. package/dist/helpers.js +391 -8
  56. package/dist/helpers.js.map +1 -1
  57. package/dist/index.cjs +4 -2
  58. package/dist/index.d.cts +4 -4
  59. package/dist/index.d.ts +4 -4
  60. package/dist/index.js +4 -4
  61. package/dist/mcp-fakes.d.cts.map +1 -1
  62. package/dist/messages.cjs +159 -141
  63. package/dist/messages.cjs.map +1 -1
  64. package/dist/messages.d.cts.map +1 -1
  65. package/dist/messages.d.ts.map +1 -1
  66. package/dist/messages.js +161 -143
  67. package/dist/messages.js.map +1 -1
  68. package/dist/misbehavior.cjs +62 -3
  69. package/dist/misbehavior.cjs.map +1 -1
  70. package/dist/misbehavior.d.cts.map +1 -1
  71. package/dist/misbehavior.d.ts.map +1 -1
  72. package/dist/misbehavior.js +63 -4
  73. package/dist/misbehavior.js.map +1 -1
  74. package/dist/ndjson-writer.cjs +21 -23
  75. package/dist/ndjson-writer.cjs.map +1 -1
  76. package/dist/ndjson-writer.d.cts +0 -1
  77. package/dist/ndjson-writer.d.cts.map +1 -1
  78. package/dist/ndjson-writer.d.ts +0 -1
  79. package/dist/ndjson-writer.d.ts.map +1 -1
  80. package/dist/ndjson-writer.js +21 -24
  81. package/dist/ndjson-writer.js.map +1 -1
  82. package/dist/ollama.cjs +153 -147
  83. package/dist/ollama.cjs.map +1 -1
  84. package/dist/ollama.d.cts.map +1 -1
  85. package/dist/ollama.d.ts.map +1 -1
  86. package/dist/ollama.js +155 -149
  87. package/dist/ollama.js.map +1 -1
  88. package/dist/openrouter-chat.cjs +1 -1
  89. package/dist/openrouter-chat.js +1 -1
  90. package/dist/raw-validation.cjs +177 -0
  91. package/dist/raw-validation.cjs.map +1 -0
  92. package/dist/raw-validation.js +176 -0
  93. package/dist/raw-validation.js.map +1 -0
  94. package/dist/response/deliver.cjs +144 -0
  95. package/dist/response/deliver.cjs.map +1 -0
  96. package/dist/response/deliver.js +143 -0
  97. package/dist/response/deliver.js.map +1 -0
  98. package/dist/response/frame-loop.cjs +102 -0
  99. package/dist/response/frame-loop.cjs.map +1 -0
  100. package/dist/response/frame-loop.js +100 -0
  101. package/dist/response/frame-loop.js.map +1 -0
  102. package/dist/response/lifecycle.cjs +223 -0
  103. package/dist/response/lifecycle.cjs.map +1 -0
  104. package/dist/response/lifecycle.js +211 -0
  105. package/dist/response/lifecycle.js.map +1 -0
  106. package/dist/response/proxy.cjs +71 -0
  107. package/dist/response/proxy.cjs.map +1 -0
  108. package/dist/response/proxy.js +70 -0
  109. package/dist/response/proxy.js.map +1 -0
  110. package/dist/response/raw.cjs +228 -0
  111. package/dist/response/raw.cjs.map +1 -0
  112. package/dist/response/raw.js +227 -0
  113. package/dist/response/raw.js.map +1 -0
  114. package/dist/response/timing.cjs +157 -0
  115. package/dist/response/timing.cjs.map +1 -0
  116. package/dist/response/timing.js +150 -0
  117. package/dist/response/timing.js.map +1 -0
  118. package/dist/responses.cjs +269 -148
  119. package/dist/responses.cjs.map +1 -1
  120. package/dist/responses.d.cts.map +1 -1
  121. package/dist/responses.d.ts.map +1 -1
  122. package/dist/responses.js +270 -149
  123. package/dist/responses.js.map +1 -1
  124. package/dist/router.cjs +20 -4
  125. package/dist/router.cjs.map +1 -1
  126. package/dist/router.d.cts +7 -1
  127. package/dist/router.d.cts.map +1 -1
  128. package/dist/router.d.ts +7 -1
  129. package/dist/router.d.ts.map +1 -1
  130. package/dist/router.js +21 -5
  131. package/dist/router.js.map +1 -1
  132. package/dist/server.cjs +89 -120
  133. package/dist/server.cjs.map +1 -1
  134. package/dist/server.d.cts.map +1 -1
  135. package/dist/server.d.ts.map +1 -1
  136. package/dist/server.js +91 -122
  137. package/dist/server.js.map +1 -1
  138. package/dist/sse-writer.cjs +49 -30
  139. package/dist/sse-writer.cjs.map +1 -1
  140. package/dist/sse-writer.d.cts +0 -1
  141. package/dist/sse-writer.d.cts.map +1 -1
  142. package/dist/sse-writer.d.ts +0 -1
  143. package/dist/sse-writer.d.ts.map +1 -1
  144. package/dist/sse-writer.js +49 -31
  145. package/dist/sse-writer.js.map +1 -1
  146. package/dist/transcription.cjs +19 -33
  147. package/dist/transcription.cjs.map +1 -1
  148. package/dist/transcription.d.cts.map +1 -1
  149. package/dist/transcription.d.ts.map +1 -1
  150. package/dist/transcription.js +20 -34
  151. package/dist/transcription.js.map +1 -1
  152. package/dist/types.cjs.map +1 -1
  153. package/dist/types.d.cts +173 -10
  154. package/dist/types.d.cts.map +1 -1
  155. package/dist/types.d.ts +173 -10
  156. package/dist/types.d.ts.map +1 -1
  157. package/dist/types.js.map +1 -1
  158. package/dist/ws-framing.cjs +12 -6
  159. package/dist/ws-framing.cjs.map +1 -1
  160. package/dist/ws-framing.d.cts +2 -0
  161. package/dist/ws-framing.d.cts.map +1 -1
  162. package/dist/ws-framing.d.ts +2 -0
  163. package/dist/ws-framing.d.ts.map +1 -1
  164. package/dist/ws-framing.js +12 -6
  165. package/dist/ws-framing.js.map +1 -1
  166. package/dist/ws-gemini-live.cjs +120 -93
  167. package/dist/ws-gemini-live.cjs.map +1 -1
  168. package/dist/ws-gemini-live.js +120 -93
  169. package/dist/ws-gemini-live.js.map +1 -1
  170. package/dist/ws-realtime.cjs +80 -70
  171. package/dist/ws-realtime.cjs.map +1 -1
  172. package/dist/ws-realtime.d.cts.map +1 -1
  173. package/dist/ws-realtime.d.ts.map +1 -1
  174. package/dist/ws-realtime.js +80 -70
  175. package/dist/ws-realtime.js.map +1 -1
  176. package/dist/ws-responses.cjs +72 -53
  177. package/dist/ws-responses.cjs.map +1 -1
  178. package/dist/ws-responses.d.cts.map +1 -1
  179. package/dist/ws-responses.d.ts.map +1 -1
  180. package/dist/ws-responses.js +73 -54
  181. package/dist/ws-responses.js.map +1 -1
  182. package/package.json +1 -1
  183. package/skills/write-fixtures/SKILL.md +34 -2
@@ -1 +1 @@
1
- {"version":3,"file":"ws-gemini-live.js","names":[],"sources":["../src/ws-gemini-live.ts"],"sourcesContent":["/**\n * WebSocket handler for Gemini Live BidiGenerateContent API.\n *\n * Accepts setup, clientContent, and toolResponse messages over WebSocket\n * and responds with setupComplete, serverContent, toolCall, and error\n * messages in the Gemini Live streaming format.\n */\nimport { resolveRequestDecisions, strictOverrideField } from \"./response/policy.js\";\n\nimport type {\n Fixture,\n ChatMessage,\n ChatCompletionRequest,\n ToolDefinition,\n AudioResponse,\n ToolCall,\n FixtureFileBlock,\n JournalEntry,\n HandlerDefaults,\n} from \"./types.js\";\nimport { planMisbehavior, recordMisbehaviorOutcome, type MisbehaviorPlan } from \"./misbehavior.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport {\n fixtureToolCallErrorCode,\n googleFixtureToolCallErrorDetails,\n isFixtureToolCallError,\n journalFixtureToolCallError,\n rejectResponsesOnlyToolCalls,\n requireServedFunctionToolCalls,\n toolArgsForWire,\n runWithToolArgumentsScope,\n servedToolArgs,\n strictToolArgumentsEnabled,\n isTextResponse,\n isToolCallResponse,\n isCombinedFixtureResponse,\n isErrorResponse,\n isAudioResponse,\n flattenHeaders,\n formatToMime,\n generateToolCallId,\n resolveFixtureBlocks,\n resolveFixtureBlockOutcome,\n resolveResponse,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay, calculateDelay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\n// ─── Gemini Live protocol types ─────────────────────────────────────────────\n\ninterface GeminiLivePart {\n text?: string;\n thought?: boolean;\n functionCall?: { name: string; args: Record<string, unknown> };\n functionResponse?: { name: string; response: unknown; id?: string };\n inlineData?: { mimeType: string; data: string };\n}\n\ninterface GeminiLiveTurn {\n role: string;\n parts: GeminiLivePart[];\n}\n\ninterface GeminiLiveFunctionDeclaration {\n name: string;\n description?: string;\n parameters?: object;\n parametersJsonSchema?: object;\n}\n\ninterface GeminiLiveToolDef {\n functionDeclarations?: GeminiLiveFunctionDeclaration[];\n}\n\ninterface GeminiLiveSetup {\n model?: string;\n generationConfig?: Record<string, unknown>;\n tools?: GeminiLiveToolDef[];\n}\n\ninterface GeminiLiveClientContent {\n turns: GeminiLiveTurn[];\n turnComplete?: boolean;\n}\n\ninterface GeminiLiveFunctionResponse {\n id?: string;\n name: string;\n response: unknown;\n}\n\ninterface GeminiLiveToolResponse {\n functionResponses: GeminiLiveFunctionResponse[];\n}\n\ninterface GeminiLiveMessage {\n setup?: GeminiLiveSetup;\n config?: GeminiLiveSetup;\n clientContent?: GeminiLiveClientContent;\n toolResponse?: GeminiLiveToolResponse;\n}\n\n// ─── Session state ──────────────────────────────────────────────────────────\n\ninterface SessionState {\n setupDone: boolean;\n model: string;\n tools: ToolDefinition[];\n conversationHistory: ChatMessage[];\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nconst WS_PATH = \"/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent\";\n\n/**\n * Map HTTP status codes to gRPC error codes.\n * Gemini Live uses gRPC codes, not HTTP status codes.\n */\nfunction httpToGrpc(httpCode: number): number {\n switch (httpCode) {\n case 400:\n return 3; // INVALID_ARGUMENT\n case 401:\n return 16; // UNAUTHENTICATED\n case 403:\n return 7; // PERMISSION_DENIED\n case 404:\n return 5; // NOT_FOUND\n case 409:\n return 10; // ABORTED\n case 429:\n return 8; // RESOURCE_EXHAUSTED\n case 501:\n return 12; // UNIMPLEMENTED\n case 503:\n return 14; // UNAVAILABLE\n default:\n return 13; // INTERNAL\n }\n}\n\n/**\n * Convert Gemini Live turns into ChatMessage[] for fixture matching.\n */\nfunction geminiTurnsToMessages(turns: GeminiLiveTurn[], logger?: Logger): ChatMessage[] {\n const messages: ChatMessage[] = [];\n\n for (const turn of turns) {\n const role = turn.role ?? \"user\";\n\n if (role === \"user\") {\n const funcResponses = turn.parts.filter((p) => p.functionResponse);\n // inlineData parts (e.g. client audio input) are silently skipped —\n // only text and functionResponse parts are relevant for fixture matching.\n const textParts = turn.parts.filter((p) => p.text !== undefined && !p.thought);\n\n if (funcResponses.length > 0) {\n for (let i = 0; i < funcResponses.length; i++) {\n const part = funcResponses[i];\n const fr = part.functionResponse!;\n messages.push({\n role: \"tool\",\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? generateToolCallId(),\n });\n }\n if (textParts.length > 0) {\n messages.push({\n role: \"user\",\n content: textParts.map((p) => p.text!).join(\"\"),\n });\n }\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"user\", content: text });\n }\n } else if (role === \"model\") {\n const funcCalls = turn.parts.filter((p) => p.functionCall);\n const textParts = turn.parts.filter((p) => p.text !== undefined && !p.thought);\n\n if (funcCalls.length > 0) {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({\n role: \"assistant\",\n content: text || null,\n tool_calls: funcCalls.map((p) => ({\n id: generateToolCallId(),\n type: \"function\" as const,\n function: {\n name: p.functionCall!.name,\n arguments: JSON.stringify(p.functionCall!.args ?? {}),\n },\n })),\n });\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"assistant\", content: text });\n }\n } else {\n logger?.warn(`[gemini-live] skipping turn with unrecognized role: ${role}`);\n }\n }\n\n return messages;\n}\n\n/**\n * Convert toolResponse messages into ChatMessage[] for fixture matching.\n */\nfunction toolResponseToMessages(toolResponse: GeminiLiveToolResponse): ChatMessage[] {\n return toolResponse.functionResponses.map((fr) => ({\n role: \"tool\" as const,\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? generateToolCallId(),\n }));\n}\n\n/**\n * Convert Gemini tool definitions to ChatCompletion ToolDefinition[].\n */\nfunction convertTools(geminiTools?: GeminiLiveToolDef[]): ToolDefinition[] {\n if (!geminiTools || geminiTools.length === 0) return [];\n const decls = geminiTools.flatMap((t) => t.functionDeclarations ?? []);\n return decls.map((d) => ({\n type: \"function\" as const,\n function: {\n name: d.name,\n description: d.description,\n parameters: d.parameters ?? d.parametersJsonSchema,\n },\n }));\n}\n\nfunction liveToolArguments(tc: Pick<ToolCall, \"name\" | \"arguments\">) {\n return servedToolArgs(tc, \"object\").value;\n}\n\nconst GEMINI_LIVE_WIRE = \"Gemini Live\";\n\n/** Prepare native object arguments and IDs once for both output and the journal. */\nfunction prepareLiveMisbehavior(plan: MisbehaviorPlan): MisbehaviorPlan {\n if (plan.summary.fault === \"tool-args-invalid-json\" || plan.summary.fault === \"empty-response\") {\n return { ...plan, summary: { ...plan.summary, servedToolCalls: [] } };\n }\n const response = plan.response;\n const combined = isCombinedFixtureResponse(response);\n const outcome =\n combined && response.blocks?.length ? resolveFixtureBlockOutcome(response.blocks) : undefined;\n const calls =\n outcome?.toolCalls ??\n (combined || isToolCallResponse(response) ? (response.toolCalls ?? []) : []);\n const duplicate = plan.duplicateId;\n const toolCalls = calls.map((call, index) => {\n const args = toolArgsForWire(call);\n // Preserve authored-invalid arguments for the existing journaled preflight error.\n return {\n ...call,\n arguments: args.kind === \"parsed\" ? args.text : args.raw,\n ...(index === duplicate?.destinationIndex ? {} : { id: call.id ?? generateToolCallId() }),\n };\n });\n if (duplicate) toolCalls[duplicate.destinationIndex].id = toolCalls[duplicate.sourceIndex].id;\n let preparedResponse = { ...response };\n if (\"usage\" in preparedResponse) delete preparedResponse.usage;\n if (outcome) {\n let index = 0;\n const blocks = outcome.ordered.map((block) =>\n block.type === \"text\" ? { ...block } : { ...block, ...toolCalls[index++] },\n );\n preparedResponse = { ...preparedResponse, content: outcome.content, toolCalls, blocks };\n } else if (combined || isToolCallResponse(response)) {\n preparedResponse = { ...preparedResponse, toolCalls };\n }\n return {\n ...plan,\n response: preparedResponse,\n summary: {\n ...plan.summary,\n servedToolCalls: toolCalls.map(({ name, arguments: args, id }) => ({\n name,\n arguments: args,\n id,\n })),\n },\n };\n}\n\n/**\n * Validate the whole selected response before audio, text, or any tool is\n * sent: every entry is validated, custom tool calls are rejected, and every\n * argument string must parse. Returns the calls narrowed to function calls. On\n * failure the journal entry becomes a 500, and the outer message handler sends\n * code 13 with `google.rpc.ErrorInfo` details carrying the aimock code, drops\n * this turn from the history, and leaves the socket open.\n */\nfunction preflightToolArguments(\n response: { toolCalls?: ToolCall[] },\n journalEntry: JournalEntry,\n): ToolCall[] {\n return journalFixtureToolCallError(journalEntry, () => {\n const functionCalls = requireServedFunctionToolCalls(response, GEMINI_LIVE_WIRE);\n if (strictToolArgumentsEnabled()) for (const tc of functionCalls) liveToolArguments(tc);\n return functionCalls;\n });\n}\n\n/** Resolve ordered blocks (rejecting custom tool calls) and preflight their arguments. */\nfunction preflightBlocks(\n response: { blocks?: FixtureFileBlock[]; customToolCalls?: unknown },\n journalEntry: JournalEntry,\n) {\n return journalFixtureToolCallError(journalEntry, () => {\n rejectResponsesOnlyToolCalls(response, GEMINI_LIVE_WIRE);\n const resolved = resolveFixtureBlocks(response.blocks ?? []);\n if (strictToolArgumentsEnabled())\n for (const block of resolved) if (block.type === \"toolCall\") liveToolArguments(block);\n return resolved;\n });\n}\n\n// ─── Main handler ───────────────────────────────────────────────────────────\n\nexport function handleWebSocketGeminiLive(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n strictToolArguments?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n misbehavior?: HandlerDefaults[\"misbehavior\"];\n misbehaviorCounters?: HandlerDefaults[\"misbehaviorCounters\"];\n registry?: HandlerDefaults[\"registry\"];\n /** @internal Called before each message is processed (the server assigns fixture positions). */\n beforeProcessMessage?: () => void;\n },\n): void {\n const { logger } = defaults;\n const session: SessionState = {\n setupDone: false,\n model: defaults.model,\n tools: [],\n conversationHistory: [],\n };\n\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(async () => {\n // Messages are processed one at a time, so this marks where this\n // message's turn begins in the history.\n const historyMark = session.conversationHistory.length;\n try {\n defaults.beforeProcessMessage?.();\n await runWithToolArgumentsScope(\n { strict: defaults.strictToolArguments === true, logger },\n () => processMessage(raw, ws, fixtures, journal, defaults, session),\n );\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket Gemini Live error: ${msg}`);\n const fixtureToolCallError = isFixtureToolCallError(err);\n // A fixture rejected by a tool-call check serves nothing, so its turn\n // must not stay in the history as a user turn with no reply. Only\n // fixture tool-call errors roll back: any other error (for example a\n // malformed text block) leaves the user turn in the history.\n if (fixtureToolCallError) session.conversationHistory.length = historyMark;\n try {\n ws.send(\n JSON.stringify({\n error: {\n code: 13,\n message: msg,\n status: \"INTERNAL\",\n ...(fixtureToolCallError\n ? { details: googleFixtureToolCallErrorDetails(fixtureToolCallErrorCode(err)) }\n : {}),\n },\n }),\n );\n } catch (sendErr) {\n defaults.logger.debug(\n `Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : \"unknown\"}`,\n );\n }\n }\n });\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n misbehavior?: HandlerDefaults[\"misbehavior\"];\n misbehaviorCounters?: HandlerDefaults[\"misbehaviorCounters\"];\n registry?: HandlerDefaults[\"registry\"];\n },\n session: SessionState,\n): Promise<void> {\n let parsed: GeminiLiveMessage;\n try {\n parsed = JSON.parse(raw) as GeminiLiveMessage;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n ws.send(\n JSON.stringify({\n error: { code: 3, message: `Malformed JSON: ${detail}`, status: \"INVALID_ARGUMENT\" },\n }),\n );\n return;\n }\n\n // Handle setup message (accept both `setup` and `config` as aliases)\n const setupMsg = parsed.setup ?? parsed.config;\n if (setupMsg) {\n session.setupDone = true;\n session.model = setupMsg.model ?? defaults.model;\n session.tools = convertTools(setupMsg.tools);\n ws.send(JSON.stringify({ setupComplete: {} }));\n return;\n }\n\n // Reject messages before setup\n if (!session.setupDone) {\n ws.send(\n JSON.stringify({\n error: { code: 9, message: \"Setup required\", status: \"FAILED_PRECONDITION\" },\n }),\n );\n return;\n }\n\n // Build messages from this interaction\n let newMessages: ChatMessage[];\n\n if (parsed.clientContent) {\n if (!parsed.clientContent.turns || !Array.isArray(parsed.clientContent.turns)) {\n ws.send(\n JSON.stringify({\n error: {\n code: 3,\n message: \"Missing 'turns' in clientContent\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = geminiTurnsToMessages(parsed.clientContent.turns, defaults.logger);\n } else if (parsed.toolResponse) {\n if (\n !parsed.toolResponse.functionResponses ||\n !Array.isArray(parsed.toolResponse.functionResponses)\n ) {\n ws.send(\n JSON.stringify({\n error: {\n code: 3,\n message: \"Missing 'functionResponses' in toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = toolResponseToMessages(parsed.toolResponse);\n } else {\n ws.send(\n JSON.stringify({\n error: {\n code: 3,\n message: \"Expected clientContent or toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Build completion request for fixture matching (include new messages speculatively)\n const geminiContextHeader = defaults.upgradeHeaders?.[\"x-aimock-context\"];\n const geminiContext =\n typeof geminiContextHeader === \"string\"\n ? geminiContextHeader\n : Array.isArray(geminiContextHeader) && geminiContextHeader.length > 0\n ? geminiContextHeader[0]\n : undefined;\n\n const completionReq: ChatCompletionRequest = {\n model: session.model,\n messages: [...session.conversationHistory, ...newMessages],\n stream: true,\n tools: session.tools.length > 0 ? session.tools : undefined,\n _endpointType: \"chat\",\n _context: geminiContext,\n };\n\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = WS_PATH;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveRequestDecisions(defaults.strict, defaults.upgradeHeaders).fixtureMiss) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(\"WS\", path, skippedBySequenceOrTurn));\n journal.add({\n method: \"WS\",\n path,\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.close(1008, strictMessage);\n return;\n }\n journal.add({\n method: \"WS\",\n path,\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.send(\n JSON.stringify({\n error: { code: 5, message: \"No fixture matched\", status: \"NOT_FOUND\" },\n }),\n );\n return;\n }\n\n // Commit messages to conversation history only after successful fixture match\n session.conversationHistory.push(...newMessages);\n\n let response = await resolveResponse(fixture, completionReq);\n const plannerDefaults = { ...defaults, replaySpeed: defaults.replaySpeed ?? 1 };\n let misbehavior = planMisbehavior({\n wire: \"gemini-live\",\n emitsToolCallIds: true,\n fixture,\n response,\n request: completionReq,\n stream: true,\n defaults: plannerDefaults,\n rawHeaders: defaults.upgradeHeaders ?? {},\n // The server resolves both header and query-string session scopes.\n url: `${path}?testId=${encodeURIComponent(testId)}`,\n });\n if (misbehavior.kind === \"applied\") {\n misbehavior = prepareLiveMisbehavior(misbehavior);\n response = misbehavior.response;\n }\n const addResponseEntry = (status: number) => {\n const entry = journal.add({\n method: \"WS\",\n path,\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status, fixture },\n });\n recordMisbehaviorOutcome({\n entry,\n summary: misbehavior.summary,\n defaults: plannerDefaults,\n testId,\n });\n return entry;\n };\n if (misbehavior.kind === \"error\") {\n addResponseEntry(misbehavior.status);\n if (!misbehavior.summary?.evaluations.length) {\n defaults.logger.error(`[misbehavior] ${misbehavior.message} (testId=${testId})`);\n }\n ws.send(\n JSON.stringify({\n error: {\n code: httpToGrpc(misbehavior.status),\n message: misbehavior.message,\n status: misbehavior.status === 400 ? \"INVALID_ARGUMENT\" : \"UNIMPLEMENTED\",\n },\n }),\n );\n return;\n }\n const latency = fixture.latency ?? defaults.latency;\n if (\n misbehavior.kind === \"applied\" &&\n (misbehavior.summary.fault === \"tool-args-invalid-json\" ||\n misbehavior.summary.fault === \"empty-response\")\n ) {\n const journalEntry = addResponseEntry(200);\n const interruption = createInterruptionSignal(fixture);\n const markInterrupted = () => {\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n ws.destroy();\n };\n try {\n if (ws.isClosed) return;\n const chunkDelay = calculateDelay(\n 0,\n undefined,\n latency,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);\n if (interruption?.signal.aborted) {\n markInterrupted();\n return;\n }\n if (ws.isClosed) return;\n const malformed = misbehavior.summary.fault === \"tool-args-invalid-json\";\n ws.send(\n JSON.stringify({\n serverContent: malformed\n ? { turnComplete: true, turnCompleteReason: \"MALFORMED_FUNCTION_CALL\" }\n : { modelTurn: { parts: [{ text: \"\" }] } },\n }),\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n markInterrupted();\n return;\n }\n // Like ordinary text streaming, completion follows the counted content chunk.\n if (!malformed && !ws.isClosed) {\n ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));\n session.conversationHistory.push({ role: \"assistant\", content: \"\" });\n }\n } finally {\n interruption?.cleanup();\n }\n return;\n }\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n addResponseEntry(status);\n ws.send(\n JSON.stringify({\n error: {\n code: httpToGrpc(status),\n message: response.error.message,\n status: response.error.type ?? \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // Audio response — an AUDIO-modality model turn, plus the companion\n // modalities `AudioResponse` documents (types.ts) as preserved alongside it.\n //\n // COMPANION PLACEMENT is protocol-specific, and the Live protocol does NOT\n // place a tool call where the HTTP protocol does. On `generateContent` a\n // functionCall is a PART, so gemini.ts's `buildGeminiAudioParts` appends it to\n // the same `content.parts` array as the audio. On Live, `serverContent` and\n // `toolCall` are alternatives of ONE union: `@google/genai`'s\n // `LiveServerMessage` declares them as sibling optional fields and the\n // BidiGenerateContent reference states the response's `messageType` union\n // \"can be only one of the following\". A turn that both speaks and calls a\n // function is therefore TWO messages, not one part list —\n //\n // serverContent{modelTurn.parts} → toolCall{functionCalls} → serverContent{turnComplete}\n //\n // — which is also the order every other branch of this handler already emits\n // and the order the live provider was observed to use (its tool turn sent a\n // `serverContent` strictly before the `toolCall`).\n //\n // Emitting only the audio and returning — the previous behavior — silently\n // discarded the tool call and the text content of a recorded audio turn, the\n // exact loss types.ts's AudioResponse companions exist to prevent.\n //\n // NOT handled here: `AudioResponse.reasoning`. This handler has no thought\n // channel in ANY branch (nothing in it emits `thought: true`), so replaying\n // reasoning from the audio branch alone would invent a Live-wide capability\n // out of one fixture field. That gap is real but separate.\n if (isAudioResponse(response)) {\n const journalEntry = addResponseEntry(200);\n\n const audioResp = response as AudioResponse;\n const audioToolCalls = preflightToolArguments(audioResp, journalEntry);\n let mimeType: string;\n let data: string;\n\n if (typeof audioResp.audio === \"string\") {\n mimeType = formatToMime(audioResp.format ?? \"mp3\");\n data = audioResp.audio;\n } else {\n mimeType = audioResp.audio.contentType ?? \"audio/mpeg\";\n data = audioResp.audio.b64Json;\n }\n\n // Part order mirrors gemini.ts's `buildGeminiAudioParts`: audio first, then\n // the text companion.\n const parts: GeminiLivePart[] = [{ inlineData: { mimeType, data } }];\n if (audioResp.content) {\n parts.push({ text: audioResp.content });\n }\n\n // Stable IDs resolved once, so the wire message and conversation history\n // agree (same contract as the tool-call branches below).\n const resolvedToolCalls = audioToolCalls.map((tc) => ({\n ...tc,\n resolvedId: tc.id ?? generateToolCallId(),\n }));\n\n if (resolvedToolCalls.length === 0) {\n ws.send(\n JSON.stringify({\n serverContent: { modelTurn: { parts }, turnComplete: true },\n }),\n );\n } else {\n ws.send(JSON.stringify({ serverContent: { modelTurn: { parts } } }));\n\n if (!ws.isClosed) {\n const functionCalls = resolvedToolCalls.map((tc) => {\n const argsObj = liveToolArguments(tc);\n return { name: tc.name, args: argsObj, id: tc.resolvedId };\n });\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n }\n\n if (!ws.isClosed) {\n ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));\n }\n }\n\n session.conversationHistory.push({\n role: \"assistant\",\n content: audioResp.content ?? \"[audio]\",\n ...(resolvedToolCalls.length > 0\n ? {\n tool_calls: resolvedToolCalls.map((tc) => ({\n id: tc.resolvedId,\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments },\n })),\n }\n : {}),\n });\n return;\n }\n\n // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n if (isCombinedFixtureResponse(response)) {\n const journalEntry = addResponseEntry(200);\n\n // BLOCKS path (#274): when the fixture carries an ordered `blocks` array,\n // honor it instead of the legacy `content ?? \"\"` / `toolCalls ?? []` path.\n // The Gemini Live WS protocol expresses ordering via SEQUENTIAL messages —\n // a text block becomes one-or-more `serverContent.modelTurn.parts[{text}]`\n // messages and a toolCall block becomes a `toolCall.functionCalls` message,\n // so emitting in array order faithfully reproduces tool-before-text (or any\n // interleaving), matching the HTTP gemini.ts blocks branch. Without this, a\n // blocks-only fixture (post-F0 it matches this guard) would stream an EMPTY\n // payload — a silent drop. Legacy fixtures (no `blocks`) skip this entirely.\n if (response.blocks && response.blocks.length > 0) {\n const resolvedBlocks = preflightBlocks(response, journalEntry);\n const interruption = createInterruptionSignal(fixture);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n const { recordedTimings } = fixture;\n let chunkIndex = 0;\n let interrupted = false;\n\n // Accumulate the equivalent assistant turn for conversation history, so a\n // follow-up turn sees the same content + tool_calls as the legacy path.\n let historyContent = \"\";\n const historyToolCalls: {\n id: string;\n type: \"function\";\n function: { name: string; arguments: string };\n }[] = [];\n\n outer: for (const block of resolvedBlocks) {\n if (block.type === \"toolCall\") {\n if (ws.isClosed) break;\n const tcDelay = calculateDelay(\n chunkIndex,\n undefined,\n latency,\n recordedTimings,\n replaySpeed,\n );\n if (tcDelay > 0) await delay(tcDelay, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n\n const resolvedId = block.id ?? generateToolCallId();\n const argsObj = liveToolArguments(block);\n\n try {\n ws.send(\n JSON.stringify({\n toolCall: { functionCalls: [{ name: block.name, args: argsObj, id: resolvedId }] },\n }),\n );\n } catch (err) {\n defaults.logger.debug(\n \"[gemini-live] send failed during blocks streaming, closing\",\n err,\n );\n break;\n }\n chunkIndex++;\n historyToolCalls.push({\n id: resolvedId,\n type: \"function\" as const,\n function: { name: block.name, arguments: block.arguments },\n });\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n } else {\n const text = block.text;\n historyContent += text;\n if (text.length === 0) {\n // An empty text block carries no wire content, so emit nothing and\n // spend no chunk: the old guard sent a useless empty `modelTurn`\n // message and `continue`d WITHOUT `chunkIndex++`/`interruption.tick()`,\n // which leaked the next block past `truncateAfterChunks` and shifted\n // `recordedTimings` indexing for every following block. Skipping\n // keeps non-empty-block output byte-identical and the chunk/timing\n // accounting correct.\n continue;\n }\n for (let i = 0; i < text.length; i += chunkSize) {\n if (ws.isClosed) break outer;\n const chunkDelay = calculateDelay(\n chunkIndex,\n undefined,\n latency,\n recordedTimings,\n replaySpeed,\n );\n if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break outer;\n }\n if (ws.isClosed) break outer;\n try {\n ws.send(\n JSON.stringify({\n serverContent: { modelTurn: { parts: [{ text: text.slice(i, i + chunkSize) }] } },\n }),\n );\n } catch (err) {\n defaults.logger.debug(\n \"[gemini-live] send failed during blocks streaming, closing\",\n err,\n );\n break outer;\n }\n chunkIndex++;\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break outer;\n }\n }\n }\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n // Send turnComplete\n if (!ws.isClosed) {\n ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));\n }\n\n session.conversationHistory.push({\n role: \"assistant\",\n content: historyContent || null,\n ...(historyToolCalls.length > 0 ? { tool_calls: historyToolCalls } : {}),\n });\n return;\n }\n\n const functionToolCalls = preflightToolArguments(response, journalEntry);\n const content = response.content ?? \"\";\n const chunkList: string[] = [];\n for (let i = 0; i < content.length; i += chunkSize) {\n chunkList.push(content.slice(i, i + chunkSize));\n }\n\n const interruption = createInterruptionSignal(fixture);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n const { recordedTimings } = fixture;\n let interrupted = false;\n\n // Stream text content chunks (turnComplete omitted — sent as a separate message later)\n if (content.length === 0) {\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: \"\" }] },\n },\n }),\n );\n }\n } else {\n for (let i = 0; i < chunkList.length; i++) {\n if (ws.isClosed) break;\n const chunkDelay = calculateDelay(i, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n\n try {\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: chunkList[i] }] },\n },\n }),\n );\n } catch (err) {\n defaults.logger.debug(\"[gemini-live] send failed during text streaming, closing\", err);\n break;\n }\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n // Pre-compute tool calls with stable IDs so wire message and history match\n const resolvedToolCalls = functionToolCalls.map((tc) => ({\n ...tc,\n resolvedId: tc.id ?? generateToolCallId(),\n }));\n\n // Send tool calls\n if (!ws.isClosed) {\n const tcDelay = calculateDelay(\n chunkList.length,\n undefined,\n latency,\n recordedTimings,\n replaySpeed,\n );\n if (tcDelay > 0) await delay(tcDelay, interruption?.signal);\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n const functionCalls = resolvedToolCalls.map((tc) => {\n const argsObj = liveToolArguments(tc);\n return {\n name: tc.name,\n args: argsObj,\n id: tc.resolvedId,\n };\n });\n\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n interruption?.tick();\n }\n\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n // Send turnComplete\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n }\n\n // Add to conversation history using the same resolved IDs from the wire message\n session.conversationHistory.push({\n role: \"assistant\",\n content: content || null,\n tool_calls: resolvedToolCalls.map((tc) => ({\n id: tc.resolvedId,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n })),\n });\n return;\n }\n\n // Text response — stream chunks with serverContent\n if (isTextResponse(response)) {\n const journalEntry = addResponseEntry(200);\n\n const content = response.content;\n\n if (content.length === 0) {\n if (ws.isClosed) return;\n // Empty content: send empty modelTurn, then separate turnComplete\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: \"\" }] },\n },\n }),\n );\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n return;\n }\n\n // Chunk the content\n const chunks: string[] = [];\n for (let i = 0; i < content.length; i += chunkSize) {\n chunks.push(content.slice(i, i + chunkSize));\n }\n\n const interruption = createInterruptionSignal(fixture);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n const { recordedTimings } = fixture;\n let interrupted = false;\n\n // Stream content chunks without turnComplete (sent separately after)\n for (let i = 0; i < chunks.length; i++) {\n if (ws.isClosed) break;\n const chunkDelay = calculateDelay(i, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n\n try {\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: chunks[i] }] },\n },\n }),\n );\n } catch (err) {\n defaults.logger.debug(\"[gemini-live] send failed during text streaming, closing\", err);\n break;\n }\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n // Send separate turnComplete message\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n }\n\n // Add assistant response to conversation history\n session.conversationHistory.push({ role: \"assistant\", content });\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = addResponseEntry(200);\n\n const functionToolCalls = preflightToolArguments(response, journalEntry);\n\n const interruption = createInterruptionSignal(fixture);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n const { recordedTimings } = fixture;\n\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n const tcDelay = calculateDelay(0, undefined, latency, recordedTimings, replaySpeed);\n if (tcDelay > 0) await delay(tcDelay, interruption?.signal);\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n\n // Pre-compute tool calls with stable IDs so wire message and history match\n const resolvedToolCalls = functionToolCalls.map((tc) => ({\n ...tc,\n resolvedId: tc.id ?? generateToolCallId(),\n }));\n\n const functionCalls = resolvedToolCalls.map((tc) => {\n const argsObj = liveToolArguments(tc);\n return {\n name: tc.name,\n args: argsObj,\n id: tc.resolvedId,\n };\n });\n\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n interruption?.tick();\n\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n // Send turnComplete after tool call\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n }\n\n // Add assistant tool_calls to conversation history using the same resolved IDs\n session.conversationHistory.push({\n role: \"assistant\",\n content: null,\n tool_calls: resolvedToolCalls.map((tc) => ({\n id: tc.resolvedId,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n })),\n });\n return;\n }\n\n // Unknown response type\n addResponseEntry(500);\n ws.send(\n JSON.stringify({\n error: {\n code: 13,\n message: \"Fixture response did not match any known type\",\n status: \"INTERNAL\",\n },\n }),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ws-gemini-live.js","names":[],"sources":["../src/ws-gemini-live.ts"],"sourcesContent":["/**\n * WebSocket handler for Gemini Live BidiGenerateContent API.\n *\n * Accepts setup, clientContent, and toolResponse messages over WebSocket\n * and responds with setupComplete, serverContent, toolCall, and error\n * messages in the Gemini Live streaming format.\n */\nimport { resolveRequestDecisions, strictOverrideField } from \"./response/policy.js\";\n\nimport type {\n Fixture,\n ChatMessage,\n ChatCompletionRequest,\n ToolDefinition,\n AudioResponse,\n ToolCall,\n FixtureFileBlock,\n JournalEntry,\n HandlerDefaults,\n} from \"./types.js\";\nimport { planMisbehavior, recordMisbehaviorOutcome, type MisbehaviorPlan } from \"./misbehavior.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport {\n fixtureToolCallErrorCode,\n googleFixtureToolCallErrorDetails,\n isFixtureToolCallError,\n journalFixtureToolCallError,\n rejectResponsesOnlyToolCalls,\n requireServedFunctionToolCalls,\n toolArgsForWire,\n runWithToolArgumentsScope,\n servedToolArgs,\n strictToolArgumentsEnabled,\n isTextResponse,\n isToolCallResponse,\n isCombinedFixtureResponse,\n isErrorResponse,\n isAudioResponse,\n flattenHeaders,\n formatToMime,\n generateToolCallId,\n resolveFixtureBlocks,\n resolveFixtureBlockOutcome,\n resolveResponse,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { markWsTurnStopped, watchWsClose, wsLifecycle } from \"./response/lifecycle.js\";\nimport { delay, calculateDelay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\n// ─── Gemini Live protocol types ─────────────────────────────────────────────\n\ninterface GeminiLivePart {\n text?: string;\n thought?: boolean;\n functionCall?: { name: string; args: Record<string, unknown> };\n functionResponse?: { name: string; response: unknown; id?: string };\n inlineData?: { mimeType: string; data: string };\n}\n\ninterface GeminiLiveTurn {\n role: string;\n parts: GeminiLivePart[];\n}\n\ninterface GeminiLiveFunctionDeclaration {\n name: string;\n description?: string;\n parameters?: object;\n parametersJsonSchema?: object;\n}\n\ninterface GeminiLiveToolDef {\n functionDeclarations?: GeminiLiveFunctionDeclaration[];\n}\n\ninterface GeminiLiveSetup {\n model?: string;\n generationConfig?: Record<string, unknown>;\n tools?: GeminiLiveToolDef[];\n}\n\ninterface GeminiLiveClientContent {\n turns: GeminiLiveTurn[];\n turnComplete?: boolean;\n}\n\ninterface GeminiLiveFunctionResponse {\n id?: string;\n name: string;\n response: unknown;\n}\n\ninterface GeminiLiveToolResponse {\n functionResponses: GeminiLiveFunctionResponse[];\n}\n\ninterface GeminiLiveMessage {\n setup?: GeminiLiveSetup;\n config?: GeminiLiveSetup;\n clientContent?: GeminiLiveClientContent;\n toolResponse?: GeminiLiveToolResponse;\n}\n\n// ─── Session state ──────────────────────────────────────────────────────────\n\ninterface SessionState {\n setupDone: boolean;\n model: string;\n tools: ToolDefinition[];\n conversationHistory: ChatMessage[];\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nconst WS_PATH = \"/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent\";\n\n/**\n * Map HTTP status codes to gRPC error codes.\n * Gemini Live uses gRPC codes, not HTTP status codes.\n */\nfunction httpToGrpc(httpCode: number): number {\n switch (httpCode) {\n case 400:\n return 3; // INVALID_ARGUMENT\n case 401:\n return 16; // UNAUTHENTICATED\n case 403:\n return 7; // PERMISSION_DENIED\n case 404:\n return 5; // NOT_FOUND\n case 409:\n return 10; // ABORTED\n case 429:\n return 8; // RESOURCE_EXHAUSTED\n case 501:\n return 12; // UNIMPLEMENTED\n case 503:\n return 14; // UNAVAILABLE\n default:\n return 13; // INTERNAL\n }\n}\n\n/**\n * Convert Gemini Live turns into ChatMessage[] for fixture matching.\n */\nfunction geminiTurnsToMessages(turns: GeminiLiveTurn[], logger?: Logger): ChatMessage[] {\n const messages: ChatMessage[] = [];\n\n for (const turn of turns) {\n const role = turn.role ?? \"user\";\n\n if (role === \"user\") {\n const funcResponses = turn.parts.filter((p) => p.functionResponse);\n // inlineData parts (e.g. client audio input) are silently skipped —\n // only text and functionResponse parts are relevant for fixture matching.\n const textParts = turn.parts.filter((p) => p.text !== undefined && !p.thought);\n\n if (funcResponses.length > 0) {\n for (let i = 0; i < funcResponses.length; i++) {\n const part = funcResponses[i];\n const fr = part.functionResponse!;\n messages.push({\n role: \"tool\",\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? generateToolCallId(),\n });\n }\n if (textParts.length > 0) {\n messages.push({\n role: \"user\",\n content: textParts.map((p) => p.text!).join(\"\"),\n });\n }\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"user\", content: text });\n }\n } else if (role === \"model\") {\n const funcCalls = turn.parts.filter((p) => p.functionCall);\n const textParts = turn.parts.filter((p) => p.text !== undefined && !p.thought);\n\n if (funcCalls.length > 0) {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({\n role: \"assistant\",\n content: text || null,\n tool_calls: funcCalls.map((p) => ({\n id: generateToolCallId(),\n type: \"function\" as const,\n function: {\n name: p.functionCall!.name,\n arguments: JSON.stringify(p.functionCall!.args ?? {}),\n },\n })),\n });\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"assistant\", content: text });\n }\n } else {\n logger?.warn(`[gemini-live] skipping turn with unrecognized role: ${role}`);\n }\n }\n\n return messages;\n}\n\n/**\n * Convert toolResponse messages into ChatMessage[] for fixture matching.\n */\nfunction toolResponseToMessages(toolResponse: GeminiLiveToolResponse): ChatMessage[] {\n return toolResponse.functionResponses.map((fr) => ({\n role: \"tool\" as const,\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? generateToolCallId(),\n }));\n}\n\n/**\n * Convert Gemini tool definitions to ChatCompletion ToolDefinition[].\n */\nfunction convertTools(geminiTools?: GeminiLiveToolDef[]): ToolDefinition[] {\n if (!geminiTools || geminiTools.length === 0) return [];\n const decls = geminiTools.flatMap((t) => t.functionDeclarations ?? []);\n return decls.map((d) => ({\n type: \"function\" as const,\n function: {\n name: d.name,\n description: d.description,\n parameters: d.parameters ?? d.parametersJsonSchema,\n },\n }));\n}\n\nfunction liveToolArguments(tc: Pick<ToolCall, \"name\" | \"arguments\">) {\n return servedToolArgs(tc, \"object\").value;\n}\n\nconst GEMINI_LIVE_WIRE = \"Gemini Live\";\n\n/** Prepare native object arguments and IDs once for both output and the journal. */\nfunction prepareLiveMisbehavior(plan: MisbehaviorPlan): MisbehaviorPlan {\n if (plan.summary.fault === \"tool-args-invalid-json\" || plan.summary.fault === \"empty-response\") {\n return { ...plan, summary: { ...plan.summary, servedToolCalls: [] } };\n }\n const response = plan.response;\n const combined = isCombinedFixtureResponse(response);\n const outcome =\n combined && response.blocks?.length ? resolveFixtureBlockOutcome(response.blocks) : undefined;\n const calls =\n outcome?.toolCalls ??\n (combined || isToolCallResponse(response) ? (response.toolCalls ?? []) : []);\n const duplicate = plan.duplicateId;\n const toolCalls = calls.map((call, index) => {\n const args = toolArgsForWire(call);\n // Preserve authored-invalid arguments for the existing journaled preflight error.\n return {\n ...call,\n arguments: args.kind === \"parsed\" ? args.text : args.raw,\n ...(index === duplicate?.destinationIndex ? {} : { id: call.id ?? generateToolCallId() }),\n };\n });\n if (duplicate) toolCalls[duplicate.destinationIndex].id = toolCalls[duplicate.sourceIndex].id;\n let preparedResponse = { ...response };\n if (\"usage\" in preparedResponse) delete preparedResponse.usage;\n if (outcome) {\n let index = 0;\n const blocks = outcome.ordered.map((block) =>\n block.type === \"text\" ? { ...block } : { ...block, ...toolCalls[index++] },\n );\n preparedResponse = { ...preparedResponse, content: outcome.content, toolCalls, blocks };\n } else if (combined || isToolCallResponse(response)) {\n preparedResponse = { ...preparedResponse, toolCalls };\n }\n return {\n ...plan,\n response: preparedResponse,\n summary: {\n ...plan.summary,\n servedToolCalls: toolCalls.map(({ name, arguments: args, id }) => ({\n name,\n arguments: args,\n id,\n })),\n },\n };\n}\n\n/**\n * Validate the whole selected response before audio, text, or any tool is\n * sent: every entry is validated, custom tool calls are rejected, and every\n * argument string must parse. Returns the calls narrowed to function calls. On\n * failure the journal entry becomes a 500, and the outer message handler sends\n * code 13 with `google.rpc.ErrorInfo` details carrying the aimock code, drops\n * this turn from the history, and leaves the socket open.\n */\nfunction preflightToolArguments(\n response: { toolCalls?: ToolCall[] },\n journalEntry: JournalEntry,\n): ToolCall[] {\n return journalFixtureToolCallError(journalEntry, () => {\n const functionCalls = requireServedFunctionToolCalls(response, GEMINI_LIVE_WIRE);\n if (strictToolArgumentsEnabled()) for (const tc of functionCalls) liveToolArguments(tc);\n return functionCalls;\n });\n}\n\n/** Resolve ordered blocks (rejecting custom tool calls) and preflight their arguments. */\nfunction preflightBlocks(\n response: { blocks?: FixtureFileBlock[]; customToolCalls?: unknown },\n journalEntry: JournalEntry,\n) {\n return journalFixtureToolCallError(journalEntry, () => {\n rejectResponsesOnlyToolCalls(response, GEMINI_LIVE_WIRE);\n const resolved = resolveFixtureBlocks(response.blocks ?? []);\n if (strictToolArgumentsEnabled())\n for (const block of resolved) if (block.type === \"toolCall\") liveToolArguments(block);\n return resolved;\n });\n}\n\n// ─── Main handler ───────────────────────────────────────────────────────────\n\nexport function handleWebSocketGeminiLive(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n strictToolArguments?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n misbehavior?: HandlerDefaults[\"misbehavior\"];\n misbehaviorCounters?: HandlerDefaults[\"misbehaviorCounters\"];\n registry?: HandlerDefaults[\"registry\"];\n /** @internal Called before each message is processed (the server assigns fixture positions). */\n beforeProcessMessage?: () => void;\n },\n): void {\n const { logger } = defaults;\n watchWsClose(ws);\n const session: SessionState = {\n setupDone: false,\n model: defaults.model,\n tools: [],\n conversationHistory: [],\n };\n\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(async () => {\n // Messages are processed one at a time, so this marks where this\n // message's turn begins in the history.\n const historyMark = session.conversationHistory.length;\n try {\n defaults.beforeProcessMessage?.();\n await runWithToolArgumentsScope(\n { strict: defaults.strictToolArguments === true, logger },\n () => processMessage(raw, ws, fixtures, journal, defaults, session),\n );\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket Gemini Live error: ${msg}`);\n const fixtureToolCallError = isFixtureToolCallError(err);\n // A fixture rejected by a tool-call check serves nothing, so its turn\n // must not stay in the history as a user turn with no reply. Only\n // fixture tool-call errors roll back: any other error (for example a\n // malformed text block) leaves the user turn in the history.\n if (fixtureToolCallError) session.conversationHistory.length = historyMark;\n try {\n ws.send(\n JSON.stringify({\n error: {\n code: 13,\n message: msg,\n status: \"INTERNAL\",\n ...(fixtureToolCallError\n ? { details: googleFixtureToolCallErrorDetails(fixtureToolCallErrorCode(err)) }\n : {}),\n },\n }),\n );\n } catch (sendErr) {\n defaults.logger.debug(\n `Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : \"unknown\"}`,\n );\n }\n }\n });\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n misbehavior?: HandlerDefaults[\"misbehavior\"];\n misbehaviorCounters?: HandlerDefaults[\"misbehaviorCounters\"];\n registry?: HandlerDefaults[\"registry\"];\n },\n session: SessionState,\n): Promise<void> {\n let parsed: GeminiLiveMessage;\n try {\n parsed = JSON.parse(raw) as GeminiLiveMessage;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n ws.send(\n JSON.stringify({\n error: { code: 3, message: `Malformed JSON: ${detail}`, status: \"INVALID_ARGUMENT\" },\n }),\n );\n return;\n }\n\n // Handle setup message (accept both `setup` and `config` as aliases)\n const setupMsg = parsed.setup ?? parsed.config;\n if (setupMsg) {\n session.setupDone = true;\n session.model = setupMsg.model ?? defaults.model;\n session.tools = convertTools(setupMsg.tools);\n ws.send(JSON.stringify({ setupComplete: {} }));\n return;\n }\n\n // Reject messages before setup\n if (!session.setupDone) {\n ws.send(\n JSON.stringify({\n error: { code: 9, message: \"Setup required\", status: \"FAILED_PRECONDITION\" },\n }),\n );\n return;\n }\n\n // Build messages from this interaction\n let newMessages: ChatMessage[];\n\n if (parsed.clientContent) {\n if (!parsed.clientContent.turns || !Array.isArray(parsed.clientContent.turns)) {\n ws.send(\n JSON.stringify({\n error: {\n code: 3,\n message: \"Missing 'turns' in clientContent\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = geminiTurnsToMessages(parsed.clientContent.turns, defaults.logger);\n } else if (parsed.toolResponse) {\n if (\n !parsed.toolResponse.functionResponses ||\n !Array.isArray(parsed.toolResponse.functionResponses)\n ) {\n ws.send(\n JSON.stringify({\n error: {\n code: 3,\n message: \"Missing 'functionResponses' in toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = toolResponseToMessages(parsed.toolResponse);\n } else {\n ws.send(\n JSON.stringify({\n error: {\n code: 3,\n message: \"Expected clientContent or toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Build completion request for fixture matching (include new messages speculatively)\n const geminiContextHeader = defaults.upgradeHeaders?.[\"x-aimock-context\"];\n const geminiContext =\n typeof geminiContextHeader === \"string\"\n ? geminiContextHeader\n : Array.isArray(geminiContextHeader) && geminiContextHeader.length > 0\n ? geminiContextHeader[0]\n : undefined;\n\n const completionReq: ChatCompletionRequest = {\n model: session.model,\n messages: [...session.conversationHistory, ...newMessages],\n stream: true,\n tools: session.tools.length > 0 ? session.tools : undefined,\n _endpointType: \"chat\",\n _context: geminiContext,\n };\n\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = WS_PATH;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveRequestDecisions(defaults.strict, defaults.upgradeHeaders).fixtureMiss) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(\"WS\", path, skippedBySequenceOrTurn));\n journal.add({\n method: \"WS\",\n path,\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.close(1008, strictMessage);\n return;\n }\n journal.add({\n method: \"WS\",\n path,\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.send(\n JSON.stringify({\n error: { code: 5, message: \"No fixture matched\", status: \"NOT_FOUND\" },\n }),\n );\n return;\n }\n\n // Commit messages to conversation history only after successful fixture match\n session.conversationHistory.push(...newMessages);\n\n let response = await resolveResponse(fixture, completionReq);\n const plannerDefaults = { ...defaults, replaySpeed: defaults.replaySpeed ?? 1 };\n let misbehavior = planMisbehavior({\n wire: \"gemini-live\",\n emitsToolCallIds: true,\n fixture,\n response,\n request: completionReq,\n stream: true,\n defaults: plannerDefaults,\n rawHeaders: defaults.upgradeHeaders ?? {},\n // The server resolves both header and query-string session scopes.\n url: `${path}?testId=${encodeURIComponent(testId)}`,\n });\n if (misbehavior.kind === \"applied\") {\n misbehavior = prepareLiveMisbehavior(misbehavior);\n response = misbehavior.response;\n }\n const addResponseEntry = (status: number) => {\n const entry = journal.add({\n method: \"WS\",\n path,\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status, fixture },\n });\n recordMisbehaviorOutcome({\n entry,\n summary: misbehavior.summary,\n defaults: plannerDefaults,\n testId,\n });\n return entry;\n };\n if (misbehavior.kind === \"error\") {\n addResponseEntry(misbehavior.status);\n if (!misbehavior.summary?.evaluations.length) {\n defaults.logger.error(`[misbehavior] ${misbehavior.message} (testId=${testId})`);\n }\n ws.send(\n JSON.stringify({\n error: {\n code: httpToGrpc(misbehavior.status),\n message: misbehavior.message,\n status: misbehavior.status === 400 ? \"INVALID_ARGUMENT\" : \"UNIMPLEMENTED\",\n },\n }),\n );\n return;\n }\n const latency = fixture.latency ?? defaults.latency;\n if (\n misbehavior.kind === \"applied\" &&\n (misbehavior.summary.fault === \"tool-args-invalid-json\" ||\n misbehavior.summary.fault === \"empty-response\")\n ) {\n const journalEntry = addResponseEntry(200);\n const lifecycle = wsLifecycle(ws, fixture);\n const interruptTurn = () => {\n markWsTurnStopped(journalEntry, ws, lifecycle);\n ws.destroy();\n };\n try {\n if (ws.isClosed) {\n interruptTurn();\n return;\n }\n const chunkDelay = calculateDelay(\n 0,\n undefined,\n latency,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (chunkDelay > 0) await delay(chunkDelay, lifecycle.signal);\n if (lifecycle.signal.aborted || ws.isClosed) {\n interruptTurn();\n return;\n }\n const malformed = misbehavior.summary.fault === \"tool-args-invalid-json\";\n ws.send(\n JSON.stringify({\n serverContent: malformed\n ? { turnComplete: true, turnCompleteReason: \"MALFORMED_FUNCTION_CALL\" }\n : { modelTurn: { parts: [{ text: \"\" }] } },\n }),\n );\n lifecycle.tick();\n if (lifecycle.signal.aborted || ws.isClosed) {\n interruptTurn();\n return;\n }\n // Like ordinary text streaming, completion follows the counted content chunk.\n if (!malformed) {\n ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));\n if (ws.isClosed) {\n interruptTurn();\n return;\n }\n session.conversationHistory.push({ role: \"assistant\", content: \"\" });\n }\n } finally {\n lifecycle.dispose();\n }\n return;\n }\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n addResponseEntry(status);\n ws.send(\n JSON.stringify({\n error: {\n code: httpToGrpc(status),\n message: response.error.message,\n status: response.error.type ?? \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // Audio response — an AUDIO-modality model turn, plus the companion\n // modalities `AudioResponse` documents (types.ts) as preserved alongside it.\n //\n // COMPANION PLACEMENT is protocol-specific, and the Live protocol does NOT\n // place a tool call where the HTTP protocol does. On `generateContent` a\n // functionCall is a PART, so gemini.ts's `buildGeminiAudioParts` appends it to\n // the same `content.parts` array as the audio. On Live, `serverContent` and\n // `toolCall` are alternatives of ONE union: `@google/genai`'s\n // `LiveServerMessage` declares them as sibling optional fields and the\n // BidiGenerateContent reference states the response's `messageType` union\n // \"can be only one of the following\". A turn that both speaks and calls a\n // function is therefore TWO messages, not one part list —\n //\n // serverContent{modelTurn.parts} → toolCall{functionCalls} → serverContent{turnComplete}\n //\n // — which is also the order every other branch of this handler already emits\n // and the order the live provider was observed to use (its tool turn sent a\n // `serverContent` strictly before the `toolCall`).\n //\n // Emitting only the audio and returning — the previous behavior — silently\n // discarded the tool call and the text content of a recorded audio turn, the\n // exact loss types.ts's AudioResponse companions exist to prevent.\n //\n // NOT handled here: `AudioResponse.reasoning`. This handler has no thought\n // channel in ANY branch (nothing in it emits `thought: true`), so replaying\n // reasoning from the audio branch alone would invent a Live-wide capability\n // out of one fixture field. That gap is real but separate.\n if (isAudioResponse(response)) {\n const journalEntry = addResponseEntry(200);\n\n const audioResp = response as AudioResponse;\n const audioToolCalls = preflightToolArguments(audioResp, journalEntry);\n let mimeType: string;\n let data: string;\n\n if (typeof audioResp.audio === \"string\") {\n mimeType = formatToMime(audioResp.format ?? \"mp3\");\n data = audioResp.audio;\n } else {\n mimeType = audioResp.audio.contentType ?? \"audio/mpeg\";\n data = audioResp.audio.b64Json;\n }\n\n // Part order mirrors gemini.ts's `buildGeminiAudioParts`: audio first, then\n // the text companion.\n const parts: GeminiLivePart[] = [{ inlineData: { mimeType, data } }];\n if (audioResp.content) {\n parts.push({ text: audioResp.content });\n }\n\n // Stable IDs resolved once, so the wire message and conversation history\n // agree (same contract as the tool-call branches below).\n const resolvedToolCalls = audioToolCalls.map((tc) => ({\n ...tc,\n resolvedId: tc.id ?? generateToolCallId(),\n }));\n\n // This branch never waits, so it holds no lifecycle. A closed socket is\n // journaled by the shared WS rule, and the turn stops there.\n const stoppedByClose = (): boolean => {\n if (!ws.isClosed) return false;\n markWsTurnStopped(journalEntry, ws);\n return true;\n };\n if (stoppedByClose()) return;\n if (resolvedToolCalls.length === 0) {\n ws.send(\n JSON.stringify({\n serverContent: { modelTurn: { parts }, turnComplete: true },\n }),\n );\n } else {\n ws.send(JSON.stringify({ serverContent: { modelTurn: { parts } } }));\n if (stoppedByClose()) return;\n const functionCalls = resolvedToolCalls.map((tc) => {\n const argsObj = liveToolArguments(tc);\n return { name: tc.name, args: argsObj, id: tc.resolvedId };\n });\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n if (stoppedByClose()) return;\n ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));\n }\n if (stoppedByClose()) return;\n\n session.conversationHistory.push({\n role: \"assistant\",\n content: audioResp.content ?? \"[audio]\",\n ...(resolvedToolCalls.length > 0\n ? {\n tool_calls: resolvedToolCalls.map((tc) => ({\n id: tc.resolvedId,\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments },\n })),\n }\n : {}),\n });\n return;\n }\n\n // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n if (isCombinedFixtureResponse(response)) {\n const journalEntry = addResponseEntry(200);\n\n // BLOCKS path (#274): when the fixture carries an ordered `blocks` array,\n // honor it instead of the legacy `content ?? \"\"` / `toolCalls ?? []` path.\n // The Gemini Live WS protocol expresses ordering via SEQUENTIAL messages —\n // a text block becomes one-or-more `serverContent.modelTurn.parts[{text}]`\n // messages and a toolCall block becomes a `toolCall.functionCalls` message,\n // so emitting in array order faithfully reproduces tool-before-text (or any\n // interleaving), matching the HTTP gemini.ts blocks branch. Without this, a\n // blocks-only fixture (post-F0 it matches this guard) would stream an EMPTY\n // payload — a silent drop. Legacy fixtures (no `blocks`) skip this entirely.\n if (response.blocks && response.blocks.length > 0) {\n const resolvedBlocks = preflightBlocks(response, journalEntry);\n const lifecycle = wsLifecycle(ws, fixture);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n const { recordedTimings } = fixture;\n let chunkIndex = 0;\n let interrupted = false;\n\n // Accumulate the equivalent assistant turn for conversation history, so a\n // follow-up turn sees the same content + tool_calls as the legacy path.\n let historyContent = \"\";\n const historyToolCalls: {\n id: string;\n type: \"function\";\n function: { name: string; arguments: string };\n }[] = [];\n\n outer: for (const block of resolvedBlocks) {\n if (block.type === \"toolCall\") {\n if (ws.isClosed) break;\n const tcDelay = calculateDelay(\n chunkIndex,\n undefined,\n latency,\n recordedTimings,\n replaySpeed,\n );\n if (tcDelay > 0) await delay(tcDelay, lifecycle.signal);\n if (lifecycle.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n\n const resolvedId = block.id ?? generateToolCallId();\n const argsObj = liveToolArguments(block);\n\n try {\n ws.send(\n JSON.stringify({\n toolCall: { functionCalls: [{ name: block.name, args: argsObj, id: resolvedId }] },\n }),\n );\n } catch (err) {\n defaults.logger.debug(\n \"[gemini-live] send failed during blocks streaming, closing\",\n err,\n );\n break;\n }\n chunkIndex++;\n historyToolCalls.push({\n id: resolvedId,\n type: \"function\" as const,\n function: { name: block.name, arguments: block.arguments },\n });\n lifecycle.tick();\n if (lifecycle.signal.aborted) {\n interrupted = true;\n break;\n }\n } else {\n const text = block.text;\n historyContent += text;\n if (text.length === 0) {\n // An empty text block carries no wire content, so emit nothing and\n // spend no chunk: the old guard sent a useless empty `modelTurn`\n // message and `continue`d WITHOUT `chunkIndex++`/`interruption.tick()`,\n // which leaked the next block past `truncateAfterChunks` and shifted\n // `recordedTimings` indexing for every following block. Skipping\n // keeps non-empty-block output byte-identical and the chunk/timing\n // accounting correct.\n continue;\n }\n for (let i = 0; i < text.length; i += chunkSize) {\n if (ws.isClosed) break outer;\n const chunkDelay = calculateDelay(\n chunkIndex,\n undefined,\n latency,\n recordedTimings,\n replaySpeed,\n );\n if (chunkDelay > 0) await delay(chunkDelay, lifecycle.signal);\n if (lifecycle.signal.aborted) {\n interrupted = true;\n break outer;\n }\n if (ws.isClosed) break outer;\n try {\n ws.send(\n JSON.stringify({\n serverContent: { modelTurn: { parts: [{ text: text.slice(i, i + chunkSize) }] } },\n }),\n );\n } catch (err) {\n defaults.logger.debug(\n \"[gemini-live] send failed during blocks streaming, closing\",\n err,\n );\n break outer;\n }\n chunkIndex++;\n lifecycle.tick();\n if (lifecycle.signal.aborted) {\n interrupted = true;\n break outer;\n }\n }\n }\n }\n\n if (interrupted || ws.isClosed) {\n ws.destroy();\n markWsTurnStopped(journalEntry, ws, lifecycle);\n lifecycle.dispose();\n return;\n }\n\n lifecycle.dispose();\n\n ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));\n if (ws.isClosed) {\n markWsTurnStopped(journalEntry, ws, lifecycle);\n return;\n }\n\n session.conversationHistory.push({\n role: \"assistant\",\n content: historyContent || null,\n ...(historyToolCalls.length > 0 ? { tool_calls: historyToolCalls } : {}),\n });\n return;\n }\n\n const functionToolCalls = preflightToolArguments(response, journalEntry);\n const content = response.content ?? \"\";\n const chunkList: string[] = [];\n for (let i = 0; i < content.length; i += chunkSize) {\n chunkList.push(content.slice(i, i + chunkSize));\n }\n\n const lifecycle = wsLifecycle(ws, fixture);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n const { recordedTimings } = fixture;\n let interrupted = false;\n\n // Stream text content chunks (turnComplete omitted — sent as a separate message later)\n if (content.length === 0) {\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: \"\" }] },\n },\n }),\n );\n }\n } else {\n for (let i = 0; i < chunkList.length; i++) {\n if (ws.isClosed) break;\n const chunkDelay = calculateDelay(i, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, lifecycle.signal);\n if (lifecycle.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n\n try {\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: chunkList[i] }] },\n },\n }),\n );\n } catch (err) {\n defaults.logger.debug(\"[gemini-live] send failed during text streaming, closing\", err);\n break;\n }\n lifecycle.tick();\n if (lifecycle.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n }\n\n if (interrupted || ws.isClosed) {\n ws.destroy();\n markWsTurnStopped(journalEntry, ws, lifecycle);\n lifecycle.dispose();\n return;\n }\n\n // Pre-compute tool calls with stable IDs so wire message and history match\n const resolvedToolCalls = functionToolCalls.map((tc) => ({\n ...tc,\n resolvedId: tc.id ?? generateToolCallId(),\n }));\n\n // Send tool calls\n if (!ws.isClosed) {\n const tcDelay = calculateDelay(\n chunkList.length,\n undefined,\n latency,\n recordedTimings,\n replaySpeed,\n );\n if (tcDelay > 0) await delay(tcDelay, lifecycle.signal);\n if (lifecycle.signal.aborted || ws.isClosed) {\n ws.destroy();\n markWsTurnStopped(journalEntry, ws, lifecycle);\n lifecycle.dispose();\n return;\n }\n\n const functionCalls = resolvedToolCalls.map((tc) => {\n const argsObj = liveToolArguments(tc);\n return {\n name: tc.name,\n args: argsObj,\n id: tc.resolvedId,\n };\n });\n\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n lifecycle.tick();\n }\n\n if (lifecycle.signal.aborted || ws.isClosed) {\n ws.destroy();\n markWsTurnStopped(journalEntry, ws, lifecycle);\n lifecycle.dispose();\n return;\n }\n\n lifecycle.dispose();\n\n // Send turnComplete\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n if (ws.isClosed) {\n markWsTurnStopped(journalEntry, ws, lifecycle);\n return;\n }\n\n // Add to conversation history using the same resolved IDs from the wire message\n session.conversationHistory.push({\n role: \"assistant\",\n content: content || null,\n tool_calls: resolvedToolCalls.map((tc) => ({\n id: tc.resolvedId,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n })),\n });\n return;\n }\n\n // Text response — stream chunks with serverContent\n if (isTextResponse(response)) {\n const journalEntry = addResponseEntry(200);\n\n const content = response.content;\n\n if (content.length === 0) {\n // No wait and no lifecycle here: as in the audio branch, a closed\n // socket is journaled by the shared WS rule, and the turn stops there.\n if (ws.isClosed) {\n markWsTurnStopped(journalEntry, ws);\n return;\n }\n // Empty content: send empty modelTurn, then separate turnComplete\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: \"\" }] },\n },\n }),\n );\n if (ws.isClosed) {\n markWsTurnStopped(journalEntry, ws);\n return;\n }\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n if (ws.isClosed) markWsTurnStopped(journalEntry, ws);\n return;\n }\n\n // Chunk the content\n const chunks: string[] = [];\n for (let i = 0; i < content.length; i += chunkSize) {\n chunks.push(content.slice(i, i + chunkSize));\n }\n\n const lifecycle = wsLifecycle(ws, fixture);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n const { recordedTimings } = fixture;\n let interrupted = false;\n\n // Stream content chunks without turnComplete (sent separately after)\n for (let i = 0; i < chunks.length; i++) {\n if (ws.isClosed) break;\n const chunkDelay = calculateDelay(i, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, lifecycle.signal);\n if (lifecycle.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n\n try {\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: chunks[i] }] },\n },\n }),\n );\n } catch (err) {\n defaults.logger.debug(\"[gemini-live] send failed during text streaming, closing\", err);\n break;\n }\n lifecycle.tick();\n if (lifecycle.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n\n if (interrupted || ws.isClosed) {\n ws.destroy();\n markWsTurnStopped(journalEntry, ws, lifecycle);\n lifecycle.dispose();\n return;\n }\n\n lifecycle.dispose();\n\n // Send separate turnComplete message\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n if (ws.isClosed) {\n markWsTurnStopped(journalEntry, ws, lifecycle);\n return;\n }\n\n // Add assistant response to conversation history\n session.conversationHistory.push({ role: \"assistant\", content });\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = addResponseEntry(200);\n\n const functionToolCalls = preflightToolArguments(response, journalEntry);\n\n const lifecycle = wsLifecycle(ws, fixture);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n const { recordedTimings } = fixture;\n\n if (ws.isClosed) {\n markWsTurnStopped(journalEntry, ws, lifecycle);\n lifecycle.dispose();\n return;\n }\n const tcDelay = calculateDelay(0, undefined, latency, recordedTimings, replaySpeed);\n if (tcDelay > 0) await delay(tcDelay, lifecycle.signal);\n if (lifecycle.signal.aborted || ws.isClosed) {\n ws.destroy();\n markWsTurnStopped(journalEntry, ws, lifecycle);\n lifecycle.dispose();\n return;\n }\n\n // Pre-compute tool calls with stable IDs so wire message and history match\n const resolvedToolCalls = functionToolCalls.map((tc) => ({\n ...tc,\n resolvedId: tc.id ?? generateToolCallId(),\n }));\n\n const functionCalls = resolvedToolCalls.map((tc) => {\n const argsObj = liveToolArguments(tc);\n return {\n name: tc.name,\n args: argsObj,\n id: tc.resolvedId,\n };\n });\n\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n lifecycle.tick();\n\n if (lifecycle.signal.aborted || ws.isClosed) {\n ws.destroy();\n markWsTurnStopped(journalEntry, ws, lifecycle);\n lifecycle.dispose();\n return;\n }\n\n lifecycle.dispose();\n\n // Send turnComplete after tool call\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n if (ws.isClosed) {\n markWsTurnStopped(journalEntry, ws, lifecycle);\n return;\n }\n\n // Add assistant tool_calls to conversation history using the same resolved IDs\n session.conversationHistory.push({\n role: \"assistant\",\n content: null,\n tool_calls: resolvedToolCalls.map((tc) => ({\n id: tc.resolvedId,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n })),\n });\n return;\n }\n\n // Unknown response type\n addResponseEntry(500);\n ws.send(\n JSON.stringify({\n error: {\n code: 13,\n message: \"Fixture response did not match any known type\",\n status: \"INTERNAL\",\n },\n }),\n );\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ const require_sse_writer = require('./sse-writer.cjs');
7
7
  const require_misbehavior = require('./misbehavior.cjs');
8
8
  const require_router = require('./router.cjs');
9
9
  require('./journal.cjs');
10
- const require_interruption = require('./interruption.cjs');
10
+ const require_lifecycle = require('./response/lifecycle.cjs');
11
11
  const require_realtime = require('./response/usage/codecs/realtime.cjs');
12
12
  let node_crypto = require("node:crypto");
13
13
 
@@ -369,6 +369,7 @@ function rejectRetiredBetaShape(ws, logger) {
369
369
  function handleWebSocketRealtime(ws, fixtures, journal, defaults) {
370
370
  const { logger } = defaults;
371
371
  const sessionId = realtimeId("sess");
372
+ require_lifecycle.watchWsClose(ws);
372
373
  const isBeta = defaults.upgradeHeaders?.["openai-beta"] ? String(defaults.upgradeHeaders["openai-beta"]).includes("realtime=v1") : false;
373
374
  if (isBeta) {
374
375
  rejectRetiredBetaShape(ws, logger);
@@ -677,18 +678,17 @@ async function emitLiveTranscriptionEvents(ws, fixtures, journal, defaults, sess
677
678
  const latency = fixture.latency ?? defaults.latency;
678
679
  const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);
679
680
  const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
680
- const interruption = require_interruption.createInterruptionSignal(fixture);
681
+ const lifecycle = require_lifecycle.wsLifecycle(ws, fixture);
681
682
  let chunkIndex = 0;
682
683
  for (let index = 0; index < transcript.length; index += chunkSize) {
683
684
  const chunkDelay = require_sse_writer.calculateDelay(chunkIndex, fixture.streamingProfile, latency, fixture.recordedTimings, replaySpeed);
684
- if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
685
- if (interruption?.signal.aborted || ws.isClosed) {
686
- if (interruption?.signal.aborted) {
687
- journalEntry.response.interrupted = true;
688
- journalEntry.response.interruptReason = interruption.reason();
685
+ if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, lifecycle.signal);
686
+ if (lifecycle.signal.aborted || ws.isClosed) {
687
+ if (lifecycle.signal.aborted) {
688
+ require_lifecycle.markInterrupted(journalEntry, lifecycle.reason());
689
689
  ws.destroy();
690
- }
691
- interruption?.cleanup();
690
+ } else require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
691
+ lifecycle.dispose();
692
692
  return;
693
693
  }
694
694
  sendEvent(ws, {
@@ -697,14 +697,13 @@ async function emitLiveTranscriptionEvents(ws, fixtures, journal, defaults, sess
697
697
  content_index: 0,
698
698
  delta: transcript.slice(index, index + chunkSize)
699
699
  }, isBeta);
700
- interruption?.tick();
700
+ lifecycle.tick();
701
701
  chunkIndex++;
702
702
  }
703
- if (interruption?.signal.aborted) {
704
- journalEntry.response.interrupted = true;
705
- journalEntry.response.interruptReason = interruption.reason();
703
+ if (lifecycle.signal.aborted || ws.isClosed) {
704
+ require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
706
705
  ws.destroy();
707
- interruption.cleanup();
706
+ lifecycle.dispose();
708
707
  return;
709
708
  }
710
709
  sendEvent(ws, {
@@ -715,7 +714,7 @@ async function emitLiveTranscriptionEvents(ws, fixtures, journal, defaults, sess
715
714
  usage,
716
715
  ...response.transcription.languages !== void 0 ? { languages: response.transcription.languages } : {}
717
716
  }, isBeta);
718
- interruption?.cleanup();
717
+ lifecycle.dispose();
719
718
  }
720
719
  function sendLiveTranscriptionFailure(ws, itemId, error, isBeta) {
721
720
  sendEvent(ws, {
@@ -977,7 +976,7 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
977
976
  usage: null
978
977
  }
979
978
  }, isBeta);
980
- const interruption = require_interruption.createInterruptionSignal(fixture);
979
+ const lifecycle = require_lifecycle.wsLifecycle(ws, fixture);
981
980
  let interrupted = false;
982
981
  const allOutputItems = [];
983
982
  const textItemId = realtimeId("item");
@@ -1025,8 +1024,8 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1025
1024
  for (let i = 0; i < content.length; i += chunkSize) {
1026
1025
  if (ws.isClosed) break;
1027
1026
  const chunkDelay = require_sse_writer.calculateDelay(eventIndex, void 0, latency, recordedTimings, replaySpeed);
1028
- if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
1029
- if (interruption?.signal.aborted) {
1027
+ if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, lifecycle.signal);
1028
+ if (lifecycle.signal.aborted) {
1030
1029
  interrupted = true;
1031
1030
  break;
1032
1031
  }
@@ -1046,21 +1045,21 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1046
1045
  break;
1047
1046
  }
1048
1047
  eventIndex++;
1049
- interruption?.tick();
1050
- if (interruption?.signal.aborted) {
1048
+ lifecycle.tick();
1049
+ if (lifecycle.signal.aborted) {
1051
1050
  interrupted = true;
1052
1051
  break;
1053
1052
  }
1054
1053
  }
1055
1054
  if (interrupted) {
1056
1055
  ws.destroy();
1057
- journalEntry.response.interrupted = true;
1058
- journalEntry.response.interruptReason = interruption?.reason();
1059
- interruption?.cleanup();
1056
+ require_lifecycle.markInterrupted(journalEntry, lifecycle.reason());
1057
+ lifecycle.dispose();
1060
1058
  return;
1061
1059
  }
1062
1060
  if (ws.isClosed) {
1063
- interruption?.cleanup();
1061
+ require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
1062
+ lifecycle.dispose();
1064
1063
  return;
1065
1064
  }
1066
1065
  sendEvent(ws, {
@@ -1072,7 +1071,8 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1072
1071
  text: content
1073
1072
  }, isBeta);
1074
1073
  if (ws.isClosed) {
1075
- interruption?.cleanup();
1074
+ require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
1075
+ lifecycle.dispose();
1076
1076
  return;
1077
1077
  }
1078
1078
  sendEvent(ws, {
@@ -1087,7 +1087,8 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1087
1087
  }
1088
1088
  }, isBeta);
1089
1089
  if (ws.isClosed) {
1090
- interruption?.cleanup();
1090
+ require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
1091
+ lifecycle.dispose();
1091
1092
  return;
1092
1093
  }
1093
1094
  sendEvent(ws, {
@@ -1111,7 +1112,8 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1111
1112
  }
1112
1113
  }, isBeta);
1113
1114
  if (ws.isClosed) {
1114
- interruption?.cleanup();
1115
+ require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
1116
+ lifecycle.dispose();
1115
1117
  return;
1116
1118
  }
1117
1119
  allOutputItems.push(textOutputItem);
@@ -1147,8 +1149,8 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1147
1149
  for (let i = 0; i < args.length; i += chunkSize) {
1148
1150
  if (ws.isClosed) break;
1149
1151
  const chunkDelay = require_sse_writer.calculateDelay(eventIndex, void 0, latency, recordedTimings, replaySpeed);
1150
- if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
1151
- if (interruption?.signal.aborted) {
1152
+ if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, lifecycle.signal);
1153
+ if (lifecycle.signal.aborted) {
1152
1154
  interrupted = true;
1153
1155
  break;
1154
1156
  }
@@ -1168,8 +1170,8 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1168
1170
  break;
1169
1171
  }
1170
1172
  eventIndex++;
1171
- interruption?.tick();
1172
- if (interruption?.signal.aborted) {
1173
+ lifecycle.tick();
1174
+ if (lifecycle.signal.aborted) {
1173
1175
  interrupted = true;
1174
1176
  break;
1175
1177
  }
@@ -1211,13 +1213,15 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1211
1213
  }
1212
1214
  if (interrupted) {
1213
1215
  ws.destroy();
1214
- journalEntry.response.interrupted = true;
1215
- journalEntry.response.interruptReason = interruption?.reason();
1216
- interruption?.cleanup();
1216
+ require_lifecycle.markInterrupted(journalEntry, lifecycle.reason());
1217
+ lifecycle.dispose();
1218
+ return;
1219
+ }
1220
+ lifecycle.dispose();
1221
+ if (ws.isClosed) {
1222
+ require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
1217
1223
  return;
1218
1224
  }
1219
- interruption?.cleanup();
1220
- if (ws.isClosed) return;
1221
1225
  sendEvent(ws, {
1222
1226
  type: "response.done",
1223
1227
  response: {
@@ -1293,14 +1297,14 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1293
1297
  const content = response.content ?? "";
1294
1298
  const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
1295
1299
  const { recordedTimings } = fixture;
1296
- const interruption = require_interruption.createInterruptionSignal(fixture);
1300
+ const lifecycle = require_lifecycle.wsLifecycle(ws, fixture);
1297
1301
  let interrupted = false;
1298
1302
  let eventIndex = 0;
1299
1303
  for (let i = 0; i < content.length; i += chunkSize) {
1300
1304
  if (ws.isClosed) break;
1301
1305
  const chunkDelay = require_sse_writer.calculateDelay(eventIndex, void 0, latency, recordedTimings, replaySpeed);
1302
- if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
1303
- if (interruption?.signal.aborted) {
1306
+ if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, lifecycle.signal);
1307
+ if (lifecycle.signal.aborted) {
1304
1308
  interrupted = true;
1305
1309
  break;
1306
1310
  }
@@ -1320,21 +1324,23 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1320
1324
  break;
1321
1325
  }
1322
1326
  eventIndex++;
1323
- interruption?.tick();
1324
- if (interruption?.signal.aborted) {
1327
+ lifecycle.tick();
1328
+ if (lifecycle.signal.aborted) {
1325
1329
  interrupted = true;
1326
1330
  break;
1327
1331
  }
1328
1332
  }
1329
1333
  if (interrupted) {
1330
1334
  ws.destroy();
1331
- journalEntry.response.interrupted = true;
1332
- journalEntry.response.interruptReason = interruption?.reason();
1333
- interruption?.cleanup();
1335
+ require_lifecycle.markInterrupted(journalEntry, lifecycle.reason());
1336
+ lifecycle.dispose();
1337
+ return;
1338
+ }
1339
+ lifecycle.dispose();
1340
+ if (ws.isClosed) {
1341
+ require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
1334
1342
  return;
1335
1343
  }
1336
- interruption?.cleanup();
1337
- if (ws.isClosed) return;
1338
1344
  sendEvent(ws, {
1339
1345
  type: "response.output_text.done",
1340
1346
  response_id: responseId,
@@ -1411,7 +1417,7 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1411
1417
  }
1412
1418
  }, isBeta);
1413
1419
  const outputItems = [];
1414
- const interruption = require_interruption.createInterruptionSignal(fixture);
1420
+ const lifecycle = require_lifecycle.wsLifecycle(ws, fixture);
1415
1421
  const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
1416
1422
  const { recordedTimings } = fixture;
1417
1423
  let interrupted = false;
@@ -1446,8 +1452,8 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1446
1452
  for (let i = 0; i < args.length; i += chunkSize) {
1447
1453
  if (ws.isClosed) break;
1448
1454
  const chunkDelay = require_sse_writer.calculateDelay(eventIndex, void 0, latency, recordedTimings, replaySpeed);
1449
- if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
1450
- if (interruption?.signal.aborted) {
1455
+ if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, lifecycle.signal);
1456
+ if (lifecycle.signal.aborted) {
1451
1457
  interrupted = true;
1452
1458
  break;
1453
1459
  }
@@ -1467,8 +1473,8 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1467
1473
  break;
1468
1474
  }
1469
1475
  eventIndex++;
1470
- interruption?.tick();
1471
- if (interruption?.signal.aborted) {
1476
+ lifecycle.tick();
1477
+ if (lifecycle.signal.aborted) {
1472
1478
  interrupted = true;
1473
1479
  break;
1474
1480
  }
@@ -1508,13 +1514,15 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
1508
1514
  }
1509
1515
  if (interrupted) {
1510
1516
  ws.destroy();
1511
- journalEntry.response.interrupted = true;
1512
- journalEntry.response.interruptReason = interruption?.reason();
1513
- interruption?.cleanup();
1517
+ require_lifecycle.markInterrupted(journalEntry, lifecycle.reason());
1518
+ lifecycle.dispose();
1519
+ return;
1520
+ }
1521
+ lifecycle.dispose();
1522
+ if (ws.isClosed) {
1523
+ require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
1514
1524
  return;
1515
1525
  }
1516
- interruption?.cleanup();
1517
- if (ws.isClosed) return;
1518
1526
  sendEvent(ws, {
1519
1527
  type: "response.done",
1520
1528
  response: {
@@ -1552,7 +1560,7 @@ async function streamRealtimeBlocks(ws, blocks, responseId, fixture, defaults, l
1552
1560
  usage: null
1553
1561
  }
1554
1562
  }, isBeta);
1555
- const interruption = require_interruption.createInterruptionSignal(fixture);
1563
+ const lifecycle = require_lifecycle.wsLifecycle(ws, fixture);
1556
1564
  let interrupted = false;
1557
1565
  const allOutputItems = [];
1558
1566
  const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
@@ -1604,8 +1612,8 @@ async function streamRealtimeBlocks(ws, blocks, responseId, fixture, defaults, l
1604
1612
  for (let i = 0; i < text.length; i += chunkSize) {
1605
1613
  if (ws.isClosed) break;
1606
1614
  const chunkDelay = require_sse_writer.calculateDelay(eventIndex, void 0, latency, recordedTimings, replaySpeed);
1607
- if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
1608
- if (interruption?.signal.aborted) {
1615
+ if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, lifecycle.signal);
1616
+ if (lifecycle.signal.aborted) {
1609
1617
  interrupted = true;
1610
1618
  break;
1611
1619
  }
@@ -1625,8 +1633,8 @@ async function streamRealtimeBlocks(ws, blocks, responseId, fixture, defaults, l
1625
1633
  break;
1626
1634
  }
1627
1635
  eventIndex++;
1628
- interruption?.tick();
1629
- if (interruption?.signal.aborted) {
1636
+ lifecycle.tick();
1637
+ if (lifecycle.signal.aborted) {
1630
1638
  interrupted = true;
1631
1639
  break;
1632
1640
  }
@@ -1705,8 +1713,8 @@ async function streamRealtimeBlocks(ws, blocks, responseId, fixture, defaults, l
1705
1713
  for (let i = 0; i < args.length; i += chunkSize) {
1706
1714
  if (ws.isClosed) break;
1707
1715
  const chunkDelay = require_sse_writer.calculateDelay(eventIndex, void 0, latency, recordedTimings, replaySpeed);
1708
- if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
1709
- if (interruption?.signal.aborted) {
1716
+ if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, lifecycle.signal);
1717
+ if (lifecycle.signal.aborted) {
1710
1718
  interrupted = true;
1711
1719
  break;
1712
1720
  }
@@ -1726,8 +1734,8 @@ async function streamRealtimeBlocks(ws, blocks, responseId, fixture, defaults, l
1726
1734
  break;
1727
1735
  }
1728
1736
  eventIndex++;
1729
- interruption?.tick();
1730
- if (interruption?.signal.aborted) {
1737
+ lifecycle.tick();
1738
+ if (lifecycle.signal.aborted) {
1731
1739
  interrupted = true;
1732
1740
  break;
1733
1741
  }
@@ -1769,13 +1777,15 @@ async function streamRealtimeBlocks(ws, blocks, responseId, fixture, defaults, l
1769
1777
  }
1770
1778
  if (interrupted) {
1771
1779
  ws.destroy();
1772
- journalEntry.response.interrupted = true;
1773
- journalEntry.response.interruptReason = interruption?.reason();
1774
- interruption?.cleanup();
1780
+ require_lifecycle.markInterrupted(journalEntry, lifecycle.reason());
1781
+ lifecycle.dispose();
1782
+ return;
1783
+ }
1784
+ lifecycle.dispose();
1785
+ if (ws.isClosed) {
1786
+ require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
1775
1787
  return;
1776
1788
  }
1777
- interruption?.cleanup();
1778
- if (ws.isClosed) return;
1779
1789
  sendEvent(ws, {
1780
1790
  type: "response.done",
1781
1791
  response: {