@effect-uai/core 0.3.0 → 0.5.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 (169) hide show
  1. package/dist/{AiError-CBuPHVKA.d.mts → AiError-CAX_48RU.d.mts} +27 -5
  2. package/dist/{AiError-CBuPHVKA.d.mts.map → AiError-CAX_48RU.d.mts.map} +1 -1
  3. package/dist/Audio-BfCTGnH3.d.mts +61 -0
  4. package/dist/Audio-BfCTGnH3.d.mts.map +1 -0
  5. package/dist/{Image-BZmKfIdq.d.mts → Image-HNmMpMTh.d.mts} +1 -1
  6. package/dist/{Image-BZmKfIdq.d.mts.map → Image-HNmMpMTh.d.mts.map} +1 -1
  7. package/dist/{Items-CB8Bo3FI.d.mts → Items-DqbaJoz7.d.mts} +5 -5
  8. package/dist/{Items-CB8Bo3FI.d.mts.map → Items-DqbaJoz7.d.mts.map} +1 -1
  9. package/dist/{StructuredFormat-BWq5Hd1O.d.mts → StructuredFormat-BbN4dosH.d.mts} +11 -4
  10. package/dist/StructuredFormat-BbN4dosH.d.mts.map +1 -0
  11. package/dist/{Tool-DjVufH7i.d.mts → Tool-Y0__Py1H.d.mts} +20 -4
  12. package/dist/Tool-Y0__Py1H.d.mts.map +1 -0
  13. package/dist/Turn-ChbL2foc.d.mts +388 -0
  14. package/dist/Turn-ChbL2foc.d.mts.map +1 -0
  15. package/dist/domain/AiError.d.mts +2 -2
  16. package/dist/domain/AiError.mjs +19 -3
  17. package/dist/domain/AiError.mjs.map +1 -1
  18. package/dist/domain/Audio.d.mts +2 -0
  19. package/dist/domain/Audio.mjs +14 -0
  20. package/dist/domain/Audio.mjs.map +1 -0
  21. package/dist/domain/Image.d.mts +1 -1
  22. package/dist/domain/Items.d.mts +1 -1
  23. package/dist/domain/Items.mjs +1 -1
  24. package/dist/domain/Items.mjs.map +1 -1
  25. package/dist/domain/Music.d.mts +116 -0
  26. package/dist/domain/Music.d.mts.map +1 -0
  27. package/dist/domain/Music.mjs +29 -0
  28. package/dist/domain/Music.mjs.map +1 -0
  29. package/dist/domain/Transcript.d.mts +95 -0
  30. package/dist/domain/Transcript.d.mts.map +1 -0
  31. package/dist/domain/Transcript.mjs +22 -0
  32. package/dist/domain/Transcript.mjs.map +1 -0
  33. package/dist/domain/Turn.d.mts +2 -2
  34. package/dist/domain/Turn.mjs +22 -4
  35. package/dist/domain/Turn.mjs.map +1 -1
  36. package/dist/domain/Turn.test.d.mts +1 -0
  37. package/dist/domain/Turn.test.mjs +136 -0
  38. package/dist/domain/Turn.test.mjs.map +1 -0
  39. package/dist/embedding-model/Embedding.d.mts +15 -3
  40. package/dist/embedding-model/Embedding.d.mts.map +1 -1
  41. package/dist/embedding-model/Embedding.mjs.map +1 -1
  42. package/dist/embedding-model/EmbeddingModel.d.mts +33 -17
  43. package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -1
  44. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
  45. package/dist/embedding-model/EmbeddingModel.test.d.mts +1 -0
  46. package/dist/embedding-model/EmbeddingModel.test.mjs +59 -0
  47. package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -0
  48. package/dist/index.d.mts +13 -7
  49. package/dist/index.mjs +7 -1
  50. package/dist/language-model/LanguageModel.d.mts +30 -8
  51. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  52. package/dist/language-model/LanguageModel.mjs +33 -3
  53. package/dist/language-model/LanguageModel.mjs.map +1 -1
  54. package/dist/language-model/LanguageModel.test.d.mts +1 -0
  55. package/dist/language-model/LanguageModel.test.mjs +143 -0
  56. package/dist/language-model/LanguageModel.test.mjs.map +1 -0
  57. package/dist/loop/Loop.d.mts +94 -11
  58. package/dist/loop/Loop.d.mts.map +1 -1
  59. package/dist/loop/Loop.mjs +92 -26
  60. package/dist/loop/Loop.mjs.map +1 -1
  61. package/dist/loop/Loop.test.mjs +171 -3
  62. package/dist/loop/Loop.test.mjs.map +1 -1
  63. package/dist/music-generator/MusicGenerator.d.mts +77 -0
  64. package/dist/music-generator/MusicGenerator.d.mts.map +1 -0
  65. package/dist/music-generator/MusicGenerator.mjs +51 -0
  66. package/dist/music-generator/MusicGenerator.mjs.map +1 -0
  67. package/dist/music-generator/MusicGenerator.test.d.mts +1 -0
  68. package/dist/music-generator/MusicGenerator.test.mjs +154 -0
  69. package/dist/music-generator/MusicGenerator.test.mjs.map +1 -0
  70. package/dist/observability/Metrics.d.mts +1 -1
  71. package/dist/observability/Metrics.mjs +1 -1
  72. package/dist/observability/Metrics.mjs.map +1 -1
  73. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +96 -0
  74. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -0
  75. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +48 -0
  76. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -0
  77. package/dist/speech-synthesizer/SpeechSynthesizer.test.d.mts +1 -0
  78. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +112 -0
  79. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -0
  80. package/dist/streaming/JSONL.d.mts +10 -3
  81. package/dist/streaming/JSONL.d.mts.map +1 -1
  82. package/dist/streaming/JSONL.mjs +15 -9
  83. package/dist/streaming/JSONL.mjs.map +1 -1
  84. package/dist/structured-format/StructuredFormat.d.mts +2 -2
  85. package/dist/structured-format/StructuredFormat.mjs +9 -1
  86. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  87. package/dist/structured-format/StructuredFormat.test.d.mts +1 -0
  88. package/dist/structured-format/StructuredFormat.test.mjs +70 -0
  89. package/dist/structured-format/StructuredFormat.test.mjs.map +1 -0
  90. package/dist/testing/MockMusicGenerator.d.mts +39 -0
  91. package/dist/testing/MockMusicGenerator.d.mts.map +1 -0
  92. package/dist/testing/MockMusicGenerator.mjs +96 -0
  93. package/dist/testing/MockMusicGenerator.mjs.map +1 -0
  94. package/dist/testing/MockProvider.d.mts +23 -18
  95. package/dist/testing/MockProvider.d.mts.map +1 -1
  96. package/dist/testing/MockProvider.mjs +56 -72
  97. package/dist/testing/MockProvider.mjs.map +1 -1
  98. package/dist/testing/MockSpeechSynthesizer.d.mts +37 -0
  99. package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -0
  100. package/dist/testing/MockSpeechSynthesizer.mjs +95 -0
  101. package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -0
  102. package/dist/testing/MockTranscriber.d.mts +37 -0
  103. package/dist/testing/MockTranscriber.d.mts.map +1 -0
  104. package/dist/testing/MockTranscriber.mjs +77 -0
  105. package/dist/testing/MockTranscriber.mjs.map +1 -0
  106. package/dist/tool/HistoryCheck.d.mts +1 -1
  107. package/dist/tool/Outcome.d.mts +1 -1
  108. package/dist/tool/Resolvers.d.mts +65 -8
  109. package/dist/tool/Resolvers.d.mts.map +1 -1
  110. package/dist/tool/Resolvers.mjs +8 -12
  111. package/dist/tool/Resolvers.mjs.map +1 -1
  112. package/dist/tool/Resolvers.test.mjs +6 -5
  113. package/dist/tool/Resolvers.test.mjs.map +1 -1
  114. package/dist/tool/Tool.d.mts +2 -2
  115. package/dist/tool/Tool.mjs +18 -1
  116. package/dist/tool/Tool.mjs.map +1 -1
  117. package/dist/tool/Tool.test.d.mts +1 -0
  118. package/dist/tool/Tool.test.mjs +66 -0
  119. package/dist/tool/Tool.test.mjs.map +1 -0
  120. package/dist/tool/Toolkit.d.mts +4 -6
  121. package/dist/tool/Toolkit.d.mts.map +1 -1
  122. package/dist/tool/Toolkit.mjs +14 -43
  123. package/dist/tool/Toolkit.mjs.map +1 -1
  124. package/dist/transcriber/Transcriber.d.mts +101 -0
  125. package/dist/transcriber/Transcriber.d.mts.map +1 -0
  126. package/dist/transcriber/Transcriber.mjs +49 -0
  127. package/dist/transcriber/Transcriber.mjs.map +1 -0
  128. package/dist/transcriber/Transcriber.test.d.mts +1 -0
  129. package/dist/transcriber/Transcriber.test.mjs +130 -0
  130. package/dist/transcriber/Transcriber.test.mjs.map +1 -0
  131. package/package.json +37 -1
  132. package/src/domain/AiError.ts +22 -1
  133. package/src/domain/Audio.ts +88 -0
  134. package/src/domain/Items.ts +1 -1
  135. package/src/domain/Music.ts +121 -0
  136. package/src/domain/Transcript.ts +83 -0
  137. package/src/domain/Turn.test.ts +141 -0
  138. package/src/domain/Turn.ts +50 -43
  139. package/src/embedding-model/Embedding.ts +23 -0
  140. package/src/embedding-model/EmbeddingModel.test.ts +92 -0
  141. package/src/embedding-model/EmbeddingModel.ts +30 -20
  142. package/src/index.ts +6 -0
  143. package/src/language-model/LanguageModel.test.ts +170 -0
  144. package/src/language-model/LanguageModel.ts +64 -1
  145. package/src/loop/Loop.test.ts +256 -3
  146. package/src/loop/Loop.ts +225 -49
  147. package/src/music-generator/MusicGenerator.test.ts +170 -0
  148. package/src/music-generator/MusicGenerator.ts +123 -0
  149. package/src/observability/Metrics.ts +1 -1
  150. package/src/speech-synthesizer/SpeechSynthesizer.test.ts +141 -0
  151. package/src/speech-synthesizer/SpeechSynthesizer.ts +131 -0
  152. package/src/streaming/JSONL.ts +16 -13
  153. package/src/structured-format/StructuredFormat.test.ts +105 -0
  154. package/src/structured-format/StructuredFormat.ts +14 -1
  155. package/src/testing/MockMusicGenerator.ts +168 -0
  156. package/src/testing/MockProvider.ts +126 -105
  157. package/src/testing/MockSpeechSynthesizer.ts +163 -0
  158. package/src/testing/MockTranscriber.ts +137 -0
  159. package/src/tool/Resolvers.test.ts +8 -5
  160. package/src/tool/Resolvers.ts +17 -19
  161. package/src/tool/Tool.test.ts +105 -0
  162. package/src/tool/Tool.ts +20 -0
  163. package/src/tool/Toolkit.ts +49 -50
  164. package/src/transcriber/Transcriber.test.ts +125 -0
  165. package/src/transcriber/Transcriber.ts +127 -0
  166. package/dist/StructuredFormat-BWq5Hd1O.d.mts.map +0 -1
  167. package/dist/Tool-DjVufH7i.d.mts.map +0 -1
  168. package/dist/Turn-OPaILVIB.d.mts +0 -194
  169. package/dist/Turn-OPaILVIB.d.mts.map +0 -1
