@effect-uai/core 0.2.0 → 0.4.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 (209) hide show
  1. package/README.md +1 -1
  2. package/dist/{AiError-CqmYjXyx.d.mts → AiError-csR8Bhxx.d.mts} +26 -4
  3. package/dist/{AiError-CqmYjXyx.d.mts.map → AiError-csR8Bhxx.d.mts.map} +1 -1
  4. package/dist/Audio-BfCTGnH3.d.mts +61 -0
  5. package/dist/Audio-BfCTGnH3.d.mts.map +1 -0
  6. package/dist/Image-DxyXqzAM.d.mts +61 -0
  7. package/dist/Image-DxyXqzAM.d.mts.map +1 -0
  8. package/dist/{Items-D1C2686t.d.mts → Items-Hg5AsYxl.d.mts} +132 -80
  9. package/dist/Items-Hg5AsYxl.d.mts.map +1 -0
  10. package/dist/Media-D_CpcM1Z.d.mts +57 -0
  11. package/dist/Media-D_CpcM1Z.d.mts.map +1 -0
  12. package/dist/{StructuredFormat-B5ueioNr.d.mts → StructuredFormat-Cl41C56K.d.mts} +5 -5
  13. package/dist/StructuredFormat-Cl41C56K.d.mts.map +1 -0
  14. package/dist/{Tool-5wxOCuOh.d.mts → Tool-B8B5qVEy.d.mts} +13 -13
  15. package/dist/Tool-B8B5qVEy.d.mts.map +1 -0
  16. package/dist/{Turn-Bi83du4I.d.mts → Turn-7geUcKsf.d.mts} +5 -11
  17. package/dist/Turn-7geUcKsf.d.mts.map +1 -0
  18. package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
  19. package/dist/dist-DV5ISja1.mjs +13782 -0
  20. package/dist/dist-DV5ISja1.mjs.map +1 -0
  21. package/dist/domain/AiError.d.mts +2 -2
  22. package/dist/domain/AiError.mjs +19 -3
  23. package/dist/domain/AiError.mjs.map +1 -1
  24. package/dist/domain/Audio.d.mts +2 -0
  25. package/dist/domain/Audio.mjs +14 -0
  26. package/dist/domain/Audio.mjs.map +1 -0
  27. package/dist/domain/Image.d.mts +2 -0
  28. package/dist/domain/Image.mjs +58 -0
  29. package/dist/domain/Image.mjs.map +1 -0
  30. package/dist/domain/Items.d.mts +2 -2
  31. package/dist/domain/Items.mjs +19 -42
  32. package/dist/domain/Items.mjs.map +1 -1
  33. package/dist/domain/Media.d.mts +2 -0
  34. package/dist/domain/Media.mjs +14 -0
  35. package/dist/domain/Media.mjs.map +1 -0
  36. package/dist/domain/Music.d.mts +116 -0
  37. package/dist/domain/Music.d.mts.map +1 -0
  38. package/dist/domain/Music.mjs +29 -0
  39. package/dist/domain/Music.mjs.map +1 -0
  40. package/dist/domain/Transcript.d.mts +95 -0
  41. package/dist/domain/Transcript.d.mts.map +1 -0
  42. package/dist/domain/Transcript.mjs +22 -0
  43. package/dist/domain/Transcript.mjs.map +1 -0
  44. package/dist/domain/Turn.d.mts +1 -1
  45. package/dist/domain/Turn.mjs +1 -1
  46. package/dist/embedding-model/Embedding.d.mts +107 -0
  47. package/dist/embedding-model/Embedding.d.mts.map +1 -0
  48. package/dist/embedding-model/Embedding.mjs +18 -0
  49. package/dist/embedding-model/Embedding.mjs.map +1 -0
  50. package/dist/embedding-model/EmbeddingModel.d.mts +97 -0
  51. package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -0
  52. package/dist/embedding-model/EmbeddingModel.mjs +17 -0
  53. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -0
  54. package/dist/index.d.mts +21 -7
  55. package/dist/index.mjs +16 -2
  56. package/dist/language-model/LanguageModel.d.mts +12 -20
  57. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  58. package/dist/language-model/LanguageModel.mjs +3 -20
  59. package/dist/language-model/LanguageModel.mjs.map +1 -1
  60. package/dist/loop/Loop.d.mts +31 -7
  61. package/dist/loop/Loop.d.mts.map +1 -1
  62. package/dist/loop/Loop.mjs +39 -6
  63. package/dist/loop/Loop.mjs.map +1 -1
  64. package/dist/loop/Loop.test.d.mts +1 -0
  65. package/dist/loop/Loop.test.mjs +411 -0
  66. package/dist/loop/Loop.test.mjs.map +1 -0
  67. package/dist/magic-string.es-BgIV5Mu3.mjs +1013 -0
  68. package/dist/magic-string.es-BgIV5Mu3.mjs.map +1 -0
  69. package/dist/math/Vector.d.mts +47 -0
  70. package/dist/math/Vector.d.mts.map +1 -0
  71. package/dist/math/Vector.mjs +117 -0
  72. package/dist/math/Vector.mjs.map +1 -0
  73. package/dist/music-generator/MusicGenerator.d.mts +77 -0
  74. package/dist/music-generator/MusicGenerator.d.mts.map +1 -0
  75. package/dist/music-generator/MusicGenerator.mjs +51 -0
  76. package/dist/music-generator/MusicGenerator.mjs.map +1 -0
  77. package/dist/music-generator/MusicGenerator.test.d.mts +1 -0
  78. package/dist/music-generator/MusicGenerator.test.mjs +154 -0
  79. package/dist/music-generator/MusicGenerator.test.mjs.map +1 -0
  80. package/dist/observability/Metrics.d.mts +2 -2
  81. package/dist/observability/Metrics.d.mts.map +1 -1
  82. package/dist/observability/Metrics.mjs +1 -1
  83. package/dist/observability/Metrics.mjs.map +1 -1
  84. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +96 -0
  85. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -0
  86. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +48 -0
  87. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -0
  88. package/dist/speech-synthesizer/SpeechSynthesizer.test.d.mts +1 -0
  89. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +112 -0
  90. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -0
  91. package/dist/streaming/JSONL.d.mts +10 -3
  92. package/dist/streaming/JSONL.d.mts.map +1 -1
  93. package/dist/streaming/JSONL.mjs +13 -2
  94. package/dist/streaming/JSONL.mjs.map +1 -1
  95. package/dist/streaming/JSONL.test.d.mts +1 -0
  96. package/dist/streaming/JSONL.test.mjs +70 -0
  97. package/dist/streaming/JSONL.test.mjs.map +1 -0
  98. package/dist/streaming/Lines.mjs +1 -1
  99. package/dist/streaming/SSE.d.mts +2 -2
  100. package/dist/streaming/SSE.d.mts.map +1 -1
  101. package/dist/streaming/SSE.mjs +1 -1
  102. package/dist/streaming/SSE.mjs.map +1 -1
  103. package/dist/streaming/SSE.test.d.mts +1 -0
  104. package/dist/streaming/SSE.test.mjs +72 -0
  105. package/dist/streaming/SSE.test.mjs.map +1 -0
  106. package/dist/structured-format/StructuredFormat.d.mts +1 -1
  107. package/dist/structured-format/StructuredFormat.mjs +1 -1
  108. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  109. package/dist/testing/MockMusicGenerator.d.mts +39 -0
  110. package/dist/testing/MockMusicGenerator.d.mts.map +1 -0
  111. package/dist/testing/MockMusicGenerator.mjs +96 -0
  112. package/dist/testing/MockMusicGenerator.mjs.map +1 -0
  113. package/dist/testing/MockProvider.d.mts +6 -6
  114. package/dist/testing/MockProvider.d.mts.map +1 -1
  115. package/dist/testing/MockProvider.mjs.map +1 -1
  116. package/dist/testing/MockSpeechSynthesizer.d.mts +37 -0
  117. package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -0
  118. package/dist/testing/MockSpeechSynthesizer.mjs +95 -0
  119. package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -0
  120. package/dist/testing/MockTranscriber.d.mts +37 -0
  121. package/dist/testing/MockTranscriber.d.mts.map +1 -0
  122. package/dist/testing/MockTranscriber.mjs +77 -0
  123. package/dist/testing/MockTranscriber.mjs.map +1 -0
  124. package/dist/tool/HistoryCheck.d.mts +6 -3
  125. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  126. package/dist/tool/HistoryCheck.mjs +7 -1
  127. package/dist/tool/HistoryCheck.mjs.map +1 -1
  128. package/dist/tool/Outcome.d.mts +138 -2
  129. package/dist/tool/Outcome.d.mts.map +1 -0
  130. package/dist/tool/Outcome.mjs +32 -10
  131. package/dist/tool/Outcome.mjs.map +1 -1
  132. package/dist/tool/Resolvers.d.mts +11 -8
  133. package/dist/tool/Resolvers.d.mts.map +1 -1
  134. package/dist/tool/Resolvers.mjs +10 -1
  135. package/dist/tool/Resolvers.mjs.map +1 -1
  136. package/dist/tool/Resolvers.test.d.mts +1 -0
  137. package/dist/tool/Resolvers.test.mjs +317 -0
  138. package/dist/tool/Resolvers.test.mjs.map +1 -0
  139. package/dist/tool/Tool.d.mts +1 -1
  140. package/dist/tool/Tool.mjs +1 -1
  141. package/dist/tool/Tool.mjs.map +1 -1
  142. package/dist/tool/ToolEvent.d.mts +151 -2
  143. package/dist/tool/ToolEvent.d.mts.map +1 -0
  144. package/dist/tool/ToolEvent.mjs +30 -4
  145. package/dist/tool/ToolEvent.mjs.map +1 -1
  146. package/dist/tool/Toolkit.d.mts +19 -10
  147. package/dist/tool/Toolkit.d.mts.map +1 -1
  148. package/dist/tool/Toolkit.mjs +5 -5
  149. package/dist/tool/Toolkit.mjs.map +1 -1
  150. package/dist/tool/Toolkit.test.d.mts +1 -0
  151. package/dist/tool/Toolkit.test.mjs +113 -0
  152. package/dist/tool/Toolkit.test.mjs.map +1 -0
  153. package/dist/transcriber/Transcriber.d.mts +101 -0
  154. package/dist/transcriber/Transcriber.d.mts.map +1 -0
  155. package/dist/transcriber/Transcriber.mjs +49 -0
  156. package/dist/transcriber/Transcriber.mjs.map +1 -0
  157. package/dist/transcriber/Transcriber.test.d.mts +1 -0
  158. package/dist/transcriber/Transcriber.test.mjs +130 -0
  159. package/dist/transcriber/Transcriber.test.mjs.map +1 -0
  160. package/package.json +65 -13
  161. package/src/domain/AiError.ts +21 -0
  162. package/src/domain/Audio.ts +88 -0
  163. package/src/domain/Image.ts +75 -0
  164. package/src/domain/Items.ts +18 -47
  165. package/src/domain/Media.ts +61 -0
  166. package/src/domain/Music.ts +121 -0
  167. package/src/domain/Transcript.ts +83 -0
  168. package/src/embedding-model/Embedding.ts +117 -0
  169. package/src/embedding-model/EmbeddingModel.ts +107 -0
  170. package/src/index.ts +15 -1
  171. package/src/language-model/LanguageModel.ts +2 -22
  172. package/src/loop/Loop.test.ts +114 -2
  173. package/src/loop/Loop.ts +69 -5
  174. package/src/math/Vector.ts +138 -0
  175. package/src/music-generator/MusicGenerator.test.ts +170 -0
  176. package/src/music-generator/MusicGenerator.ts +123 -0
  177. package/src/observability/Metrics.ts +1 -1
  178. package/src/speech-synthesizer/SpeechSynthesizer.test.ts +141 -0
  179. package/src/speech-synthesizer/SpeechSynthesizer.ts +131 -0
  180. package/src/streaming/JSONL.ts +12 -0
  181. package/src/streaming/SSE.ts +1 -1
  182. package/src/structured-format/StructuredFormat.ts +2 -2
  183. package/src/testing/MockMusicGenerator.ts +170 -0
  184. package/src/testing/MockProvider.ts +2 -2
  185. package/src/testing/MockSpeechSynthesizer.ts +165 -0
  186. package/src/testing/MockTranscriber.ts +139 -0
  187. package/src/tool/HistoryCheck.ts +2 -5
  188. package/src/tool/Outcome.ts +36 -36
  189. package/src/tool/Resolvers.test.ts +11 -35
  190. package/src/tool/Resolvers.ts +5 -14
  191. package/src/tool/Tool.ts +9 -9
  192. package/src/tool/ToolEvent.ts +28 -24
  193. package/src/tool/Toolkit.test.ts +97 -2
  194. package/src/tool/Toolkit.ts +57 -33
  195. package/src/transcriber/Transcriber.test.ts +125 -0
  196. package/src/transcriber/Transcriber.ts +127 -0
  197. package/dist/Items-D1C2686t.d.mts.map +0 -1
  198. package/dist/Outcome-GiaNvt7i.d.mts +0 -32
  199. package/dist/Outcome-GiaNvt7i.d.mts.map +0 -1
  200. package/dist/StructuredFormat-B5ueioNr.d.mts.map +0 -1
  201. package/dist/Tool-5wxOCuOh.d.mts.map +0 -1
  202. package/dist/ToolEvent-wTMgb2GO.d.mts +0 -29
  203. package/dist/ToolEvent-wTMgb2GO.d.mts.map +0 -1
  204. package/dist/Turn-Bi83du4I.d.mts.map +0 -1
  205. package/dist/match/Match.d.mts +0 -16
  206. package/dist/match/Match.d.mts.map +0 -1
  207. package/dist/match/Match.mjs +0 -15
  208. package/dist/match/Match.mjs.map +0 -1
  209. package/src/match/Match.ts +0 -9
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Per-word timing + metadata. `confidence` and `speakerId` are optional
3
+ * because providers vary widely in what they emit and when (some only on
4
+ * final, some only with diarization enabled, some not at all).
5
+ */
6
+ export type WordTimestamp = {
7
+ readonly text: string
8
+ readonly startSeconds: number
9
+ readonly endSeconds: number
10
+ readonly confidence?: number
11
+ readonly speakerId?: string
12
+ readonly languageCode?: string
13
+ }
14
+
15
+ /**
16
+ * Sync STT result. `raw` preserves the provider-specific response for
17
+ * consumers that need fields the common shape doesn't expose
18
+ * (alternatives, segments, NBest, audio events, etc.).
19
+ */
20
+ export type TranscriptResult = {
21
+ readonly text: string
22
+ readonly languageCode?: string
23
+ readonly durationSeconds?: number
24
+ readonly words?: ReadonlyArray<WordTimestamp>
25
+ readonly raw?: unknown
26
+ }
27
+
28
+ /**
29
+ * Streaming STT event union. Collapses every provider's vocabulary into
30
+ * a small set; provider-specific shapes survive on `metadata.raw`.
31
+ *
32
+ * - `partial`: interim hypothesis. `stability` is Google-only.
33
+ * - `final`: committed transcript for the current utterance / segment.
34
+ * - `speech-started` / `utterance-ended`: VAD-derived boundaries. Not
35
+ * all providers emit them (OpenAI Realtime, Google with
36
+ * `voice_activity_events`, Deepgram with `vad_events`, AssemblyAI).
37
+ * - `audio-event`: non-speech label (`(laughter)`, `(music)`) — ElevenLabs only.
38
+ * - `metadata`: opaque server-side bookkeeping (request_id, model info).
39
+ * - `error`: non-fatal provider error mid-stream. Fatal errors surface
40
+ * on the `Stream`'s error channel as `AiError.AiError`.
41
+ */
42
+ export type TranscriptEvent =
43
+ | {
44
+ readonly _tag: "partial"
45
+ readonly text: string
46
+ readonly words?: ReadonlyArray<WordTimestamp>
47
+ readonly stability?: number
48
+ }
49
+ | {
50
+ readonly _tag: "final"
51
+ readonly text: string
52
+ readonly words?: ReadonlyArray<WordTimestamp>
53
+ readonly languageCode?: string
54
+ }
55
+ | { readonly _tag: "speech-started"; readonly atSeconds: number }
56
+ | { readonly _tag: "utterance-ended"; readonly atSeconds: number }
57
+ | {
58
+ readonly _tag: "audio-event"
59
+ readonly label: string
60
+ readonly startSeconds: number
61
+ readonly endSeconds: number
62
+ }
63
+ | { readonly _tag: "metadata"; readonly raw: unknown }
64
+ | { readonly _tag: "error"; readonly code?: string; readonly message: string }
65
+
66
+ export const isPartial = (e: TranscriptEvent): e is Extract<TranscriptEvent, { _tag: "partial" }> =>
67
+ e._tag === "partial"
68
+ export const isFinal = (e: TranscriptEvent): e is Extract<TranscriptEvent, { _tag: "final" }> =>
69
+ e._tag === "final"
70
+ export const isSpeechStarted = (
71
+ e: TranscriptEvent,
72
+ ): e is Extract<TranscriptEvent, { _tag: "speech-started" }> => e._tag === "speech-started"
73
+ export const isUtteranceEnded = (
74
+ e: TranscriptEvent,
75
+ ): e is Extract<TranscriptEvent, { _tag: "utterance-ended" }> => e._tag === "utterance-ended"
76
+ export const isAudioEvent = (
77
+ e: TranscriptEvent,
78
+ ): e is Extract<TranscriptEvent, { _tag: "audio-event" }> => e._tag === "audio-event"
79
+ export const isMetadata = (
80
+ e: TranscriptEvent,
81
+ ): e is Extract<TranscriptEvent, { _tag: "metadata" }> => e._tag === "metadata"
82
+ export const isError = (e: TranscriptEvent): e is Extract<TranscriptEvent, { _tag: "error" }> =>
83
+ e._tag === "error"
@@ -0,0 +1,117 @@
1
+ import type { ImageSource } from "../domain/Image.js"
2
+
3
+ /**
4
+ * One part of a mixed text+image input. Used inside `EmbedInput.content[]`
5
+ * for providers that accept interleaved modalities in a single embed call
6
+ * (Cohere v4, Voyage multimodal, Jina v4, Google `gemini-embedding-2`).
7
+ */
8
+ export type EmbedContentPart = { readonly text: string } | { readonly image: ImageSource }
9
+
10
+ /**
11
+ * What you embed. The `string` shorthand covers the common text-only case;
12
+ * structured variants exist for image-only and mixed-modality inputs.
13
+ *
14
+ * Not every provider accepts every variant: text-only providers (OpenAI,
15
+ * Mixedbread today) handle `string` and `{ text }`; multimodal providers
16
+ * (Google, Jina v4, Voyage multimodal, Cohere v4) handle all four. A
17
+ * provider layer rejects shapes it can't encode as `AiError.InvalidRequest`.
18
+ */
19
+ export type EmbedInput =
20
+ | string
21
+ | { readonly text: string }
22
+ | { readonly image: ImageSource }
23
+ | { readonly content: ReadonlyArray<EmbedContentPart> }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Embedding representations
27
+ //
28
+ // The `_tag` reflects the wire form the provider returned, *not* what the
29
+ // consumer asked for - request `encoding: "int8"` and you get back an
30
+ // `Int8Embedding`. Math primitives are typed against the named interfaces
31
+ // (see `Vector.ts`) so e.g. `sparseCosine` only accepts `SparseEmbedding`.
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /** Dense float32 vector. The default representation across all providers. */
35
+ export type Float32Embedding = {
36
+ readonly _tag: "float32"
37
+ readonly vector: Float32Array
38
+ }
39
+
40
+ /**
41
+ * Dense int8-quantized vector. ~4x smaller than float32 with minimal
42
+ * recall loss on most benchmarks.
43
+ */
44
+ export type Int8Embedding = {
45
+ readonly _tag: "int8"
46
+ readonly vector: Int8Array
47
+ }
48
+
49
+ /**
50
+ * Dense binary-quantized vector. One bit per dimension, packed into bytes.
51
+ * ~32x smaller than float32; meaningful recall loss but useful for hot
52
+ * indexes paired with a float32 reranker pass.
53
+ */
54
+ export type BinaryEmbedding = {
55
+ readonly _tag: "binary"
56
+ readonly vector: Uint8Array
57
+ }
58
+
59
+ /**
60
+ * Sparse vector. Token-keyed weights for hybrid search (dense + lexical-
61
+ * style sparse). The single hosted producer today is Jina's `elser-v2`
62
+ * model, which returns subword tokens (e.g. `"bread"`, `"##ing"`) with
63
+ * their relevance weights.
64
+ *
65
+ * The shape is `Record<string, number>` rather than `(indices, values)`
66
+ * because real hosted learned-sparse encoders (ELSER, SPLADE) emit token
67
+ * strings with no shared vocabulary index. Converting to integer indices
68
+ * would either need a vocabulary table the model doesn't expose, or
69
+ * lose the cross-vector matching semantics. If a provider ever exposes
70
+ * index-valued sparse vectors (Pinecone-style, where you bring your own
71
+ * vocab), add an `IndexSparseEmbedding` sibling arm with `_tag:
72
+ * "sparse-indexed"`.
73
+ *
74
+ * Score with `Vector.sparseCosine` — dot product over the intersection
75
+ * of keys, normalized by the L2 norms of both maps.
76
+ */
77
+ export type SparseEmbedding = {
78
+ readonly _tag: "sparse"
79
+ readonly weights: Readonly<Record<string, number>>
80
+ }
81
+
82
+ /**
83
+ * Multivector / late-interaction output: one float32 vector per token.
84
+ * Score documents with `Vector.maxSim` (ColBERT-style: per query vector,
85
+ * max dot product across doc vectors, summed). Typically ~50-500 vectors
86
+ * per document, each shorter than a single-vector embedding (~128 dim
87
+ * vs ~1024).
88
+ *
89
+ * Quantized multivector forms aren't modeled for the same reason as
90
+ * sparse - nothing on hosted APIs ships them yet.
91
+ */
92
+ export type MultivectorEmbedding = {
93
+ readonly _tag: "multivector"
94
+ readonly vectors: ReadonlyArray<Float32Array>
95
+ }
96
+
97
+ export type Embedding =
98
+ | Float32Embedding
99
+ | Int8Embedding
100
+ | BinaryEmbedding
101
+ | SparseEmbedding
102
+ | MultivectorEmbedding
103
+
104
+ export const isFloat32 = (e: Embedding): e is Float32Embedding => e._tag === "float32"
105
+ export const isInt8 = (e: Embedding): e is Int8Embedding => e._tag === "int8"
106
+ export const isBinary = (e: Embedding): e is BinaryEmbedding => e._tag === "binary"
107
+ export const isSparse = (e: Embedding): e is SparseEmbedding => e._tag === "sparse"
108
+ export const isMultivector = (e: Embedding): e is MultivectorEmbedding => e._tag === "multivector"
109
+
110
+ /**
111
+ * Token usage for one embed / embedMany call. One value per HTTP request,
112
+ * not per input vector. Most providers populate `inputTokens`; the field
113
+ * is optional for those that don't (or for mock layers in tests).
114
+ */
115
+ export type Usage = {
116
+ readonly inputTokens?: number
117
+ }
@@ -0,0 +1,107 @@
1
+ import { Context, Effect } from "effect"
2
+ import * as AiError from "../domain/AiError.js"
3
+ import type { Embedding, EmbedInput, Usage } from "./Embedding.js"
4
+
5
+ /**
6
+ * Output representation requested from the provider.
7
+ *
8
+ * Dense quantizations - same vector at different storage cost:
9
+ * - `float32` — universal default.
10
+ * - `int8` — ~4x smaller; minimal recall loss on most benchmarks.
11
+ * - `binary` — ~32x smaller; meaningful recall loss but pairs well with
12
+ * a float32 reranker pass over a small candidate set.
13
+ *
14
+ * Non-dense representations:
15
+ * - `sparse` — learned sparse vector for hybrid (dense + lexical) search.
16
+ * Currently Jina ELSER only on hosted APIs.
17
+ * - `multivector` — one vector per token for late-interaction (ColBERT-
18
+ * style) scoring via `Vector.maxSim`. Currently Jina v4 only.
19
+ *
20
+ * Each provider's typed request narrows this to its supported set at
21
+ * compile time (e.g. `JinaEncoding = "float32" | "binary" | "sparse" |
22
+ * "multivector"`). On the generic `EmbeddingModel` path, callers can
23
+ * pass any `Encoding` and the provider's API will reject mismatches at
24
+ * runtime.
25
+ */
26
+ export type Encoding = "float32" | "int8" | "binary" | "sparse" | "multivector"
27
+
28
+ /**
29
+ * Cross-provider single-embed request. Mirrors the shape of
30
+ * `LanguageModel.CommonRequest`: cross-cutting fields here, vendor
31
+ * specifics in the provider's typed request.
32
+ *
33
+ * Provider-specific extensions (Cohere widened `task` enum, Jina LoRA
34
+ * tasks, Mixedbread free-form `prompt`, etc.) live in that provider's own
35
+ * request interface, which extends this and narrows `model` / widens
36
+ * `task`.
37
+ */
38
+ export type CommonEmbedRequest = {
39
+ readonly input: EmbedInput
40
+ /**
41
+ * Model identifier. Each provider narrows this to its typed literal
42
+ * union, so code that yields a typed provider tag gets autocompletion.
43
+ */
44
+ readonly model: string
45
+ /**
46
+ * Retrieval-task hint. Applies to the input. OpenAI ignores this;
47
+ * Mixedbread doesn't have it; Cohere v3+ requires it on the wire (typed
48
+ * as required in `CohereEmbedRequest`). Provider-specific task enums
49
+ * (classification, clustering, code retrieval, …) live on the
50
+ * provider's own request type.
51
+ */
52
+ readonly task?: "query" | "document"
53
+ /**
54
+ * Matryoshka truncation. Default: provider's native dimension.
55
+ * Discrete-value providers (Cohere, Vertex `multimodalembedding@001`)
56
+ * narrow this to a literal union in their typed request.
57
+ */
58
+ readonly dimensions?: number
59
+ /**
60
+ * Output representation - see {@link Encoding}. Dense float32 is the
61
+ * default; provider layers reject unsupported values up front with
62
+ * `InvalidRequest`.
63
+ */
64
+ readonly encoding?: Encoding
65
+ }
66
+
67
+ /**
68
+ * Cross-provider batch-embed request. One `task` for the whole batch -
69
+ * mixed-task batches aren't a real provider feature (rerankers exist for
70
+ * that).
71
+ */
72
+ export type CommonEmbedManyRequest = Omit<CommonEmbedRequest, "input"> & {
73
+ readonly inputs: ReadonlyArray<EmbedInput>
74
+ }
75
+
76
+ export type EmbedResponse = {
77
+ readonly embedding: Embedding
78
+ readonly usage: Usage
79
+ }
80
+
81
+ export type EmbedManyResponse = {
82
+ readonly embeddings: ReadonlyArray<Embedding>
83
+ readonly usage: Usage
84
+ }
85
+
86
+ export type EmbeddingModelService = {
87
+ readonly embed: (request: CommonEmbedRequest) => Effect.Effect<EmbedResponse, AiError.AiError>
88
+ readonly embedMany: (
89
+ request: CommonEmbedManyRequest,
90
+ ) => Effect.Effect<EmbedManyResponse, AiError.AiError>
91
+ }
92
+
93
+ export class EmbeddingModel extends Context.Service<EmbeddingModel, EmbeddingModelService>()(
94
+ "@betalyra/effect-uai/EmbeddingModel",
95
+ ) {}
96
+
97
+ /** Embed a single input. */
98
+ export const embed = (
99
+ request: CommonEmbedRequest,
100
+ ): Effect.Effect<EmbedResponse, AiError.AiError, EmbeddingModel> =>
101
+ Effect.flatMap(EmbeddingModel.asEffect(), (m) => m.embed(request))
102
+
103
+ /** Embed a batch in one provider call. Same `task` for every input. */
104
+ export const embedMany = (
105
+ request: CommonEmbedManyRequest,
106
+ ): Effect.Effect<EmbedManyResponse, AiError.AiError, EmbeddingModel> =>
107
+ Effect.flatMap(EmbeddingModel.asEffect(), (m) => m.embedMany(request))
package/src/index.ts CHANGED
@@ -1,11 +1,25 @@
1
1
  export * as AiError from "./domain/AiError.js"
