theorum 0.1.15 → 1.1.3

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 (305) hide show
  1. package/README.md +241 -98
  2. package/esm/mod.d.ts +57 -28
  3. package/esm/mod.js +43 -23
  4. package/esm/src/cli/commands/bench.js +18 -18
  5. package/esm/src/cli/commands/fuzz-canary.d.ts +13 -0
  6. package/esm/src/cli/commands/fuzz-canary.js +191 -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/guardrails-eval.d.ts +14 -0
  10. package/esm/src/cli/commands/guardrails-eval.js +15 -0
  11. package/esm/src/cli/commands/profile.js +35 -15
  12. package/esm/src/cli/commands/run.d.ts +3 -0
  13. package/esm/src/cli/commands/run.js +23 -32
  14. package/esm/src/cli/commands/test.d.ts +10 -1
  15. package/esm/src/cli/commands/test.js +34 -34
  16. package/esm/src/cli/event-log.d.ts +19 -0
  17. package/esm/src/cli/event-log.js +147 -0
  18. package/esm/src/cli/index.js +57 -11
  19. package/esm/src/cli/matrix/synthesizer.d.ts +10 -12
  20. package/esm/src/cli/matrix/synthesizer.js +45 -118
  21. package/esm/src/guardrails/canary-gate.d.ts +21 -0
  22. package/esm/src/guardrails/canary-gate.js +32 -0
  23. package/esm/src/guardrails/canary.d.ts +34 -0
  24. package/esm/src/guardrails/canary.js +150 -0
  25. package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +17 -0
  26. package/esm/src/guardrails/corpus/canary-egress-attacks.js +151 -0
  27. package/esm/src/guardrails/corpus/fuzz-inbound.d.ts +11 -0
  28. package/esm/src/guardrails/corpus/fuzz-inbound.js +213 -0
  29. package/esm/src/guardrails/corpus/inbound-payloads.d.ts +10 -0
  30. package/esm/src/guardrails/corpus/inbound-payloads.js +125 -0
  31. package/esm/src/guardrails/corpus/live-attacks.d.ts +20 -0
  32. package/esm/src/guardrails/corpus/live-attacks.js +231 -0
  33. package/esm/src/guardrails/corpus/mod.d.ts +14 -0
  34. package/esm/src/guardrails/corpus/mod.js +11 -0
  35. package/esm/src/guardrails/corpus/secrets.d.ts +17 -0
  36. package/esm/src/guardrails/corpus/secrets.js +17 -0
  37. package/esm/src/guardrails/corpus/strings.d.ts +28 -0
  38. package/esm/src/guardrails/corpus/strings.js +34 -0
  39. package/esm/src/guardrails/corpus/types.d.ts +38 -0
  40. package/esm/src/guardrails/corpus/types.js +6 -0
  41. package/esm/src/guardrails/egress.d.ts +32 -0
  42. package/esm/src/guardrails/egress.js +87 -0
  43. package/esm/src/guardrails/error.d.ts +14 -23
  44. package/esm/src/guardrails/error.js +87 -76
  45. package/esm/src/guardrails/eval/corpus.d.ts +108 -0
  46. package/esm/src/guardrails/eval/corpus.js +978 -0
  47. package/esm/src/guardrails/eval/mod.d.ts +51 -0
  48. package/esm/src/guardrails/eval/mod.js +133 -0
  49. package/esm/src/guardrails/eval/score.d.ts +66 -0
  50. package/esm/src/guardrails/eval/score.js +114 -0
  51. package/esm/src/guardrails/events.d.ts +25 -0
  52. package/esm/src/guardrails/events.js +56 -0
  53. package/esm/src/guardrails/hits.d.ts +24 -0
  54. package/esm/src/guardrails/hits.js +45 -0
  55. package/esm/src/guardrails/injection.js +28 -5
  56. package/esm/src/guardrails/lexicon.d.ts +39 -0
  57. package/esm/src/guardrails/lexicon.js +200 -0
  58. package/esm/src/guardrails/live-outbound-gate.d.ts +41 -0
  59. package/esm/src/guardrails/live-outbound-gate.js +222 -0
  60. package/esm/src/guardrails/mod.d.ts +30 -6
  61. package/esm/src/guardrails/mod.js +20 -5
  62. package/esm/src/guardrails/network.d.ts +19 -0
  63. package/esm/src/guardrails/network.js +234 -0
  64. package/esm/src/guardrails/policy.d.ts +35 -0
  65. package/esm/src/guardrails/policy.js +50 -0
  66. package/esm/src/guardrails/progressive-yield.d.ts +51 -0
  67. package/esm/src/guardrails/progressive-yield.js +98 -0
  68. package/esm/src/guardrails/quota.d.ts +17 -3
  69. package/esm/src/guardrails/quota.js +18 -4
  70. package/esm/src/guardrails/sanitize.d.ts +45 -19
  71. package/esm/src/guardrails/sanitize.js +177 -94
  72. package/esm/src/guardrails/sensitive.js +2 -1
  73. package/esm/src/guardrails/serialize.d.ts +35 -0
  74. package/esm/src/guardrails/serialize.js +58 -0
  75. package/esm/src/guardrails/testing.d.ts +17 -0
  76. package/esm/src/guardrails/testing.js +13 -0
  77. package/esm/src/guardrails/theorum-error.d.ts +12 -0
  78. package/esm/src/guardrails/theorum-error.js +15 -0
  79. package/esm/src/guardrails/tool-directives.d.ts +48 -0
  80. package/esm/src/guardrails/tool-directives.js +124 -0
  81. package/esm/src/guardrails/tool-result.d.ts +93 -0
  82. package/esm/src/guardrails/tool-result.js +276 -0
  83. package/esm/src/guardrails/types.d.ts +291 -0
  84. package/esm/src/guardrails/types.js +72 -0
  85. package/esm/src/host/client-turn.d.ts +19 -0
  86. package/esm/src/host/client-turn.js +36 -0
  87. package/esm/src/host/mint-trace.d.ts +1 -1
  88. package/esm/src/host/mod.d.ts +5 -3
  89. package/esm/src/host/mod.js +4 -3
  90. package/esm/src/kernel/auth/crypto.d.ts +42 -0
  91. package/esm/src/kernel/auth/crypto.js +106 -0
  92. package/esm/src/kernel/auth/mod.d.ts +11 -0
  93. package/esm/src/kernel/auth/mod.js +11 -0
  94. package/esm/src/kernel/auth/oauth.d.ts +47 -0
  95. package/esm/src/kernel/auth/oauth.js +278 -0
  96. package/esm/src/kernel/auth/types.d.ts +133 -0
  97. package/esm/src/kernel/auth/types.js +13 -0
  98. package/esm/src/kernel/engine/delta.d.ts +24 -2
  99. package/esm/src/kernel/engine/delta.js +478 -39
  100. package/esm/src/kernel/engine/live-inbound.d.ts +21 -0
  101. package/esm/src/kernel/engine/live-inbound.js +31 -0
  102. package/esm/src/kernel/engine/live-ingress.d.ts +19 -0
  103. package/esm/src/kernel/engine/live-ingress.js +47 -0
  104. package/esm/src/kernel/engine/repair.js +13 -12
  105. package/esm/src/kernel/engine/runner/gates.d.ts +1 -1
  106. package/esm/src/kernel/engine/runner/gates.js +130 -43
  107. package/esm/src/kernel/engine/runner/mod.d.ts +6 -4
  108. package/esm/src/kernel/engine/runner/mod.js +192 -53
  109. package/esm/src/kernel/engine/runner/schema-validation.js +3 -3
  110. package/esm/src/kernel/engine/runner/stages.d.ts +39 -0
  111. package/esm/src/kernel/engine/runner/stages.js +89 -0
  112. package/esm/src/kernel/engine/runner/state.d.ts +31 -0
  113. package/esm/src/kernel/engine/runner/steps.d.ts +1 -1
  114. package/esm/src/kernel/engine/runner/steps.js +244 -43
  115. package/esm/src/kernel/engine/runner/stream.d.ts +9 -3
  116. package/esm/src/kernel/engine/runner/stream.js +140 -44
  117. package/esm/src/kernel/engine/session/mod.d.ts +25 -0
  118. package/esm/src/kernel/engine/session/mod.js +557 -0
  119. package/esm/src/kernel/interaction-parts.d.ts +14 -0
  120. package/esm/src/kernel/interaction-parts.js +23 -0
  121. package/esm/src/kernel/mod.d.ts +21 -10
  122. package/esm/src/kernel/mod.js +11 -8
  123. package/esm/src/kernel/profile-graph.d.ts +159 -0
  124. package/esm/src/kernel/profile-graph.js +156 -0
  125. package/esm/src/kernel/registry/attachments.d.ts +12 -10
  126. package/esm/src/kernel/registry/attachments.js +33 -27
  127. package/esm/src/kernel/registry/catalog.d.ts +25 -24
  128. package/esm/src/kernel/registry/catalog.js +60 -101
  129. package/esm/src/kernel/registry/ingress.d.ts +9 -4
  130. package/esm/src/kernel/registry/ingress.js +97 -75
  131. package/esm/src/kernel/registry/profile-outputs.d.ts +4 -0
  132. package/esm/src/kernel/registry/profile-outputs.js +8 -0
  133. package/esm/src/kernel/registry/profiles.d.ts +55 -12
  134. package/esm/src/kernel/registry/profiles.js +413 -73
  135. package/esm/src/kernel/registry/provider-request.js +13 -7
  136. package/esm/src/kernel/registry/resolve.d.ts +8 -8
  137. package/esm/src/kernel/registry/resolve.js +169 -154
  138. package/esm/src/kernel/registry/schemas.js +1 -1
  139. package/esm/src/kernel/registry/sole-model.d.ts +8 -0
  140. package/esm/src/kernel/registry/sole-model.js +10 -0
  141. package/esm/src/kernel/registry/system-prompt.d.ts +10 -0
  142. package/esm/src/kernel/registry/system-prompt.js +40 -0
  143. package/esm/src/kernel/registry/system-role.d.ts +8 -0
  144. package/esm/src/kernel/registry/system-role.js +14 -0
  145. package/esm/src/kernel/registry/vault.d.ts +12 -7
  146. package/esm/src/kernel/registry/vault.js +32 -10
  147. package/esm/src/kernel/schema.d.ts +231 -0
  148. package/esm/src/kernel/schema.js +607 -0
  149. package/esm/src/kernel/stages.d.ts +175 -0
  150. package/esm/src/kernel/stages.js +476 -0
  151. package/esm/src/kernel/stop.d.ts +78 -19
  152. package/esm/src/kernel/stop.js +51 -16
  153. package/esm/src/kernel/tools/events.d.ts +41 -0
  154. package/esm/src/kernel/tools/events.js +71 -0
  155. package/esm/src/kernel/tools/execute.d.ts +84 -0
  156. package/esm/src/kernel/tools/execute.js +614 -0
  157. package/esm/src/kernel/tools/harness.d.ts +8 -0
  158. package/esm/src/kernel/tools/harness.js +46 -0
  159. package/esm/src/kernel/tools/invoke.d.ts +10 -0
  160. package/esm/src/kernel/tools/invoke.js +101 -0
  161. package/esm/src/kernel/tools/mod.d.ts +13 -0
  162. package/esm/src/kernel/tools/mod.js +11 -0
  163. package/esm/src/kernel/tools/permission.d.ts +15 -0
  164. package/esm/src/kernel/tools/permission.js +47 -0
  165. package/esm/src/kernel/tools/project.d.ts +12 -0
  166. package/esm/src/kernel/tools/project.js +36 -0
  167. package/esm/src/kernel/tools/registry.d.ts +23 -0
  168. package/esm/src/kernel/tools/registry.js +81 -0
  169. package/esm/src/kernel/tools/remote.d.ts +94 -0
  170. package/esm/src/kernel/tools/remote.js +577 -0
  171. package/esm/src/kernel/tools/resolve.d.ts +39 -0
  172. package/esm/src/kernel/tools/resolve.js +283 -0
  173. package/esm/src/kernel/tools/schema.d.ts +15 -0
  174. package/esm/src/kernel/tools/schema.js +176 -0
  175. package/esm/src/kernel/tools/stage-run.d.ts +105 -0
  176. package/esm/src/kernel/tools/stage-run.js +155 -0
  177. package/esm/src/kernel/tools/types.d.ts +394 -0
  178. package/esm/src/kernel/tools/types.js +9 -0
  179. package/esm/src/kernel/types.d.ts +540 -256
  180. package/esm/src/kernel/util/find-last.d.ts +2 -0
  181. package/esm/src/kernel/util/find-last.js +10 -0
  182. package/esm/src/observability/destinations.d.ts +31 -0
  183. package/esm/src/observability/destinations.js +67 -0
  184. package/esm/src/observability/mod.d.ts +10 -3
  185. package/esm/src/observability/mod.js +6 -2
  186. package/esm/src/observability/policy.d.ts +27 -0
  187. package/esm/src/observability/policy.js +80 -0
  188. package/esm/src/observability/resolve-policy.d.ts +16 -0
  189. package/esm/src/observability/resolve-policy.js +64 -0
  190. package/esm/src/observability/trace-attach.d.ts +8 -4
  191. package/esm/src/observability/trace-attach.js +50 -29
  192. package/esm/src/observability/trace-record.d.ts +23 -13
  193. package/esm/src/observability/trace-record.js +96 -39
  194. package/esm/src/observability/trace-sink.d.ts +19 -0
  195. package/esm/src/observability/trace-sink.js +10 -0
  196. package/esm/src/observability/trace-usage.d.ts +10 -3
  197. package/esm/src/observability/trace-usage.js +70 -17
  198. package/esm/src/observability/trace.d.ts +18 -7
  199. package/esm/src/observability/trace.js +34 -17
  200. package/esm/src/observability/types.d.ts +113 -0
  201. package/esm/src/observability/types.js +11 -0
  202. package/esm/src/presets/google/speech-voices.d.ts +11 -0
  203. package/esm/src/presets/google/speech-voices.js +41 -0
  204. package/esm/src/presets/google.d.ts +36 -24
  205. package/esm/src/presets/google.js +50 -63
  206. package/esm/src/presets/mod.d.ts +2 -2
  207. package/esm/src/presets/mod.js +1 -1
  208. package/esm/src/providers/create-provider.d.ts +20 -17
  209. package/esm/src/providers/create-provider.js +72 -26
  210. package/esm/src/providers/google/interactions/framing.d.ts +23 -0
  211. package/esm/src/providers/google/interactions/framing.js +269 -0
  212. package/esm/src/providers/google/interactions/mod.d.ts +7 -0
  213. package/esm/src/providers/google/interactions/mod.js +7 -0
  214. package/esm/src/providers/google/interactions/stream.d.ts +83 -0
  215. package/esm/src/providers/google/interactions/stream.js +588 -0
  216. package/esm/src/providers/google/keys.d.ts +26 -0
  217. package/esm/src/providers/{keys.js → google/keys.js} +19 -31
  218. package/esm/src/providers/google/live/framing.d.ts +49 -0
  219. package/esm/src/providers/google/live/framing.js +552 -0
  220. package/esm/src/providers/google/live/openapi-schema.d.ts +6 -0
  221. package/esm/src/providers/google/live/openapi-schema.js +46 -0
  222. package/esm/src/providers/google/live/session.d.ts +25 -0
  223. package/esm/src/providers/google/live/session.js +134 -0
  224. package/esm/src/providers/google/live/stream.d.ts +45 -0
  225. package/esm/src/providers/google/live/stream.js +214 -0
  226. package/esm/src/providers/google/urls.d.ts +6 -0
  227. package/esm/src/providers/google/urls.js +6 -0
  228. package/esm/src/providers/local/local.d.ts +30 -0
  229. package/esm/src/providers/{local.js → local/local.js} +66 -126
  230. package/esm/src/providers/local/mod.d.ts +9 -0
  231. package/esm/src/providers/local/mod.js +9 -0
  232. package/esm/src/providers/mod.d.ts +6 -3
  233. package/esm/src/providers/mod.js +3 -1
  234. package/esm/src/providers/openrouter/cache-control.d.ts +24 -0
  235. package/esm/src/providers/openrouter/cache-control.js +23 -0
  236. package/esm/src/providers/openrouter/chat.d.ts +107 -0
  237. package/esm/src/providers/{openrouter.js → openrouter/chat.js} +117 -231
  238. package/esm/src/providers/openrouter/image.d.ts +34 -0
  239. package/esm/src/providers/openrouter/image.js +275 -0
  240. package/esm/src/providers/openrouter/openai/chat-payload.d.ts +24 -0
  241. package/esm/src/providers/openrouter/openai/chat-payload.js +82 -0
  242. package/esm/src/providers/openrouter/openai/compat.d.ts +53 -0
  243. package/esm/src/providers/openrouter/openai/compat.js +213 -0
  244. package/esm/src/providers/openrouter/openai/image-payload.d.ts +18 -0
  245. package/esm/src/providers/openrouter/openai/image-payload.js +90 -0
  246. package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +22 -0
  247. package/esm/src/providers/openrouter/openai/sdk-messages.js +122 -0
  248. package/esm/src/providers/openrouter/resolve-api-key.d.ts +9 -0
  249. package/esm/src/providers/openrouter/resolve-api-key.js +24 -0
  250. package/esm/src/providers/openrouter/speech.d.ts +23 -0
  251. package/esm/src/providers/{speech.js → openrouter/speech.js} +32 -55
  252. package/esm/src/providers/probe.d.ts +1 -0
  253. package/esm/src/providers/probe.js +22 -0
  254. package/esm/src/providers/shared/pcm.d.ts +12 -0
  255. package/esm/src/providers/{pcm.js → shared/pcm.js} +16 -3
  256. package/esm/src/providers/shared/sse.d.ts +18 -0
  257. package/esm/src/providers/shared/sse.js +87 -0
  258. package/esm/src/providers/shared/tool-args.d.ts +17 -0
  259. package/esm/src/providers/shared/tool-args.js +45 -0
  260. package/esm/src/providers/shared/upstream-tap.d.ts +5 -0
  261. package/esm/src/providers/{google-tap.js → shared/upstream-tap.js} +4 -7
  262. package/esm/src/providers/shared/upstream-tape.d.ts +6 -0
  263. package/esm/src/providers/{gemini-tape.js → shared/upstream-tape.js} +12 -22
  264. package/esm/src/providers/types.d.ts +27 -0
  265. package/esm/src/providers/types.js +1 -0
  266. package/package.json +11 -7
  267. package/docs/cli.md +0 -97
  268. package/docs/guardrails.md +0 -178
  269. package/docs/host.md +0 -97
  270. package/docs/kernel.md +0 -404
  271. package/docs/observability.md +0 -105
  272. package/docs/openrouter.md +0 -125
  273. package/docs/presets-google.md +0 -91
  274. package/docs/presets.md +0 -88
  275. package/docs/providers.md +0 -202
  276. package/docs/streaming.md +0 -96
  277. package/esm/src/kernel/engine/boundary.d.ts +0 -10
  278. package/esm/src/kernel/engine/boundary.js +0 -55
  279. package/esm/src/kernel/engine/runner/tools.d.ts +0 -13
  280. package/esm/src/kernel/engine/runner/tools.js +0 -198
  281. package/esm/src/kernel/registry/tools.d.ts +0 -12
  282. package/esm/src/kernel/registry/tools.js +0 -36
  283. package/esm/src/providers/expose-for-tests.d.ts +0 -1
  284. package/esm/src/providers/expose-for-tests.js +0 -25
  285. package/esm/src/providers/gemini-tape.d.ts +0 -2
  286. package/esm/src/providers/google-tap.d.ts +0 -3
  287. package/esm/src/providers/interactions.d.ts +0 -5
  288. package/esm/src/providers/interactions.js +0 -169
  289. package/esm/src/providers/keys.d.ts +0 -19
  290. package/esm/src/providers/local.d.ts +0 -29
  291. package/esm/src/providers/openrouter-mod.d.ts +0 -13
  292. package/esm/src/providers/openrouter-mod.js +0 -12
  293. package/esm/src/providers/openrouter-payload.d.ts +0 -39
  294. package/esm/src/providers/openrouter-payload.js +0 -195
  295. package/esm/src/providers/openrouter.d.ts +0 -15
  296. package/esm/src/providers/pcm.d.ts +0 -7
  297. package/esm/src/providers/provider.d.ts +0 -15
  298. package/esm/src/providers/provider.js +0 -202
  299. package/esm/src/providers/speech.d.ts +0 -23
  300. package/esm/src/providers/sse.d.ts +0 -7
  301. package/esm/src/providers/sse.js +0 -55
  302. package/esm/src/streaming/mod.d.ts +0 -9
  303. package/esm/src/streaming/mod.js +0 -8
  304. /package/esm/src/{streaming → host}/readStreamingJsonStringField.d.ts +0 -0
  305. /package/esm/src/{streaming → host}/readStreamingJsonStringField.js +0 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Google Gemini Live session transport — long-lived BidiGenerateContent WebSocket.
