@effect-uai/core 0.5.2 → 0.7.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 (175) hide show
  1. package/README.md +1 -1
  2. package/dist/{Audio-BfCTGnH3.d.mts → Audio-DPlTsNdz.d.mts} +3 -2
  3. package/dist/Audio-DPlTsNdz.d.mts.map +1 -0
  4. package/dist/{Items-BH8xUkoR.d.mts → Items-D8UaXQx_.d.mts} +15 -15
  5. package/dist/Items-D8UaXQx_.d.mts.map +1 -0
  6. package/dist/SandboxError-BvKypkJm.d.mts +158 -0
  7. package/dist/SandboxError-BvKypkJm.d.mts.map +1 -0
  8. package/dist/{Tool-87ViKCCO.d.mts → Tool-CiFXYjmR.d.mts} +11 -11
  9. package/dist/{Tool-87ViKCCO.d.mts.map → Tool-CiFXYjmR.d.mts.map} +1 -1
  10. package/dist/capabilities/Capabilities.d.mts +78 -0
  11. package/dist/capabilities/Capabilities.d.mts.map +1 -0
  12. package/dist/capabilities/Capabilities.mjs +65 -0
  13. package/dist/capabilities/Capabilities.mjs.map +1 -0
  14. package/dist/domain/Audio.d.mts +1 -1
  15. package/dist/domain/Audio.mjs.map +1 -1
  16. package/dist/domain/Items.d.mts +2 -2
  17. package/dist/domain/Items.mjs +15 -15
  18. package/dist/domain/Items.mjs.map +1 -1
  19. package/dist/domain/Music.d.mts +131 -72
  20. package/dist/domain/Music.d.mts.map +1 -1
  21. package/dist/domain/Music.mjs +36 -9
  22. package/dist/domain/Music.mjs.map +1 -1
  23. package/dist/domain/Transcript.d.mts +8 -2
  24. package/dist/domain/Transcript.d.mts.map +1 -1
  25. package/dist/domain/Transcript.mjs.map +1 -1
  26. package/dist/domain/Turn.d.mts +388 -2
  27. package/dist/domain/Turn.d.mts.map +1 -0
  28. package/dist/domain/Turn.mjs +13 -13
  29. package/dist/domain/Turn.mjs.map +1 -1
  30. package/dist/embedding-model/Embedding.d.mts +12 -2
  31. package/dist/embedding-model/Embedding.d.mts.map +1 -1
  32. package/dist/embedding-model/Embedding.mjs.map +1 -1
  33. package/dist/embedding-model/EmbeddingModel.d.mts +36 -23
  34. package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -1
  35. package/dist/embedding-model/EmbeddingModel.mjs +20 -1
  36. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
  37. package/dist/embedding-model/EmbeddingModel.test.mjs +15 -3
  38. package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
  39. package/dist/index.d.mts +13 -7
  40. package/dist/index.mjs +9 -3
  41. package/dist/language-model/LanguageModel.d.mts +5 -5
  42. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  43. package/dist/language-model/LanguageModel.mjs.map +1 -1
  44. package/dist/loop/Loop.d.mts +37 -90
  45. package/dist/loop/Loop.d.mts.map +1 -1
  46. package/dist/loop/Loop.mjs +39 -93
  47. package/dist/loop/Loop.mjs.map +1 -1
  48. package/dist/loop/Loop.test.mjs +46 -54
  49. package/dist/loop/Loop.test.mjs.map +1 -1
  50. package/dist/music-generator/MusicGenerator.d.mts +48 -25
  51. package/dist/music-generator/MusicGenerator.d.mts.map +1 -1
  52. package/dist/music-generator/MusicGenerator.mjs +17 -9
  53. package/dist/music-generator/MusicGenerator.mjs.map +1 -1
  54. package/dist/music-generator/MusicGenerator.test.mjs +81 -56
  55. package/dist/music-generator/MusicGenerator.test.mjs.map +1 -1
  56. package/dist/sandbox/Memory.d.mts +53 -0
  57. package/dist/sandbox/Memory.d.mts.map +1 -0
  58. package/dist/sandbox/Memory.mjs +68 -0
  59. package/dist/sandbox/Memory.mjs.map +1 -0
  60. package/dist/sandbox/Sandbox.d.mts +717 -0
  61. package/dist/sandbox/Sandbox.d.mts.map +1 -0
  62. package/dist/sandbox/Sandbox.mjs +192 -0
  63. package/dist/sandbox/Sandbox.mjs.map +1 -0
  64. package/dist/sandbox/SandboxError.d.mts +2 -0
  65. package/dist/sandbox/SandboxError.mjs +89 -0
  66. package/dist/sandbox/SandboxError.mjs.map +1 -0
  67. package/dist/sandbox/SandboxImage.d.mts +28 -0
  68. package/dist/sandbox/SandboxImage.d.mts.map +1 -0
  69. package/dist/sandbox/SandboxImage.mjs +32 -0
  70. package/dist/sandbox/SandboxImage.mjs.map +1 -0
  71. package/dist/sandbox/SandboxNetwork.d.mts +34 -0
  72. package/dist/sandbox/SandboxNetwork.d.mts.map +1 -0
  73. package/dist/sandbox/SandboxNetwork.mjs +36 -0
  74. package/dist/sandbox/SandboxNetwork.mjs.map +1 -0
  75. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +84 -3
  76. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
  77. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +32 -2
  78. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
  79. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +59 -5
  80. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
  81. package/dist/testing/MockMusicGenerator.d.mts +12 -7
  82. package/dist/testing/MockMusicGenerator.d.mts.map +1 -1
  83. package/dist/testing/MockMusicGenerator.mjs +4 -3
  84. package/dist/testing/MockMusicGenerator.mjs.map +1 -1
  85. package/dist/testing/MockProvider.d.mts +3 -3
  86. package/dist/testing/MockProvider.d.mts.map +1 -1
  87. package/dist/testing/MockProvider.mjs.map +1 -1
  88. package/dist/testing/MockSandbox.d.mts +604 -0
  89. package/dist/testing/MockSandbox.d.mts.map +1 -0
  90. package/dist/testing/MockSandbox.mjs +160 -0
  91. package/dist/testing/MockSandbox.mjs.map +1 -0
  92. package/dist/testing/MockSpeechSynthesizer.d.mts +27 -12
  93. package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
  94. package/dist/testing/MockSpeechSynthesizer.mjs +75 -51
  95. package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
  96. package/dist/tool/{Resolvers.d.mts → Approval.d.mts} +27 -27
  97. package/dist/tool/Approval.d.mts.map +1 -0
  98. package/dist/tool/{Resolvers.mjs → Approval.mjs} +18 -18
  99. package/dist/tool/Approval.mjs.map +1 -0
  100. package/dist/tool/{Resolvers.test.mjs → Approval.test.mjs} +40 -40
  101. package/dist/tool/Approval.test.mjs.map +1 -0
  102. package/dist/tool/HistoryCheck.d.mts +6 -6
  103. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  104. package/dist/tool/HistoryCheck.mjs +5 -5
  105. package/dist/tool/HistoryCheck.mjs.map +1 -1
  106. package/dist/tool/Tool.d.mts +2 -2
  107. package/dist/tool/Tool.mjs +4 -4
  108. package/dist/tool/Tool.mjs.map +1 -1
  109. package/dist/tool/ToolEvent.d.mts +19 -19
  110. package/dist/tool/ToolEvent.d.mts.map +1 -1
  111. package/dist/tool/ToolEvent.mjs +8 -8
  112. package/dist/tool/ToolEvent.mjs.map +1 -1
  113. package/dist/tool/{Outcome.d.mts → ToolResult.d.mts} +29 -29
  114. package/dist/tool/ToolResult.d.mts.map +1 -0
  115. package/dist/tool/ToolResult.mjs +61 -0
  116. package/dist/tool/ToolResult.mjs.map +1 -0
  117. package/dist/tool/Toolkit.d.mts +58 -26
  118. package/dist/tool/Toolkit.d.mts.map +1 -1
  119. package/dist/tool/Toolkit.mjs +62 -34
  120. package/dist/tool/Toolkit.mjs.map +1 -1
  121. package/dist/tool/Toolkit.test.mjs +13 -13
  122. package/dist/tool/Toolkit.test.mjs.map +1 -1
  123. package/dist/transcriber/Transcriber.d.mts +11 -10
  124. package/dist/transcriber/Transcriber.d.mts.map +1 -1
  125. package/dist/transcriber/Transcriber.mjs.map +1 -1
  126. package/dist/transcriber/Transcriber.test.mjs +3 -3
  127. package/dist/transcriber/Transcriber.test.mjs.map +1 -1
  128. package/package.json +35 -7
  129. package/src/capabilities/Capabilities.ts +81 -0
  130. package/src/domain/Audio.ts +2 -1
  131. package/src/domain/Items.ts +9 -9
  132. package/src/domain/Music.ts +142 -68
  133. package/src/domain/Transcript.ts +7 -1
  134. package/src/domain/Turn.ts +16 -16
  135. package/src/embedding-model/Embedding.ts +11 -0
  136. package/src/embedding-model/EmbeddingModel.test.ts +15 -4
  137. package/src/embedding-model/EmbeddingModel.ts +48 -21
  138. package/src/index.ts +8 -2
  139. package/src/language-model/LanguageModel.ts +3 -3
  140. package/src/loop/Loop.test.ts +93 -91
  141. package/src/loop/Loop.ts +72 -170
  142. package/src/music-generator/MusicGenerator.test.ts +78 -53
  143. package/src/music-generator/MusicGenerator.ts +65 -24
  144. package/src/sandbox/Memory.ts +128 -0
  145. package/src/sandbox/Sandbox.ts +639 -0
  146. package/src/sandbox/SandboxError.ts +140 -0
  147. package/src/sandbox/SandboxImage.ts +26 -0
  148. package/src/sandbox/SandboxNetwork.ts +34 -0
  149. package/src/speech-synthesizer/SpeechSynthesizer.test.ts +92 -3
  150. package/src/speech-synthesizer/SpeechSynthesizer.ts +109 -0
  151. package/src/testing/MockMusicGenerator.ts +28 -18
  152. package/src/testing/MockProvider.ts +21 -20
  153. package/src/testing/MockSandbox.ts +342 -0
  154. package/src/testing/MockSpeechSynthesizer.ts +137 -77
  155. package/src/tool/{Resolvers.test.ts → Approval.test.ts} +45 -54
  156. package/src/tool/{Resolvers.ts → Approval.ts} +25 -25
  157. package/src/tool/HistoryCheck.ts +10 -13
  158. package/src/tool/Tool.ts +13 -15
  159. package/src/tool/ToolEvent.ts +7 -7
  160. package/src/tool/{Outcome.ts → ToolResult.ts} +20 -20
  161. package/src/tool/Toolkit.test.ts +13 -13
  162. package/src/tool/Toolkit.ts +97 -73
  163. package/src/transcriber/Transcriber.test.ts +3 -3
  164. package/src/transcriber/Transcriber.ts +10 -7
  165. package/dist/Audio-BfCTGnH3.d.mts.map +0 -1
  166. package/dist/Items-BH8xUkoR.d.mts.map +0 -1
  167. package/dist/Turn-0CwCAyVe.d.mts +0 -388
  168. package/dist/Turn-0CwCAyVe.d.mts.map +0 -1
  169. package/dist/tool/Outcome.d.mts.map +0 -1
  170. package/dist/tool/Outcome.mjs +0 -61
  171. package/dist/tool/Outcome.mjs.map +0 -1
  172. package/dist/tool/Resolvers.d.mts.map +0 -1
  173. package/dist/tool/Resolvers.mjs.map +0 -1
  174. package/dist/tool/Resolvers.test.mjs.map +0 -1
  175. /package/dist/tool/{Resolvers.test.d.mts → Approval.test.d.mts} +0 -0