2
+ export * as Audio from "./domain/Audio.js"
3
+ export * as Image from "./domain/Image.js"
2
4
  export * as Items from "./domain/Items.js"
5
+ export * as Media from "./domain/Media.js"
6
+ export * as Music from "./domain/Music.js"
7
+ export * as Transcript from "./domain/Transcript.js"
3
8
  export * as Turn from "./domain/Turn.js"
9
+ export * as Embedding from "./embedding-model/Embedding.js"
10
+ export * as EmbeddingModel from "./embedding-model/EmbeddingModel.js"
4
11
  export * as LanguageModel from "./language-model/LanguageModel.js"
12
+ export * as MusicGenerator from "./music-generator/MusicGenerator.js"
13
+ export * as SpeechSynthesizer from "./speech-synthesizer/SpeechSynthesizer.js"
14
+ export * as Transcriber from "./transcriber/Transcriber.js"
15
+ export * as Vector from "./math/Vector.js"
5
16
  export * as Loop from "./loop/Loop.js"
6
- export * as Match from "./match/Match.js"
7
17
  export * as Tool from "./tool/Tool.js"
8
18
  export * as Toolkit from "./tool/Toolkit.js"
19
+ export * as Outcome from "./tool/Outcome.js"
20
+ export * as ToolEvent from "./tool/ToolEvent.js"
21
+ export * as Resolvers from "./tool/Resolvers.js"
22
+ export * as HistoryCheck from "./tool/HistoryCheck.js"
9
23
  export * as JSONL from "./streaming/JSONL.js"
