theorum 0.1.14 → 1.0.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 (229) hide show
  1. package/README.md +178 -89
  2. package/esm/mod.d.ts +21 -11
  3. package/esm/mod.js +15 -8
  4. package/esm/src/cli/commands/bench.js +6 -3
  5. package/esm/src/cli/commands/fuzz-canary.d.ts +13 -0
  6. package/esm/src/cli/commands/fuzz-canary.js +195 -0
  7. package/esm/src/cli/commands/fuzz-guardrails.d.ts +3 -5
  8. package/esm/src/cli/commands/fuzz-guardrails.js +4 -581
  9. package/esm/src/cli/commands/profile.js +25 -14
  10. package/esm/src/cli/commands/run.d.ts +3 -0
  11. package/esm/src/cli/commands/run.js +22 -30
  12. package/esm/src/cli/commands/test.d.ts +10 -1
  13. package/esm/src/cli/commands/test.js +31 -31
  14. package/esm/src/cli/event-log.d.ts +19 -0
  15. package/esm/src/cli/event-log.js +147 -0
  16. package/esm/src/cli/index.js +45 -11
  17. package/esm/src/cli/matrix/synthesizer.d.ts +7 -9
  18. package/esm/src/cli/matrix/synthesizer.js +32 -106
  19. package/esm/src/guardrails/canary-gate.d.ts +20 -0
  20. package/esm/src/guardrails/canary-gate.js +31 -0
  21. package/esm/src/guardrails/canary.d.ts +26 -0
  22. package/esm/src/guardrails/canary.js +139 -0
  23. package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +16 -0
  24. package/esm/src/guardrails/corpus/canary-egress-attacks.js +151 -0
  25. package/esm/src/guardrails/corpus/fuzz-inbound.d.ts +11 -0
  26. package/esm/src/guardrails/corpus/fuzz-inbound.js +216 -0
  27. package/esm/src/guardrails/corpus/inbound-payloads.d.ts +10 -0
  28. package/esm/src/guardrails/corpus/inbound-payloads.js +124 -0
  29. package/esm/src/guardrails/corpus/live-attacks.d.ts +20 -0
  30. package/esm/src/guardrails/corpus/live-attacks.js +232 -0
  31. package/esm/src/guardrails/corpus/mod.d.ts +13 -0
  32. package/esm/src/guardrails/corpus/mod.js +11 -0
  33. package/esm/src/guardrails/corpus/secrets.d.ts +16 -0
  34. package/esm/src/guardrails/corpus/secrets.js +16 -0
  35. package/esm/src/guardrails/corpus/strings.d.ts +27 -0
  36. package/esm/src/guardrails/corpus/strings.js +33 -0
  37. package/esm/src/guardrails/corpus/types.d.ts +37 -0
  38. package/esm/src/guardrails/corpus/types.js +6 -0
  39. package/esm/src/guardrails/egress.d.ts +9 -0
  40. package/esm/src/guardrails/egress.js +36 -0
  41. package/esm/src/guardrails/error.js +6 -4
  42. package/esm/src/guardrails/injection.js +28 -5
  43. package/esm/src/guardrails/live-outbound-gate.d.ts +39 -0
  44. package/esm/src/guardrails/live-outbound-gate.js +180 -0
  45. package/esm/src/guardrails/mod.d.ts +13 -4
  46. package/esm/src/guardrails/mod.js +10 -4
  47. package/esm/src/guardrails/quota.js +1 -1
  48. package/esm/src/guardrails/sanitize.d.ts +11 -15
  49. package/esm/src/guardrails/sanitize.js +40 -49
  50. package/esm/src/guardrails/sensitive.js +2 -1
  51. package/esm/src/guardrails/testing.d.ts +10 -0
  52. package/esm/src/guardrails/testing.js +9 -0
  53. package/esm/src/host/client-turn.d.ts +19 -0
  54. package/esm/src/host/client-turn.js +32 -0
  55. package/esm/src/host/mod.d.ts +5 -3
  56. package/esm/src/host/mod.js +4 -3
  57. package/esm/src/interface/blocks.d.ts +20 -0
  58. package/esm/src/interface/blocks.js +180 -0
  59. package/esm/src/interface/from-profile.d.ts +14 -0
  60. package/esm/src/interface/from-profile.js +104 -0
  61. package/esm/src/interface/inputs.d.ts +8 -0
  62. package/esm/src/interface/inputs.js +127 -0
  63. package/esm/src/interface/mod.d.ts +10 -0
  64. package/esm/src/interface/mod.js +9 -0
  65. package/esm/src/interface/types.d.ts +153 -0
  66. package/esm/src/interface/types.js +13 -0
  67. package/esm/src/kernel/engine/delta.d.ts +24 -2
  68. package/esm/src/kernel/engine/delta.js +519 -29
  69. package/esm/src/kernel/engine/live-inbound.d.ts +9 -0
  70. package/esm/src/kernel/engine/live-inbound.js +17 -0
  71. package/esm/src/kernel/engine/repair.js +1 -1
  72. package/esm/src/kernel/engine/runner/gates.d.ts +1 -1
  73. package/esm/src/kernel/engine/runner/gates.js +7 -7
  74. package/esm/src/kernel/engine/runner/mod.js +13 -13
  75. package/esm/src/kernel/engine/runner/state.d.ts +7 -0
  76. package/esm/src/kernel/engine/runner/steps.d.ts +1 -1
  77. package/esm/src/kernel/engine/runner/steps.js +171 -37
  78. package/esm/src/kernel/engine/runner/stream.d.ts +1 -2
  79. package/esm/src/kernel/engine/runner/stream.js +66 -29
  80. package/esm/src/kernel/engine/session/mod.d.ts +23 -0
  81. package/esm/src/kernel/engine/session/mod.js +205 -0
  82. package/esm/src/kernel/mod.d.ts +9 -10
  83. package/esm/src/kernel/mod.js +6 -8
  84. package/esm/src/kernel/registry/attachments.d.ts +4 -2
  85. package/esm/src/kernel/registry/attachments.js +6 -3
  86. package/esm/src/kernel/registry/catalog.d.ts +3 -16
  87. package/esm/src/kernel/registry/catalog.js +5 -81
  88. package/esm/src/kernel/registry/ingress.d.ts +9 -4
  89. package/esm/src/kernel/registry/ingress.js +73 -63
  90. package/esm/src/kernel/registry/profiles.d.ts +35 -12
  91. package/esm/src/kernel/registry/profiles.js +159 -66
  92. package/esm/src/kernel/registry/provider-request.js +11 -7
  93. package/esm/src/kernel/registry/resolve.d.ts +5 -6
  94. package/esm/src/kernel/registry/resolve.js +100 -70
  95. package/esm/src/kernel/registry/vault.d.ts +12 -7
  96. package/esm/src/kernel/registry/vault.js +30 -8
  97. package/esm/src/kernel/schema.d.ts +145 -0
  98. package/esm/src/kernel/schema.js +462 -0
  99. package/esm/src/kernel/stop.d.ts +15 -10
  100. package/esm/src/kernel/stop.js +5 -5
  101. package/esm/src/kernel/tools/execute.d.ts +45 -0
  102. package/esm/src/kernel/tools/execute.js +363 -0
  103. package/esm/src/kernel/tools/harness.d.ts +8 -0
  104. package/esm/src/kernel/tools/harness.js +44 -0
  105. package/esm/src/kernel/tools/invoke.d.ts +10 -0
  106. package/esm/src/kernel/tools/invoke.js +83 -0
  107. package/esm/src/kernel/tools/mod.d.ts +11 -0
  108. package/esm/src/kernel/tools/mod.js +10 -0
  109. package/esm/src/kernel/tools/project.d.ts +12 -0
  110. package/esm/src/kernel/tools/project.js +34 -0
  111. package/esm/src/kernel/tools/registry.d.ts +23 -0
  112. package/esm/src/kernel/tools/registry.js +71 -0
  113. package/esm/src/kernel/tools/resolve.d.ts +29 -0
  114. package/esm/src/kernel/tools/resolve.js +262 -0
  115. package/esm/src/kernel/tools/schema.d.ts +13 -0
  116. package/esm/src/kernel/tools/schema.js +159 -0
  117. package/esm/src/kernel/tools/types.d.ts +216 -0
  118. package/esm/src/kernel/tools/types.js +9 -0
  119. package/esm/src/kernel/types.d.ts +338 -193
  120. package/esm/src/observability/trace-attach.d.ts +6 -4
  121. package/esm/src/observability/trace-attach.js +39 -25
  122. package/esm/src/observability/trace-record.d.ts +11 -9
  123. package/esm/src/observability/trace-record.js +36 -23
  124. package/esm/src/observability/trace-usage.d.ts +10 -3
  125. package/esm/src/observability/trace-usage.js +70 -17
  126. package/esm/src/observability/trace.d.ts +9 -1
  127. package/esm/src/observability/trace.js +11 -3
  128. package/esm/src/presets/google/speech-voices.d.ts +11 -0
  129. package/esm/src/presets/google/speech-voices.js +41 -0
  130. package/esm/src/presets/google.d.ts +36 -24
  131. package/esm/src/presets/google.js +50 -63
  132. package/esm/src/presets/mod.d.ts +2 -2
  133. package/esm/src/presets/mod.js +1 -1
  134. package/esm/src/providers/create-provider.d.ts +14 -14
  135. package/esm/src/providers/create-provider.js +55 -23
  136. package/esm/src/providers/google/interactions/framing.d.ts +15 -0
  137. package/esm/src/providers/google/interactions/framing.js +208 -0
  138. package/esm/src/providers/google/interactions/mod.d.ts +7 -0
  139. package/esm/src/providers/google/interactions/mod.js +7 -0
  140. package/esm/src/providers/google/interactions/stream.d.ts +83 -0
  141. package/esm/src/providers/google/interactions/stream.js +588 -0
  142. package/esm/src/providers/google/keys.d.ts +26 -0
  143. package/esm/src/providers/{keys.js → google/keys.js} +19 -31
  144. package/esm/src/providers/google/live/framing.d.ts +46 -0
  145. package/esm/src/providers/google/live/framing.js +515 -0
  146. package/esm/src/providers/google/live/openapi-schema.d.ts +6 -0
  147. package/esm/src/providers/google/live/openapi-schema.js +45 -0
  148. package/esm/src/providers/google/live/session.d.ts +25 -0
  149. package/esm/src/providers/google/live/session.js +134 -0
  150. package/esm/src/providers/google/live/stream.d.ts +36 -0
  151. package/esm/src/providers/google/live/stream.js +201 -0
  152. package/esm/src/providers/google/urls.d.ts +6 -0
  153. package/esm/src/providers/google/urls.js +6 -0
  154. package/esm/src/providers/local/local.d.ts +30 -0
  155. package/esm/src/providers/{local.js → local/local.js} +66 -126
  156. package/esm/src/providers/local/mod.d.ts +9 -0
  157. package/esm/src/providers/local/mod.js +9 -0
  158. package/esm/src/providers/mod.d.ts +6 -3
  159. package/esm/src/providers/mod.js +3 -1
  160. package/esm/src/providers/openrouter/chat.d.ts +87 -0
  161. package/esm/src/providers/{openrouter.js → openrouter/chat.js} +70 -229
  162. package/esm/src/providers/openrouter/image.d.ts +34 -0
  163. package/esm/src/providers/openrouter/image.js +286 -0
  164. package/esm/src/providers/openrouter/openai/chat-payload.d.ts +24 -0
  165. package/esm/src/providers/openrouter/openai/chat-payload.js +60 -0
  166. package/esm/src/providers/openrouter/openai/compat.d.ts +47 -0
  167. package/esm/src/providers/openrouter/openai/compat.js +194 -0
  168. package/esm/src/providers/openrouter/openai/image-payload.d.ts +18 -0
  169. package/esm/src/providers/openrouter/openai/image-payload.js +84 -0
  170. package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +22 -0
  171. package/esm/src/providers/openrouter/openai/sdk-messages.js +95 -0
  172. package/esm/src/providers/openrouter/resolve-api-key.d.ts +9 -0
  173. package/esm/src/providers/openrouter/resolve-api-key.js +24 -0
  174. package/esm/src/providers/openrouter/speech.d.ts +23 -0
  175. package/esm/src/providers/{speech.js → openrouter/speech.js} +32 -55
  176. package/esm/src/providers/probe.d.ts +1 -0
  177. package/esm/src/providers/probe.js +22 -0
  178. package/esm/src/providers/shared/pcm.d.ts +12 -0
  179. package/esm/src/providers/{pcm.js → shared/pcm.js} +16 -3
  180. package/esm/src/providers/shared/sse.d.ts +18 -0
  181. package/esm/src/providers/shared/sse.js +87 -0
  182. package/esm/src/providers/shared/tool-args.d.ts +17 -0
  183. package/esm/src/providers/shared/tool-args.js +45 -0
  184. package/esm/src/providers/shared/upstream-tap.d.ts +5 -0
  185. package/esm/src/providers/{google-tap.js → shared/upstream-tap.js} +4 -7
  186. package/esm/src/providers/shared/upstream-tape.d.ts +6 -0
  187. package/esm/src/providers/{gemini-tape.js → shared/upstream-tape.js} +12 -22
  188. package/esm/src/providers/types.d.ts +27 -0
  189. package/esm/src/providers/types.js +1 -0
  190. package/package.json +11 -7
  191. package/docs/cli.md +0 -97
  192. package/docs/guardrails.md +0 -178
  193. package/docs/host.md +0 -97
  194. package/docs/kernel.md +0 -404
  195. package/docs/observability.md +0 -105
  196. package/docs/openrouter.md +0 -125
  197. package/docs/presets-google.md +0 -91
  198. package/docs/presets.md +0 -88
  199. package/docs/providers.md +0 -201
  200. package/docs/streaming.md +0 -96
  201. package/esm/src/kernel/engine/boundary.d.ts +0 -10
  202. package/esm/src/kernel/engine/boundary.js +0 -55
  203. package/esm/src/kernel/engine/runner/tools.d.ts +0 -13
  204. package/esm/src/kernel/engine/runner/tools.js +0 -198
  205. package/esm/src/kernel/registry/tools.d.ts +0 -12
  206. package/esm/src/kernel/registry/tools.js +0 -36
  207. package/esm/src/providers/expose-for-tests.d.ts +0 -1
  208. package/esm/src/providers/expose-for-tests.js +0 -25
  209. package/esm/src/providers/gemini-tape.d.ts +0 -2
  210. package/esm/src/providers/google-tap.d.ts +0 -3
  211. package/esm/src/providers/interactions.d.ts +0 -5
  212. package/esm/src/providers/interactions.js +0 -169
  213. package/esm/src/providers/keys.d.ts +0 -19
  214. package/esm/src/providers/local.d.ts +0 -29
  215. package/esm/src/providers/openrouter-mod.d.ts +0 -13
  216. package/esm/src/providers/openrouter-mod.js +0 -12
  217. package/esm/src/providers/openrouter-payload.d.ts +0 -39
  218. package/esm/src/providers/openrouter-payload.js +0 -195
  219. package/esm/src/providers/openrouter.d.ts +0 -15
  220. package/esm/src/providers/pcm.d.ts +0 -7
  221. package/esm/src/providers/provider.d.ts +0 -15
  222. package/esm/src/providers/provider.js +0 -176
  223. package/esm/src/providers/speech.d.ts +0 -23
  224. package/esm/src/providers/sse.d.ts +0 -7
  225. package/esm/src/providers/sse.js +0 -55
  226. package/esm/src/streaming/mod.d.ts +0 -9
  227. package/esm/src/streaming/mod.js +0 -8
  228. /package/esm/src/{streaming → host}/readStreamingJsonStringField.d.ts +0 -0
  229. /package/esm/src/{streaming → host}/readStreamingJsonStringField.js +0 -0