@@ -1,2 +1,2 @@
1
- import { c as decodeJsonLines, i as StructuredFormat, l as fromEffectSchema, n as JsonParseError, o as StructuredSchema, r as StructuredDecodeError, s as decode, t as DecodeIssue, u as parseJson } from "../StructuredFormat-BWq5Hd1O.mjs";
2
- export { DecodeIssue, JsonParseError, StructuredDecodeError, StructuredFormat, StructuredSchema, decode, decodeJsonLines, fromEffectSchema, parseJson };
1
+ import { c as decodeJsonLines, d as parseJson, i as StructuredFormat, l as decodeJsonLinesRecoverable, n as JsonParseError, o as StructuredSchema, r as StructuredDecodeError, s as decode, t as DecodeIssue, u as fromEffectSchema } from "../StructuredFormat-BbN4dosH.mjs";
2
+ export { DecodeIssue, JsonParseError, StructuredDecodeError, StructuredFormat, StructuredSchema, decode, decodeJsonLines, decodeJsonLinesRecoverable, fromEffectSchema, parseJson };
@@ -6,6 +6,7 @@ var StructuredFormat_exports = /* @__PURE__ */ __exportAll({
6
6
  StructuredDecodeError: () => StructuredDecodeError,
7
7
  decode: () => decode,
8
8
  decodeJsonLines: () => decodeJsonLines,
9
+ decodeJsonLinesRecoverable: () => decodeJsonLinesRecoverable,
9
10
  fromEffectSchema: () => fromEffectSchema,
10
11
  parseJson: () => parseJson
11
12
  });