10
24
  export * as Lines from "./streaming/Lines.js"
11
25
  export * as SSE from "./streaming/SSE.js"
@@ -11,7 +11,7 @@ import { isTurnComplete, type Turn, type TurnEvent } from "../domain/Turn.js"
11
11
  * to a single provider (reasoning effort, prompt caching, store flags,
12
12
  * ...) lives in that provider's own request interface, which extends this.
13
13
  */
14
- export interface CommonRequest {
14
+ export type CommonRequest = {
15
15
  readonly history: ReadonlyArray<Item>
16
16
  /**
17
17
  * Model identifier. Each provider narrows this to its typed literal union,
@@ -36,7 +36,7 @@ export interface CommonRequest {
36
36
  readonly structured?: StructuredFormat.StructuredFormat<unknown>
37
37
  }
38
38
 
39
- export interface LanguageModelService {
39
+ export type LanguageModelService = {
40
40
  readonly streamTurn: (request: CommonRequest) => Stream.Stream<TurnEvent, AiError.AiError>
41
41
  }
42
42
 
@@ -51,23 +51,3 @@ export const streamTurn = (
51
51
  request: CommonRequest,
52
52
  ): Stream.Stream<TurnEvent, AiError.AiError, LanguageModel> =>
53
53
  Stream.unwrap(Effect.map(LanguageModel.asEffect(), (m) => m.streamTurn(request)))
54
-
55
- /**
56
- * Run a single turn to completion and return the assembled `Turn`.
57
- *
58
- * Implementation: drain the delta stream and pluck the terminal
59
- * `turn_complete` event. The provider is contractually required to emit
60
- * exactly one such event as the last delta.
61
- */
62
- export const turn = (request: CommonRequest): Effect.Effect<Turn, AiError.AiError, LanguageModel> =>
63
- Effect.flatMap(Stream.runCollect(streamTurn(request)), (deltas) => {
64
- const last = deltas[deltas.length - 1]
65
- return last !== undefined && isTurnComplete(last)
66
- ? Effect.succeed(last.turn)
67
- : Effect.fail(
68
- new AiError.Unavailable({
69
- provider: "unknown",
70
- raw: "Provider stream ended without a turn_complete event",
71
- }),
72
- )
73
- })
@@ -1,6 +1,15 @@
1
- import { Deferred, Effect, Fiber, Ref, Stream } from "effect"
1
+ import { Deferred, Effect, Fiber, Latch, Ref, Stream, SubscriptionRef } from "effect"
2
2
  import { describe, expect, it } from "vitest"
3
- import { type Event, loop, next, nextAfter, stopEvent, stopAfter, value } from "./Loop.js"
3
+ import {
4
+ type Event,
5
+ loop,
6
+ loopWithState,
7
+ next,
8
+ nextAfter,
9
+ stopEvent,
10
+ stopAfter,
11
+ value,
12
+ } from "./Loop.js"
4
13
 
5
14
  describe("Loop.loop", () => {
6
15
  it("threads state across iterations and emits each iteration's substream in order", async () => {
@@ -410,3 +419,106 @@ describe("Loop.loop - pull-specific stream semantics", () => {
410
419
  expect(result._tag).toBe("Failure")
411
420
  })
412
421
  })
422
+
423
+ describe("Loop.loopWithState", () => {
424
+ it("exposes the final state in the SubscriptionRef after the stream completes", async () => {
425
+ const program = Effect.gen(function* () {
426
+ const { stream, state } = yield* loopWithState(0, (n: number) =>
427
+ n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),
428
+ )
429
+ const values = yield* Stream.runCollect(stream)
430
+ const finalState = yield* SubscriptionRef.get(state)
431
+ return { values: Array.from(values), finalState }
432
+ })
433
+
434
+ const { values, finalState } = await Effect.runPromise(program)
435
+ expect(values).toEqual([0, 1, 2, 3])
436
+ // Last `next(state)` was `next(3)` before the iteration that emitted Stop.
437
+ expect(finalState).toBe(3)
438
+ })
439
+
440
+ it("the state ref starts at `initial` and stays there if the loop stops without advancing", async () => {
441
+ const program = Effect.gen(function* () {
442
+ const { stream, state } = yield* loopWithState({ count: 7 }, () =>
443
+ Stream.fromIterable([stopEvent]),
444
+ )
445
+ yield* Stream.runDrain(stream)
446
+ return yield* SubscriptionRef.get(state)
447
+ })
448
+
449
+ expect(await Effect.runPromise(program)).toEqual({ count: 7 })
450
+ })
451
+
452
+ it("a downstream consumer can read the live state between emitted values", async () => {
453
+ // Body emits one value per iteration, then advances. A `Stream.runForEach`
454
+ // consumer reads the ref each time a value arrives — proving the ref
455
+ // tracks loop state without the body needing to surface it.
456
+ const program = Effect.gen(function* () {
457
+ const { stream, state } = yield* loopWithState(0, (n: number) =>
458
+ n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),
459
+ )
460
+ const seen: Array<{ value: number; stateAfter: number }> = []
461
+ yield* Stream.runForEach(stream, (v) =>
462
+ Effect.gen(function* () {
463
+ seen.push({ value: v, stateAfter: yield* SubscriptionRef.get(state) })
464
+ }),
465
+ )
466
+ return seen
467
+ })
468
+
469
+ // For each iter `n`, the consumer reads the ref between values: it sees
470
+ // the iteration's input state. The terminal iter (n=3) stops without
471
+ // advancing, so its read still shows 3.
472
+ expect(await Effect.runPromise(program)).toEqual([
473
+ { value: 0, stateAfter: 0 },
474
+ { value: 1, stateAfter: 1 },
475
+ { value: 2, stateAfter: 2 },
476
+ { value: 3, stateAfter: 3 },
477
+ ])
478
+ })
479
+
480
+ it("SubscriptionRef.changes emits every state transition to a concurrent observer", async () => {
481
+ const program = Effect.gen(function* () {
482
+ const start = yield* Latch.make(false)
483
+
484
+ // Body waits on the latch in iter 0 so the observer can subscribe first.
485
+ const { stream, state } = yield* loopWithState(0, (n: number) =>
486
+ Effect.gen(function* () {
487
+ if (n === 0) yield* Latch.await(start)
488
+ return n >= 3 ? stopAfter(Stream.empty) : nextAfter(Stream.empty, n + 1)
489
+ }),
490
+ )
491
+
492
+ // Fork the observer; take 4 distinct states (initial + 3 transitions).
493
+ const observerFiber = yield* Effect.forkChild(
494
+ SubscriptionRef.changes(state).pipe(Stream.take(4), Stream.runCollect),
495
+ )
496
+
497
+ // Give the observer fiber a chance to actually subscribe before the
498
+ // loop starts advancing the ref. Without this, the loop could finish
499
+ // before the observer's pubsub subscription is in place.
500
+ yield* Effect.sleep("10 millis")
501
+
502
+ yield* Latch.open(start)
503
+ yield* Stream.runDrain(stream)
504
+
505
+ return Array.from(yield* Fiber.join(observerFiber))
506
+ })
507
+
508
+ // initial 0, then next(1), next(2), next(3) — four distinct states.
509
+ expect(await Effect.runPromise(program)).toEqual([0, 1, 2, 3])
510
+ })
511
+
512
+ it("does not interfere with the body's value stream", async () => {
513
+ const program = Effect.gen(function* () {
514
+ const { stream } = yield* loopWithState(0, (n: number) =>
515
+ n >= 3
516
+ ? stopAfter(Stream.fromIterable([n]))
517
+ : nextAfter(Stream.fromIterable([n, n + 0.5]), n + 1),
518
+ )
519
+ return Array.from(yield* Stream.runCollect(stream))
520
+ })
521
+
522
+ expect(await Effect.runPromise(program)).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3])
523
+ })
524
+ })
package/src/loop/Loop.ts CHANGED
@@ -17,7 +17,19 @@
17
17
  * (their producing side effects may already have run). Prefer the