@@ -1,13 +1,12 @@
1
1
  /**
2
- * Gemini key vault selection, quota overflow, and fetch retries.
2
+ * Google transport credentials, quota overflow, and fetch retries.
3
3
  *
4
- * Host applications supply vault credentials through `GeminiTransport`.
4
+ * Hosts supply a provider-neutral `KeyVault` via `GeminiTransport`.
5
5
  * THEORUM does not read environment variables for these keys.
6
6
  *
7
7
  * @module
8
8
  */
9
- import { isAbortError, TheorumError, UPSTREAM_FAILED } from '../guardrails/error.js';
10
- import { exposeForTests } from './expose-for-tests.js';
9
+ import { isAbortError, TheorumError, UPSTREAM_FAILED } from '../../guardrails/error.js';
11
10
  const ATTEMPTS = 3;
12
11
  const LAST_ATTEMPT = ATTEMPTS - 1;
13
12
  const BACKOFF_FIRST_MS = 1000;
@@ -22,16 +21,16 @@ const HTTP_UNAVAILABLE = 503;
22
21
  const HTTP_GATEWAY_TIMEOUT = 504;
23
22
  const QUOTA_RE = /quota/i;
24
23
  const TRANSIENT_THROWN_RE = /name resolution|dns|econnreset|econnrefused|etimedout|network|fetch failed|temporarily unavailable|socket|503|502|504/i;