3
+ *
4
+ * `turnComplete` is a conversational turn boundary, not session teardown.
5
+ *
6
+ * @module
7
+ */
8
+ import { isAbortError, TheorumError } from '../../../guardrails/error.js';
9
+ import { requireKey } from '../keys.js';
10
+ import { buildGeminiLiveWebSocketUrl } from './framing.js';
11
+ import { attachLiveSessionHandlers, createLiveQueue, performLiveSetup, sendInitialPayloads, } from './stream.js';
12
+ function defaultOpenWebSocket(url) {
13
+ return Promise.resolve(new WebSocket(url));
14
+ }
15
+ function attachAbort(ws, liveQueue, signal) {
16
+ const onAbort = () => {
17
+ if (!liveQueue.isClosed()) {
18
+ liveQueue.close();
19
+ try {
20
+ ws.close(1000, 'aborted');
21
+ }
22
+ catch {
23
+ // Ignore
24
+ }
25
+ liveQueue.push({
26
+ type: 'error',
27
+ error: new DOMException('The operation was aborted.', 'AbortError'),
28
+ });
29
+ }
30
+ };
31
+ if (!signal) {
32
+ return () => { };
33
+ }
34
+ if (signal.aborted) {
35
+ onAbort();
36
+ return () => { };
37
+ }
38
+ signal.addEventListener('abort', onAbort, { once: true });
39
+ return () => signal.removeEventListener('abort', onAbort);
40
+ }
41
+ /**
42
+ * Open a long-lived Gemini Live WebSocket after setup handshake.
43
+ * Callers own send / batch drain / close — typically via `runSession`.
44
+ *
45
+ * @param openWebSocket Host override for Cloudflare fetch-upgrade (etc.).
46
+ */
47
+ export async function openGoogleLiveSession(req, transport, openWebSocket = defaultOpenWebSocket) {
48
+ if (!req.keySlot) {
49
+ throw new TheorumError('Request requires keySlot');
50
+ }
51
+ const apiKey = requireKey(transport.vault, req.keySlot);
52
+ const wsUrl = buildGeminiLiveWebSocketUrl(apiKey);
53
+ let ws;
54
+ try {
55
+ ws = await openWebSocket(wsUrl);
56
+ }
57
+ catch (err) {
58
+ throw err instanceof Error ? err : new Error(String(err));
59
+ }
60
+ const liveQueue = createLiveQueue();
61
+ const detachAbort = attachAbort(ws, liveQueue, req.signal);
62
+ if (req.signal?.aborted) {
63
+ detachAbort();
64
+ try {
65
+ ws.close();
66
+ }
67
+ catch {
68
+ // Ignore
69
+ }
70
+ throw new DOMException('The operation was aborted.', 'AbortError');
71
+ }
72
+ try {
73
+ await performLiveSetup(ws, req);
74
+ }
75
+ catch (err) {
76
+ detachAbort();
77
+ try {
78
+ ws.close();
79
+ }
80
+ catch {
81
+ // Ignore
82
+ }
83
+ throw err;
84
+ }
85
+ attachLiveSessionHandlers(ws, liveQueue);
86
+ sendInitialPayloads(ws, req);
87
+ return {
88
+ send(payload) {
89
+ if (ws.readyState === WebSocket.OPEN) {
90
+ ws.send(payload);
91
+ }
92
+ },
93
+ async *batches() {
94
+ try {
95
+ while (true) {
96
+ const item = await liveQueue.next();
97
+ if (!item)
98
+ break;
99
+ if (item.type === 'error' && isAbortError(item.error)) {
100
+ throw item.error;
101
+ }
102
+ yield item;
103
+ if (item.type === 'closed' || item.type === 'error') {
104
+ break;
105
+ }
106
+ }
107
+ }
108
+ finally {
109
+ detachAbort();
110
+ liveQueue.close();
111
+ try {
112
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
113
+ ws.close(1000, 'session-closed');
114
+ }
115
+ }
116
+ catch {
117
+ // Ignore
118
+ }
119
+ }
120
+ },
121
+ close(code = 1000, reason = 'session-closed') {
122
+ detachAbort();
123
+ liveQueue.close();
124
+ try {
125
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
126
+ ws.close(code, reason);
127
+ }
128
+ }
129
+ catch {
130
+ // Ignore
131
+ }
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Shared Gemini Live WebSocket transport helpers.
3
+ *
4
+ * Used by `openGoogleLiveSession` / `runSession`. No ModelProvider.complete() path —
5
+ * live is session-scoped, not turn-scoped.
6
+ *
7
+ * @module
8
+ */
9
+ import type { ProviderCompleteRequest, TurnEvent } from '../../../kernel/types.js';
10
+ export type LiveTurnPhase = 'streaming' | 'complete' | 'abort';
11
+ export type SessionQueueItem = {
12
+ type: 'batch';
13
+ events: TurnEvent[];
14
+ turnPhase: LiveTurnPhase;
15
+ } | {
16
+ type: 'error';
17
+ error: Error;
18
+ } | {
19
+ type: 'closed';
20
+ };
21
+ export declare function readMessageData(data: unknown): Promise<string>;
22
+ export declare function readGeminiLiveErrorMessage(message: Record<string, unknown>): string | null;
23
+ export declare function performLiveSetup(ws: WebSocket, req: ProviderCompleteRequest): Promise<void>;
24
+ export type LiveSocketSender = {
25
+ send(data: string): void;
26
+ };
27
+ export declare function sendInitialPayloads(ws: LiveSocketSender, req: ProviderCompleteRequest): void;
28
+ export interface LiveQueue {
29
+ push: (item: SessionQueueItem) => void;
30
+ next: () => Promise<SessionQueueItem | undefined>;
31
+ close: () => void;
32
+ isClosed: () => boolean;
33
+ }
34
+ export declare function createLiveQueue(): LiveQueue;
35
+ /**
36
+ * Conversational cycle boundary.
37
+ *
38
+ * When the server reports `interactionStatus`, that is authoritative: `IDLE`
39
+ * closes the cycle and `IN_PROGRESS` keeps it open even across `turnComplete`
40
+ * (background reasoning / async tools may still produce output). Without the
41
+ * field, `turnComplete` is the boundary as before.
42
+ */
43
+ export declare function turnPhaseFromMessage(message: Record<string, unknown>, events: TurnEvent[]): LiveTurnPhase;
44
+ /** Attach handlers that keep the socket open across conversational turns. */
45
+ export declare function attachLiveSessionHandlers(ws: WebSocket, liveQueue: LiveQueue): void;
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Shared Gemini Live WebSocket transport helpers.
3
+ *
4
+ * Used by `openGoogleLiveSession` / `runSession`. No ModelProvider.complete() path —
5
+ * live is session-scoped, not turn-scoped.
6
+ *
7
+ * @module
8
+ */
9
+ import { TheorumError } from '../../../guardrails/error.js';
10
+ import { buildGeminiLiveClientContent, buildGeminiLiveRealtimeInput, buildGeminiLiveSetupMessage, foldGeminiLiveServerMessage, parseGeminiLiveMessage, readLiveInteractionStatus, } from './framing.js';
11
+ const SETUP_TIMEOUT_MS = 20_000;
12
+ export async function readMessageData(data) {
13
+ if (typeof data === 'string')
14
+ return data;
15
+ if (data instanceof ArrayBuffer) {
16
+ return new TextDecoder().decode(data);
17
+ }
18
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
19
+ return await data.text();
20
+ }
21
+ return String(data);
22
+ }
23
+ export function readGeminiLiveErrorMessage(message) {
24
+ const error = message.error;
25
+ if (!error || typeof error !== 'object')
26
+ return null;
27
+ const record = error;
28
+ if (typeof record.message === 'string' && record.message.length > 0) {
29
+ const status = typeof record.status === 'string' ? record.status : null;
30
+ return status ? `${status}: ${record.message}` : record.message;
31
+ }
32
+ return 'Gemini returned an error during live session.';
33
+ }
34
+ export function performLiveSetup(ws, req) {
35
+ return new Promise((resolve, reject) => {
36
+ let setupResolved = false;
37
+ const timeout = setTimeout(() => {
38
+ if (!setupResolved) {
39
+ setupResolved = true;
40
+ reject(new TheorumError(`Gemini Live setup timed out after ${SETUP_TIMEOUT_MS}ms`));
41
+ }
42
+ }, SETUP_TIMEOUT_MS);
43
+ ws.onopen = () => {
44
+ try {
45
+ const setupMsg = buildGeminiLiveSetupMessage(req);
46
+ ws.send(JSON.stringify(setupMsg));
47
+ }
48
+ catch (err) {
49
+ clearTimeout(timeout);
50
+ setupResolved = true;
51
+ reject(err instanceof Error ? err : new Error(String(err)));
52
+ }
53
+ };
54
+ ws.onerror = () => {
55
+ clearTimeout(timeout);
56
+ if (!setupResolved) {
57
+ setupResolved = true;
58
+ reject(new TheorumError('Gemini Live WebSocket error during setup'));
59
+ }
60
+ };
61
+ ws.onclose = (evt) => {
62
+ clearTimeout(timeout);
63
+ if (!setupResolved) {
64
+ setupResolved = true;
65
+ reject(new TheorumError(`Gemini Live WebSocket closed during setup (${evt.code}: ${evt.reason})`));
66
+ }
67
+ };
68
+ const initialMessageHandler = async (evt) => {
69
+ const rawText = await readMessageData(evt.data);
70
+ const parsed = parseGeminiLiveMessage(rawText);
71
+ if (!parsed.ok) {
72
+ if (parsed.reason === 'empty')
73
+ return;
74
+ clearTimeout(timeout);
75
+ setupResolved = true;
76
+ reject(new TheorumError('malformed Gemini Live message during setup'));
77
+ return;
78
+ }
79
+ const errMsg = readGeminiLiveErrorMessage(parsed.value);
80
+ if (errMsg) {
81
+ clearTimeout(timeout);
82
+ setupResolved = true;
83
+ reject(new TheorumError(errMsg));
84
+ return;
85
+ }
86
+ if (parsed.value.setupComplete) {
87
+ clearTimeout(timeout);
88
+ setupResolved = true;
89
+ ws.removeEventListener('message', initialMessageHandler);
90
+ resolve();
91
+ }
92
+ };
93
+ ws.addEventListener('message', initialMessageHandler);
94
+ });
95
+ }
96
+ export function sendInitialPayloads(ws, req) {
97
+ if (req.history && req.history.length > 0) {
98
+ const historyMsg = buildGeminiLiveClientContent(req.history);
99
+ if (historyMsg) {
100
+ ws.send(JSON.stringify(historyMsg));
101
+ }
102
+ }
103
+ if (req.input && req.input.length > 0) {
104
+ for (const part of req.input) {
105
+ const inputMsg = buildGeminiLiveRealtimeInput(part);
106
+ ws.send(JSON.stringify(inputMsg));
107
+ }
108
+ }
109
+ }
110
+ export function createLiveQueue() {
111
+ const queue = [];
112
+ let notify = null;
113
+ let closed = false;
114
+ return {
115
+ push(item) {
116
+ queue.push(item);
117
+ if (notify) {
118
+ const fn = notify;
119
+ notify = null;
120
+ fn();
121
+ }
122
+ },
123
+ async next() {
124
+ while (queue.length === 0) {
125
+ if (closed)
126
+ return undefined;
127
+ await new Promise((resolve) => {
128
+ notify = resolve;
129
+ });
130
+ }
131
+ return queue.shift();
132
+ },
133
+ close() {
134
+ closed = true;
135
+ if (notify) {
136
+ const fn = notify;
137
+ notify = null;
138
+ fn();
139
+ }
140
+ },
141
+ isClosed() {
142
+ return closed;
143
+ },
144
+ };
145
+ }
146
+ /**
147
+ * Conversational cycle boundary.
148
+ *
149
+ * When the server reports `interactionStatus`, that is authoritative: `IDLE`
150
+ * closes the cycle and `IN_PROGRESS` keeps it open even across `turnComplete`
151
+ * (background reasoning / async tools may still produce output). Without the
152
+ * field, `turnComplete` is the boundary as before.
153
+ */
154
+ export function turnPhaseFromMessage(message, events) {
155
+ const interrupted = events.some((ev) => ev.type === 'done' && ev.interrupted === true);
156
+ if (interrupted)
157
+ return 'abort';
158
+ const status = readLiveInteractionStatus(message);
159
+ if (status === 'IDLE')
160
+ return 'complete';
161
+ if (status === 'IN_PROGRESS')
162
+ return 'streaming';
163
+ const serverContent = message.serverContent;
164
+ if (serverContent?.turnComplete || serverContent?.turn_complete)
165
+ return 'complete';
166
+ return 'streaming';
167
+ }
168
+ /** Attach handlers that keep the socket open across conversational turns. */
169
+ export function attachLiveSessionHandlers(ws, liveQueue) {
170
+ ws.onmessage = async (evt) => {
171
+ try {
172
+ const rawText = await readMessageData(evt.data);
173
+ const parsed = parseGeminiLiveMessage(rawText);
174
+ if (!parsed.ok) {
175
+ if (parsed.reason === 'empty')
176
+ return;
177
+ liveQueue.push({
178
+ type: 'error',
179
+ error: new TheorumError('malformed Gemini Live message'),
180
+ });
181
+ return;
182
+ }
183
+ if (parsed.value.setupComplete)
184
+ return;
185
+ const errMsg = readGeminiLiveErrorMessage(parsed.value);
186
+ if (errMsg) {
187
+ liveQueue.push({ type: 'error', error: new TheorumError(errMsg) });
188
+ return;
189
+ }
190
+ const events = foldGeminiLiveServerMessage(parsed.value);
191
+ const turnPhase = turnPhaseFromMessage(parsed.value, events);
192
+ if (events.length > 0 || turnPhase !== 'streaming') {
193
+ liveQueue.push({ type: 'batch', events, turnPhase });
194
+ }
195
+ }
196
+ catch (err) {
197
+ liveQueue.push({
198
+ type: 'error',
199
+ error: err instanceof Error ? err : new Error(String(err)),
200
+ });
201
+ }
202
+ };
203
+ ws.onerror = () => {
204
+ if (!liveQueue.isClosed()) {
205
+ liveQueue.push({ type: 'error', error: new TheorumError('Gemini Live WebSocket error') });
206
+ }
207
+ };
208
+ ws.onclose = () => {
209
+ if (!liveQueue.isClosed()) {
210
+ liveQueue.push({ type: 'closed' });
211
+ liveQueue.close();
212
+ }
213
+ };
214
+ }
@@ -0,0 +1,6 @@
1
+ /** Google Interactions API endpoints. */
2
+ declare const INTERACTIONS_URL = "https://generativelanguage.googleapis.com/v1beta/interactions?alt=sse";
3
+ declare const INTERACTIONS_JSON_URL = "https://generativelanguage.googleapis.com/v1beta/interactions";
4
+ /** Google Gemini Live WebSocket endpoint. */
5
+ declare const GEMINI_LIVE_WS_URL = "wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
6
+ export { GEMINI_LIVE_WS_URL, INTERACTIONS_JSON_URL, INTERACTIONS_URL };
@@ -0,0 +1,6 @@
1
+ /** Google Interactions API endpoints. */
2
+ const INTERACTIONS_URL = 'https://generativelanguage.googleapis.com/v1beta/interactions?alt=sse';
3
+ const INTERACTIONS_JSON_URL = 'https://generativelanguage.googleapis.com/v1beta/interactions';
4
+ /** Google Gemini Live WebSocket endpoint. */
5
+ const GEMINI_LIVE_WS_URL = 'wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent';
6
+ export { GEMINI_LIVE_WS_URL, INTERACTIONS_JSON_URL, INTERACTIONS_URL };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Local provider adapter for OpenAI-compatible endpoints (Ollama, llama.cpp,
3
+ * vLLM, LM Studio, etc.).
4
+ *
5
+ * Streams SSE from `/v1/chat/completions`, accumulates tool calls, and yields
6
+ * normalized `TurnEvent` objects. No external SDK dependency — raw fetch + SSE.
7
+ *
8
+ * Wire-format message building delegates to the shared `openai/compat` module.
9
+ * SSE parsing delegates to the shared `parseSseStream` from `sse.ts`.
10
+ *
11
+ * Hosts pass `baseUrl` explicitly. THEORUM does not read `OLLAMA_HOST` or other
12
+ * environment variables (see docs/contracts/providers.md).
13
+ *
14
+ * @module
15
+ */
16
+ import type { ModelProvider, TurnEvent } from '../../kernel/types.js';
17
+ import { buildChatMessages, wireTools } from '../openrouter/openai/compat.js';
18
+ import type { LocalProviderConfig } from '../types.js';
19
+ /** Default OpenAI-compat base when the host omits `baseUrl` (Ollama's default port). */
20
+ export declare const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:11434";
21
+ export type PendingToolCall = {
22
+ id: string;
23
+ name: string;
24
+ args: string;
25
+ };
26
+ export declare function resolveBaseUrl(config?: LocalProviderConfig): string;
27
+ export declare function flushPending(pending: Map<number, PendingToolCall>): TurnEvent[];
28
+ /** Create a `ModelProvider` for a local OpenAI-compatible server (Ollama, llama.cpp, vLLM, LM Studio). */
29
+ declare function createLocalProvider(config?: LocalProviderConfig): ModelProvider;
30
+ export { buildChatMessages as historyToWire, createLocalProvider, wireTools as toolsToWire };
@@ -5,14 +5,19 @@
5
5
  * Streams SSE from `/v1/chat/completions`, accumulates tool calls, and yields