18
18
  * `Loop.nextAfter` / `Loop.stopAfter` helpers to terminate cleanly.
19
19
  */
20
- import { Cause, Channel, Data, Effect, Exit, Function, Option, Ref, Scope, Stream } from "effect"
20
+ import {
21
+ Cause,
22
+ Channel,
23
+ Data,
24
+ Effect,
25
+ Exit,
26
+ Function,
27
+ Option,
28
+ Ref,
29
+ Scope,
30
+ Stream,
31
+ SubscriptionRef,
32
+ } from "effect"
21
33
  import { IncompleteTurn } from "../domain/AiError.js"
22
34
  import { isTurnComplete, type Turn, type TurnEvent } from "../domain/Turn.js"
23
35
 
@@ -83,7 +95,7 @@ export const stopAfter = <A, E, R>(
83
95
  * into an accumulator, and at end-of-stream emit one `next(build(finalAcc))`.
84
96
  *
85
97
  * Subsumes `nextAfter` when state is constant (`reduce: (s, _) => s`,
86
- * `build: (s) => s`). Used by `Toolkit.nextStateFrom` to collect tool
98
+ * `build: (s) => s`). Used by `Toolkit.continueWith` to collect tool
87
99
  * results and build next state without exposing a Ref to recipes.
88
100
  */