25
- function waitDefault(ms) {
24
+ export function waitDefault(ms) {
26
25
  return new Promise((resolve) => {
27
26
  setTimeout(resolve, ms);
28
27
  });
29
28
  }
30
- function isQuota(err) {
29
+ export function isQuota(err) {
31
30
  const s = String(err);
32
31
  return s.includes(String(HTTP_QUOTA)) || s.includes('RESOURCE_EXHAUSTED') || QUOTA_RE.test(s);
33
32
  }
34
- function isTransientHttp(status) {
33
+ export function isTransientHttp(status) {
35
34
  return (status === HTTP_TIMEOUT ||
36
35
  status === HTTP_QUOTA ||
37
36
  status === HTTP_SERVER ||
@@ -39,20 +38,20 @@ function isTransientHttp(status) {
39
38
  status === HTTP_UNAVAILABLE ||
40
39
  status === HTTP_GATEWAY_TIMEOUT);
41
40
  }
42
- function isTransientThrown(err) {
41
+ export function isTransientThrown(err) {
43
42
  if (isAbortError(err)) {
44
43
  return false;
45
44
  }
46
45
  return TRANSIENT_THROWN_RE.test(String(err));
47
46
  }
48
- function requireKey(vault, bucket) {
49
- const key = vault[bucket];
47
+ export function requireKey(vault, slot) {
48
+ const key = vault[slot];
50
49
  if (!key) {
51
50
  throw new TheorumError(UPSTREAM_FAILED);
52
51
  }
53
52
  return key;
54
53
  }
55
- function backoffMs(attempt) {
54
+ export function backoffMs(attempt) {
56
55
  return BACKOFF_MS[attempt] ?? BACKOFF_SECOND_MS;
57
56
  }
58
57
  async function runWithBackoff(apiKey, run, wait, attempt) {
@@ -69,8 +68,8 @@ async function runWithBackoff(apiKey, run, wait, attempt) {
69
68
  return runWithBackoff(apiKey, run, wait, attempt + 1);
70
69
  }
71
70
  }
72
- function canOverflow(bucket, vault, primary) {
73
- if (bucket === 'paid') {
71
+ export function canOverflow(slot, vault, primary) {
72
+ if (slot === 'paid') {
74
73
  return undefined;
75
74
  }
76
75
  const { paid } = vault;
@@ -79,21 +78,21 @@ function canOverflow(bucket, vault, primary) {
79
78
  }
80
79
  return paid;
81
80
  }
82
- async function withGeminiKey(bucket, run, transport) {
81
+ export async function withGeminiKey(slot, run, transport) {
83
82
  const wait = transport.wait ?? waitDefault;
84
- const primary = requireKey(transport.vault, bucket);
83
+ const primary = requireKey(transport.vault, slot);
85
84
  try {
86
85
  return await runWithBackoff(primary, run, wait, 0);
87
86
  }
88
87
  catch (err) {
89
- const paid = canOverflow(bucket, transport.vault, primary);
88
+ const paid = canOverflow(slot, transport.vault, primary);
90
89
  if (!(isQuota(err) && paid)) {
91
90
  throw err;
92
91
  }
93
92
  return await runWithBackoff(paid, run, wait, 0);
94
93
  }
95
94
  }
96
- function withApiKey(init, apiKey) {
95
+ export function withApiKey(init, apiKey) {
97
96
  const headers = new Headers(init.headers);
98
97
  headers.set('x-goog-api-key', apiKey);
99
98
  if (!headers.has('Content-Type') && (init.method || 'GET').toUpperCase() !== 'GET') {
@@ -125,26 +124,15 @@ async function fetchWithBackoff(args) {
125
124
  return fetchWithBackoff({ ...args, attempt: args.attempt + 1 });
126
125
  }
127
126
  }
128
- async function fetchGemini(url, init, bucket, transport) {
127
+ export async function fetchGemini(url, init, slot, transport) {
129
128
  const parsed = new URL(url);
130
129
  parsed.searchParams.delete('key');
131
130
  const href = parsed.toString();
132
- const primary = requireKey(transport.vault, bucket);
131
+ const primary = requireKey(transport.vault, slot);
133
132
  let last = await fetchWithBackoff({ href, init, apiKey: primary, transport, attempt: 0 });
134
- const paid = canOverflow(bucket, transport.vault, primary);
133
+ const paid = canOverflow(slot, transport.vault, primary);
135
134
  if (last.status === HTTP_QUOTA && paid) {
136
135
  last = await fetchWithBackoff({ href, init, apiKey: paid, transport, attempt: 0 });
137
136
  }
138
137
  return last;
139
138
  }
140
- export { fetchGemini, withGeminiKey };
141
- exposeForTests('keys', {
142
- waitDefault,
143
- isQuota,
144
- isTransientHttp,
145
- isTransientThrown,
146
- requireKey,
147
- backoffMs,
148
- canOverflow,
149
- withApiKey,
150
- });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Pure protocol framing & serialization for Google Gemini Live WebSocket API (`BidiGenerateContent`).
3
+ *
4
+ * All functions are pure data transformations with no network I/O.
5
+ *
6
+ * @module
7
+ */
8
+ import type { InteractionPart, ProviderCompleteRequest, TurnEvent, TurnHistoryMessage, TurnTokens, WireFunctionTool } from '../../../kernel/types.js';
9
+ import { parseToolArgumentsObject } from '../../shared/tool-args.js';
10
+ /** Construct authenticated WebSocket URL for Gemini Live API. */
11
+ export declare function buildGeminiLiveWebSocketUrl(apiKey: string): string;
12
+ export declare function wireFunctionDeclaration(decl: WireFunctionTool): Record<string, unknown>;
13
+ export declare function wireLiveTools(req: ProviderCompleteRequest): Array<Record<string, unknown>>;
14
+ /** Build the initial `setup` message sent once immediately after WebSocket open. */
15
+ export declare function buildGeminiLiveSetupMessage(req: ProviderCompleteRequest): Record<string, unknown>;
16
+ /** Build the `clientContent` message used for seeding conversation history before realtime streaming. */
17
+ export declare function buildGeminiLiveClientContent(history: TurnHistoryMessage[]): Record<string, unknown> | null;
18
+ /** Build a `realtimeInput` message for streaming audio, video, or text chunks. */
19
+ export declare function buildGeminiLiveRealtimeInput(part: InteractionPart): Record<string, unknown>;
20
+ /** Build a `realtimeInput` message with text. */
21
+ export declare function buildGeminiLiveRealtimeText(text: string): Record<string, unknown>;
22
+ /** Build a `toolResponse` message returning the execution result of a tool call. */
23
+ export declare function buildGeminiLiveToolResponse(id: string, name: string, output: unknown): Record<string, unknown>;
24
+ /** Build a batched `toolResponse` for one or more function results. */
25
+ export declare function buildGeminiLiveToolResponses(responses: Array<{
26
+ id: string;
27
+ name: string;
28
+ output: unknown;
29
+ }>): Record<string, unknown>;
30
+ /** Parse raw WebSocket message text / buffer into a JSON record. */
31
+ export type ParsedLiveMessage = {
32
+ ok: true;
33
+ value: Record<string, unknown>;
34
+ } | {
35
+ ok: false;
36
+ reason: 'empty' | 'malformed';
37
+ };
38
+ export declare function parseGeminiLiveMessage(raw: unknown): ParsedLiveMessage;
39
+ export declare function parseFunctionArguments(raw: unknown): ReturnType<typeof parseToolArgumentsObject>;
40
+ export declare function extractUsageTokens(metadata: Record<string, unknown>): TurnTokens | undefined;
41
+ /** Parse Gemini goAway.timeLeft (seconds number, "10s", duration string) → ms when known. */
42
+ export declare function parseGoAwayTimeLeftMs(timeLeft: unknown): number | undefined;
43
+ /**
44
+ * Fold a raw `BidiGenerateContentServerMessage` into normalized `TurnEvent` items.
45
+ */
46
+ export declare function foldGeminiLiveServerMessage(message: Record<string, unknown> | null | undefined): TurnEvent[];
@@ -0,0 +1,515 @@
1
+ /**
2
+ * Pure protocol framing & serialization for Google Gemini Live WebSocket API (`BidiGenerateContent`).
3
+ *
4
+ * All functions are pure data transformations with no network I/O.
5
+ *
6
+ * @module
7
+ */
8
+ import { getTool } from '../../../kernel/tools/registry.js';
9
+ import { groundingFromEvent } from '../../../kernel/engine/delta.js';
10
+ import { base64ToBytes, bytesToBase64, wrapPcmAsWav } from '../../shared/pcm.js';
11
+ import { parseToolArgumentsObject } from '../../shared/tool-args.js';
12
+ import { GEMINI_LIVE_WS_URL } from '../urls.js';
13
+ import { toGeminiOpenApiSchema } from './openapi-schema.js';
14
+ /** Construct authenticated WebSocket URL for Gemini Live API. */
15
+ export function buildGeminiLiveWebSocketUrl(apiKey) {
16
+ return `${GEMINI_LIVE_WS_URL}?key=${encodeURIComponent(apiKey)}`;
17
+ }
18
+ export function wireFunctionDeclaration(decl) {
19
+ const parameters = toGeminiOpenApiSchema(decl.parameters);
20
+ return {
21
+ name: decl.name,
22
+ description: decl.description,
23
+ parameters: parameters && typeof parameters === 'object'
24
+ ? parameters
25
+ : { type: 'OBJECT', properties: {} },
26
+ };
27
+ }
28
+ export function wireLiveTools(req) {
29
+ const functionDeclarations = [];
30
+ for (const id of req.builtins) {
31
+ const entry = getTool(id);
32
+ if (entry?.type === 'builtin' && entry.wire.live) {
33
+ functionDeclarations.push({
34
+ name: id,
35
+ description: entry.description,
36
+ });
37
+ }
38
+ }
39
+ for (const decl of req.wireTools ?? []) {
40
+ functionDeclarations.push(wireFunctionDeclaration(decl));
41
+ }
42
+ if (functionDeclarations.length === 0) {
43
+ return [];
44
+ }
45
+ return [{ functionDeclarations }];
46
+ }
47
+ function buildLiveGenerationConfig(req) {
48
+ const generationConfig = {
49
+ responseModalities: ['AUDIO'],
50
+ temperature: req.temperature,
51
+ maxOutputTokens: req.maxOutputTokens,
52
+ };
53
+ if (req.live?.voice) {
54
+ generationConfig.speechConfig = {
55
+ voiceConfig: {
56
+ prebuiltVoiceConfig: {
57
+ voiceName: req.live.voice,
58
+ },
59
+ },
60
+ };
61
+ }
62
+ if (req.thinking && req.thinking !== 'none') {
63
+ generationConfig.thinkingConfig = {
64
+ thinkingLevel: req.thinking,
65
+ };
66
+ }
67
+ return generationConfig;
68
+ }
69
+ function normalizeStartSensitivity(val) {
70
+ return val?.includes('HIGH') ? 'START_SENSITIVITY_HIGH' : 'START_SENSITIVITY_LOW';
71
+ }
72
+ function normalizeEndSensitivity(val) {
73
+ return val?.includes('HIGH') ? 'END_SENSITIVITY_HIGH' : 'END_SENSITIVITY_LOW';
74
+ }
75
+ function buildLiveRealtimeInputConfig(vad) {
76
+ const automaticActivityDetection = {};
77
+ if (vad.startSensitivity !== undefined) {
78
+ automaticActivityDetection.startOfSpeechSensitivity = normalizeStartSensitivity(vad.startSensitivity);
79
+ }
80
+ if (vad.endSensitivity !== undefined) {
81
+ automaticActivityDetection.endOfSpeechSensitivity = normalizeEndSensitivity(vad.endSensitivity);
82
+ }
83
+ if (vad.prefixPaddingMs !== undefined) {
84
+ automaticActivityDetection.prefixPaddingMs = vad.prefixPaddingMs;
85
+ }
86
+ if (vad.silenceDurationMs !== undefined) {
87
+ automaticActivityDetection.silenceDurationMs = vad.silenceDurationMs;
88
+ }
89
+ const config = {};
90
+ if (vad.activityHandling !== undefined) {
91
+ config.activityHandling = vad.activityHandling;
92
+ }
93
+ if (Object.keys(automaticActivityDetection).length > 0) {
94
+ config.automaticActivityDetection = automaticActivityDetection;
95
+ }
96
+ return Object.keys(config).length > 0 ? config : undefined;
97
+ }
98
+ function buildLiveSessionResumption(req) {
99
+ if (req.sessionResumptionHandle) {
100
+ return { handle: req.sessionResumptionHandle };
101
+ }
102
+ if (req.live?.sessionResumption) {
103
+ return {};
104
+ }
105
+ return undefined;
106
+ }
107
+ function liveModelName(apiId) {
108
+ return apiId.startsWith('models/') ? apiId : `models/${apiId}`;
109
+ }
110
+ function applyLiveOptionalFeatures(live, setup) {
111
+ if (live?.transcription?.input) {
112
+ setup.inputAudioTranscription = {};
113
+ }
114
+ if (live?.transcription?.output) {
115
+ setup.outputAudioTranscription = {};
116
+ }
117
+ if (live?.proactiveAudio === true) {
118
+ setup.proactivity = { proactiveAudio: true };
119
+ }
120
+ }
121
+ /** Build the initial `setup` message sent once immediately after WebSocket open. */
122
+ export function buildGeminiLiveSetupMessage(req) {
123
+ const live = req.live;
124
+ const realtimeInputConfig = live?.vad ? buildLiveRealtimeInputConfig(live.vad) : undefined;
125
+ const tools = wireLiveTools(req);
126
+ const sessionResumption = buildLiveSessionResumption(req);
127
+ // Only opt into initial-history gating when we actually have history to seed.
128
+ // With `initialHistoryInClientContent: true`, Gemini waits for clientContent
129
+ // after setupComplete and will not start realtime generation until that lands —
130
+ // empty sessions (e.g. Th30) would hang forever if this were always set.
131
+ const seedInitialHistory = Boolean(req.history && req.history.length > 0);
132
+ const setup = {
133
+ model: liveModelName(req.apiId),
134
+ generationConfig: buildLiveGenerationConfig(req),
135
+ systemInstruction: {
136
+ parts: [{ text: req.system }],
137
+ },
138
+ ...(tools.length > 0 ? { tools } : {}),
139
+ ...(sessionResumption ? { sessionResumption } : {}),
140
+ ...(live?.contextCompression === 'slidingWindow'
141
+ ? { contextWindowCompression: { slidingWindow: {} } }
142
+ : {}),
143
+ ...(seedInitialHistory ? { historyConfig: { initialHistoryInClientContent: true } } : {}),
144
+ ...(realtimeInputConfig ? { realtimeInputConfig } : {}),
145
+ };
146
+ applyLiveOptionalFeatures(live, setup);
147
+ return { setup };
148
+ }
149
+ /** Format a single history message into a Google turn object. */
150
+ function historyTurnToGoogleTurn(msg) {
151
+ const role = msg.role === 'assistant' ? 'model' : 'user';
152
+ const parts = [];
153
+ if (msg.content) {
154
+ parts.push({ text: msg.content });
155
+ }
156
+ for (const part of msg.parts ?? []) {
157
+ if (part.type === 'text') {
158
+ parts.push({ text: part.text });
159
+ }
160
+ else {
161
+ parts.push({
162
+ inlineData: {
163
+ mimeType: part.mimeType,
164
+ data: part.data,
165
+ },
166
+ });
167
+ }
168
+ }
169
+ return { role, parts };
170
+ }
171
+ /** Build the `clientContent` message used for seeding conversation history before realtime streaming. */
172
+ export function buildGeminiLiveClientContent(history) {
173
+ if (!history || history.length === 0) {
174
+ return null;
175
+ }
176
+ return {
177
+ clientContent: {
178
+ turns: history.map(historyTurnToGoogleTurn),
179
+ turnComplete: true,
180
+ },
181
+ };
182
+ }
183
+ /** Build a `realtimeInput` message for streaming audio, video, or text chunks. */
184
+ export function buildGeminiLiveRealtimeInput(part) {
185
+ if (part.type === 'text') {
186
+ return {
187
+ realtimeInput: {
188
+ text: part.text,
189
+ },
190
+ };
191
+ }
192
+ if (part.type === 'audio') {
193
+ return {
194
+ realtimeInput: {
195
+ audio: {
196
+ mimeType: part.mimeType.includes('rate=') ? part.mimeType : 'audio/pcm;rate=16000',
197
+ data: part.data,
198
+ },
199
+ },
200
+ };
201
+ }
202
+ // Image / video frame
203
+ return {
204
+ realtimeInput: {
205
+ video: {
206
+ mimeType: part.mimeType || 'image/jpeg',
207
+ data: part.data,
208
+ },
209
+ },
210
+ };
211
+ }
212
+ /** Build a `realtimeInput` message with text. */
213
+ export function buildGeminiLiveRealtimeText(text) {
214
+ return {
215
+ realtimeInput: {
216
+ text,
217
+ },
218
+ };
219
+ }
220
+ /** Build the Gemini Live `response` struct for a function result. */
221
+ function liveFunctionResponsePayload(output) {
222
+ if (typeof output === 'object' &&
223
+ output !== null &&
224
+ 'error' in output &&
225
+ typeof output.error === 'string') {
226
+ return { error: output.error };
227
+ }
228
+ return { result: output };
229
+ }
230
+ /** Build a `toolResponse` message returning the execution result of a tool call. */
231
+ export function buildGeminiLiveToolResponse(id, name, output) {
232
+ return buildGeminiLiveToolResponses([{ id, name, output }]);
233
+ }
234
+ /** Build a batched `toolResponse` for one or more function results. */
235
+ export function buildGeminiLiveToolResponses(responses) {
236
+ return {
237
+ toolResponse: {
238
+ functionResponses: responses.map(({ id, name, output }) => ({
239
+ id,
240
+ name,
241
+ response: liveFunctionResponsePayload(output),
242
+ })),
243
+ },
244
+ };
245
+ }
246
+ export function parseGeminiLiveMessage(raw) {
247
+ if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) {
248
+ if (raw instanceof ArrayBuffer || raw instanceof Uint8Array) {
249
+ const text = new TextDecoder().decode(raw);
250
+ return parseGeminiLiveMessage(text);
251
+ }
252
+ }
253
+ if (typeof raw !== 'string' || !raw.trim()) {
254
+ return { ok: false, reason: 'empty' };
255
+ }
256
+ try {
257
+ const parsed = JSON.parse(raw);
258
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
259
+ return { ok: true, value: parsed };
260
+ }
261
+ return { ok: false, reason: 'malformed' };
262
+ }
263
+ catch {
264
+ return { ok: false, reason: 'malformed' };
265
+ }
266
+ }
267
+ export function parseFunctionArguments(raw) {
268
+ return parseToolArgumentsObject(raw);
269
+ }
270
+ function readTokenCount(metadata, camelKey, snakeKey) {
271
+ const val = metadata[camelKey] ?? metadata[snakeKey];
272
+ return typeof val === 'number' ? val : 0;
273
+ }
274
+ export function extractUsageTokens(metadata) {
275
+ const prompt = readTokenCount(metadata, 'promptTokenCount', 'prompt_token_count');
276
+ const output = readTokenCount(metadata, 'responseTokenCount', 'response_token_count');
277
+ const thinking = readTokenCount(metadata, 'thoughtsTokenCount', 'thoughts_token_count');
278
+ const rawTotal = metadata.totalTokenCount ?? metadata.total_token_count;
279
+ const total = typeof rawTotal === 'number' ? rawTotal : prompt + output;
280
+ if (prompt === 0 && output === 0 && total === 0) {
281
+ return undefined;
282
+ }
283
+ return {
284
+ input: prompt,
285
+ output,
286
+ thinking: thinking > 0 ? thinking : undefined,
287
+ total,
288
+ };
289
+ }
290
+ function foldSessionUpdate(message, events) {
291
+ const sessionUpdate = message.sessionResumptionUpdate;
292
+ if (!sessionUpdate || typeof sessionUpdate !== 'object')
293
+ return;
294
+ const hasHandle = typeof sessionUpdate.newHandle === 'string' && sessionUpdate.newHandle.length > 0;
295
+ const hasResumable = typeof sessionUpdate.resumable === 'boolean';
296
+ if (!hasHandle && !hasResumable)
297
+ return;
298
+ events.push({
299
+ type: 'evidence',
300
+ ...(hasHandle ? { sessionResumptionHandle: sessionUpdate.newHandle } : {}),
301
+ evidence: {
302
+ provider: 'google',
303
+ kind: 'session_resumption',
304
+ resumable: hasResumable ? sessionUpdate.resumable : hasHandle,
305
+ raw: sessionUpdate,
306
+ },
307
+ });
308
+ }
309
+ function foldToolCalls(message, events) {
310
+ const toolCall = message.toolCall;
311
+ if (!toolCall?.functionCalls || !Array.isArray(toolCall.functionCalls))
312
+ return;
313
+ for (const call of toolCall.functionCalls) {
314
+ if (!call.name)
315
+ continue;
316
+ const parsed = parseFunctionArguments(call.args);
317
+ if (!parsed.ok) {
318
+ events.push({
319
+ type: 'tool',
320
+ tool: {
321
+ id: call.id,
322
+ name: call.name,
323
+ arguments: {},
324
+ phase: 'error',
325
+ failure: {
326
+ code: 'malformed_arguments',
327
+ message: parsed.error,
328
+ details: { raw: parsed.raw },
329
+ },
330
+ },
331
+ });
332
+ continue;
333
+ }
334
+ events.push({
335
+ type: 'tool',
336
+ tool: {
337
+ id: call.id,
338
+ name: call.name,
339
+ arguments: parsed.value,
340
+ },
341
+ });
342
+ }
343
+ }
344
+ function foldToolCancellations(message, events) {
345
+ const cancellation = message.toolCallCancellation;
346
+ const ids = cancellation?.ids;
347
+ if (!Array.isArray(ids))
348
+ return;
349
+ for (const id of ids) {
350
+ if (typeof id !== 'string' || id.length === 0)
351
+ continue;
352
+ events.push({
353
+ type: 'tool',
354
+ tool: {
355
+ id,
356
+ name: '',
357
+ phase: 'cancel',
358
+ },
359
+ });
360
+ }
361
+ }
362
+ /** Parse Gemini goAway.timeLeft (seconds number, "10s", duration string) → ms when known. */
363
+ export function parseGoAwayTimeLeftMs(timeLeft) {
364
+ if (typeof timeLeft === 'number' && Number.isFinite(timeLeft) && timeLeft >= 0) {
365
+ return Math.round(timeLeft * 1000);
366
+ }
367
+ if (typeof timeLeft !== 'string')
368
+ return undefined;
369
+ const trimmed = timeLeft.trim();
370
+ if (!trimmed)
371
+ return undefined;
372
+ const seconds = Number(trimmed);
373
+ if (Number.isFinite(seconds) && seconds >= 0) {
374
+ return Math.round(seconds * 1000);
375
+ }
376
+ const match = /^(\d+(?:\.\d+)?)\s*s$/i.exec(trimmed);
377
+ if (match?.[1]) {
378
+ const s = Number(match[1]);
379
+ if (Number.isFinite(s) && s >= 0)
380
+ return Math.round(s * 1000);
381
+ }
382
+ return undefined;
383
+ }
384
+ function foldGoAway(message, events) {
385
+ const goAway = message.goAway;
386
+ if (!goAway || typeof goAway !== 'object')
387
+ return;
388
+ const timeLeftMs = parseGoAwayTimeLeftMs(goAway.timeLeft);
389
+ events.push({
390
+ type: 'session',
391
+ session: {
392
+ kind: 'closing_soon',
393
+ ...(timeLeftMs !== undefined ? { timeLeftMs } : {}),
394
+ },
395
+ });
396
+ }
397
+ function foldModelPart(part, events) {
398
+ if (part.text) {
399
+ if (part.thought) {
400
+ events.push({ type: 'thought', text: part.text });
401
+ }
402
+ else {
403
+ events.push({ type: 'text', text: part.text });
404
+ }
405
+ }
406
+ if (part.inlineData?.data) {
407
+ const mime = part.inlineData.mimeType ?? 'audio/pcm;rate=24000';
408
+ if (mime.startsWith('audio/pcm') ||
409
+ mime.startsWith('audio/raw') ||
410
+ mime.startsWith('audio/l16')) {
411
+ const wav = wrapPcmAsWav(base64ToBytes(part.inlineData.data), 24000);
412
+ events.push({
413
+ type: 'media',
414
+ media: { mimeType: 'audio/wav', data: bytesToBase64(wav) },
415
+ });
416
+ }
417
+ else {
418
+ events.push({
419
+ type: 'media',
420
+ media: { mimeType: mime, data: part.inlineData.data },
421
+ });
422
+ }
423
+ }
424
+ }
425
+ function foldTranscription(text, kind, events, interim) {
426
+ if (!text)
427
+ return;
428
+ events.push({
429
+ type: 'evidence',
430
+ text,
431
+ evidence: {
432
+ provider: 'google',
433
+ kind,
434
+ ...(interim ? { interim: true } : {}),
435
+ },
436
+ });
437
+ }
438
+ function foldLiveGrounding(serverContent, events) {
439
+ const groundingEvent = groundingFromEvent({
440
+ groundingMetadata: serverContent.groundingMetadata ?? serverContent.grounding_metadata,
441
+ });
442
+ if (groundingEvent) {
443
+ events.push(groundingEvent);
444
+ }
445
+ const urlContext = serverContent.urlContextMetadata ?? serverContent.url_context_metadata;
446
+ if (urlContext && typeof urlContext === 'object') {
447
+ events.push({
448
+ type: 'evidence',
449
+ evidence: {
450
+ provider: 'google',
451
+ kind: 'url_context',
452
+ raw: urlContext,
453
+ },
454
+ });
455
+ }
456
+ }
457
+ function foldServerContent(message, events) {
458
+ const serverContent = message.serverContent;
459
+ if (!serverContent || typeof serverContent !== 'object')
460
+ return;
461
+ if (serverContent.interrupted === true) {
462
+ events.push({
463
+ type: 'done',
464
+ interrupted: true,
465
+ stop: { kind: 'interrupted' },
466
+ });
467
+ }
468
+ if (serverContent.waitingForInput === true || serverContent.waiting_for_input === true) {
469
+ events.push({
470
+ type: 'session',
471
+ session: { kind: 'waiting_for_input' },
472
+ });
473
+ }
474
+ if (serverContent.generationComplete === true || serverContent.generation_complete === true) {
475
+ events.push({
476
+ type: 'done',
477
+ stop: { kind: 'generation_complete' },
478
+ });
479
+ }
480
+ const inputTranscription = serverContent.inputTranscription;
481
+ foldTranscription(inputTranscription?.text, 'input_transcription', events);
482
+ const interimInput = serverContent.interimInputTranscription;
483
+ foldTranscription(interimInput?.text, 'input_transcription', events, true);
484
+ const outputTranscription = serverContent.outputTranscription;
485
+ foldTranscription(outputTranscription?.text, 'output_transcription', events);
486
+ const modelTurn = serverContent.modelTurn;
487
+ for (const part of modelTurn?.parts ?? []) {
488
+ foldModelPart(part, events);
489
+ }
490
+ foldLiveGrounding(serverContent, events);
491
+ }
492
+ function foldUsageMetadata(message, events) {
493
+ const usageMetadata = message.usageMetadata;
494
+ if (usageMetadata) {
495
+ const tokens = extractUsageTokens(usageMetadata);
496
+ if (tokens) {
497
+ events.push({ type: 'tokens', tokens });
498
+ }
499
+ }
500
+ }
501
+ /**
502
+ * Fold a raw `BidiGenerateContentServerMessage` into normalized `TurnEvent` items.
503
+ */
504
+ export function foldGeminiLiveServerMessage(message) {
505
+ if (!message || typeof message !== 'object')
506
+ return [];
507
+ const events = [];
508
+ foldGoAway(message, events);
509
+ foldSessionUpdate(message, events);
510
+ foldToolCalls(message, events);
511
+ foldToolCancellations(message, events);
512
+ foldServerContent(message, events);
513
+ foldUsageMetadata(message, events);
514
+ return events;
515
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Gemini Live / OpenAPI Schema 3.0 style type uppercasing for function parameters.
3
+ * JSON Schema uses lowercase `object`/`string`; Gemini Live expects `OBJECT`/`STRING`.
4
+ */
5
+ /** Convert JSON-Schema-ish parameters to Gemini Live OpenAPI Schema shape. */
6
+ export declare function toGeminiOpenApiSchema(value: unknown): unknown;