6
6
  * normalized `TurnEvent` objects. No external SDK dependency — raw fetch + SSE.
7
7
  *
8
+ * Wire-format message building delegates to the shared `openai/compat` module.
9
+ * SSE parsing delegates to the shared `parseSseStream` from `sse.ts`.
10
+ *
8
11
  * Hosts pass `baseUrl` explicitly. THEORUM does not read `OLLAMA_HOST` or other
9
- * environment variables (see src/providers/CONTRACT.md).
12
+ * environment variables (see docs/contracts/providers.md).
10
13
  *
11
14
  * @module
12
15
  */
13
- import { isAbortError, toErrorEvent } from '../guardrails/error.js';
14
- import { turnStopFromOpenRouter } from '../kernel/stop.js';
15
- import { exposeForTests } from './expose-for-tests.js';
16
+ import { isAbortError, toErrorEvent } from '../../guardrails/error.js';
17
+ import { turnStopFromOpenAiFinishReason } from '../../kernel/stop.js';
18
+ import { buildChatMessages, wireTools } from '../openrouter/openai/compat.js';
19
+ import { parseSseStream } from '../shared/sse.js';
20
+ import { parseToolArgumentsObject } from '../shared/tool-args.js';
16
21
  /** Default OpenAI-compat base when the host omits `baseUrl` (Ollama's default port). */