89
101
  export const nextAfterFold = <A, B, S, E, R>(
@@ -107,7 +119,7 @@ export const nextAfterFold = <A, B, S, E, R>(
107
119
  )
108
120
 
109
121
  // ---------------------------------------------------------------------------
110
- // streamUntilComplete - turn-aware stream operator for loop bodies
122
+ // onTurnComplete - turn-aware stream operator for loop bodies
111
123
  // ---------------------------------------------------------------------------
112
124
 
113
125
  /**
@@ -125,7 +137,7 @@ export const nextAfterFold = <A, B, S, E, R>(
125
137
  * fails with `AiError.IncompleteTurn`. Catch it via `Stream.catchTag` if
126
138
  * you want to recover.
127
139
  */
128
- export const streamUntilComplete =
140
+ export const onTurnComplete =
129
141
  <S, A, E2 = never, R2 = never>(
130
142
  then: (turn: Turn) => Effect.Effect<Stream.Stream<Event<A, S>, E2, R2>, E2, R2>,
131
143
  ) =>
@@ -162,7 +174,7 @@ export const streamUntilComplete =
162
174
  const isNonEmpty = <A>(array: ReadonlyArray<A>): array is readonly [A, ...Array<A>] =>
163
175
  array.length > 0
164
176
 
165
- interface CurrentBody<S, A, E, R> {
177
+ type CurrentBody<S, A, E, R> = {
166
178
  readonly scope: Scope.Closeable
167
179
  readonly pull: Effect.Effect<ReadonlyArray<Event<A, S>>, E | Cause.Done<void>, R>
168
180
  }
@@ -293,3 +305,55 @@ export const loop: {
293
305
  ),
294
306
  ),
295
307
  )
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // loopWithState - same body protocol, plus a live state observable.
311
+ // ---------------------------------------------------------------------------
312
+
313
+ /**
314
+ * Like `loop`, but exposes the current loop state as a `SubscriptionRef`
315
+ * alongside the value stream.
316
+ *
317
+ * Allocates one `SubscriptionRef<S>` seeded with `initial`, then runs the
318
+ * loop with a wrapped body that taps every `Next(s)` event into the ref
319
+ * before forwarding it. The caller decides how to consume both channels:
320
+ *
321
+ * - **Final state**: drain the stream, then `SubscriptionRef.get(state)`
322
+ * - the ref holds the state from the last `Next` (or `initial` if the
323
+ * loop ended without advancing).
324
+ * - **Live transitions**: `SubscriptionRef.changes(state)` is a
325
+ * `Stream<S>` of every state observed; subscribe alongside the value
326
+ * stream.
327
+ * - **Mid-iteration peek**: `SubscriptionRef.get(state)` at any time.
328
+ *
329
+ * The returned stream and ref are independent of each other - the ref
330
+ * lives outside the stream's scope, so reading it after the stream
331
+ * completes is safe.
332
+ */
333
+ export const loopWithState = <S, A, E, R>(
334
+ initial: S,
335
+ body: LoopBody<S, A, E, R>,
336
+ ): Effect.Effect<{
337
+ readonly stream: Stream.Stream<A, E, R>
338
+ readonly state: SubscriptionRef.SubscriptionRef<S>
339
+ }> =>
340
+ Effect.gen(function* () {
341
+ const stateRef = yield* SubscriptionRef.make(initial)
342
+
343
+ const tap = (stream: Stream.Stream<Event<A, S>, E, R>): Stream.Stream<Event<A, S>, E, R> =>
344
+ stream.pipe(
345
+ Stream.tap((event) =>
346
+ event._tag === "Next" ? SubscriptionRef.set(stateRef, event.state) : Effect.void,
347
+ ),
348
+ )
349
+
350
+ const wrappedBody: LoopBody<S, A, E, R> = (s) => {
351
+ const result = body(s)
352
+ return Effect.isEffect(result) ? Effect.map(result, tap) : tap(result)
353
+ }
354
+
355
+ return {
356
+ stream: loop(initial, wrappedBody),
357
+ state: stateRef,
358
+ }
359
+ })