@@ -62,7 +63,14 @@ const parseJson = (format) => (raw) => pipe(Effect.try({
62
63
  * Failures surface in the stream's failure channel, distinguished by tag.
63
64
  */
64
65
  const decodeJsonLines = (format) => (self) => self.pipe(Stream.mapEffect(parseJson(format)));
66
+ /**
67
+ * Like {@link decodeJsonLines}, but each line yields a `Result` instead of
68
+ * failing the stream. Use when one bad line shouldn't abort the rest —
69
+ * log-and-continue, or partial-recovery with a corrective re-prompt.
70
+ * Upstream errors (the input stream's own `E`) still propagate normally.
71
+ */
72
+ const decodeJsonLinesRecoverable = (format) => (self) => self.pipe(Stream.mapEffect((line) => Effect.result(parseJson(format)(line))));
65
73
  //#endregion
66
- export { JsonParseError, StructuredDecodeError, decode, decodeJsonLines, fromEffectSchema, parseJson, StructuredFormat_exports as t };
74
+ export { JsonParseError, StructuredDecodeError, decode, decodeJsonLines, decodeJsonLinesRecoverable, fromEffectSchema, parseJson, StructuredFormat_exports as t };
67
75
 
68
76
  //# sourceMappingURL=StructuredFormat.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"StructuredFormat.mjs","names":[],"sources":["../../src/structured-format/StructuredFormat.ts"],"sourcesContent":["import type { StandardJSONSchemaV1, StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { Data, Effect, Match, Schema, Stream, pipe } from \"effect\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Cross-validator schema constraint for structured outputs. Any schema\n * implementing both Standard Schema (runtime validation) and Standard\n * JSON Schema (wire encoding) works directly: Zod 4+, Valibot, ArkType,\n * and Effect Schema after `fromEffectSchema`.\n */\nexport type StructuredSchema<Output = unknown> = StandardSchemaV1<unknown, Output> &\n StandardJSONSchemaV1<unknown, Output>\n\n/**\n * A schema-bound output the user wants the model to produce. Pairs the\n * cross-validator schema with metadata providers need (name, description,\n * strict-mode flag).\n */\nexport type StructuredFormat<A> = {\n readonly name: string\n readonly description?: string\n readonly schema: StructuredSchema<A>\n /**\n * Provider strict-mode flag. OpenAI, Anthropic, and Mistral honour it\n * (constrained decoding); other providers ignore.\n */\n readonly strict?: boolean\n}\n\n/** A single path-scoped validation problem. Library-agnostic shape. */\nexport type DecodeIssue = {\n readonly path: ReadonlyArray<string | number>\n readonly message: string\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/**\n * Schema validation failed. `raw` is the original text (or stringified\n * value) that failed; `issues` is a flat list of per-field problems.\n */\nexport class StructuredDecodeError extends Data.TaggedError(\"StructuredDecodeError\")<{\n readonly raw: string\n readonly issues: ReadonlyArray<DecodeIssue>\n}> {}\n\n/**\n * `JSON.parse` threw on a string that was supposed to be JSON. Distinct\n * from `StructuredDecodeError`: the bytes weren't even JSON.\n */\nexport class JsonParseError extends Data.TaggedError(\"StructuredJsonParseError\")<{\n readonly raw: string\n readonly cause: unknown\n}> {}\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap an Effect `Schema` as a `StructuredFormat`. Effect Schema doesn't\n * natively implement Standard Schema; this helper installs the\n * `~standard` and JSON Schema interfaces.\n */\nexport const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(\n schema: S,\n options?: {\n readonly name?: string\n readonly description?: string\n readonly strict?: boolean\n },\n): StructuredFormat<S[\"Type\"]> => ({\n name: options?.name ?? \"output\",\n schema: Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema)),\n ...(options?.description !== undefined && {\n description: options.description,\n }),\n ...(options?.strict !== undefined && { strict: options.strict }),\n})\n\n// ---------------------------------------------------------------------------\n// Standard Schema → DecodeIssue\n// ---------------------------------------------------------------------------\n\nconst propertyKeyToScalar = Match.type<PropertyKey>().pipe(\n Match.when(Match.string, (s) => s),\n Match.when(Match.number, (n) => n),\n Match.when(Match.symbol, (s) => s.toString()),\n Match.exhaustive,\n)\n\nconst segmentToKey = Match.type<PropertyKey | StandardSchemaV1.PathSegment>().pipe(\n Match.when(Match.string, (s) => s),\n Match.when(Match.number, (n) => n),\n Match.when(Match.symbol, (s) => s.toString()),\n Match.orElse((segment) => propertyKeyToScalar(segment.key)),\n)\n\nconst issueToDecode = (issue: StandardSchemaV1.Issue): DecodeIssue => ({\n path: (issue.path ?? []).map(segmentToKey),\n message: issue.message,\n})\n\n// ---------------------------------------------------------------------------\n// Decoding\n// ---------------------------------------------------------------------------\n\n/**\n * Validate an `unknown` against the format's schema. Returns the typed\n * value or a `StructuredDecodeError`. Standard Schema's `validate` may\n * be async; this function handles both sync and async results.\n */\nexport const decode =\n <A>(format: StructuredFormat<A>) =>\n (raw: unknown): Effect.Effect<A, StructuredDecodeError> =>\n pipe(\n Effect.promise(async () => format.schema[\"~standard\"].validate(raw)),\n Effect.flatMap((result) =>\n result.issues === undefined\n ? Effect.succeed(result.value)\n : Effect.fail(\n new StructuredDecodeError({\n raw: typeof raw === \"string\" ? raw : JSON.stringify(raw),\n issues: result.issues.map(issueToDecode),\n }),\n ),\n ),\n )\n\n/**\n * Parse a JSON string then validate against the format's schema. Two\n * failure modes: `JsonParseError` (bytes weren't JSON) and\n * `StructuredDecodeError` (JSON didn't match the schema).\n */\nexport const parseJson =\n <A>(format: StructuredFormat<A>) =>\n (raw: string): Effect.Effect<A, JsonParseError | StructuredDecodeError> =>\n pipe(\n Effect.try({\n try: () => JSON.parse(raw),\n catch: (cause) => new JsonParseError({ raw, cause }),\n }),\n Effect.flatMap(decode(format)),\n )\n\n/**\n * Stream operator: each input string is JSON-parsed and validated.\n * Failures surface in the stream's failure channel, distinguished by tag.\n */\nexport const decodeJsonLines =\n <A>(format: StructuredFormat<A>) =>\n <E, R>(\n self: Stream.Stream<string, E, R>,\n ): Stream.Stream<A, E | JsonParseError | StructuredDecodeError, R> =>\n self.pipe(Stream.mapEffect(parseJson(format)))\n"],"mappings":";;;;;;;;;;;;;;;AA8CA,IAAa,wBAAb,cAA2C,KAAK,YAAY,wBAAwB,CAGjF;;;;;AAMH,IAAa,iBAAb,cAAoC,KAAK,YAAY,2BAA2B,CAG7E;;;;;;AAWH,MAAa,oBACX,QACA,aAKiC;CACjC,MAAM,SAAS,QAAQ;CACvB,QAAQ,OAAO,uBAAuB,OAAO,mBAAmB,OAAO,CAAC;CACxE,GAAI,SAAS,gBAAgB,KAAA,KAAa,EACxC,aAAa,QAAQ,aACtB;CACD,GAAI,SAAS,WAAW,KAAA,KAAa,EAAE,QAAQ,QAAQ,QAAQ;CAChE;AAMD,MAAM,sBAAsB,MAAM,MAAmB,CAAC,KACpD,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,UAAU,CAAC,EAC7C,MAAM,WACP;AAED,MAAM,eAAe,MAAM,MAAkD,CAAC,KAC5E,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,UAAU,CAAC,EAC7C,MAAM,QAAQ,YAAY,oBAAoB,QAAQ,IAAI,CAAC,CAC5D;AAED,MAAM,iBAAiB,WAAgD;CACrE,OAAO,MAAM,QAAQ,EAAE,EAAE,IAAI,aAAa;CAC1C,SAAS,MAAM;CAChB;;;;;;AAWD,MAAa,UACP,YACH,QACC,KACE,OAAO,QAAQ,YAAY,OAAO,OAAO,aAAa,SAAS,IAAI,CAAC,EACpE,OAAO,SAAS,WACd,OAAO,WAAW,KAAA,IACd,OAAO,QAAQ,OAAO,MAAM,GAC5B,OAAO,KACL,IAAI,sBAAsB;CACxB,KAAK,OAAO,QAAQ,WAAW,MAAM,KAAK,UAAU,IAAI;CACxD,QAAQ,OAAO,OAAO,IAAI,cAAc;CACzC,CAAC,CACH,CACN,CACF;;;;;;AAOL,MAAa,aACP,YACH,QACC,KACE,OAAO,IAAI;CACT,WAAW,KAAK,MAAM,IAAI;CAC1B,QAAQ,UAAU,IAAI,eAAe;EAAE;EAAK;EAAO,CAAC;CACrD,CAAC,EACF,OAAO,QAAQ,OAAO,OAAO,CAAC,CAC/B;;;;;AAML,MAAa,mBACP,YAEF,SAEA,KAAK,KAAK,OAAO,UAAU,UAAU,OAAO,CAAC,CAAC"}
1
+ {"version":3,"file":"StructuredFormat.mjs","names":[],"sources":["../../src/structured-format/StructuredFormat.ts"],"sourcesContent":["import type { StandardJSONSchemaV1, StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { Data, Effect, Match, Result, Schema, Stream, pipe } from \"effect\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Cross-validator schema constraint for structured outputs. Any schema\n * implementing both Standard Schema (runtime validation) and Standard\n * JSON Schema (wire encoding) works directly: Zod 4+, Valibot, ArkType,\n * and Effect Schema after `fromEffectSchema`.\n */\nexport type StructuredSchema<Output = unknown> = StandardSchemaV1<unknown, Output> &\n StandardJSONSchemaV1<unknown, Output>\n\n/**\n * A schema-bound output the user wants the model to produce. Pairs the\n * cross-validator schema with metadata providers need (name, description,\n * strict-mode flag).\n */\nexport type StructuredFormat<A> = {\n readonly name: string\n readonly description?: string\n readonly schema: StructuredSchema<A>\n /**\n * Provider strict-mode flag. OpenAI, Anthropic, and Mistral honour it\n * (constrained decoding); other providers ignore.\n */\n readonly strict?: boolean\n}\n\n/** A single path-scoped validation problem. Library-agnostic shape. */\nexport type DecodeIssue = {\n readonly path: ReadonlyArray<string | number>\n readonly message: string\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/**\n * Schema validation failed. `raw` is the original text (or stringified\n * value) that failed; `issues` is a flat list of per-field problems.\n */\nexport class StructuredDecodeError extends Data.TaggedError(\"StructuredDecodeError\")<{\n readonly raw: string\n readonly issues: ReadonlyArray<DecodeIssue>\n}> {}\n\n/**\n * `JSON.parse` threw on a string that was supposed to be JSON. Distinct\n * from `StructuredDecodeError`: the bytes weren't even JSON.\n */\nexport class JsonParseError extends Data.TaggedError(\"StructuredJsonParseError\")<{\n readonly raw: string\n readonly cause: unknown\n}> {}\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap an Effect `Schema` as a `StructuredFormat`. Effect Schema doesn't\n * natively implement Standard Schema; this helper installs the\n * `~standard` and JSON Schema interfaces.\n */\nexport const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(\n schema: S,\n options?: {\n readonly name?: string\n readonly description?: string\n readonly strict?: boolean\n },\n): StructuredFormat<S[\"Type\"]> => ({\n name: options?.name ?? \"output\",\n schema: Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema)),\n ...(options?.description !== undefined && {\n description: options.description,\n }),\n ...(options?.strict !== undefined && { strict: options.strict }),\n})\n\n// ---------------------------------------------------------------------------\n// Standard Schema → DecodeIssue\n// ---------------------------------------------------------------------------\n\nconst propertyKeyToScalar = Match.type<PropertyKey>().pipe(\n Match.when(Match.string, (s) => s),\n Match.when(Match.number, (n) => n),\n Match.when(Match.symbol, (s) => s.toString()),\n Match.exhaustive,\n)\n\nconst segmentToKey = Match.type<PropertyKey | StandardSchemaV1.PathSegment>().pipe(\n Match.when(Match.string, (s) => s),\n Match.when(Match.number, (n) => n),\n Match.when(Match.symbol, (s) => s.toString()),\n Match.orElse((segment) => propertyKeyToScalar(segment.key)),\n)\n\nconst issueToDecode = (issue: StandardSchemaV1.Issue): DecodeIssue => ({\n path: (issue.path ?? []).map(segmentToKey),\n message: issue.message,\n})\n\n// ---------------------------------------------------------------------------\n// Decoding\n// ---------------------------------------------------------------------------\n\n/**\n * Validate an `unknown` against the format's schema. Returns the typed\n * value or a `StructuredDecodeError`. Standard Schema's `validate` may\n * be async; this function handles both sync and async results.\n */\nexport const decode =\n <A>(format: StructuredFormat<A>) =>\n (raw: unknown): Effect.Effect<A, StructuredDecodeError> =>\n pipe(\n Effect.promise(async () => format.schema[\"~standard\"].validate(raw)),\n Effect.flatMap((result) =>\n result.issues === undefined\n ? Effect.succeed(result.value)\n : Effect.fail(\n new StructuredDecodeError({\n raw: typeof raw === \"string\" ? raw : JSON.stringify(raw),\n issues: result.issues.map(issueToDecode),\n }),\n ),\n ),\n )\n\n/**\n * Parse a JSON string then validate against the format's schema. Two\n * failure modes: `JsonParseError` (bytes weren't JSON) and\n * `StructuredDecodeError` (JSON didn't match the schema).\n */\nexport const parseJson =\n <A>(format: StructuredFormat<A>) =>\n (raw: string): Effect.Effect<A, JsonParseError | StructuredDecodeError> =>\n pipe(\n Effect.try({\n try: () => JSON.parse(raw),\n catch: (cause) => new JsonParseError({ raw, cause }),\n }),\n Effect.flatMap(decode(format)),\n )\n\n/**\n * Stream operator: each input string is JSON-parsed and validated.\n * Failures surface in the stream's failure channel, distinguished by tag.\n */\nexport const decodeJsonLines =\n <A>(format: StructuredFormat<A>) =>\n <E, R>(\n self: Stream.Stream<string, E, R>,\n ): Stream.Stream<A, E | JsonParseError | StructuredDecodeError, R> =>\n self.pipe(Stream.mapEffect(parseJson(format)))\n\n/**\n * Like {@link decodeJsonLines}, but each line yields a `Result` instead of\n * failing the stream. Use when one bad line shouldn't abort the rest —\n * log-and-continue, or partial-recovery with a corrective re-prompt.\n * Upstream errors (the input stream's own `E`) still propagate normally.\n */\nexport const decodeJsonLinesRecoverable =\n <A>(format: StructuredFormat<A>) =>\n <E, R>(\n self: Stream.Stream<string, E, R>,\n ): Stream.Stream<Result.Result<A, JsonParseError | StructuredDecodeError>, E, R> =>\n self.pipe(Stream.mapEffect((line) => Effect.result(parseJson(format)(line))))\n"],"mappings":";;;;;;;;;;;;;;;;AA8CA,IAAa,wBAAb,cAA2C,KAAK,YAAY,wBAAwB,CAGjF;;;;;AAMH,IAAa,iBAAb,cAAoC,KAAK,YAAY,2BAA2B,CAG7E;;;;;;AAWH,MAAa,oBACX,QACA,aAKiC;CACjC,MAAM,SAAS,QAAQ;CACvB,QAAQ,OAAO,uBAAuB,OAAO,mBAAmB,OAAO,CAAC;CACxE,GAAI,SAAS,gBAAgB,KAAA,KAAa,EACxC,aAAa,QAAQ,aACtB;CACD,GAAI,SAAS,WAAW,KAAA,KAAa,EAAE,QAAQ,QAAQ,QAAQ;CAChE;AAMD,MAAM,sBAAsB,MAAM,MAAmB,CAAC,KACpD,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,UAAU,CAAC,EAC7C,MAAM,WACP;AAED,MAAM,eAAe,MAAM,MAAkD,CAAC,KAC5E,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,UAAU,CAAC,EAC7C,MAAM,QAAQ,YAAY,oBAAoB,QAAQ,IAAI,CAAC,CAC5D;AAED,MAAM,iBAAiB,WAAgD;CACrE,OAAO,MAAM,QAAQ,EAAE,EAAE,IAAI,aAAa;CAC1C,SAAS,MAAM;CAChB;;;;;;AAWD,MAAa,UACP,YACH,QACC,KACE,OAAO,QAAQ,YAAY,OAAO,OAAO,aAAa,SAAS,IAAI,CAAC,EACpE,OAAO,SAAS,WACd,OAAO,WAAW,KAAA,IACd,OAAO,QAAQ,OAAO,MAAM,GAC5B,OAAO,KACL,IAAI,sBAAsB;CACxB,KAAK,OAAO,QAAQ,WAAW,MAAM,KAAK,UAAU,IAAI;CACxD,QAAQ,OAAO,OAAO,IAAI,cAAc;CACzC,CAAC,CACH,CACN,CACF;;;;;;AAOL,MAAa,aACP,YACH,QACC,KACE,OAAO,IAAI;CACT,WAAW,KAAK,MAAM,IAAI;CAC1B,QAAQ,UAAU,IAAI,eAAe;EAAE;EAAK;EAAO,CAAC;CACrD,CAAC,EACF,OAAO,QAAQ,OAAO,OAAO,CAAC,CAC/B;;;;;AAML,MAAa,mBACP,YAEF,SAEA,KAAK,KAAK,OAAO,UAAU,UAAU,OAAO,CAAC,CAAC;;;;;;;AAQlD,MAAa,8BACP,YAEF,SAEA,KAAK,KAAK,OAAO,WAAW,SAAS,OAAO,OAAO,UAAU,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,70 @@
1
+ import { JsonParseError, StructuredDecodeError, decodeJsonLines, decodeJsonLinesRecoverable, fromEffectSchema } from "./StructuredFormat.mjs";
2
+ import { i as it, n as globalExpect, r as describe } from "../dist-DV5ISja1.mjs";
3
+ import { Effect, Exit, Filter, Result, Schema, Stream } from "effect";
4
+ //#region src/structured-format/StructuredFormat.test.ts
5
+ const itemFormat = fromEffectSchema(Schema.Struct({
6
+ id: Schema.Number,
7
+ name: Schema.String
8
+ }), { name: "Item" });
9
+ const linesOf = (...xs) => Stream.fromIterable(xs);
10
+ const collect = (s) => Effect.runPromise(Stream.runCollect(s).pipe(Effect.map((c) => Array.from(c))));
11
+ describe("decodeJsonLinesRecoverable", () => {
12
+ it("yields a Success for each well-formed line", async () => {
13
+ const out = await collect(linesOf("{\"id\":1,\"name\":\"a\"}", "{\"id\":2,\"name\":\"b\"}").pipe(decodeJsonLinesRecoverable(itemFormat)));
14
+ globalExpect(out).toHaveLength(2);
15
+ globalExpect(Result.isSuccess(out[0])).toBe(true);
16
+ globalExpect(Result.isSuccess(out[1])).toBe(true);
17
+ if (Result.isSuccess(out[0]) && Result.isSuccess(out[1])) {
18
+ globalExpect(out[0].success).toEqual({
19
+ id: 1,
20
+ name: "a"
21
+ });
22
+ globalExpect(out[1].success).toEqual({
23
+ id: 2,
24
+ name: "b"
25
+ });
26
+ }
27
+ });
28
+ it("yields a Failure for a malformed JSON line WITHOUT aborting the stream", async () => {
29
+ const out = await collect(linesOf("{\"id\":1,\"name\":\"a\"}", "not json at all", "{\"id\":3,\"name\":\"c\"}").pipe(decodeJsonLinesRecoverable(itemFormat)));
30
+ globalExpect(out).toHaveLength(3);
31
+ globalExpect(Result.isSuccess(out[0])).toBe(true);
32
+ globalExpect(Result.isFailure(out[1])).toBe(true);
33
+ globalExpect(Result.isSuccess(out[2])).toBe(true);
34
+ if (Result.isFailure(out[1])) globalExpect(out[1].failure).toBeInstanceOf(JsonParseError);
35
+ });
36
+ it("yields a Failure for a schema-invalid line without aborting", async () => {
37
+ const out = await collect(linesOf("{\"id\":1,\"name\":\"a\"}", "{\"id\":\"not-a-number\",\"name\":\"b\"}", "{\"id\":3,\"name\":\"c\"}").pipe(decodeJsonLinesRecoverable(itemFormat)));
38
+ globalExpect(out).toHaveLength(3);
39
+ globalExpect(Result.isSuccess(out[0])).toBe(true);
40
+ globalExpect(Result.isFailure(out[1])).toBe(true);
41
+ globalExpect(Result.isSuccess(out[2])).toBe(true);
42
+ if (Result.isFailure(out[1])) globalExpect(out[1].failure).toBeInstanceOf(StructuredDecodeError);
43
+ });
44
+ it("propagates upstream errors normally (only DECODE failures are lifted into Result)", async () => {
45
+ const boom = /* @__PURE__ */ new Error("upstream broke");
46
+ const stream = Stream.concat(linesOf("{\"id\":1,\"name\":\"a\"}"), Stream.fail(boom)).pipe(decodeJsonLinesRecoverable(itemFormat));
47
+ const exit = await Effect.runPromise(Effect.exit(Stream.runCollect(stream)));
48
+ globalExpect(Exit.isFailure(exit)).toBe(true);
49
+ });
50
+ it("composes with filter-success / log-and-continue", async () => {
51
+ globalExpect(await collect(linesOf("{\"id\":1,\"name\":\"a\"}", "garbage", "{\"id\":2,\"name\":\"b\"}").pipe(decodeJsonLinesRecoverable(itemFormat), Stream.filterMap(Filter.fromPredicateOption(Result.getSuccess))))).toEqual([{
52
+ id: 1,
53
+ name: "a"
54
+ }, {
55
+ id: 2,
56
+ name: "b"
57
+ }]);
58
+ });
59
+ });
60
+ describe("decodeJsonLines (fail-fast, sanity)", () => {
61
+ it("aborts the stream on the first bad line", async () => {
62
+ const stream = linesOf("{\"id\":1,\"name\":\"a\"}", "garbage", "{\"id\":3,\"name\":\"c\"}").pipe(decodeJsonLines(itemFormat));
63
+ const exit = await Effect.runPromise(Effect.exit(Stream.runCollect(stream)));
64
+ globalExpect(Exit.isFailure(exit)).toBe(true);
65
+ });
66
+ });
67
+ //#endregion
68
+ export {};
69
+
70
+ //# sourceMappingURL=StructuredFormat.test.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StructuredFormat.test.mjs","names":[],"sources":["../../src/structured-format/StructuredFormat.test.ts"],"sourcesContent":["import { Effect, Exit, Filter, Result, Schema, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport {\n decodeJsonLines,\n decodeJsonLinesRecoverable,\n fromEffectSchema,\n JsonParseError,\n StructuredDecodeError,\n} from \"./StructuredFormat.js\"\n\nconst Item = Schema.Struct({ id: Schema.Number, name: Schema.String })\ntype Item = typeof Item.Type\nconst itemFormat = fromEffectSchema(Item, { name: \"Item\" })\n\nconst linesOf = (...xs: ReadonlyArray<string>): Stream.Stream<string> => Stream.fromIterable(xs)\n\nconst collect = <A, E>(s: Stream.Stream<A, E>) =>\n Effect.runPromise(Stream.runCollect(s).pipe(Effect.map((c) => Array.from(c))))\n\ndescribe(\"decodeJsonLinesRecoverable\", () => {\n it(\"yields a Success for each well-formed line\", async () => {\n const out = await collect(\n linesOf('{\"id\":1,\"name\":\"a\"}', '{\"id\":2,\"name\":\"b\"}').pipe(\n decodeJsonLinesRecoverable(itemFormat),\n ),\n )\n\n expect(out).toHaveLength(2)\n expect(Result.isSuccess(out[0]!)).toBe(true)\n expect(Result.isSuccess(out[1]!)).toBe(true)\n if (Result.isSuccess(out[0]!) && Result.isSuccess(out[1]!)) {\n expect(out[0]!.success).toEqual<Item>({ id: 1, name: \"a\" })\n expect(out[1]!.success).toEqual<Item>({ id: 2, name: \"b\" })\n }\n })\n\n it(\"yields a Failure for a malformed JSON line WITHOUT aborting the stream\", async () => {\n const out = await collect(\n linesOf('{\"id\":1,\"name\":\"a\"}', \"not json at all\", '{\"id\":3,\"name\":\"c\"}').pipe(\n decodeJsonLinesRecoverable(itemFormat),\n ),\n )\n\n expect(out).toHaveLength(3)\n expect(Result.isSuccess(out[0]!)).toBe(true)\n expect(Result.isFailure(out[1]!)).toBe(true)\n expect(Result.isSuccess(out[2]!)).toBe(true)\n if (Result.isFailure(out[1]!)) {\n expect(out[1]!.failure).toBeInstanceOf(JsonParseError)\n }\n })\n\n it(\"yields a Failure for a schema-invalid line without aborting\", async () => {\n const out = await collect(\n linesOf(\n '{\"id\":1,\"name\":\"a\"}',\n '{\"id\":\"not-a-number\",\"name\":\"b\"}', // schema fail\n '{\"id\":3,\"name\":\"c\"}',\n ).pipe(decodeJsonLinesRecoverable(itemFormat)),\n )\n\n expect(out).toHaveLength(3)\n expect(Result.isSuccess(out[0]!)).toBe(true)\n expect(Result.isFailure(out[1]!)).toBe(true)\n expect(Result.isSuccess(out[2]!)).toBe(true)\n if (Result.isFailure(out[1]!)) {\n expect(out[1]!.failure).toBeInstanceOf(StructuredDecodeError)\n }\n })\n\n it(\"propagates upstream errors normally (only DECODE failures are lifted into Result)\", async () => {\n const boom = new Error(\"upstream broke\")\n const stream = Stream.concat(linesOf('{\"id\":1,\"name\":\"a\"}'), Stream.fail(boom)).pipe(\n decodeJsonLinesRecoverable(itemFormat),\n )\n\n const exit = await Effect.runPromise(Effect.exit(Stream.runCollect(stream)))\n expect(Exit.isFailure(exit)).toBe(true)\n })\n\n it(\"composes with filter-success / log-and-continue\", async () => {\n const out = await collect(\n linesOf('{\"id\":1,\"name\":\"a\"}', \"garbage\", '{\"id\":2,\"name\":\"b\"}').pipe(\n decodeJsonLinesRecoverable(itemFormat),\n Stream.filterMap(Filter.fromPredicateOption(Result.getSuccess)),\n ),\n )\n\n expect(out).toEqual<Array<Item>>([\n { id: 1, name: \"a\" },\n { id: 2, name: \"b\" },\n ])\n })\n})\n\ndescribe(\"decodeJsonLines (fail-fast, sanity)\", () => {\n it(\"aborts the stream on the first bad line\", async () => {\n const stream = linesOf('{\"id\":1,\"name\":\"a\"}', \"garbage\", '{\"id\":3,\"name\":\"c\"}').pipe(\n decodeJsonLines(itemFormat),\n )\n\n const exit = await Effect.runPromise(Effect.exit(Stream.runCollect(stream)))\n expect(Exit.isFailure(exit)).toBe(true)\n })\n})\n"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import { n as AudioChunk } from "../Audio-BfCTGnH3.mjs";
2
+ import { CommonGenerateMusicRequest, CommonStreamGenerateMusicRequest, MusicResult } from "../domain/Music.mjs";
3
+ import { MusicGenerator, MusicInteractiveSession } from "../music-generator/MusicGenerator.mjs";
4
+ import { Effect, Layer } from "effect";
5
+
6
+ //#region src/testing/MockMusicGenerator.d.ts
7
+ type MockMusicGeneratorRecorder = {
8
+ readonly generateCalls: ReadonlyArray<CommonGenerateMusicRequest>;
9
+ readonly streamGenerationCalls: ReadonlyArray<CommonStreamGenerateMusicRequest>;
10
+ readonly streamGenerationFromCalls: ReadonlyArray<CommonStreamGenerateMusicRequest>;
11
+ };
12
+ type MockMusicGeneratorScript = {
13
+ /** One result per `generate` call, consumed in order. */readonly results?: ReadonlyArray<MusicResult>; /** One chunk-list per `streamGeneration` call, consumed in order. */
14
+ readonly streamGenerationChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>; /** One chunk-list per `streamGenerationFrom` call, consumed in order. */
15
+ readonly streamGenerationFromChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>;
16
+ };
17
+ /**
18
+ * Layer providing the `MusicGenerator` service AND the
19
+ * `MusicInteractiveSession` capability marker. Use for the common case
20
+ * where code under test exercises `streamGenerationFrom`.
21
+ */
22
+ declare const layer: (script: MockMusicGeneratorScript) => {
23
+ readonly layer: Layer.Layer<MusicGenerator | MusicInteractiveSession>;
24
+ readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>;
25
+ };
26
+ /**
27
+ * Variant that omits the `MusicInteractiveSession` marker — simulates a
28
+ * provider without bidirectional support (Lyria 3 sync, ElevenLabs,
29
+ * Mureka, MiniMax, Stable Audio, Suno). Calls to
30
+ * `streamGenerationFrom` in code under test should be a compile-time
31
+ * error against this Layer alone.
32
+ */
33
+ declare const layerWithoutInteractive: (script: MockMusicGeneratorScript) => {
34
+ readonly layer: Layer.Layer<MusicGenerator>;
35
+ readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>;
36
+ };
37
+ //#endregion
38
+ export { MockMusicGeneratorRecorder, MockMusicGeneratorScript, layer, layerWithoutInteractive };
39
+ //# sourceMappingURL=MockMusicGenerator.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MockMusicGenerator.d.mts","names":[],"sources":["../../src/testing/MockMusicGenerator.ts"],"mappings":";;;;;;KAeY,0BAAA;EAAA,SACD,aAAA,EAAe,aAAA,CAAc,0BAAA;EAAA,SAC7B,qBAAA,EAAuB,aAAA,CAAc,gCAAA;EAAA,SACrC,yBAAA,EAA2B,aAAA,CAAc,gCAAA;AAAA;AAAA,KAGxC,wBAAA;EALc,kEAOf,OAAA,GAAU,aAAA,CAAc,WAAA,GAND;EAAA,SAQvB,sBAAA,GAAyB,aAAA,CAAc,aAAA,CAAc,UAAA,IAP1B;EAAA,SAS3B,0BAAA,GAA6B,aAAA,CAAc,aAAA,CAAc,UAAA;AAAA;;;;;;cA8EvD,KAAA,GACX,MAAA,EAAQ,wBAAA;EAAA,SAEC,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,cAAA,GAAiB,uBAAA;EAAA,SACpC,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,0BAAA;AAAA;;;AAxFnC;;;;;cAwHa,uBAAA,GACX,MAAA,EAAQ,wBAAA;EAAA,SAEC,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,cAAA;EAAA,SACnB,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,0BAAA;AAAA"}
@@ -0,0 +1,96 @@
1
+ import { InvalidRequest } from "../domain/AiError.mjs";
2
+ import { MusicGenerator, MusicInteractiveSession } from "../music-generator/MusicGenerator.mjs";
3
+ import { Effect, Layer, Ref, Stream } from "effect";
4
+ //#region src/testing/MockMusicGenerator.ts
5
+ const makeService = (script, record) => Effect.gen(function* () {
6
+ const gCursor = yield* Ref.make(0);
7
+ const sgCursor = yield* Ref.make(0);
8
+ const sgfCursor = yield* Ref.make(0);
9
+ return {
10
+ generate: (request) => Effect.gen(function* () {
11
+ yield* record.generate(request);
12
+ const i = yield* Ref.getAndUpdate(gCursor, (n) => n + 1);
13
+ const scripted = script.results ?? [];
14
+ if (i >= scripted.length) return yield* new InvalidRequest({
15
+ provider: "mock",
16
+ raw: `MockMusicGenerator exhausted: ${scripted.length} results scripted, but call ${i + 1} was made`
17
+ });
18
+ return scripted[i];
19
+ }),
20
+ streamGeneration: (request) => Stream.unwrap(Effect.gen(function* () {
21
+ yield* record.streamGeneration(request);
22
+ const i = yield* Ref.getAndUpdate(sgCursor, (n) => n + 1);
23
+ const scripted = script.streamGenerationChunks ?? [];
24
+ if (i >= scripted.length) return Stream.fail(new InvalidRequest({
25
+ provider: "mock",
26
+ raw: `MockMusicGenerator exhausted: ${scripted.length} streamGeneration lists scripted, but call ${i + 1} was made`
27
+ }));
28
+ return Stream.fromIterable(scripted[i]);
29
+ })),
30
+ streamGenerationFrom: (input, request) => Stream.unwrap(Effect.gen(function* () {
31
+ yield* record.streamGenerationFrom(request);
32
+ const i = yield* Ref.getAndUpdate(sgfCursor, (n) => n + 1);
33
+ const scripted = script.streamGenerationFromChunks ?? [];
34
+ if (i >= scripted.length) return Stream.fail(new InvalidRequest({
35
+ provider: "mock",
36
+ raw: `MockMusicGenerator exhausted: ${scripted.length} streamGenerationFrom lists scripted, but call ${i + 1} was made`
37
+ }));
38
+ return Stream.drain(input).pipe(Stream.concat(Stream.fromIterable(scripted[i])));
39
+ }))
40
+ };
41
+ });
42
+ /**
43
+ * Layer providing the `MusicGenerator` service AND the
44
+ * `MusicInteractiveSession` capability marker. Use for the common case
45
+ * where code under test exercises `streamGenerationFrom`.
46
+ */
47
+ const layer = (script) => {
48
+ const gCalls = Ref.makeUnsafe([]);
49
+ const sgCalls = Ref.makeUnsafe([]);
50
+ const sgfCalls = Ref.makeUnsafe([]);
51
+ const generatorLayer = Layer.effect(MusicGenerator, makeService(script, {
52
+ generate: (req) => Ref.update(gCalls, (xs) => [...xs, req]),
53
+ streamGeneration: (req) => Ref.update(sgCalls, (xs) => [...xs, req]),
54
+ streamGenerationFrom: (req) => Ref.update(sgfCalls, (xs) => [...xs, req])
55
+ }));
56
+ return {
57
+ layer: Layer.merge(generatorLayer, Layer.succeed(MusicInteractiveSession, void 0)),
58
+ recorder: Effect.gen(function* () {
59
+ return {
60
+ generateCalls: yield* Ref.get(gCalls),
61
+ streamGenerationCalls: yield* Ref.get(sgCalls),
62
+ streamGenerationFromCalls: yield* Ref.get(sgfCalls)
63
+ };
64
+ })
65
+ };
66
+ };
67
+ /**
68
+ * Variant that omits the `MusicInteractiveSession` marker — simulates a
69
+ * provider without bidirectional support (Lyria 3 sync, ElevenLabs,
70
+ * Mureka, MiniMax, Stable Audio, Suno). Calls to
71
+ * `streamGenerationFrom` in code under test should be a compile-time
72
+ * error against this Layer alone.
73
+ */
74
+ const layerWithoutInteractive = (script) => {
75
+ const gCalls = Ref.makeUnsafe([]);
76
+ const sgCalls = Ref.makeUnsafe([]);
77
+ const sgfCalls = Ref.makeUnsafe([]);
78
+ return {
79
+ layer: Layer.effect(MusicGenerator, makeService(script, {
80
+ generate: (req) => Ref.update(gCalls, (xs) => [...xs, req]),
81
+ streamGeneration: (req) => Ref.update(sgCalls, (xs) => [...xs, req]),
82
+ streamGenerationFrom: (req) => Ref.update(sgfCalls, (xs) => [...xs, req])
83
+ })),
84
+ recorder: Effect.gen(function* () {
85
+ return {
86
+ generateCalls: yield* Ref.get(gCalls),
87
+ streamGenerationCalls: yield* Ref.get(sgCalls),
88
+ streamGenerationFromCalls: yield* Ref.get(sgfCalls)
89
+ };
90
+ })
91
+ };
92
+ };
93
+ //#endregion
94
+ export { layer, layerWithoutInteractive };
95
+
96
+ //# sourceMappingURL=MockMusicGenerator.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MockMusicGenerator.mjs","names":["AiError.InvalidRequest"],"sources":["../../src/testing/MockMusicGenerator.ts"],"sourcesContent":["import { Effect, Layer, Ref, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport type { AudioChunk } from \"../domain/Audio.js\"\nimport type {\n CommonGenerateMusicRequest,\n CommonStreamGenerateMusicRequest,\n MusicResult,\n MusicSessionInput,\n} from \"../domain/Music.js\"\nimport {\n MusicGenerator,\n MusicInteractiveSession,\n type MusicGeneratorService,\n} from \"../music-generator/MusicGenerator.js\"\n\nexport type MockMusicGeneratorRecorder = {\n readonly generateCalls: ReadonlyArray<CommonGenerateMusicRequest>\n readonly streamGenerationCalls: ReadonlyArray<CommonStreamGenerateMusicRequest>\n readonly streamGenerationFromCalls: ReadonlyArray<CommonStreamGenerateMusicRequest>\n}\n\nexport type MockMusicGeneratorScript = {\n /** One result per `generate` call, consumed in order. */\n readonly results?: ReadonlyArray<MusicResult>\n /** One chunk-list per `streamGeneration` call, consumed in order. */\n readonly streamGenerationChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>\n /** One chunk-list per `streamGenerationFrom` call, consumed in order. */\n readonly streamGenerationFromChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>\n}\n\nconst makeService = (\n script: MockMusicGeneratorScript,\n record: {\n readonly generate: (req: CommonGenerateMusicRequest) => Effect.Effect<void>\n readonly streamGeneration: (req: CommonStreamGenerateMusicRequest) => Effect.Effect<void>\n readonly streamGenerationFrom: (req: CommonStreamGenerateMusicRequest) => Effect.Effect<void>\n },\n) =>\n Effect.gen(function* () {\n const gCursor = yield* Ref.make(0)\n const sgCursor = yield* Ref.make(0)\n const sgfCursor = yield* Ref.make(0)\n const service: MusicGeneratorService = {\n generate: (request) =>\n Effect.gen(function* () {\n yield* record.generate(request)\n const i = yield* Ref.getAndUpdate(gCursor, (n) => n + 1)\n const scripted = script.results ?? []\n if (i >= scripted.length) {\n return yield* new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockMusicGenerator exhausted: ${scripted.length} results scripted, but call ${i + 1} was made`,\n })\n }\n return scripted[i]!\n }),\n streamGeneration: (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* record.streamGeneration(request)\n const i = yield* Ref.getAndUpdate(sgCursor, (n) => n + 1)\n const scripted = script.streamGenerationChunks ?? []\n if (i >= scripted.length) {\n return Stream.fail(\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockMusicGenerator exhausted: ${scripted.length} streamGeneration lists scripted, but call ${i + 1} was made`,\n }),\n )\n }\n return Stream.fromIterable(scripted[i]!)\n }),\n ),\n streamGenerationFrom: <E, R>(\n input: Stream.Stream<MusicSessionInput, E, R>,\n request: CommonStreamGenerateMusicRequest,\n ): Stream.Stream<AudioChunk, AiError.AiError | E, R> =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* record.streamGenerationFrom(request)\n const i = yield* Ref.getAndUpdate(sgfCursor, (n) => n + 1)\n const scripted = script.streamGenerationFromChunks ?? []\n if (i >= scripted.length) {\n const exhausted: Stream.Stream<AudioChunk, AiError.AiError | E, R> = Stream.fail(\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockMusicGenerator exhausted: ${scripted.length} streamGenerationFrom lists scripted, but call ${i + 1} was made`,\n }),\n )\n return exhausted\n }\n // Drain the input fully before emitting scripted audio chunks,\n // so consumers can assert on what session messages were pushed.\n return Stream.drain(input).pipe(Stream.concat(Stream.fromIterable(scripted[i]!)))\n }),\n ),\n }\n return service\n })\n\n/**\n * Layer providing the `MusicGenerator` service AND the\n * `MusicInteractiveSession` capability marker. Use for the common case\n * where code under test exercises `streamGenerationFrom`.\n */\nexport const layer = (\n script: MockMusicGeneratorScript,\n): {\n readonly layer: Layer.Layer<MusicGenerator | MusicInteractiveSession>\n readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>\n} => {\n const gCalls = Ref.makeUnsafe<ReadonlyArray<CommonGenerateMusicRequest>>([])\n const sgCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const sgfCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const generatorLayer = Layer.effect(\n MusicGenerator,\n makeService(script, {\n generate: (req) => Ref.update(gCalls, (xs) => [...xs, req]),\n streamGeneration: (req) => Ref.update(sgCalls, (xs) => [...xs, req]),\n streamGenerationFrom: (req) => Ref.update(sgfCalls, (xs) => [...xs, req]),\n }),\n )\n const live = Layer.merge(generatorLayer, Layer.succeed(MusicInteractiveSession, undefined))\n return {\n layer: live,\n recorder: Effect.gen(function* () {\n const generateCalls = yield* Ref.get(gCalls)\n const streamGenerationCalls = yield* Ref.get(sgCalls)\n const streamGenerationFromCalls = yield* Ref.get(sgfCalls)\n return { generateCalls, streamGenerationCalls, streamGenerationFromCalls }\n }),\n }\n}\n\n/**\n * Variant that omits the `MusicInteractiveSession` marker — simulates a\n * provider without bidirectional support (Lyria 3 sync, ElevenLabs,\n * Mureka, MiniMax, Stable Audio, Suno). Calls to\n * `streamGenerationFrom` in code under test should be a compile-time\n * error against this Layer alone.\n */\nexport const layerWithoutInteractive = (\n script: MockMusicGeneratorScript,\n): {\n readonly layer: Layer.Layer<MusicGenerator>\n readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>\n} => {\n const gCalls = Ref.makeUnsafe<ReadonlyArray<CommonGenerateMusicRequest>>([])\n const sgCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const sgfCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const live = Layer.effect(\n MusicGenerator,\n makeService(script, {\n generate: (req) => Ref.update(gCalls, (xs) => [...xs, req]),\n streamGeneration: (req) => Ref.update(sgCalls, (xs) => [...xs, req]),\n streamGenerationFrom: (req) => Ref.update(sgfCalls, (xs) => [...xs, req]),\n }),\n )\n return {\n layer: live,\n recorder: Effect.gen(function* () {\n const generateCalls = yield* Ref.get(gCalls)\n const streamGenerationCalls = yield* Ref.get(sgCalls)\n const streamGenerationFromCalls = yield* Ref.get(sgfCalls)\n return { generateCalls, streamGenerationCalls, streamGenerationFromCalls }\n }),\n }\n}\n"],"mappings":";;;;AA8BA,MAAM,eACJ,QACA,WAMA,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,OAAO,IAAI,KAAK,EAAE;CAClC,MAAM,WAAW,OAAO,IAAI,KAAK,EAAE;CACnC,MAAM,YAAY,OAAO,IAAI,KAAK,EAAE;AAwDpC,QAAO;EAtDL,WAAW,YACT,OAAO,IAAI,aAAa;AACtB,UAAO,OAAO,SAAS,QAAQ;GAC/B,MAAM,IAAI,OAAO,IAAI,aAAa,UAAU,MAAM,IAAI,EAAE;GACxD,MAAM,WAAW,OAAO,WAAW,EAAE;AACrC,OAAI,KAAK,SAAS,OAChB,QAAO,OAAO,IAAIA,eAAuB;IACvC,UAAU;IACV,KAAK,iCAAiC,SAAS,OAAO,8BAA8B,IAAI,EAAE;IAC3F,CAAC;AAEJ,UAAO,SAAS;IAChB;EACJ,mBAAmB,YACjB,OAAO,OACL,OAAO,IAAI,aAAa;AACtB,UAAO,OAAO,iBAAiB,QAAQ;GACvC,MAAM,IAAI,OAAO,IAAI,aAAa,WAAW,MAAM,IAAI,EAAE;GACzD,MAAM,WAAW,OAAO,0BAA0B,EAAE;AACpD,OAAI,KAAK,SAAS,OAChB,QAAO,OAAO,KACZ,IAAIA,eAAuB;IACzB,UAAU;IACV,KAAK,iCAAiC,SAAS,OAAO,6CAA6C,IAAI,EAAE;IAC1G,CAAC,CACH;AAEH,UAAO,OAAO,aAAa,SAAS,GAAI;IACxC,CACH;EACH,uBACE,OACA,YAEA,OAAO,OACL,OAAO,IAAI,aAAa;AACtB,UAAO,OAAO,qBAAqB,QAAQ;GAC3C,MAAM,IAAI,OAAO,IAAI,aAAa,YAAY,MAAM,IAAI,EAAE;GAC1D,MAAM,WAAW,OAAO,8BAA8B,EAAE;AACxD,OAAI,KAAK,SAAS,OAOhB,QANqE,OAAO,KAC1E,IAAIA,eAAuB;IACzB,UAAU;IACV,KAAK,iCAAiC,SAAS,OAAO,iDAAiD,IAAI,EAAE;IAC9G,CAAC,CAEY;AAIlB,UAAO,OAAO,MAAM,MAAM,CAAC,KAAK,OAAO,OAAO,OAAO,aAAa,SAAS,GAAI,CAAC,CAAC;IACjF,CACH;EAES;EACd;;;;;;AAOJ,MAAa,SACX,WAIG;CACH,MAAM,SAAS,IAAI,WAAsD,EAAE,CAAC;CAC5E,MAAM,UAAU,IAAI,WAA4D,EAAE,CAAC;CACnF,MAAM,WAAW,IAAI,WAA4D,EAAE,CAAC;CACpF,MAAM,iBAAiB,MAAM,OAC3B,gBACA,YAAY,QAAQ;EAClB,WAAW,QAAQ,IAAI,OAAO,SAAS,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;EAC3D,mBAAmB,QAAQ,IAAI,OAAO,UAAU,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;EACpE,uBAAuB,QAAQ,IAAI,OAAO,WAAW,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;EAC1E,CAAC,CACH;AAED,QAAO;EACL,OAFW,MAAM,MAAM,gBAAgB,MAAM,QAAQ,yBAAyB,KAAA,EAAU,CAE7E;EACX,UAAU,OAAO,IAAI,aAAa;AAIhC,UAAO;IAAE,eAAA,OAHoB,IAAI,IAAI,OAAO;IAGpB,uBAAA,OAFa,IAAI,IAAI,QAAQ;IAEN,2BAAA,OADN,IAAI,IAAI,SAAS;IACgB;IAC1E;EACH;;;;;;;;;AAUH,MAAa,2BACX,WAIG;CACH,MAAM,SAAS,IAAI,WAAsD,EAAE,CAAC;CAC5E,MAAM,UAAU,IAAI,WAA4D,EAAE,CAAC;CACnF,MAAM,WAAW,IAAI,WAA4D,EAAE,CAAC;AASpF,QAAO;EACL,OATW,MAAM,OACjB,gBACA,YAAY,QAAQ;GAClB,WAAW,QAAQ,IAAI,OAAO,SAAS,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;GAC3D,mBAAmB,QAAQ,IAAI,OAAO,UAAU,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;GACpE,uBAAuB,QAAQ,IAAI,OAAO,WAAW,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;GAC1E,CAAC,CAGS;EACX,UAAU,OAAO,IAAI,aAAa;AAIhC,UAAO;IAAE,eAAA,OAHoB,IAAI,IAAI,OAAO;IAGpB,uBAAA,OAFa,IAAI,IAAI,QAAQ;IAEN,2BAAA,OADN,IAAI,IAAI,SAAS;IACgB;IAC1E;EACH"}
@@ -1,5 +1,5 @@
1
- import { d as Item } from "../Items-CB8Bo3FI.mjs";
2
- import { r as Turn } from "../Turn-OPaILVIB.mjs";
1
+ import { d as Item } from "../Items-DqbaJoz7.mjs";
2
+ import { r as Turn } from "../Turn-ChbL2foc.mjs";
3
3
  import { LanguageModel, LanguageModelService } from "../language-model/LanguageModel.mjs";
4
4
  import { Duration, Effect, Layer } from "effect";
5
5
 
@@ -12,37 +12,42 @@ type MockOptions = {
12
12
  */
13
13
  readonly deltaInterval?: Duration.Input;
14
14
  };
15
+ type Call = {
16
+ readonly history: ReadonlyArray<Item>;
17
+ readonly turn: Turn;
18
+ };
15
19
  /**
16
20
  * A scripted mock provider. Pre-canned `Turn` outputs are returned in order,
17
21
  * one per call to `streamTurn`. Each scripted turn is split into synthetic
18
- * deltas (text → tool_call_starttool_call_args_delta → ... → turn_complete)
22
+ * deltas (text → ToolCallStartToolCallArgsDelta → ... → TurnComplete)
19
23
  * so streaming consumers can see realistic delta shapes.
20
24
  */
21
25
  type MockRecorder = {
22
- readonly calls: ReadonlyArray<{
23
- readonly history: ReadonlyArray<Item>;
24
- readonly turn: Turn;
25
- }>;
26
+ readonly calls: ReadonlyArray<Call>;
26
27
  };
27
- declare const layer: (scriptedTurns: ReadonlyArray<Turn>, options?: MockOptions) => Layer.Layer<LanguageModel>;
28
28
  /**
29
- * Synchronous constructor that returns the `LanguageModelService` value
30
- * directly, plus a recorder. Use this when you want to swap models
31
- * mid-stream via `Effect.provideService` instead of providing one model
32
- * for the whole program via `Layer`.
29
+ * Layer that registers a `MockProvider` against the `LanguageModel` tag.
30
+ * Calls beyond the scripted turn count fail with `InvalidRequest`.
33
31
  */
34
- declare const make: (scriptedTurns: ReadonlyArray<Turn>, options?: MockOptions) => {
35
- readonly service: LanguageModelService;
36
- readonly recorder: Effect.Effect<MockRecorder>;
37
- };
32
+ declare const layer: (scriptedTurns: ReadonlyArray<Turn>, options?: MockOptions) => Layer.Layer<LanguageModel>;
38
33
  /**
39
- * Same as `layer`, but also exposes a recorder that captures every call
34
+ * Like `layer`, but also exposes a recorder that captures every call
40
35
  * (history + returned turn).
41
36
  */
42
37
  declare const layerWithRecorder: (scriptedTurns: ReadonlyArray<Turn>, options?: MockOptions) => {
43
38
  readonly layer: Layer.Layer<LanguageModel>;
44
39
  readonly recorder: Effect.Effect<MockRecorder>;
45
40
  };
41
+ /**
42
+ * Build the `LanguageModelService` value directly (no Layer), plus a
43
+ * recorder. Use this when you want to swap models mid-program via
44
+ * `Effect.provideService` instead of providing one model for the whole
45
+ * program via `Layer`.
46
+ */
47
+ declare const make: (scriptedTurns: ReadonlyArray<Turn>, options?: MockOptions) => {
48
+ readonly service: LanguageModelService;
49
+ readonly recorder: Effect.Effect<MockRecorder>;
50
+ };
46
51
  //#endregion
47
- export { MockOptions, MockRecorder, layer, layerWithRecorder, make };
52
+ export { Call, MockOptions, MockRecorder, layer, layerWithRecorder, make };
48
53
  //# sourceMappingURL=MockProvider.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MockProvider.d.mts","names":[],"sources":["../../src/testing/MockProvider.ts"],"mappings":";;;;;;KAMY,WAAA;;AAAZ;;;;WAMW,aAAA,GAAgB,QAAA,CAAS,KAAA;AAAA;;;;AASpC;;;KAAY,YAAA;EAAA,SACD,KAAA,EAAO,aAAA;IAAA,SACL,OAAA,EAAS,aAAA,CAAc,IAAA;IAAA,SACvB,IAAA,EAAM,IAAA;EAAA;AAAA;AAAA,cAqEN,KAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA,KACT,KAAA,CAAM,KAAA,CAAM,aAAA;;;;;;;cAQF,IAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA;EAAA,SAED,OAAA,EAAS,oBAAA;EAAA,SACT,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAA;AAAA;;;;;cAiCtB,iBAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA;EAAA,SAED,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,aAAA;EAAA,SACnB,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAA;AAAA"}
1
+ {"version":3,"file":"MockProvider.d.mts","names":[],"sources":["../../src/testing/MockProvider.ts"],"mappings":";;;;;;KAMY,WAAA;;AAAZ;;;;WAMW,aAAA,GAAgB,QAAA,CAAS,KAAA;AAAA;AAAA,KAGxB,IAAA;EAAA,SACD,OAAA,EAAS,aAAA,CAAc,IAAA;EAAA,SACvB,IAAA,EAAM,IAAA;AAAA;;;;;;;KASL,YAAA;EAAA,SACD,KAAA,EAAO,aAAA,CAAc,IAAA;AAAA;;;;;cAqGnB,KAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA,KACT,KAAA,CAAM,KAAA,CAAM,aAAA;AAzGf;;;;AAAA,cAqHa,iBAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA;EAAA,SAED,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,aAAA;EAAA,SACnB,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAA;AAAA;;AApBnC;;;;;cAwCa,IAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA;EAAA,SAED,OAAA,EAAS,oBAAA;EAAA,SACT,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAA;AAAA"}
@@ -1,92 +1,76 @@
1
1
  import { InvalidRequest } from "../domain/AiError.mjs";
2
+ import { isOutputText } from "../domain/Items.mjs";
3
+ import { TurnEvent } from "../domain/Turn.mjs";
2
4
  import { LanguageModel } from "../language-model/LanguageModel.mjs";
3
- import { Effect, Layer, Ref, Schedule, Stream } from "effect";
5
+ import { Array, Effect, Layer, Match, Option, Ref, Schedule, Stream } from "effect";
4
6
  //#region src/testing/MockProvider.ts
5
- const turnToDeltas = (turn) => {
6
- const deltas = [];
7
- for (const item of turn.items) if (item.type === "message" && item.role === "assistant") {
8
- for (const block of item.content) if (block.type === "output_text") deltas.push({
9
- type: "text_delta",
10
- text: block.text
11
- });
12
- } else if (item.type === "function_call") {
13
- deltas.push({
14
- type: "tool_call_start",
15
- call_id: item.call_id,
16
- name: item.name
17
- });
18
- deltas.push({
19
- type: "tool_call_args_delta",
20
- call_id: item.call_id,
21
- delta: item.arguments
22
- });
23
- } else if (item.type === "reasoning" && item.summary !== void 0) deltas.push({
24
- type: "reasoning_delta",
25
- text: item.summary,
7
+ const itemToDeltas = Match.type().pipe(Match.discriminators("type")({
8
+ message: (m) => m.role === "assistant" ? m.content.filter(isOutputText).map((b) => TurnEvent.TextDelta({ text: b.text })) : [],
9
+ function_call: (fc) => [TurnEvent.ToolCallStart({
10
+ call_id: fc.call_id,
11
+ name: fc.name
12
+ }), TurnEvent.ToolCallArgsDelta({
13
+ call_id: fc.call_id,
14
+ delta: fc.arguments
15
+ })],
16
+ function_call_output: () => [],
17
+ reasoning: (r) => r.summary !== void 0 ? [TurnEvent.ReasoningDelta({
18
+ text: r.summary,
26
19
  kind: "summary"
27
- });
28
- deltas.push({
29
- type: "turn_complete",
30
- turn
31
- });
32
- return deltas;
33
- };
20
+ })] : []
21
+ }), Match.exhaustive);
22
+ const turnToDeltas = (turn) => [...turn.items.flatMap(itemToDeltas), TurnEvent.TurnComplete({ turn })];
34
23
  const pacedDeltas = (turn, options) => {
35
24
  const base = Stream.fromIterable(turnToDeltas(turn));
36
25
  return options?.deltaInterval === void 0 ? base : base.pipe(Stream.schedule(Schedule.spaced(options.deltaInterval)));
37
26
  };
38
- const makeService = (scriptedTurns, options, recordCall) => Effect.gen(function* () {
39
- const cursor = yield* Ref.make(0);
40
- return LanguageModel.of({ streamTurn: (request) => Stream.unwrap(Effect.gen(function* () {
41
- const i = yield* Ref.getAndUpdate(cursor, (n) => n + 1);
42
- if (i >= scriptedTurns.length) return Stream.fail(new InvalidRequest({
43
- provider: "mock",
44
- raw: `MockProvider exhausted: ${scriptedTurns.length} turns scripted, but call ${i + 1} was made`
45
- }));
46
- const turn = scriptedTurns[i];
47
- if (recordCall !== void 0) yield* recordCall(request.history, turn);
48
- return pacedDeltas(turn, options);
49
- })) });
27
+ const exhausted = (n, attempt) => new InvalidRequest({
28
+ provider: "mock",
29
+ raw: `MockProvider exhausted: ${n} turns scripted, but call ${attempt} was made`
50
30
  });
51
- const layer = (scriptedTurns, options) => Layer.effect(LanguageModel, makeService(scriptedTurns, options));
52
- /**
53
- * Synchronous constructor that returns the `LanguageModelService` value
54
- * directly, plus a recorder. Use this when you want to swap models
55
- * mid-stream via `Effect.provideService` instead of providing one model
56
- * for the whole program via `Layer`.
57
- */
58
- const make = (scriptedTurns, options) => {
59
- const cursor = Ref.makeUnsafe(0);
60
- const callsRef = Ref.makeUnsafe([]);
31
+ const noRecord = (_) => Effect.void;
32
+ const buildService = (scriptedTurns, options, cursor, record) => ({ streamTurn: (request) => Stream.unwrap(Ref.getAndUpdate(cursor, (n) => n + 1).pipe(Effect.flatMap((i) => Option.match(Array.get(scriptedTurns, i), {
33
+ onNone: () => Effect.succeed(Stream.fail(exhausted(scriptedTurns.length, i + 1))),
34
+ onSome: (turn) => record({
35
+ history: request.history,
36
+ turn
37
+ }).pipe(Effect.as(pacedDeltas(turn, options)))
38
+ })))) });
39
+ const makeRecorderUnsafe = () => {
40
+ const ref = Ref.makeUnsafe([]);
61
41
  return {
62
- service: { streamTurn: (request) => Stream.unwrap(Effect.gen(function* () {
63
- const i = yield* Ref.getAndUpdate(cursor, (n) => n + 1);
64
- if (i >= scriptedTurns.length) return Stream.fail(new InvalidRequest({
65
- provider: "mock",
66
- raw: `MockProvider exhausted: ${scriptedTurns.length} turns scripted, but call ${i + 1} was made`
67
- }));
68
- const turn = scriptedTurns[i];
69
- yield* Ref.update(callsRef, (xs) => [...xs, {
70
- history: request.history,
71
- turn
72
- }]);
73
- return pacedDeltas(turn, options);
74
- })) },
75
- recorder: Ref.get(callsRef).pipe(Effect.map((calls) => ({ calls })))
42
+ record: (call) => Ref.update(ref, Array.append(call)),
43
+ recorder: Ref.get(ref).pipe(Effect.map((calls) => ({ calls })))
76
44
  };
77
45
  };
78
46
  /**
79
- * Same as `layer`, but also exposes a recorder that captures every call
47
+ * Layer that registers a `MockProvider` against the `LanguageModel` tag.
48
+ * Calls beyond the scripted turn count fail with `InvalidRequest`.
49
+ */
50
+ const layer = (scriptedTurns, options) => Layer.effect(LanguageModel, Ref.make(0).pipe(Effect.map((cursor) => buildService(scriptedTurns, options, cursor, noRecord))));
51
+ /**
52
+ * Like `layer`, but also exposes a recorder that captures every call
80
53
  * (history + returned turn).
81
54
  */
82
55
  const layerWithRecorder = (scriptedTurns, options) => {
83
- const callsRef = Ref.makeUnsafe([]);
56
+ const { record, recorder } = makeRecorderUnsafe();
57
+ return {
58
+ layer: Layer.effect(LanguageModel, Ref.make(0).pipe(Effect.map((cursor) => buildService(scriptedTurns, options, cursor, record)))),
59
+ recorder
60
+ };
61
+ };
62
+ /**
63
+ * Build the `LanguageModelService` value directly (no Layer), plus a
64
+ * recorder. Use this when you want to swap models mid-program via
65
+ * `Effect.provideService` instead of providing one model for the whole
66
+ * program via `Layer`.
67
+ */
68
+ const make = (scriptedTurns, options) => {
69
+ const cursor = Ref.makeUnsafe(0);
70
+ const { record, recorder } = makeRecorderUnsafe();
84
71
  return {
85
- layer: Layer.effect(LanguageModel, makeService(scriptedTurns, options, (history, turn) => Ref.update(callsRef, (xs) => [...xs, {
86
- history,
87
- turn
88
- }]))),
89
- recorder: Ref.get(callsRef).pipe(Effect.map((calls) => ({ calls })))
72
+ service: buildService(scriptedTurns, options, cursor, record),
73
+ recorder
90
74
  };
91
75
  };
92
76
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"MockProvider.mjs","names":["AiError.InvalidRequest"],"sources":["../../src/testing/MockProvider.ts"],"sourcesContent":["import { Duration, Effect, Layer, Ref, Schedule, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport type { Item } from \"../domain/Items.js\"\nimport { LanguageModel, type LanguageModelService } from \"../language-model/LanguageModel.js\"\nimport type { Turn, TurnEvent } from \"../domain/Turn.js\"\n\nexport type MockOptions = {\n /**\n * If set, deltas of each scripted turn are spaced by this duration via\n * `Schedule.spaced`. Combine with `TestClock.adjust` for deterministic\n * timing in tests.\n */\n readonly deltaInterval?: Duration.Input\n}\n\n/**\n * A scripted mock provider. Pre-canned `Turn` outputs are returned in order,\n * one per call to `streamTurn`. Each scripted turn is split into synthetic\n * deltas (text → tool_call_starttool_call_args_delta → ... → turn_complete)\n * so streaming consumers can see realistic delta shapes.\n */\nexport type MockRecorder = {\n readonly calls: ReadonlyArray<{\n readonly history: ReadonlyArray<Item>\n readonly turn: Turn\n }>\n}\n\nconst turnToDeltas = (turn: Turn): ReadonlyArray<TurnEvent> => {\n const deltas: TurnEvent[] = []\n for (const item of turn.items) {\n if (item.type === \"message\" && item.role === \"assistant\") {\n for (const block of item.content) {\n if (block.type === \"output_text\") {\n deltas.push({ type: \"text_delta\", text: block.text })\n }\n }\n } else if (item.type === \"function_call\") {\n deltas.push({\n type: \"tool_call_start\",\n call_id: item.call_id,\n name: item.name,\n })\n deltas.push({\n type: \"tool_call_args_delta\",\n call_id: item.call_id,\n delta: item.arguments,\n })\n } else if (item.type === \"reasoning\" && item.summary !== undefined) {\n deltas.push({ type: \"reasoning_delta\", text: item.summary, kind: \"summary\" })\n }\n }\n deltas.push({ type: \"turn_complete\", turn })\n return deltas\n}\n\nconst pacedDeltas = (turn: Turn, options?: MockOptions): Stream.Stream<TurnEvent> => {\n const base = Stream.fromIterable(turnToDeltas(turn))\n return options?.deltaInterval === undefined\n ? base\n : base.pipe(Stream.schedule(Schedule.spaced(options.deltaInterval)))\n}\n\nconst makeService = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n recordCall?: (history: ReadonlyArray<Item>, turn: Turn) => Effect.Effect<void>,\n) =>\n Effect.gen(function* () {\n const cursor = yield* Ref.make(0)\n return LanguageModel.of({\n streamTurn: (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const i = yield* Ref.getAndUpdate(cursor, (n) => n + 1)\n if (i >= scriptedTurns.length) {\n return Stream.fail(\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockProvider exhausted: ${scriptedTurns.length} turns scripted, but call ${i + 1} was made`,\n }),\n )\n }\n const turn = scriptedTurns[i]!\n if (recordCall !== undefined) {\n yield* recordCall(request.history, turn)\n }\n return pacedDeltas(turn, options)\n }),\n ),\n })\n })\n\nexport const layer = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): Layer.Layer<LanguageModel> => Layer.effect(LanguageModel, makeService(scriptedTurns, options))\n\n/**\n * Synchronous constructor that returns the `LanguageModelService` value\n * directly, plus a recorder. Use this when you want to swap models\n * mid-stream via `Effect.provideService` instead of providing one model\n * for the whole program via `Layer`.\n */\nexport const make = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): {\n readonly service: LanguageModelService\n readonly recorder: Effect.Effect<MockRecorder>\n} => {\n const cursor = Ref.makeUnsafe(0)\n const callsRef = Ref.makeUnsafe<ReadonlyArray<{ history: ReadonlyArray<Item>; turn: Turn }>>([])\n const service: LanguageModelService = {\n streamTurn: (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const i = yield* Ref.getAndUpdate(cursor, (n) => n + 1)\n if (i >= scriptedTurns.length) {\n return Stream.fail(\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockProvider exhausted: ${scriptedTurns.length} turns scripted, but call ${i + 1} was made`,\n }),\n )\n }\n const turn = scriptedTurns[i]!\n yield* Ref.update(callsRef, (xs) => [...xs, { history: request.history, turn }])\n return pacedDeltas(turn, options)\n }),\n ),\n }\n return {\n service,\n recorder: Ref.get(callsRef).pipe(Effect.map((calls) => ({ calls }))),\n }\n}\n\n/**\n * Same as `layer`, but also exposes a recorder that captures every call\n * (history + returned turn).\n */\nexport const layerWithRecorder = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): {\n readonly layer: Layer.Layer<LanguageModel>\n readonly recorder: Effect.Effect<MockRecorder>\n} => {\n const callsRef = Ref.makeUnsafe<ReadonlyArray<{ history: ReadonlyArray<Item>; turn: Turn }>>([])\n const live = Layer.effect(\n LanguageModel,\n makeService(scriptedTurns, options, (history, turn) =>\n Ref.update(callsRef, (xs) => [...xs, { history, turn }]),\n ),\n )\n return {\n layer: live,\n recorder: Ref.get(callsRef).pipe(Effect.map((calls) => ({ calls }))),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"MockProvider.mjs","names":["AiError.InvalidRequest","Arr"],"sources":["../../src/testing/MockProvider.ts"],"sourcesContent":["import { Array as Arr, Duration, Effect, Layer, Match, Option, Ref, Schedule, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport { type Item, isOutputText } from \"../domain/Items.js\"\nimport { LanguageModel, type LanguageModelService } from \"../language-model/LanguageModel.js\"\nimport { type Turn, TurnEvent } from \"../domain/Turn.js\"\n\nexport type MockOptions = {\n /**\n * If set, deltas of each scripted turn are spaced by this duration via\n * `Schedule.spaced`. Combine with `TestClock.adjust` for deterministic\n * timing in tests.\n */\n readonly deltaInterval?: Duration.Input\n}\n\nexport type Call = {\n readonly history: ReadonlyArray<Item>\n readonly turn: Turn\n}\n\n/**\n * A scripted mock provider. Pre-canned `Turn` outputs are returned in order,\n * one per call to `streamTurn`. Each scripted turn is split into synthetic\n * deltas (text → ToolCallStartToolCallArgsDelta → ... → TurnComplete)\n * so streaming consumers can see realistic delta shapes.\n */\nexport type MockRecorder = {\n readonly calls: ReadonlyArray<Call>\n}\n\n// ---------------------------------------------------------------------------\n// Pure projection: Turn → ReadonlyArray<TurnEvent>\n// ---------------------------------------------------------------------------\n\nconst itemToDeltas: (item: Item) => ReadonlyArray<TurnEvent> = Match.type<Item>().pipe(\n Match.discriminators(\"type\")({\n message: (m): ReadonlyArray<TurnEvent> =>\n m.role === \"assistant\"\n ? m.content.filter(isOutputText).map((b) => TurnEvent.TextDelta({ text: b.text }))\n : [],\n function_call: (fc) => [\n TurnEvent.ToolCallStart({ call_id: fc.call_id, name: fc.name }),\n TurnEvent.ToolCallArgsDelta({ call_id: fc.call_id, delta: fc.arguments }),\n ],\n function_call_output: () => [],\n reasoning: (r) =>\n r.summary !== undefined\n ? [TurnEvent.ReasoningDelta({ text: r.summary, kind: \"summary\" as const })]\n : [],\n }),\n Match.exhaustive,\n)\n\nconst turnToDeltas = (turn: Turn): ReadonlyArray<TurnEvent> => [\n ...turn.items.flatMap(itemToDeltas),\n TurnEvent.TurnComplete({ turn }),\n]\n\nconst pacedDeltas = (turn: Turn, options?: MockOptions): Stream.Stream<TurnEvent> => {\n const base = Stream.fromIterable(turnToDeltas(turn))\n return options?.deltaInterval === undefined\n ? base\n : base.pipe(Stream.schedule(Schedule.spaced(options.deltaInterval)))\n}\n\n// ---------------------------------------------------------------------------\n// Canonical service factory. One implementation; sync/Layer/recorder\n// variants below are just different ways to wire the cursor + record hook.\n// ---------------------------------------------------------------------------\n\nconst exhausted = (n: number, attempt: number): AiError.AiError =>\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockProvider exhausted: ${n} turns scripted, but call ${attempt} was made`,\n })\n\nconst noRecord = (_: Call): Effect.Effect<void> => Effect.void\n\nconst buildService = (\n scriptedTurns: ReadonlyArray<Turn>,\n options: MockOptions | undefined,\n cursor: Ref.Ref<number>,\n record: (call: Call) => Effect.Effect<void>,\n): LanguageModelService => ({\n streamTurn: (request) =>\n Stream.unwrap(\n Ref.getAndUpdate(cursor, (n) => n + 1).pipe(\n Effect.flatMap(\n (i): Effect.Effect<Stream.Stream<TurnEvent, AiError.AiError>> =>\n Option.match(Arr.get(scriptedTurns, i), {\n onNone: () => Effect.succeed(Stream.fail(exhausted(scriptedTurns.length, i + 1))),\n onSome: (turn) =>\n record({ history: request.history, turn }).pipe(\n Effect.as(pacedDeltas(turn, options)),\n ),\n }),\n ),\n ),\n ),\n})\n\n// ---------------------------------------------------------------------------\n// Recorder handle. Unsafe Ref is local: it backs both the `record` write\n// hook (called inside the service) and the `recorder` read effect (called\n// by the test). Both close over the same cell.\n// ---------------------------------------------------------------------------\n\ntype RecorderHandle = {\n readonly record: (call: Call) => Effect.Effect<void>\n readonly recorder: Effect.Effect<MockRecorder>\n}\n\nconst makeRecorderUnsafe = (): RecorderHandle => {\n const ref = Ref.makeUnsafe<ReadonlyArray<Call>>([])\n return {\n record: (call) => Ref.update(ref, Arr.append(call)),\n recorder: Ref.get(ref).pipe(Effect.map((calls): MockRecorder => ({ calls }))),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Layer that registers a `MockProvider` against the `LanguageModel` tag.\n * Calls beyond the scripted turn count fail with `InvalidRequest`.\n */\nexport const layer = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): Layer.Layer<LanguageModel> =>\n Layer.effect(\n LanguageModel,\n Ref.make(0).pipe(\n Effect.map((cursor) => buildService(scriptedTurns, options, cursor, noRecord)),\n ),\n )\n\n/**\n * Like `layer`, but also exposes a recorder that captures every call\n * (history + returned turn).\n */\nexport const layerWithRecorder = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): {\n readonly layer: Layer.Layer<LanguageModel>\n readonly recorder: Effect.Effect<MockRecorder>\n} => {\n const { record, recorder } = makeRecorderUnsafe()\n return {\n layer: Layer.effect(\n LanguageModel,\n Ref.make(0).pipe(\n Effect.map((cursor) => buildService(scriptedTurns, options, cursor, record)),\n ),\n ),\n recorder,\n }\n}\n\n/**\n * Build the `LanguageModelService` value directly (no Layer), plus a\n * recorder. Use this when you want to swap models mid-program via\n * `Effect.provideService` instead of providing one model for the whole\n * program via `Layer`.\n */\nexport const make = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): {\n readonly service: LanguageModelService\n readonly recorder: Effect.Effect<MockRecorder>\n} => {\n const cursor = Ref.makeUnsafe(0)\n const { record, recorder } = makeRecorderUnsafe()\n return {\n service: buildService(scriptedTurns, options, cursor, record),\n recorder,\n }\n}\n"],"mappings":";;;;;;AAkCA,MAAM,eAAyD,MAAM,MAAY,CAAC,KAChF,MAAM,eAAe,OAAO,CAAC;CAC3B,UAAU,MACR,EAAE,SAAS,cACP,EAAE,QAAQ,OAAO,aAAa,CAAC,KAAK,MAAM,UAAU,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,GAChF,EAAE;CACR,gBAAgB,OAAO,CACrB,UAAU,cAAc;EAAE,SAAS,GAAG;EAAS,MAAM,GAAG;EAAM,CAAC,EAC/D,UAAU,kBAAkB;EAAE,SAAS,GAAG;EAAS,OAAO,GAAG;EAAW,CAAC,CAC1E;CACD,4BAA4B,EAAE;CAC9B,YAAY,MACV,EAAE,YAAY,KAAA,IACV,CAAC,UAAU,eAAe;EAAE,MAAM,EAAE;EAAS,MAAM;EAAoB,CAAC,CAAC,GACzE,EAAE;CACT,CAAC,EACF,MAAM,WACP;AAED,MAAM,gBAAgB,SAAyC,CAC7D,GAAG,KAAK,MAAM,QAAQ,aAAa,EACnC,UAAU,aAAa,EAAE,MAAM,CAAC,CACjC;AAED,MAAM,eAAe,MAAY,YAAoD;CACnF,MAAM,OAAO,OAAO,aAAa,aAAa,KAAK,CAAC;AACpD,QAAO,SAAS,kBAAkB,KAAA,IAC9B,OACA,KAAK,KAAK,OAAO,SAAS,SAAS,OAAO,QAAQ,cAAc,CAAC,CAAC;;AAQxE,MAAM,aAAa,GAAW,YAC5B,IAAIA,eAAuB;CACzB,UAAU;CACV,KAAK,2BAA2B,EAAE,4BAA4B,QAAQ;CACvE,CAAC;AAEJ,MAAM,YAAY,MAAiC,OAAO;AAE1D,MAAM,gBACJ,eACA,SACA,QACA,YAC0B,EAC1B,aAAa,YACX,OAAO,OACL,IAAI,aAAa,SAAS,MAAM,IAAI,EAAE,CAAC,KACrC,OAAO,SACJ,MACC,OAAO,MAAMC,MAAI,IAAI,eAAe,EAAE,EAAE;CACtC,cAAc,OAAO,QAAQ,OAAO,KAAK,UAAU,cAAc,QAAQ,IAAI,EAAE,CAAC,CAAC;CACjF,SAAS,SACP,OAAO;EAAE,SAAS,QAAQ;EAAS;EAAM,CAAC,CAAC,KACzC,OAAO,GAAG,YAAY,MAAM,QAAQ,CAAC,CACtC;CACJ,CAAC,CACL,CACF,CACF,EACJ;AAaD,MAAM,2BAA2C;CAC/C,MAAM,MAAM,IAAI,WAAgC,EAAE,CAAC;AACnD,QAAO;EACL,SAAS,SAAS,IAAI,OAAO,KAAKA,MAAI,OAAO,KAAK,CAAC;EACnD,UAAU,IAAI,IAAI,IAAI,CAAC,KAAK,OAAO,KAAK,WAAyB,EAAE,OAAO,EAAE,CAAC;EAC9E;;;;;;AAWH,MAAa,SACX,eACA,YAEA,MAAM,OACJ,eACA,IAAI,KAAK,EAAE,CAAC,KACV,OAAO,KAAK,WAAW,aAAa,eAAe,SAAS,QAAQ,SAAS,CAAC,CAC/E,CACF;;;;;AAMH,MAAa,qBACX,eACA,YAIG;CACH,MAAM,EAAE,QAAQ,aAAa,oBAAoB;AACjD,QAAO;EACL,OAAO,MAAM,OACX,eACA,IAAI,KAAK,EAAE,CAAC,KACV,OAAO,KAAK,WAAW,aAAa,eAAe,SAAS,QAAQ,OAAO,CAAC,CAC7E,CACF;EACD;EACD;;;;;;;;AASH,MAAa,QACX,eACA,YAIG;CACH,MAAM,SAAS,IAAI,WAAW,EAAE;CAChC,MAAM,EAAE,QAAQ,aAAa,oBAAoB;AACjD,QAAO;EACL,SAAS,aAAa,eAAe,SAAS,QAAQ,OAAO;EAC7D;EACD"}