17
22
  export const DEFAULT_LOCAL_BASE_URL = 'http://127.0.0.1:11434';
18
23
  // ── request mapping ─────────────────────────────────
@@ -22,132 +27,48 @@ function normalizeBaseUrl(baseUrl) {
22
27
  end -= 1;
23
28
  return baseUrl.slice(0, end);
24
29
  }
25
- function resolveBaseUrl(config) {
30
+ export function resolveBaseUrl(config) {
26
31
  return normalizeBaseUrl(config?.baseUrl?.trim() || DEFAULT_LOCAL_BASE_URL);
27
32
  }
28
- function inputToContent(parts) {
29
- if (parts.every((p) => p.type === 'text')) {
30
- return parts.map((p) => ('text' in p ? p.text : '')).join('\n');
31
- }
32
- return parts.map((p) => {
33
- if (p.type === 'text')
34
- return { type: 'text', text: p.text };
35
- return {
36
- type: 'image_url',
37
- image_url: { url: `data:${p.mimeType};base64,${p.data}` },
38
- };
39
- });
40
- }
41
- function historyMessageContent(msg) {
42
- if (msg.parts && msg.parts.length > 0) {
43
- return inputToContent(msg.parts);
44
- }
45
- if (msg.content != null)
46
- return msg.content;
47
- return undefined;
48
- }
49
- function historyToWire(req) {
50
- const msgs = [];
51
- if (req.system)
52
- msgs.push({ role: 'system', content: req.system });
53
- for (const msg of req.history ?? []) {
54
- msgs.push(historyMessageToWire(msg));
55
- }
56
- if (req.input.length > 0) {
57
- msgs.push({ role: 'user', content: inputToContent(req.input) });
58
- }
59
- return msgs;
60
- }
61
- function historyMessageToWire(msg) {
62
- if (msg.role === 'tool') {
63
- return {
64
- role: 'tool',
65
- tool_call_id: msg.tool_call_id ?? `call_${msg.name ?? 'tool'}`,
66
- name: msg.name,
67
- content: msg.content ?? '',
68
- };
69
- }
70
- if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
71
- return {
72
- role: 'assistant',
73
- content: historyMessageContent(msg) ?? undefined,
74
- tool_calls: msg.tool_calls.map((tc) => ({
75
- id: tc.id,
76
- type: 'function',
77
- function: { name: tc.function.name, arguments: tc.function.arguments },
78
- })),
79
- };
80
- }
81
- return { role: msg.role, content: historyMessageContent(msg) ?? '' };
82
- }
83
- function toolsToWire(dynamicTools) {
84
- if (!dynamicTools || dynamicTools.length === 0)
85
- return undefined;
86
- return dynamicTools.map((t) => ({
87
- type: 'function',
88
- function: {
89
- name: t.name,
90
- description: t.description ?? '',
91
- parameters: t.parameters ?? { type: 'object', properties: {} },
92
- },
93
- }));
94
- }
95
33
  function buildBody(req) {
96
34
  const body = {
97
35
  model: req.apiId,
98
- messages: historyToWire(req),
36
+ messages: buildChatMessages(req),
99
37
  stream: true,
100
38
  stream_options: { include_usage: true },
101
39
  temperature: req.temperature,
102
40
  max_tokens: req.maxOutputTokens,
103
41
  };
104
- const tools = toolsToWire(req.dynamicTools);
42
+ const tools = wireTools(req.wireTools);
105
43
  if (tools)
106
44
  body.tools = tools;
107
45
  return body;
108
46
  }