@@ -1,8 +1,16 @@
1
- import { Effect, Stream } from "effect"
1
+ import { Duration, Effect, Stream } from "effect"
2
2
  import { describe, expect, expectTypeOf, it } from "vitest"
3
3
  import type * as AiError from "../domain/AiError.js"
4
4
  import type { AudioChunk, AudioFormat } from "../domain/Audio.js"
5
- import { configInput, promptsInput, type MusicResult } from "../domain/Music.js"
5
+ import {
6
+ audioEvent,
7
+ controlInput,
8
+ type GenerateResult,
9
+ type MusicResult,
10
+ type MusicStreamEvent,
11
+ promptsInput,
12
+ singleVariant,
13
+ } from "../domain/Music.js"
6
14
  import * as MockMusicGenerator from "../testing/MockMusicGenerator.js"
7
15
  import * as MusicGenerator from "./MusicGenerator.js"
8
16
 
@@ -13,50 +21,69 @@ const mp3Format: AudioFormat = {
13
21
  channels: 2,
14
22
  }
15
23
 
16
- const result: MusicResult = {
17
- format: mp3Format,
18
- bytes: new Uint8Array([0xff, 0xfb, 0x90, 0x00]),
19
- durationSeconds: 30,
24
+ const oneResult: MusicResult = {
25
+ audio: {
26
+ format: mp3Format,
27
+ bytes: new Uint8Array([0xff, 0xfb, 0x90, 0x00]),
28
+ duration: Duration.seconds(30),
29
+ },
30
+ provider: "mock-music",
20
31
  lyrics: "[Verse]\nhello\n",
21
- watermark: { kind: "synthid" },
32
+ watermark: "synthid",
22
33
  }
23
34
 
35
+ const oneVariant: GenerateResult = singleVariant(oneResult)
36
+
24
37
  const chunk = (n: number): AudioChunk => ({ bytes: new Uint8Array([n]) })
25
38
 
26
39
  describe("MusicGenerator.generate", () => {
27
- it("returns the scripted MusicResult", async () => {
28
- const mock = MockMusicGenerator.layer({ results: [result] })
40
+ it("returns the scripted GenerateResult", async () => {
41
+ const mock = MockMusicGenerator.layer({ results: [oneVariant] })
29
42
  const program = MusicGenerator.generate({
30
43
  model: "mock-music",
31
- prompts: "upbeat indie pop",
44
+ prompt: "upbeat indie pop",
32
45
  })
33
46
  const out = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
34
- expect(out.bytes).toEqual(result.bytes)
35
- expect(out.durationSeconds).toBe(30)
36
- expect(out.watermark?.kind).toBe("synthid")
37
- expect(out.lyrics).toContain("[Verse]")
47
+ expect(out.primary.audio.bytes).toEqual(oneResult.audio.bytes)
48
+ expect(out.primary.audio.duration).toEqual(Duration.seconds(30))
49
+ expect(out.primary.watermark).toBe("synthid")
50
+ expect(out.primary.lyrics).toContain("[Verse]")
51
+ expect(out.variants).toHaveLength(1)
52
+ expect(out.variants[0]).toBe(out.primary)
53
+ })
54
+
55
+ it("exposes multiple variants when the provider returned more than one", async () => {
56
+ const secondTrack: MusicResult = {
57
+ ...oneResult,
58
+ audio: { ...oneResult.audio, bytes: new Uint8Array([0xff, 0xfb, 0xa0, 0x00]) },
59
+ }
60
+ const twoVariants: GenerateResult = {
61
+ primary: oneResult,
62
+ variants: [oneResult, secondTrack],
63
+ }
64
+ const mock = MockMusicGenerator.layer({ results: [twoVariants] })
65
+ const program = MusicGenerator.generate({ model: "m", prompt: "house" })
66
+ const out = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
67
+ expect(out.variants).toHaveLength(2)
68
+ expect(out.variants[1]!.audio.bytes).toEqual(secondTrack.audio.bytes)
38
69
  })
39
70
 
40
71
  it("records the request shape on the recorder", async () => {
41
- const mock = MockMusicGenerator.layer({ results: [result, result] })
72
+ const mock = MockMusicGenerator.layer({ results: [oneVariant, oneVariant] })
42
73
  const program = Effect.gen(function* () {
43
- yield* MusicGenerator.generate({ model: "m", prompts: "techno" })
74
+ yield* MusicGenerator.generate({ model: "m", prompt: "techno" })
44
75
  yield* MusicGenerator.generate({
45
76
  model: "m",
46
- prompts: [
47
- { text: "synthwave", weight: 1.0 },
48
- { text: "80s movie OST", weight: 0.4 },
49
- ],
50
- bpm: 120,
51
- instrumental: true,
77
+ prompt: "synthwave",
78
+ duration: Duration.seconds(45),
79
+ seed: 42,
52
80
  })
53
81
  return yield* mock.recorder
54
82
  })
55
83
  const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
56
84
  expect(rec.generateCalls.length).toBe(2)
57
- expect(rec.generateCalls[1]!.bpm).toBe(120)
58
- expect(rec.generateCalls[1]!.instrumental).toBe(true)
59
- expect(Array.isArray(rec.generateCalls[1]!.prompts)).toBe(true)
85
+ expect(rec.generateCalls[1]!.duration).toEqual(Duration.seconds(45))
86
+ expect(rec.generateCalls[1]!.seed).toBe(42)
60
87
  })
61
88
  })
62
89
 
@@ -66,7 +93,7 @@ describe("MusicGenerator.streamGeneration", () => {
66
93
  streamGenerationChunks: [[chunk(1), chunk(2), chunk(3)]],
67
94
  })
68
95
  const program = Stream.runCollect(
69
- MusicGenerator.streamGeneration({ model: "m", prompts: "ambient" }),
96
+ MusicGenerator.streamGeneration({ model: "m", prompt: "ambient" }),
70
97
  )
71
98
  const out = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
72
99
  expect(out.map((c) => Array.from(c.bytes))).toEqual([[1], [2], [3]])
@@ -76,15 +103,15 @@ describe("MusicGenerator.streamGeneration", () => {
76
103
  describe("MusicGenerator capability marker (compile-time)", () => {
77
104
  const sgfReq: MusicGenerator.CommonStreamGenerateMusicRequest = {
78
105
  model: "m",
79
- prompts: "",
106
+ prompt: "",
80
107
  }
81
108
 
82
109
  it("requires `MusicInteractiveSession` on the R channel of streamGenerationFrom", () => {
83
110
  const inputs = Stream.fromIterable([promptsInput([{ text: "techno" }])])
84
- const audio = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
85
- expectTypeOf(audio).toEqualTypeOf<
111
+ const events = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
112
+ expectTypeOf(events).toEqualTypeOf<
86
113
  Stream.Stream<
87
- AudioChunk,
114
+ MusicStreamEvent,
88
115
  AiError.AiError,
89
116
  MusicGenerator.MusicGenerator | MusicGenerator.MusicInteractiveSession
90
117
  >
@@ -92,14 +119,14 @@ describe("MusicGenerator capability marker (compile-time)", () => {
92
119
  })
93
120
 
94
121
  it("does NOT require `MusicInteractiveSession` for sync `generate`", () => {
95
- const eff = MusicGenerator.generate({ model: "m", prompts: "ambient" })
122
+ const eff = MusicGenerator.generate({ model: "m", prompt: "ambient" })
96
123
  expectTypeOf(eff).toEqualTypeOf<
97
- Effect.Effect<MusicResult, AiError.AiError, MusicGenerator.MusicGenerator>
124
+ Effect.Effect<GenerateResult, AiError.AiError, MusicGenerator.MusicGenerator>
98
125
  >()
99
126
  })
100
127
 
101
128
  it("does NOT require `MusicInteractiveSession` for `streamGeneration`", () => {
102
- const audio = MusicGenerator.streamGeneration({ model: "m", prompts: "ambient" })
129
+ const audio = MusicGenerator.streamGeneration({ model: "m", prompt: "ambient" })
103
130
  expectTypeOf(audio).toEqualTypeOf<
104
131
  Stream.Stream<AudioChunk, AiError.AiError, MusicGenerator.MusicGenerator>
105
132
  >()
@@ -108,8 +135,8 @@ describe("MusicGenerator capability marker (compile-time)", () => {
108
135
  it("a layer without the marker leaves `MusicInteractiveSession` unsatisfied in R", () => {
109
136
  const noMarker = MockMusicGenerator.layerWithoutInteractive({})
110
137
  const inputs = Stream.fromIterable([promptsInput([{ text: "techno" }])])
111
- const audio = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
112
- const program = Stream.runDrain(audio).pipe(Effect.provide(noMarker.layer))
138
+ const events = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
139
+ const program = Stream.runDrain(events).pipe(Effect.provide(noMarker.layer))
113
140
  expectTypeOf(program).toEqualTypeOf<
114
141
  Effect.Effect<void, AiError.AiError, MusicGenerator.MusicInteractiveSession>
115
142
  >()
@@ -117,53 +144,51 @@ describe("MusicGenerator capability marker (compile-time)", () => {
117
144
 
118
145
  it("a full layer (with marker) clears R to never", () => {
119
146
  const fullMock = MockMusicGenerator.layer({
120
- streamGenerationFromChunks: [[]],
147
+ streamGenerationFromEvents: [[]],
121
148
  })
122
149
  const inputs = Stream.fromIterable([promptsInput([{ text: "techno" }])])
123
- const audio = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
124
- const program = Stream.runDrain(audio).pipe(Effect.provide(fullMock.layer))
150
+ const events = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
151
+ const program = Stream.runDrain(events).pipe(Effect.provide(fullMock.layer))
125
152
  expectTypeOf(program).toEqualTypeOf<Effect.Effect<void, AiError.AiError, never>>()
126
153
  })
127
154
  })
128
155
 
129
156
  describe("MusicGenerator.streamGenerationFrom", () => {
130
157
  const sgfReq: MusicGenerator.CommonStreamGenerateMusicRequest = {
131
- model: "lyria-realtime-001",
132
- prompts: "",
158
+ model: "lyria-realtime-exp",
159
+ prompt: "",
133
160
  }
134
161
 
135
- it("drains a session-input stream and emits scripted audio", async () => {
162
+ it("drains a session-input stream and emits scripted events", async () => {
136
163
  const mock = MockMusicGenerator.layer({
137
- streamGenerationFromChunks: [[chunk(10), chunk(20)]],
164
+ streamGenerationFromEvents: [[audioEvent(chunk(10)), audioEvent(chunk(20))]],
138
165
  })
139
166
  const inputs = Stream.fromIterable([
140
167
  promptsInput([{ text: "minimal techno", weight: 1.0 }]),
141
- configInput({ bpm: 124 }),
168
+ controlInput("play"),
142
169
  promptsInput([
143
170
  { text: "minimal techno", weight: 1.0 },
144
171
  { text: "1980s synthwave", weight: 0.3 },
145
172
  ]),
146
173
  ])
147
- const audio = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
148
- const out = await Effect.runPromise(Stream.runCollect(audio).pipe(Effect.provide(mock.layer)))
149
- expect(out.map((c) => Array.from(c.bytes))).toEqual([[10], [20]])
174
+ const events = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
175
+ const out = await Effect.runPromise(Stream.runCollect(events).pipe(Effect.provide(mock.layer)))
176
+ const audioChunks = out.flatMap((e) => (e._tag === "audio" ? [e.chunk] : []))
177
+ expect(audioChunks.map((c) => Array.from(c.bytes))).toEqual([[10], [20]])
150
178
  })
151
179
 
152
180
  it("records the request on the streamGenerationFrom call channel", async () => {
153
181
  const mock = MockMusicGenerator.layer({
154
- streamGenerationFromChunks: [[chunk(42)]],
182
+ streamGenerationFromEvents: [[audioEvent(chunk(42))]],
155
183
  })
156
184
  const program = Effect.gen(function* () {
157
- yield* Stream.runDrain(
158
- Stream.fromIterable([promptsInput([{ text: "x" }])]).pipe(
159
- MusicGenerator.streamGenerationFrom(sgfReq),
160
- ),
161
- )
185
+ const inputs = Stream.fromIterable([promptsInput([{ text: "x" }])])
186
+ yield* Stream.runDrain(inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq)))
162
187
  return yield* mock.recorder
163
188
  })
164
189
  const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
165
190
  expect(rec.streamGenerationFromCalls.length).toBe(1)
166
- expect(rec.streamGenerationFromCalls[0]!.model).toBe("lyria-realtime-001")
191
+ expect(rec.streamGenerationFromCalls[0]!.model).toBe("lyria-realtime-exp")
167
192
  expect(rec.generateCalls.length).toBe(0)
168
193
  expect(rec.streamGenerationCalls.length).toBe(0)
169
194
  })
@@ -1,18 +1,24 @@
1
1
  import { Context, Effect, Function, Stream } from "effect"
2
2
  import * as AiError from "../domain/AiError.js"
3
- import type { AudioChunk } from "../domain/Audio.js"
4
3
  import type {
5
4
  CommonGenerateMusicRequest,
6
5
  CommonStreamGenerateMusicRequest,
7
- MusicResult,
6
+ GenerateResult,
8
7
  MusicSessionInput,
8
+ MusicStreamEvent,
9
9
  } from "../domain/Music.js"
10
+ import type { AudioChunk } from "../domain/Audio.js"
10
11
 
11
12
  export type {
12
13
  CommonGenerateMusicRequest,
13
14
  CommonStreamGenerateMusicRequest,
15
+ GenerateResult,
14
16
  MusicResult,
17
+ MusicSection,
18
+ MusicSessionControl,
15
19
  MusicSessionInput,
20
+ MusicStreamEvent,
21
+ Watermark,
16
22
  WeightedPrompt,
17
23
  } from "../domain/Music.js"
18
24
 
@@ -20,35 +26,50 @@ export type MusicGeneratorService = {
20
26
  /**
21
27
  * One-shot. Prompt in, full audio bytes out. Universally supported.
22
28
  * Async/poll-based providers (Suno, Mureka) hide their poll loop
23
- * inside the adapter — caller still sees a single `Effect`.
29
+ * inside the adapter, caller still sees a single `Effect`.
30
+ *
31
+ * Returns `GenerateResult` with `primary` plus a `variants` array.
32
+ * Suno and Mureka always return 2 tracks; every other provider
33
+ * returns 1 (and `primary === variants[0]`).
24
34
  */
25
35
  readonly generate: (
26
36
  request: CommonGenerateMusicRequest,
27
- ) => Effect.Effect<MusicResult, AiError.AiError>
37
+ ) => Effect.Effect<GenerateResult, AiError.AiError>
28
38
  /**
29
39
  * Prompt in, audio chunks streamed out. Providers without a native
30
40
  * chunked-output endpoint (Lyria 3 sync, Mureka, MiniMax, Stable
31
41
  * Audio) emulate this by calling `generate` and emitting a single
32
- * `AudioChunk` — first-class, no `Unsupported`.
42
+ * `AudioChunk`, first-class, no `Unsupported`.
33
43
  */
34
44
  readonly streamGeneration: (
35
45
  request: CommonStreamGenerateMusicRequest,
36
46
  ) => Stream.Stream<AudioChunk, AiError.AiError>
37
47
  /**
38
- * Bidirectional session: a `Stream` of prompt-or-config updates flows
39
- * in, a `Stream` of audio chunks flows out. The session WS / RPC is
40
- * acquired on first pull and released when the output stream is
41
- * finalized via `Stream.scoped`.
48
+ * Bidirectional session: a `Stream` of `MusicSessionInput` (prompts
49
+ * + playback control) flows in, a `Stream` of `MusicStreamEvent`
50
+ * flows out. The session WS / RPC is acquired on first pull and
51
+ * released when the output stream is finalized via `Stream.scoped`.
52
+ *
53
+ * Cross-provider callers send `prompts` (steer the active
54
+ * generation) and `control` (play / pause / stop / reset) — the two
55
+ * actions that converge across interactive-media protocols.
56
+ * Provider-specific model knobs (Lyria RealTime's density /
57
+ * brightness / mute-stems / BPM / scale, etc.) live on the
58
+ * provider-typed service (`LyriaRealtimeGenerator.streamGenerationFrom`),
59
+ * which extends this union with its own `config` variant.
60
+ *
61
+ * Output is `MusicStreamEvent`: `audio` chunks alongside in-band
62
+ * `warning` and `filteredPrompt` events the model emits server-side.
42
63
  *
43
64
  * Gated by the `MusicInteractiveSession` capability marker on the
44
- * top-level helper — providers without bidirectional support don't
65
+ * top-level helper. Providers without bidirectional support don't
45
66
  * ship the marker, so calls fail at `Effect.provide` with a type
46
67
  * error.
47
68
  */
48
69
  readonly streamGenerationFrom: <E, R>(
49
70
  input: Stream.Stream<MusicSessionInput, E, R>,
50
71
  request: CommonStreamGenerateMusicRequest,
51
- ) => Stream.Stream<AudioChunk, AiError.AiError | E, R>
72
+ ) => Stream.Stream<MusicStreamEvent, AiError.AiError | E, R>
52
73
  }
53
74
 
54
75
  export class MusicGenerator extends Context.Service<MusicGenerator, MusicGeneratorService>()(
@@ -56,23 +77,23 @@ export class MusicGenerator extends Context.Service<MusicGenerator, MusicGenerat
56
77
  ) {}
57
78
 
58
79
  /**
59
- * Capability marker — provided by provider layers whose
80
+ * Capability marker, provided by provider layers whose
60
81
  * `streamGenerationFrom` is wired up at the wire level. Currently only
61
82
  * Lyria RealTime (via the BidiGenerateMusic WebSocket) ships it.
62
83
  * Calling `streamGenerationFrom` while only a non-interactive Layer is
63
84
  * in scope fails at `Effect.provide` with a type error.
64
85
  *
65
- * Phantom — the value is `void`; providers register with
86
+ * Phantom: the value is `void`; providers register with
66
87
  * `Layer.succeed(MusicInteractiveSession, undefined)`.
67
88
  */
68
89
  export class MusicInteractiveSession extends Context.Service<MusicInteractiveSession, void>()(
69
90
  "@betalyra/effect-uai/capability/MusicInteractiveSession",
70
91
  ) {}
71
92
 
72
- /** One-shot generation. */
93
+ /** One-shot generation. Returns the full `GenerateResult`. */
73
94
  export const generate = (
74
95
  request: CommonGenerateMusicRequest,
75
- ): Effect.Effect<MusicResult, AiError.AiError, MusicGenerator> =>
96
+ ): Effect.Effect<GenerateResult, AiError.AiError, MusicGenerator> =>
76
97
  Effect.flatMap(MusicGenerator.asEffect(), (s) => s.generate(request))
77
98
 
78
99
  /** Prompt in, audio chunks out. */
@@ -83,17 +104,25 @@ export const streamGeneration = (
83
104
 
84
105
  /**
85
106
  * Bidirectional generation. Dual-arity: pipeable (data-last) and
86
- * direct (data-first). Requires `MusicInteractiveSession` in R —
107
+ * direct (data-first). Requires `MusicInteractiveSession` in R,
87
108
  * providers without bidirectional support are a type error at provide
88
109
  * time.
89
110
  *
111
+ * On the cross-provider surface, `input` is `Stream<unknown>` and
112
+ * output is `Stream<MusicStreamEvent>`. Use the provider-typed
113
+ * service (`LyriaRealtimeGenerator`) when you want to construct
114
+ * typed session messages.
115
+ *
90
116
  * @example
91
117
  * ```ts
92
- * const audio = Stream.fromIterable([
93
- * Music.promptsInput([{ text: "minimal techno", weight: 1.0 }]),
94
- * Music.configInput({ bpm: 124 }),
95
- * ]).pipe(
96
- * MusicGenerator.streamGenerationFrom({ model: "lyria-realtime-001", prompts: "" }),
118
+ * // Generic surface (opaque input):
119
+ * const audio = inputs.pipe(
120
+ * MusicGenerator.streamGenerationFrom({ model: "lyria-realtime-exp", prompt: "" }),
121
+ * )
122
+ *
123
+ * // Lyria-typed surface (full config knobs):
124
+ * const audio = inputs.pipe(
125
+ * LyriaRealtimeGenerator.streamGenerationFrom({ model: "lyria-realtime-exp", prompt: "" }),
97
126
  * )
98
127
  * ```
99
128
  */
@@ -102,17 +131,29 @@ export const streamGenerationFrom: {
102
131
  request: CommonStreamGenerateMusicRequest,
103
132
  ): <E, R>(
104
133
  input: Stream.Stream<MusicSessionInput, E, R>,
105
- ) => Stream.Stream<AudioChunk, AiError.AiError | E, R | MusicGenerator | MusicInteractiveSession>
134
+ ) => Stream.Stream<
135
+ MusicStreamEvent,
136
+ AiError.AiError | E,
137
+ R | MusicGenerator | MusicInteractiveSession
138
+ >
106
139
  <E, R>(
107
140
  input: Stream.Stream<MusicSessionInput, E, R>,
108
141
  request: CommonStreamGenerateMusicRequest,
109
- ): Stream.Stream<AudioChunk, AiError.AiError | E, R | MusicGenerator | MusicInteractiveSession>
142
+ ): Stream.Stream<
143
+ MusicStreamEvent,
144
+ AiError.AiError | E,
145
+ R | MusicGenerator | MusicInteractiveSession
146
+ >
110
147
  } = Function.dual(
111
148
  2,
112
149
  <E, R>(
113
150
  input: Stream.Stream<MusicSessionInput, E, R>,
114
151
  request: CommonStreamGenerateMusicRequest,
115
- ) =>
152
+ ): Stream.Stream<
153
+ MusicStreamEvent,
154
+ AiError.AiError | E,
155
+ R | MusicGenerator | MusicInteractiveSession
156
+ > =>
116
157
  Stream.unwrap(
117
158
  Effect.gen(function* () {
118
159
  const s = yield* MusicGenerator.asEffect()
@@ -0,0 +1,128 @@
1
+ import { Brand } from "effect"
2
+
3
+ /**
4
+ * A memory / data size, always represented internally as a number of
5
+ * bytes. Mirrors the `Duration` shape from `effect` — a branded value
6
+ * type with a wider `Input` union for ergonomic construction at call
7
+ * sites.
8
+ *
9
+ * `effect` itself has no byte-count primitive (only `Duration`), so we
10
+ * own this here rather than leak provider-specific shapes (Deno's
11
+ * template-literal `Memory`, Vercel's `mb`, Modal's `gpuMemory`, …)
12
+ * through the cross-provider `Sandbox` request types.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import * as Memory from "@effect-uai/core/Memory"
17
+ *
18
+ * Memory.gib(2) // 2 GiB → 2_147_483_648 bytes
19
+ * Memory.fromInputUnsafe("1.5 GiB") // human-string parsing
20
+ * Memory.fromInputUnsafe(1_280_000_000) // raw bytes
21
+ * Memory.toBytes(m) // unwrap
22
+ * ```
23
+ */
24
+
25
+ export type Memory = Brand.Branded<number, "Memory">
26
+
27
+ const make = Brand.nominal<Memory>()
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Input — what the public APIs accept.
31
+ //
32
+ // Supported string forms (case-insensitive on the unit, whitespace
33
+ // optional between number and unit):
34
+ //
35
+ // "<n>" bytes (no unit)
36
+ // "<n> B" bytes
37
+ // "<n> kB" 1_000 bytes (decimal)
38
+ // "<n> MB" 1_000_000 (decimal)
39
+ // "<n> GB" 1_000_000_000 (decimal)
40
+ // "<n> KiB" 1_024 (binary)
41
+ // "<n> MiB" 1_048_576 (binary)
42
+ // "<n> GiB" 1_073_741_824 (binary)
43
+ //
44
+ // Decimal vs binary follows the SI / IEC convention (`MB` = 10^6,
45
+ // `MiB` = 2^20). When in doubt, prefer the binary `*iB` units.
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export type Input = number | Memory | string
49
+
50
+ const UNIT_BYTES: Record<string, number> = {
51
+ b: 1,
52
+ kb: 1_000,
53
+ mb: 1_000_000,
54
+ gb: 1_000_000_000,
55
+ tb: 1_000_000_000_000,
56
+ kib: 1_024,
57
+ mib: 1_024 * 1_024,
58
+ gib: 1_024 * 1_024 * 1_024,
59
+ tib: 1_024 * 1_024 * 1_024 * 1_024,
60
+ }
61
+
62
+ const PARSE_RE = /^\s*(\d+(?:\.\d+)?)\s*([a-zA-Z]*)\s*$/
63
+
64
+ const parseString = (s: string): number => {
65
+ const m = PARSE_RE.exec(s)
66
+ if (m === null) {
67
+ throw new Error(`Memory: cannot parse "${s}" — expected forms like "1024", "10 MB", "1GiB"`)
68
+ }
69
+ const value = Number(m[1])
70
+ const unit = (m[2] ?? "").toLowerCase()
71
+ if (unit === "") return value
72
+ const factor = UNIT_BYTES[unit]
73
+ if (factor === undefined) {
74
+ throw new Error(
75
+ `Memory: unknown unit "${m[2]}" in "${s}" — supported: ${Object.keys(UNIT_BYTES).join(", ")}`,
76
+ )
77
+ }
78
+ return value * factor
79
+ }
80
+
81
+ /**
82
+ * Coerce a {@link Input} to a {@link Memory}. Throws on malformed
83
+ * strings or non-finite numbers. Mirrors `Duration.fromInputUnsafe`.
84
+ */
85
+ export const fromInputUnsafe = (input: Input): Memory => {
86
+ if (typeof input === "number") {
87
+ if (!Number.isFinite(input) || input < 0) {
88
+ throw new Error(`Memory: bytes must be a non-negative finite number, got ${input}`)
89
+ }
90
+ return make(Math.floor(input))
91
+ }
92
+ return make(Math.floor(parseString(input)))
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Unwrap
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /** Return the underlying byte count. */
100
+ export const toBytes = (m: Memory | Input): number =>
101
+ typeof m === "number" && (m as number) === Math.floor(m as number) && (m as number) >= 0
102
+ ? (m as number)
103
+ : (fromInputUnsafe(m) as unknown as number)
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Unit constructors — terse for use at call sites.
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /** N bytes. */
110
+ export const bytes = (n: number): Memory => make(Math.floor(n))
111
+
112
+ /** N KiB (binary kibibytes, `n * 1024`). */
113
+ export const kib = (n: number): Memory => make(Math.floor(n * 1_024))
114
+
115
+ /** N MiB (binary mebibytes, `n * 1024^2`). */
116
+ export const mib = (n: number): Memory => make(Math.floor(n * 1_024 * 1_024))
117
+
118
+ /** N GiB (binary gibibytes, `n * 1024^3`). */
119
+ export const gib = (n: number): Memory => make(Math.floor(n * 1_024 * 1_024 * 1_024))
120
+
121
+ /** N kB (decimal kilobytes, `n * 1000`). */
122
+ export const kb = (n: number): Memory => make(Math.floor(n * 1_000))
123
+
124
+ /** N MB (decimal megabytes, `n * 1_000_000`). */
125
+ export const mb = (n: number): Memory => make(Math.floor(n * 1_000_000))
126
+
127
+ /** N GB (decimal gigabytes, `n * 1_000_000_000`). */
128
+ export const gb = (n: number): Memory => make(Math.floor(n * 1_000_000_000))