109
- // ── SSE parsing ─────────────────────────────────────
110
- async function* parseSse(body) {
111
- const reader = body.getReader();
112
- const decoder = new TextDecoder();
113
- let buffer = '';
114
- for (;;) {
115
- const { done, value } = await reader.read();
116
- if (done)
117
- break;
118
- buffer += decoder.decode(value, { stream: true });
119
- const lines = buffer.split('\n');
120
- buffer = lines.pop() ?? '';
121
- for (const line of lines) {
122
- const trimmed = line.trim();
123
- if (!trimmed.startsWith('data: '))
124
- continue;
125
- const data = trimmed.slice(6);
126
- if (data === '[DONE]')
127
- return;
128
- try {
129
- yield JSON.parse(data);
130
- }
131
- catch {
132
- // skip malformed
133
- }
134
- }
135
- }
136
- }
137
47
  // ── stream → TurnEvent ──────────────────────────────
138
- function flushPending(pending) {
48
+ export function flushPending(pending) {
139
49
  const events = [];
140
50
  for (const [, tc] of pending) {
141
- let parsed = {};
142
- try {
143
- parsed = JSON.parse(tc.args);
144
- }
145
- catch {
146
- // empty
51
+ const parsed = parseToolArgumentsObject(tc.args);
52
+ if (!parsed.ok) {
53
+ events.push({
54
+ type: 'tool',
55
+ tool: {
56
+ name: tc.name,
57
+ arguments: {},
58
+ id: tc.id,
59
+ phase: 'error',
60
+ failure: {
61
+ code: 'malformed_arguments',
62
+ message: parsed.error,
63
+ details: { raw: parsed.raw },
64
+ },
65
+ },
66
+ });
67
+ continue;
147
68
  }
148
69
  events.push({
149
70
  type: 'tool',
150
- tool: { name: tc.name, arguments: parsed, id: tc.id },
71
+ tool: { name: tc.name, arguments: parsed.value, id: tc.id },
151
72
  });
152
73
  }
153
74
  pending.clear();
@@ -174,11 +95,11 @@ async function* streamComplete(baseUrl, req, fetchFn) {
174
95
  async function* streamOpenAiBody(body) {
175
96
  const pending = new Map();
176
97
  let finishReason;
177
- for await (const chunk of parseSse(body)) {
178
- const usageEvent = tokensFromUsage(chunk.usage);
98
+ for await (const raw of parseSseStream(body)) {
99
+ const usageEvent = tokensFromUsage(readOpenAiUsage(raw));
179
100
  if (usageEvent)
180
101
  yield usageEvent;
181
- const choice = chunk.choices?.[0];
102
+ const choice = firstOpenAiChoice(raw);
182
103
  if (!choice)
183
104
  continue;
184
105
  yield* eventsFromChoiceDelta(choice.delta, pending);
@@ -190,7 +111,37 @@ async function* streamOpenAiBody(body) {
190
111
  }
191
112
  for (const event of flushPending(pending))
192
113
  yield event;
193
- yield { type: 'done', stop: turnStopFromOpenRouter(finishReason) };
114
+ yield { type: 'done', stop: turnStopFromOpenAiFinishReason(finishReason) };
115
+ }
116
+ function readOpenAiUsage(raw) {
117
+ const usage = raw.usage;
118
+ if (!usage || typeof usage !== 'object' || Array.isArray(usage))
119
+ return undefined;
120
+ const row = usage;
121
+ return {
122
+ prompt_tokens: typeof row.prompt_tokens === 'number' ? row.prompt_tokens : undefined,
123
+ completion_tokens: typeof row.completion_tokens === 'number' ? row.completion_tokens : undefined,
124
+ total_tokens: typeof row.total_tokens === 'number' ? row.total_tokens : undefined,
125
+ };
126
+ }
127
+ function firstOpenAiChoice(raw) {
128
+ if (!Array.isArray(raw.choices) || raw.choices.length === 0)
129
+ return undefined;
130
+ const choice = raw.choices[0];
131
+ if (!choice || typeof choice !== 'object' || Array.isArray(choice))
132
+ return undefined;
133
+ const row = choice;
134
+ const deltaRaw = row.delta;
135
+ const delta = deltaRaw && typeof deltaRaw === 'object' && !Array.isArray(deltaRaw)
136
+ ? deltaRaw
137
+ : undefined;
138
+ return {
139
+ index: typeof row.index === 'number' ? row.index : 0,
140
+ delta,
141
+ finish_reason: typeof row.finish_reason === 'string' || row.finish_reason === null
142
+ ? row.finish_reason
143
+ : undefined,
144
+ };
194
145
  }
195
146
  function tokensFromUsage(usage) {
196
147
  if (!usage)
@@ -245,15 +196,4 @@ function createLocalProvider(config) {
245
196
  },
246
197
  };
247
198
  }
248
- export { createLocalProvider };
249
- exposeForTests('local', {
250
- inputToContent,
251
- historyMessageContent,
252
- historyToWire,
253
- toolsToWire,
254
- buildBody,
255
- parseSse,
256
- flushPending,
257
- resolveBaseUrl,
258
- DEFAULT_LOCAL_BASE_URL,
259
- });
199
+ export { buildChatMessages as historyToWire, createLocalProvider, wireTools as toolsToWire };