@effect-uai/core 0.4.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 (124) hide show
  1. package/dist/{AiError-csR8Bhxx.d.mts → AiError-CAX_48RU.d.mts} +2 -2
  2. package/dist/{AiError-csR8Bhxx.d.mts.map → AiError-CAX_48RU.d.mts.map} +1 -1
  3. package/dist/{Image-DxyXqzAM.d.mts → Image-HNmMpMTh.d.mts} +4 -4
  4. package/dist/{Image-DxyXqzAM.d.mts.map → Image-HNmMpMTh.d.mts.map} +1 -1
  5. package/dist/{Items-Hg5AsYxl.d.mts → Items-DqbaJoz7.d.mts} +8 -8
  6. package/dist/{Items-Hg5AsYxl.d.mts.map → Items-DqbaJoz7.d.mts.map} +1 -1
  7. package/dist/{StructuredFormat-Cl41C56K.d.mts → StructuredFormat-BbN4dosH.d.mts} +11 -4
  8. package/dist/StructuredFormat-BbN4dosH.d.mts.map +1 -0
  9. package/dist/{Tool-B8B5qVEy.d.mts → Tool-Y0__Py1H.d.mts} +20 -4
  10. package/dist/Tool-Y0__Py1H.d.mts.map +1 -0
  11. package/dist/Turn-ChbL2foc.d.mts +388 -0
  12. package/dist/Turn-ChbL2foc.d.mts.map +1 -0
  13. package/dist/domain/AiError.d.mts +1 -1
  14. package/dist/domain/AiError.mjs +1 -1
  15. package/dist/domain/AiError.mjs.map +1 -1
  16. package/dist/domain/Image.d.mts +1 -1
  17. package/dist/domain/Items.d.mts +1 -1
  18. package/dist/domain/Items.mjs +1 -1
  19. package/dist/domain/Items.mjs.map +1 -1
  20. package/dist/domain/Turn.d.mts +2 -2
  21. package/dist/domain/Turn.mjs +22 -4
  22. package/dist/domain/Turn.mjs.map +1 -1
  23. package/dist/domain/Turn.test.d.mts +1 -0
  24. package/dist/domain/Turn.test.mjs +136 -0
  25. package/dist/domain/Turn.test.mjs.map +1 -0
  26. package/dist/embedding-model/Embedding.d.mts +15 -3
  27. package/dist/embedding-model/Embedding.d.mts.map +1 -1
  28. package/dist/embedding-model/Embedding.mjs.map +1 -1
  29. package/dist/embedding-model/EmbeddingModel.d.mts +33 -17
  30. package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -1
  31. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
  32. package/dist/embedding-model/EmbeddingModel.test.d.mts +1 -0
  33. package/dist/embedding-model/EmbeddingModel.test.mjs +59 -0
  34. package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -0
  35. package/dist/index.d.mts +6 -6
  36. package/dist/language-model/LanguageModel.d.mts +30 -8
  37. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  38. package/dist/language-model/LanguageModel.mjs +33 -3
  39. package/dist/language-model/LanguageModel.mjs.map +1 -1
  40. package/dist/language-model/LanguageModel.test.d.mts +1 -0
  41. package/dist/language-model/LanguageModel.test.mjs +143 -0
  42. package/dist/language-model/LanguageModel.test.mjs.map +1 -0
  43. package/dist/loop/Loop.d.mts +94 -11
  44. package/dist/loop/Loop.d.mts.map +1 -1
  45. package/dist/loop/Loop.mjs +92 -26
  46. package/dist/loop/Loop.mjs.map +1 -1
  47. package/dist/loop/Loop.test.mjs +171 -3
  48. package/dist/loop/Loop.test.mjs.map +1 -1
  49. package/dist/music-generator/MusicGenerator.d.mts +1 -1
  50. package/dist/observability/Metrics.d.mts +1 -1
  51. package/dist/observability/Metrics.mjs +1 -1
  52. package/dist/observability/Metrics.mjs.map +1 -1
  53. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +1 -1
  54. package/dist/streaming/JSONL.d.mts +1 -1
  55. package/dist/streaming/JSONL.d.mts.map +1 -1
  56. package/dist/streaming/JSONL.mjs +7 -12
  57. package/dist/streaming/JSONL.mjs.map +1 -1
  58. package/dist/structured-format/StructuredFormat.d.mts +2 -2
  59. package/dist/structured-format/StructuredFormat.mjs +9 -1
  60. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  61. package/dist/structured-format/StructuredFormat.test.d.mts +1 -0
  62. package/dist/structured-format/StructuredFormat.test.mjs +70 -0
  63. package/dist/structured-format/StructuredFormat.test.mjs.map +1 -0
  64. package/dist/testing/MockMusicGenerator.d.mts.map +1 -1
  65. package/dist/testing/MockMusicGenerator.mjs +2 -2
  66. package/dist/testing/MockMusicGenerator.mjs.map +1 -1
  67. package/dist/testing/MockProvider.d.mts +23 -18
  68. package/dist/testing/MockProvider.d.mts.map +1 -1
  69. package/dist/testing/MockProvider.mjs +56 -72
  70. package/dist/testing/MockProvider.mjs.map +1 -1
  71. package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
  72. package/dist/testing/MockSpeechSynthesizer.mjs +2 -2
  73. package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
  74. package/dist/testing/MockTranscriber.d.mts.map +1 -1
  75. package/dist/testing/MockTranscriber.mjs +2 -2
  76. package/dist/testing/MockTranscriber.mjs.map +1 -1
  77. package/dist/tool/HistoryCheck.d.mts +1 -1
  78. package/dist/tool/Outcome.d.mts +1 -1
  79. package/dist/tool/Resolvers.d.mts +65 -8
  80. package/dist/tool/Resolvers.d.mts.map +1 -1
  81. package/dist/tool/Resolvers.mjs +8 -12
  82. package/dist/tool/Resolvers.mjs.map +1 -1
  83. package/dist/tool/Resolvers.test.mjs +6 -5
  84. package/dist/tool/Resolvers.test.mjs.map +1 -1
  85. package/dist/tool/Tool.d.mts +2 -2
  86. package/dist/tool/Tool.mjs +18 -1
  87. package/dist/tool/Tool.mjs.map +1 -1
  88. package/dist/tool/Tool.test.d.mts +1 -0
  89. package/dist/tool/Tool.test.mjs +66 -0
  90. package/dist/tool/Tool.test.mjs.map +1 -0
  91. package/dist/tool/Toolkit.d.mts +4 -6
  92. package/dist/tool/Toolkit.d.mts.map +1 -1
  93. package/dist/tool/Toolkit.mjs +14 -43
  94. package/dist/tool/Toolkit.mjs.map +1 -1
  95. package/dist/transcriber/Transcriber.d.mts +1 -1
  96. package/package.json +1 -1
  97. package/src/domain/AiError.ts +1 -1
  98. package/src/domain/Items.ts +1 -1
  99. package/src/domain/Turn.test.ts +141 -0
  100. package/src/domain/Turn.ts +50 -43
  101. package/src/embedding-model/Embedding.ts +23 -0
  102. package/src/embedding-model/EmbeddingModel.test.ts +92 -0
  103. package/src/embedding-model/EmbeddingModel.ts +30 -20
  104. package/src/language-model/LanguageModel.test.ts +170 -0
  105. package/src/language-model/LanguageModel.ts +64 -1
  106. package/src/loop/Loop.test.ts +256 -3
  107. package/src/loop/Loop.ts +225 -49
  108. package/src/observability/Metrics.ts +1 -1
  109. package/src/streaming/JSONL.ts +9 -18
  110. package/src/structured-format/StructuredFormat.test.ts +105 -0
  111. package/src/structured-format/StructuredFormat.ts +14 -1
  112. package/src/testing/MockMusicGenerator.ts +4 -6
  113. package/src/testing/MockProvider.ts +126 -105
  114. package/src/testing/MockSpeechSynthesizer.ts +4 -6
  115. package/src/testing/MockTranscriber.ts +4 -6
  116. package/src/tool/Resolvers.test.ts +8 -5
  117. package/src/tool/Resolvers.ts +17 -19
  118. package/src/tool/Tool.test.ts +105 -0
  119. package/src/tool/Tool.ts +20 -0
  120. package/src/tool/Toolkit.ts +49 -50
  121. package/dist/StructuredFormat-Cl41C56K.d.mts.map +0 -1
  122. package/dist/Tool-B8B5qVEy.d.mts.map +0 -1
  123. package/dist/Turn-7geUcKsf.d.mts +0 -194
  124. package/dist/Turn-7geUcKsf.d.mts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"Loop.test.mjs","names":[],"sources":["../../src/loop/Loop.test.ts"],"sourcesContent":["import { Deferred, Effect, Fiber, Latch, Ref, Stream, SubscriptionRef } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport {\n type Event,\n loop,\n loopWithState,\n next,\n nextAfter,\n stopEvent,\n stopAfter,\n value,\n} from \"./Loop.js\"\n\ndescribe(\"Loop.loop\", () => {\n it(\"threads state across iterations and emits each iteration's substream in order\", async () => {\n // Each iter emits [n, n + 0.5] then continues; final iter emits [n] and stops.\n const stream = loop(0, (n: number) =>\n n >= 3\n ? stopAfter(Stream.fromIterable([n]))\n : nextAfter(Stream.fromIterable([n, n + 0.5]), n + 1),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3])\n })\n\n it(\"supports iterations that emit zero values and only decide\", async () => {\n // Every iteration emits nothing, just bumps state; stops at 5.\n const stream = loop(0, (n: number) =>\n n >= 5 ? Stream.fromIterable([stopEvent]) : Stream.fromIterable([next(n + 1)]),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([])\n })\n\n it(\"supports Effect-returning bodies directly (no Stream.unwrap needed)\", async () => {\n // Each iter yields an Effect that doubles the state, then emits it.\n // Body returns Effect<Stream> directly; loop unwraps internally.\n const stream = loop(1, (n: number) =>\n Effect.gen(function* () {\n const doubled = yield* Effect.succeed(n * 2)\n return doubled >= 16\n ? stopAfter(Stream.fromIterable([doubled]))\n : nextAfter(Stream.fromIterable([doubled]), doubled)\n }),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([2, 4, 8, 16])\n })\n\n it(\"still accepts Stream.unwrap-wrapped bodies for backward compatibility\", async () => {\n const stream = loop(1, (n: number) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const doubled = yield* Effect.succeed(n * 2)\n return doubled >= 4\n ? stopAfter(Stream.fromIterable([doubled]))\n : nextAfter(Stream.fromIterable([doubled]), doubled)\n }),\n ),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([2, 4])\n })\n\n it(\"propagates errors from the body's stream\", async () => {\n const boom = new Error(\"boom\")\n const stream = loop(\n 0,\n (n: number): Stream.Stream<Event<number, number>, Error> =>\n n === 2 ? Stream.fail(boom) : Stream.fromIterable([value(n), next(n + 1)]),\n )\n\n const result = await Effect.runPromiseExit(Stream.runCollect(stream))\n expect(result._tag).toBe(\"Failure\")\n })\n\n it(\"terminates silently if the body emits no Decision (mirrors paginate's silent stop)\", async () => {\n // No decision emitted - loop just ends after the body's stream completes.\n const stream = loop(0, (n: number) => Stream.fromIterable([value(n), value(n + 1)]))\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 1])\n })\n\n it(\"short-circuits the body's stream when a Decision is seen\", async () => {\n // Body emits [n, next(n+1), n+10]. Once the Decision is encountered, the\n // body's stream is interrupted - `n+10` is never pulled, so it never\n // flows to the outer stream. This is the correct behavior: a Decision\n // marks \"I'm done with this iteration\"; anything after it is dead code.\n const stream = loop(0, (n: number) =>\n n >= 2\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1), value(n + 10)]),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 1, 2])\n })\n\n it(\"is stack-safe and linear-time across many iterations\", async () => {\n // 100k iterations far exceeds V8's typical stack depth (~10–15k frames).\n const N = 100_000\n const stream = loop(0, (n: number) =>\n n >= N\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1)]),\n )\n\n const count = await Effect.runPromise(\n Stream.runFold(\n stream,\n (): number => 0,\n (acc) => acc + 1,\n ),\n )\n expect(count).toBe(N + 1) // 0..N inclusive\n }, 10_000)\n})\n\n// ---------------------------------------------------------------------------\n// Mock LLM scenario - proves the loop forwards deltas in real time and\n// correctly threads tool-result events between turns.\n// ---------------------------------------------------------------------------\n\ntype Delta =\n | { readonly type: \"text\"; readonly text: string }\n | { readonly type: \"tool_call\"; readonly id: string; readonly name: string }\n\ntype HistoryItem =\n | { readonly type: \"user\"; readonly text: string }\n | { readonly type: \"assistant\"; readonly text: string }\n | { readonly type: \"tool_call\"; readonly id: string; readonly name: string }\n | { readonly type: \"tool_result\"; readonly id: string; readonly output: string }\n\ntype UiEvent =\n | { readonly type: \"text\"; readonly text: string }\n | { readonly type: \"tool_started\"; readonly id: string; readonly name: string }\n | { readonly type: \"tool_result\"; readonly id: string; readonly output: string }\n\ninterface MockModel {\n readonly streamTurn: (history: ReadonlyArray<HistoryItem>) => Stream.Stream<Delta>\n}\n\ninterface State {\n readonly history: ReadonlyArray<HistoryItem>\n readonly model: MockModel\n}\n\ninterface ToolOutcome {\n readonly output: string\n readonly nextModel?: MockModel\n}\n\ntype ToolRunner = (call: { id: string; name: string }) => ToolOutcome\n\nconst scriptedModel = (script: ReadonlyArray<ReadonlyArray<Delta>>): MockModel => {\n let i = 0\n return {\n streamTurn: () => {\n const turn = script[i] ?? []\n i += 1\n return Stream.fromIterable(turn)\n },\n }\n}\n\n/**\n * Body factored out so both tests share it. Per iteration:\n * 1. Stream the model's deltas; tap captures texts + tool calls into Refs.\n * 2. flatMap projects deltas into UiEvents forwarded to the outer stream.\n * 3. Continuation reads the captured calls; if any, runs them, emits\n * tool_result events, builds the next state (with model swap if a tool\n * asked for one), and emits `next(state)`. Otherwise `stop`.\n */\nconst conversationLoop = (initial: State, runTool: ToolRunner) =>\n loop(initial, (state) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const textsRef = yield* Ref.make<ReadonlyArray<string>>([])\n const toolCallsRef = yield* Ref.make<\n ReadonlyArray<{ readonly id: string; readonly name: string }>\n >([])\n\n const deltas: Stream.Stream<Event<UiEvent, State>> = state.model\n .streamTurn(state.history)\n .pipe(\n Stream.tap((d) =>\n d.type === \"text\"\n ? Ref.update(textsRef, (t) => [...t, d.text])\n : Ref.update(toolCallsRef, (t) => [...t, { id: d.id, name: d.name }]),\n ),\n Stream.flatMap(\n (d): Stream.Stream<Event<UiEvent, State>> =>\n d.type === \"text\"\n ? Stream.fromIterable([value<UiEvent>({ type: \"text\", text: d.text })])\n : Stream.fromIterable([\n value<UiEvent>({ type: \"tool_started\", id: d.id, name: d.name }),\n ]),\n ),\n )\n\n const continuation: Stream.Stream<Event<UiEvent, State>> = Stream.unwrap(\n Effect.gen(function* () {\n const texts = yield* Ref.get(textsRef)\n const toolCalls = yield* Ref.get(toolCallsRef)\n\n if (toolCalls.length === 0) {\n return stopAfter(Stream.empty)\n }\n\n const turnItems: ReadonlyArray<HistoryItem> = [\n ...(texts.length > 0 ? [{ type: \"assistant\" as const, text: texts.join(\"\") }] : []),\n ...toolCalls.map(\n (tc): HistoryItem => ({ type: \"tool_call\", id: tc.id, name: tc.name }),\n ),\n ]\n\n const outcomes = toolCalls.map((call) => ({ call, outcome: runTool(call) }))\n\n const events: ReadonlyArray<UiEvent> = outcomes.map(({ call, outcome }) => ({\n type: \"tool_result\",\n id: call.id,\n output: outcome.output,\n }))\n\n const resultItems: ReadonlyArray<HistoryItem> = outcomes.map(\n ({ call, outcome }): HistoryItem => ({\n type: \"tool_result\",\n id: call.id,\n output: outcome.output,\n }),\n )\n\n // Last requested model wins; default to the current one.\n const nextModel = outcomes.reduce(\n (m, { outcome }) => outcome.nextModel ?? m,\n state.model,\n )\n\n const nextState: State = {\n history: [...state.history, ...turnItems, ...resultItems],\n model: nextModel,\n }\n\n return nextAfter(Stream.fromIterable(events), nextState)\n }),\n )\n\n return Stream.concat(deltas, continuation)\n }),\n ),\n )\n\ndescribe(\"Loop.loop - LLM-style scenarios\", () => {\n it(\"forwards text deltas, tool start, tool result, and post-tool text in order\", async () => {\n const m = scriptedModel([\n [\n { type: \"text\", text: \"hello\" },\n { type: \"text\", text: \" \" },\n { type: \"text\", text: \"world\" },\n { type: \"tool_call\", id: \"c1\", name: \"get_time\" },\n ],\n [\n { type: \"text\", text: \" time is \" },\n { type: \"text\", text: \"12:00\" },\n ],\n ])\n\n const runTool: ToolRunner = (call) => ({\n output: call.name === \"get_time\" ? \"12:00\" : \"?\",\n })\n\n const initial: State = {\n history: [{ type: \"user\", text: \"what time is it?\" }],\n model: m,\n }\n\n const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))\n\n expect(events).toEqual([\n { type: \"text\", text: \"hello\" },\n { type: \"text\", text: \" \" },\n { type: \"text\", text: \"world\" },\n { type: \"tool_started\", id: \"c1\", name: \"get_time\" },\n { type: \"tool_result\", id: \"c1\", output: \"12:00\" },\n { type: \"text\", text: \" time is \" },\n { type: \"text\", text: \"12:00\" },\n ])\n })\n\n it(\"model swap mid-stream: m1 calls upgrade, m2 finishes the response\", async () => {\n const m2 = scriptedModel([\n [\n { type: \"text\", text: \"I am m2.\" },\n { type: \"text\", text: \" The answer is 42.\" },\n ],\n ])\n\n const m1 = scriptedModel([\n [\n { type: \"text\", text: \"Hard question.\" },\n { type: \"text\", text: \" Upgrading.\" },\n { type: \"tool_call\", id: \"u1\", name: \"upgrade\" },\n ],\n ])\n\n const runTool: ToolRunner = (call) =>\n call.name === \"upgrade\" ? { output: \"ok\", nextModel: m2 } : { output: \"?\" }\n\n const initial: State = {\n history: [{ type: \"user\", text: \"what is the meaning of life?\" }],\n model: m1,\n }\n\n const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))\n\n expect(events).toEqual([\n { type: \"text\", text: \"Hard question.\" },\n { type: \"text\", text: \" Upgrading.\" },\n { type: \"tool_started\", id: \"u1\", name: \"upgrade\" },\n { type: \"tool_result\", id: \"u1\", output: \"ok\" },\n { type: \"text\", text: \"I am m2.\" },\n { type: \"text\", text: \" The answer is 42.\" },\n ])\n })\n})\n\ndescribe(\"Loop.loop - pull-specific stream semantics\", () => {\n it(\"does not start the next iteration when downstream only takes the first value\", async () => {\n const bodyCalls = await Effect.runPromise(\n Effect.gen(function* () {\n const callsRef = yield* Ref.make(0)\n const stream = loop(0, (n: number) =>\n Stream.unwrap(\n Ref.update(callsRef, (calls) => calls + 1).pipe(\n Effect.as(\n n >= 10\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1)]),\n ),\n ),\n ),\n )\n\n yield* stream.pipe(Stream.take(1), Stream.runCollect)\n return yield* Ref.get(callsRef)\n }),\n )\n\n expect(bodyCalls).toBe(1)\n })\n\n it(\"propagates defects from the body instead of leaving the consumer waiting\", async () => {\n const defect = new Error(\"defect\")\n const stream = loop(0, () => Stream.die(defect))\n\n const result = await Effect.runPromiseExit(Stream.runCollect(stream))\n\n expect(result._tag).toBe(\"Failure\")\n })\n\n it(\"runs body finalizers when a Decision short-circuits the body\", async () => {\n const releases = await Effect.runPromise(\n Effect.gen(function* () {\n const releasesRef = yield* Ref.make<ReadonlyArray<number>>([])\n const stream = loop(0, (n: number) =>\n (n >= 1\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1), value(n + 10)])\n ).pipe(Stream.ensuring(Ref.update(releasesRef, (values) => [...values, n]))),\n )\n\n const values = yield* Stream.runCollect(stream)\n expect(values).toEqual([0, 1])\n return yield* Ref.get(releasesRef)\n }),\n )\n\n expect(releases).toEqual([0, 1])\n })\n\n it(\"runs the active body finalizer when the downstream consumer is interrupted\", async () => {\n const releases = await Effect.runPromise(\n Effect.gen(function* () {\n const started = yield* Deferred.make<void>()\n const releasesRef = yield* Ref.make(0)\n const body = (): Stream.Stream<Event<number, never>> =>\n Stream.concat(\n Stream.fromEffect(Deferred.succeed(started, undefined).pipe(Effect.as(value(0)))),\n Stream.never,\n ).pipe(Stream.ensuring(Ref.update(releasesRef, (n) => n + 1)))\n const stream = loop(0, body)\n\n const fiber = yield* Effect.forkChild(Stream.runCollect(stream))\n yield* Deferred.await(started)\n yield* Fiber.interrupt(fiber)\n\n return yield* Ref.get(releasesRef)\n }),\n )\n\n expect(releases).toBe(1)\n })\n\n it(\"does not create a body scope if constructing the body stream defects\", async () => {\n const defect = new Error(\"body construction failed\")\n const result = await Effect.runPromiseExit(\n Stream.runCollect(\n loop(0, (): Stream.Stream<Event<number, never>> => {\n throw defect\n }),\n ),\n )\n\n expect(result._tag).toBe(\"Failure\")\n })\n})\n\ndescribe(\"Loop.loopWithState\", () => {\n it(\"exposes the final state in the SubscriptionRef after the stream completes\", async () => {\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),\n )\n const values = yield* Stream.runCollect(stream)\n const finalState = yield* SubscriptionRef.get(state)\n return { values: Array.from(values), finalState }\n })\n\n const { values, finalState } = await Effect.runPromise(program)\n expect(values).toEqual([0, 1, 2, 3])\n // Last `next(state)` was `next(3)` before the iteration that emitted Stop.\n expect(finalState).toBe(3)\n })\n\n it(\"the state ref starts at `initial` and stays there if the loop stops without advancing\", async () => {\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState({ count: 7 }, () =>\n Stream.fromIterable([stopEvent]),\n )\n yield* Stream.runDrain(stream)\n return yield* SubscriptionRef.get(state)\n })\n\n expect(await Effect.runPromise(program)).toEqual({ count: 7 })\n })\n\n it(\"a downstream consumer can read the live state between emitted values\", async () => {\n // Body emits one value per iteration, then advances. A `Stream.runForEach`\n // consumer reads the ref each time a value arrives — proving the ref\n // tracks loop state without the body needing to surface it.\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),\n )\n const seen: Array<{ value: number; stateAfter: number }> = []\n yield* Stream.runForEach(stream, (v) =>\n Effect.gen(function* () {\n seen.push({ value: v, stateAfter: yield* SubscriptionRef.get(state) })\n }),\n )\n return seen\n })\n\n // For each iter `n`, the consumer reads the ref between values: it sees\n // the iteration's input state. The terminal iter (n=3) stops without\n // advancing, so its read still shows 3.\n expect(await Effect.runPromise(program)).toEqual([\n { value: 0, stateAfter: 0 },\n { value: 1, stateAfter: 1 },\n { value: 2, stateAfter: 2 },\n { value: 3, stateAfter: 3 },\n ])\n })\n\n it(\"SubscriptionRef.changes emits every state transition to a concurrent observer\", async () => {\n const program = Effect.gen(function* () {\n const start = yield* Latch.make(false)\n\n // Body waits on the latch in iter 0 so the observer can subscribe first.\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n Effect.gen(function* () {\n if (n === 0) yield* Latch.await(start)\n return n >= 3 ? stopAfter(Stream.empty) : nextAfter(Stream.empty, n + 1)\n }),\n )\n\n // Fork the observer; take 4 distinct states (initial + 3 transitions).\n const observerFiber = yield* Effect.forkChild(\n SubscriptionRef.changes(state).pipe(Stream.take(4), Stream.runCollect),\n )\n\n // Give the observer fiber a chance to actually subscribe before the\n // loop starts advancing the ref. Without this, the loop could finish\n // before the observer's pubsub subscription is in place.\n yield* Effect.sleep(\"10 millis\")\n\n yield* Latch.open(start)\n yield* Stream.runDrain(stream)\n\n return Array.from(yield* Fiber.join(observerFiber))\n })\n\n // initial 0, then next(1), next(2), next(3) — four distinct states.\n expect(await Effect.runPromise(program)).toEqual([0, 1, 2, 3])\n })\n\n it(\"does not interfere with the body's value stream\", async () => {\n const program = Effect.gen(function* () {\n const { stream } = yield* loopWithState(0, (n: number) =>\n n >= 3\n ? stopAfter(Stream.fromIterable([n]))\n : nextAfter(Stream.fromIterable([n, n + 0.5]), n + 1),\n )\n return Array.from(yield* Stream.runCollect(stream))\n })\n\n expect(await Effect.runPromise(program)).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3])\n })\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"Loop.test.mjs","names":["AiError.RateLimited"],"sources":["../../src/loop/Loop.test.ts"],"sourcesContent":["import { Deferred, Effect, Fiber, Latch, pipe, Ref, Stream, SubscriptionRef } from \"effect\"\nimport { describe, expect, expectTypeOf, it } from \"vitest\"\nimport * as AiError from \"../domain/AiError.js\"\nimport { TurnEvent } from \"../domain/Turn.js\"\nimport {\n type Event,\n loop,\n loopFrom,\n loopWithState,\n next,\n nextAfter,\n onTurnComplete,\n stop,\n stopAfter,\n stopEvent,\n stopWith,\n value,\n} from \"./Loop.js\"\n\ndescribe(\"Loop.loop\", () => {\n it(\"threads state across iterations and emits each iteration's substream in order\", async () => {\n // Each iter emits [n, n + 0.5] then continues; final iter emits [n] and stops.\n const stream = loop(0, (n: number) =>\n n >= 3\n ? stopAfter(Stream.fromIterable([n]))\n : nextAfter(Stream.fromIterable([n, n + 0.5]), n + 1),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3])\n })\n\n it(\"supports iterations that emit zero values and only decide\", async () => {\n // Every iteration emits nothing, just bumps state; stops at 5.\n const stream = loop(0, (n: number) =>\n n >= 5 ? Stream.fromIterable([stopEvent]) : Stream.fromIterable([next(n + 1)]),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([])\n })\n\n it(\"supports Effect-returning bodies directly (no Stream.unwrap needed)\", async () => {\n // Each iter yields an Effect that doubles the state, then emits it.\n // Body returns Effect<Stream> directly; loop unwraps internally.\n const stream = loop(1, (n: number) =>\n Effect.gen(function* () {\n const doubled = yield* Effect.succeed(n * 2)\n return doubled >= 16\n ? stopAfter(Stream.fromIterable([doubled]))\n : nextAfter(Stream.fromIterable([doubled]), doubled)\n }),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([2, 4, 8, 16])\n })\n\n it(\"still accepts Stream.unwrap-wrapped bodies for backward compatibility\", async () => {\n const stream = loop(1, (n: number) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const doubled = yield* Effect.succeed(n * 2)\n return doubled >= 4\n ? stopAfter(Stream.fromIterable([doubled]))\n : nextAfter(Stream.fromIterable([doubled]), doubled)\n }),\n ),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([2, 4])\n })\n\n it(\"propagates errors from the body's stream\", async () => {\n const boom = new Error(\"boom\")\n const stream = loop(\n 0,\n (n: number): Stream.Stream<Event<number, number>, Error> =>\n n === 2 ? Stream.fail(boom) : Stream.fromIterable([value(n), next(n + 1)]),\n )\n\n const result = await Effect.runPromiseExit(Stream.runCollect(stream))\n expect(result._tag).toBe(\"Failure\")\n })\n\n it(\"terminates silently if the body emits no Decision (mirrors paginate's silent stop)\", async () => {\n // No decision emitted - loop just ends after the body's stream completes.\n const stream = loop(0, (n: number) => Stream.fromIterable([value(n), value(n + 1)]))\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 1])\n })\n\n it(\"short-circuits the body's stream when a Decision is seen\", async () => {\n // Body emits [n, next(n+1), n+10]. Once the Decision is encountered, the\n // body's stream is interrupted - `n+10` is never pulled, so it never\n // flows to the outer stream. This is the correct behavior: a Decision\n // marks \"I'm done with this iteration\"; anything after it is dead code.\n const stream = loop(0, (n: number) =>\n n >= 2\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1), value(n + 10)]),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 1, 2])\n })\n\n it(\"type: data-last (pipe) form preserves the body's E channel\", () => {\n // Regression for the prior inference bug: when used as\n // `pipe(initial, loop(body))`, the body's E must propagate to the outer\n // stream instead of collapsing to `never`. Generics live on the outer\n // return of each overload, so neither calling form can erase them.\n const result = pipe(\n { count: 0 },\n loop((_state) => Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null }))),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited>()\n })\n\n it(\"type: data-first form preserves the body's E channel\", () => {\n const result = loop({ count: 0 }, (_state) =>\n Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null })),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited>()\n })\n\n it(\"type: onTurnComplete inside loop infers S and A from the handler without annotation\", () => {\n // Regression: when piped through loop, onTurnComplete's handler return\n // type (Stream<Event<A, S>>) is the single source of truth for the loop\n // body's element type. The previous workaround required explicit\n // <S, A> at the call site. Now the loop's outer-return generics pull\n // them through automatically.\n type LoopState = { readonly turns: number }\n type ToolEvent = { readonly _tag: \"tool\"; readonly name: string }\n\n const result = pipe(\n { turns: 0 } as LoopState,\n loop((state) =>\n Effect.gen(function* () {\n const deltas: Stream.Stream<TurnEvent> = Stream.empty\n return deltas.pipe(\n onTurnComplete(() =>\n Effect.sync(() =>\n state.turns >= 1\n ? stop\n : nextAfter(Stream.succeed<ToolEvent>({ _tag: \"tool\", name: \"x\" }), {\n turns: state.turns + 1,\n }),\n ),\n ),\n )\n }),\n ),\n )\n\n type Element = typeof result extends Stream.Stream<infer X, unknown, unknown> ? X : never\n expectTypeOf<Element>().toEqualTypeOf<TurnEvent | ToolEvent>()\n })\n\n it(\"onTurnComplete: data-first form (Function.dual) works at runtime\", async () => {\n // Pin both calling forms: deltas.pipe(onTurnComplete(handler)) and\n // onTurnComplete(deltas, handler). Same dispatch as loop's dual.\n const turnComplete: TurnEvent = TurnEvent.TurnComplete({\n turn: { items: [], usage: { input_tokens: 0, output_tokens: 0 }, stop_reason: \"stop\" },\n })\n const textDelta: TurnEvent = TurnEvent.TextDelta({ text: \"hi\" })\n const deltas: Stream.Stream<TurnEvent> = Stream.fromIterable([textDelta, turnComplete])\n\n const dataFirst = onTurnComplete(deltas, () => Effect.sync(() => stop))\n const dataLast = deltas.pipe(onTurnComplete(() => Effect.sync(() => stop)))\n\n const a = await Effect.runPromise(Stream.runCollect(dataFirst))\n const b = await Effect.runPromise(Stream.runCollect(dataLast))\n\n // Two value(delta) wraps + one stop sentinel from the handler.\n expect(a.length).toBe(3)\n expect(b.length).toBe(3)\n })\n\n it(\"is stack-safe and linear-time across many iterations\", async () => {\n // 100k iterations far exceeds V8's typical stack depth (~10–15k frames).\n const N = 100_000\n const stream = loop(0, (n: number) =>\n n >= N\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1)]),\n )\n\n const count = await Effect.runPromise(\n Stream.runFold(\n stream,\n (): number => 0,\n (acc) => acc + 1,\n ),\n )\n expect(count).toBe(N + 1) // 0..N inclusive\n }, 10_000)\n})\n\n// ---------------------------------------------------------------------------\n// Mock LLM scenario - proves the loop forwards deltas in real time and\n// correctly threads tool-result events between turns.\n// ---------------------------------------------------------------------------\n\ntype Delta =\n | { readonly type: \"text\"; readonly text: string }\n | { readonly type: \"tool_call\"; readonly id: string; readonly name: string }\n\ntype HistoryItem =\n | { readonly type: \"user\"; readonly text: string }\n | { readonly type: \"assistant\"; readonly text: string }\n | { readonly type: \"tool_call\"; readonly id: string; readonly name: string }\n | { readonly type: \"tool_result\"; readonly id: string; readonly output: string }\n\ntype UiEvent =\n | { readonly type: \"text\"; readonly text: string }\n | { readonly type: \"tool_started\"; readonly id: string; readonly name: string }\n | { readonly type: \"tool_result\"; readonly id: string; readonly output: string }\n\ninterface MockModel {\n readonly streamTurn: (history: ReadonlyArray<HistoryItem>) => Stream.Stream<Delta>\n}\n\ninterface State {\n readonly history: ReadonlyArray<HistoryItem>\n readonly model: MockModel\n}\n\ninterface ToolOutcome {\n readonly output: string\n readonly nextModel?: MockModel\n}\n\ntype ToolRunner = (call: { id: string; name: string }) => ToolOutcome\n\nconst scriptedModel = (script: ReadonlyArray<ReadonlyArray<Delta>>): MockModel => {\n let i = 0\n return {\n streamTurn: () => {\n const turn = script[i] ?? []\n i += 1\n return Stream.fromIterable(turn)\n },\n }\n}\n\n/**\n * Body factored out so both tests share it. Per iteration:\n * 1. Stream the model's deltas; tap captures texts + tool calls into Refs.\n * 2. flatMap projects deltas into UiEvents forwarded to the outer stream.\n * 3. Continuation reads the captured calls; if any, runs them, emits\n * tool_result events, builds the next state (with model swap if a tool\n * asked for one), and emits `next(state)`. Otherwise `stop`.\n */\nconst conversationLoop = (initial: State, runTool: ToolRunner) =>\n loop(initial, (state) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const textsRef = yield* Ref.make<ReadonlyArray<string>>([])\n const toolCallsRef = yield* Ref.make<\n ReadonlyArray<{ readonly id: string; readonly name: string }>\n >([])\n\n const deltas: Stream.Stream<Event<UiEvent, State>> = state.model\n .streamTurn(state.history)\n .pipe(\n Stream.tap((d) =>\n d.type === \"text\"\n ? Ref.update(textsRef, (t) => [...t, d.text])\n : Ref.update(toolCallsRef, (t) => [...t, { id: d.id, name: d.name }]),\n ),\n Stream.flatMap(\n (d): Stream.Stream<Event<UiEvent, State>> =>\n d.type === \"text\"\n ? Stream.fromIterable([value<UiEvent>({ type: \"text\", text: d.text })])\n : Stream.fromIterable([\n value<UiEvent>({ type: \"tool_started\", id: d.id, name: d.name }),\n ]),\n ),\n )\n\n const continuation: Stream.Stream<Event<UiEvent, State>> = Stream.unwrap(\n Effect.gen(function* () {\n const texts = yield* Ref.get(textsRef)\n const toolCalls = yield* Ref.get(toolCallsRef)\n\n if (toolCalls.length === 0) {\n return stopAfter(Stream.empty)\n }\n\n const turnItems: ReadonlyArray<HistoryItem> = [\n ...(texts.length > 0 ? [{ type: \"assistant\" as const, text: texts.join(\"\") }] : []),\n ...toolCalls.map(\n (tc): HistoryItem => ({ type: \"tool_call\", id: tc.id, name: tc.name }),\n ),\n ]\n\n const outcomes = toolCalls.map((call) => ({ call, outcome: runTool(call) }))\n\n const events: ReadonlyArray<UiEvent> = outcomes.map(({ call, outcome }) => ({\n type: \"tool_result\",\n id: call.id,\n output: outcome.output,\n }))\n\n const resultItems: ReadonlyArray<HistoryItem> = outcomes.map(\n ({ call, outcome }): HistoryItem => ({\n type: \"tool_result\",\n id: call.id,\n output: outcome.output,\n }),\n )\n\n // Last requested model wins; default to the current one.\n const nextModel = outcomes.reduce(\n (m, { outcome }) => outcome.nextModel ?? m,\n state.model,\n )\n\n const nextState: State = {\n history: [...state.history, ...turnItems, ...resultItems],\n model: nextModel,\n }\n\n return nextAfter(Stream.fromIterable(events), nextState)\n }),\n )\n\n return Stream.concat(deltas, continuation)\n }),\n ),\n )\n\ndescribe(\"Loop.loop - LLM-style scenarios\", () => {\n it(\"forwards text deltas, tool start, tool result, and post-tool text in order\", async () => {\n const m = scriptedModel([\n [\n { type: \"text\", text: \"hello\" },\n { type: \"text\", text: \" \" },\n { type: \"text\", text: \"world\" },\n { type: \"tool_call\", id: \"c1\", name: \"get_time\" },\n ],\n [\n { type: \"text\", text: \" time is \" },\n { type: \"text\", text: \"12:00\" },\n ],\n ])\n\n const runTool: ToolRunner = (call) => ({\n output: call.name === \"get_time\" ? \"12:00\" : \"?\",\n })\n\n const initial: State = {\n history: [{ type: \"user\", text: \"what time is it?\" }],\n model: m,\n }\n\n const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))\n\n expect(events).toEqual([\n { type: \"text\", text: \"hello\" },\n { type: \"text\", text: \" \" },\n { type: \"text\", text: \"world\" },\n { type: \"tool_started\", id: \"c1\", name: \"get_time\" },\n { type: \"tool_result\", id: \"c1\", output: \"12:00\" },\n { type: \"text\", text: \" time is \" },\n { type: \"text\", text: \"12:00\" },\n ])\n })\n\n it(\"model swap mid-stream: m1 calls upgrade, m2 finishes the response\", async () => {\n const m2 = scriptedModel([\n [\n { type: \"text\", text: \"I am m2.\" },\n { type: \"text\", text: \" The answer is 42.\" },\n ],\n ])\n\n const m1 = scriptedModel([\n [\n { type: \"text\", text: \"Hard question.\" },\n { type: \"text\", text: \" Upgrading.\" },\n { type: \"tool_call\", id: \"u1\", name: \"upgrade\" },\n ],\n ])\n\n const runTool: ToolRunner = (call) =>\n call.name === \"upgrade\" ? { output: \"ok\", nextModel: m2 } : { output: \"?\" }\n\n const initial: State = {\n history: [{ type: \"user\", text: \"what is the meaning of life?\" }],\n model: m1,\n }\n\n const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))\n\n expect(events).toEqual([\n { type: \"text\", text: \"Hard question.\" },\n { type: \"text\", text: \" Upgrading.\" },\n { type: \"tool_started\", id: \"u1\", name: \"upgrade\" },\n { type: \"tool_result\", id: \"u1\", output: \"ok\" },\n { type: \"text\", text: \"I am m2.\" },\n { type: \"text\", text: \" The answer is 42.\" },\n ])\n })\n})\n\ndescribe(\"Loop.loop - pull-specific stream semantics\", () => {\n it(\"does not start the next iteration when downstream only takes the first value\", async () => {\n const bodyCalls = await Effect.runPromise(\n Effect.gen(function* () {\n const callsRef = yield* Ref.make(0)\n const stream = loop(0, (n: number) =>\n Stream.unwrap(\n Ref.update(callsRef, (calls) => calls + 1).pipe(\n Effect.as(\n n >= 10\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1)]),\n ),\n ),\n ),\n )\n\n yield* stream.pipe(Stream.take(1), Stream.runCollect)\n return yield* Ref.get(callsRef)\n }),\n )\n\n expect(bodyCalls).toBe(1)\n })\n\n it(\"propagates defects from the body instead of leaving the consumer waiting\", async () => {\n const defect = new Error(\"defect\")\n const stream = loop(0, () => Stream.die(defect))\n\n const result = await Effect.runPromiseExit(Stream.runCollect(stream))\n\n expect(result._tag).toBe(\"Failure\")\n })\n\n it(\"runs body finalizers when a Decision short-circuits the body\", async () => {\n const releases = await Effect.runPromise(\n Effect.gen(function* () {\n const releasesRef = yield* Ref.make<ReadonlyArray<number>>([])\n const stream = loop(0, (n: number) =>\n (n >= 1\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1), value(n + 10)])\n ).pipe(Stream.ensuring(Ref.update(releasesRef, (values) => [...values, n]))),\n )\n\n const values = yield* Stream.runCollect(stream)\n expect(values).toEqual([0, 1])\n return yield* Ref.get(releasesRef)\n }),\n )\n\n expect(releases).toEqual([0, 1])\n })\n\n it(\"runs the active body finalizer when the downstream consumer is interrupted\", async () => {\n const releases = await Effect.runPromise(\n Effect.gen(function* () {\n const started = yield* Deferred.make<void>()\n const releasesRef = yield* Ref.make(0)\n const body = (): Stream.Stream<Event<number, never>> =>\n Stream.concat(\n Stream.fromEffect(Deferred.succeed(started, undefined).pipe(Effect.as(value(0)))),\n Stream.never,\n ).pipe(Stream.ensuring(Ref.update(releasesRef, (n) => n + 1)))\n const stream = loop(0, body)\n\n const fiber = yield* Effect.forkChild(Stream.runCollect(stream))\n yield* Deferred.await(started)\n yield* Fiber.interrupt(fiber)\n\n return yield* Ref.get(releasesRef)\n }),\n )\n\n expect(releases).toBe(1)\n })\n\n it(\"does not create a body scope if constructing the body stream defects\", async () => {\n const defect = new Error(\"body construction failed\")\n const result = await Effect.runPromiseExit(\n Stream.runCollect(\n loop(0, (): Stream.Stream<Event<number, never>> => {\n throw defect\n }),\n ),\n )\n\n expect(result._tag).toBe(\"Failure\")\n })\n})\n\ndescribe(\"Loop.loopWithState\", () => {\n it(\"exposes the final state in the SubscriptionRef after the stream completes\", async () => {\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),\n )\n const values = yield* Stream.runCollect(stream)\n const finalState = yield* SubscriptionRef.get(state)\n return { values: Array.from(values), finalState }\n })\n\n const { values, finalState } = await Effect.runPromise(program)\n expect(values).toEqual([0, 1, 2, 3])\n // Last `next(state)` was `next(3)` before the iteration that emitted Stop.\n expect(finalState).toBe(3)\n })\n\n it(\"the state ref starts at `initial` and stays there if the loop stops without advancing\", async () => {\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState({ count: 7 }, () =>\n Stream.fromIterable([stopEvent]),\n )\n yield* Stream.runDrain(stream)\n return yield* SubscriptionRef.get(state)\n })\n\n expect(await Effect.runPromise(program)).toEqual({ count: 7 })\n })\n\n it(\"a downstream consumer can read the live state between emitted values\", async () => {\n // Body emits one value per iteration, then advances. A `Stream.runForEach`\n // consumer reads the ref each time a value arrives — proving the ref\n // tracks loop state without the body needing to surface it.\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),\n )\n const seen: Array<{ value: number; stateAfter: number }> = []\n yield* Stream.runForEach(stream, (v) =>\n Effect.gen(function* () {\n seen.push({ value: v, stateAfter: yield* SubscriptionRef.get(state) })\n }),\n )\n return seen\n })\n\n // For each iter `n`, the consumer reads the ref between values: it sees\n // the iteration's input state. The terminal iter (n=3) stops without\n // advancing, so its read still shows 3.\n expect(await Effect.runPromise(program)).toEqual([\n { value: 0, stateAfter: 0 },\n { value: 1, stateAfter: 1 },\n { value: 2, stateAfter: 2 },\n { value: 3, stateAfter: 3 },\n ])\n })\n\n it(\"SubscriptionRef.changes emits every state transition to a concurrent observer\", async () => {\n const program = Effect.gen(function* () {\n const start = yield* Latch.make(false)\n\n // Body waits on the latch in iter 0 so the observer can subscribe first.\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n Effect.gen(function* () {\n if (n === 0) yield* Latch.await(start)\n return n >= 3 ? stopAfter(Stream.empty) : nextAfter(Stream.empty, n + 1)\n }),\n )\n\n // Fork the observer; take 4 distinct states (initial + 3 transitions).\n const observerFiber = yield* Effect.forkChild(\n SubscriptionRef.changes(state).pipe(Stream.take(4), Stream.runCollect),\n )\n\n // Give the observer fiber a chance to actually subscribe before the\n // loop starts advancing the ref. Without this, the loop could finish\n // before the observer's pubsub subscription is in place.\n yield* Effect.sleep(\"10 millis\")\n\n yield* Latch.open(start)\n yield* Stream.runDrain(stream)\n\n return Array.from(yield* Fiber.join(observerFiber))\n })\n\n // initial 0, then next(1), next(2), next(3) — four distinct states.\n expect(await Effect.runPromise(program)).toEqual([0, 1, 2, 3])\n })\n\n it(\"does not interfere with the body's value stream\", async () => {\n const program = Effect.gen(function* () {\n const { stream } = yield* loopWithState(0, (n: number) =>\n n >= 3\n ? stopAfter(Stream.fromIterable([n]))\n : nextAfter(Stream.fromIterable([n, n + 0.5]), n + 1),\n )\n return Array.from(yield* Stream.runCollect(stream))\n })\n\n expect(await Effect.runPromise(program)).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3])\n })\n})\n\ndescribe(\"Loop.loopFrom\", () => {\n it(\"runs a multi-turn inner loop per input until the body emits stop\", async () => {\n // Per input: emit (input + turnsSoFar) twice, then stop. State counts\n // total turns ACROSS inputs. Demonstrates that `next(s)` continues with\n // the SAME input, multiple times per input — not one body call per item.\n const result = await Effect.runPromise(\n Stream.fromIterable([\"a\", \"b\"]).pipe(\n loopFrom(0, (turns: number, input: string) => {\n if (turns >= 2 * (input === \"a\" ? 1 : 2)) return Stream.fromIterable([stopEvent])\n return Stream.fromIterable([value(`${input}:${turns}`), next(turns + 1)])\n }),\n Stream.runCollect,\n ),\n )\n\n // input=\"a\": turns 0,1 → emit \"a:0\",\"a:1\"; turns=2 → stop. State threads.\n // input=\"b\": turns 2,3 → emit \"b:2\",\"b:3\"; turns=4 → stop.\n expect(result).toEqual([\"a:0\", \"a:1\", \"b:2\", \"b:3\"])\n })\n\n it(\"threads state across inputs (audio-pipeline shape)\", async () => {\n // History accumulates across inputs. Each input emits its joined view of\n // history+input, then `stopWith` ends the inner loop AND carries the\n // updated history to the next input.\n const result = await Effect.runPromise(\n Stream.fromIterable([\"x\", \"y\", \"z\"]).pipe(\n loopFrom([] as ReadonlyArray<string>, (history: ReadonlyArray<string>, input: string) =>\n Stream.fromIterable([\n value([...history, input].join(\",\")),\n stopWith([...history, input]),\n ]),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\"x\", \"x,y\", \"x,y,z\"])\n })\n\n it(\"simulates a stream of documents with multi-turn tool calls per document\", async () => {\n // Document arrives → model \"thinks\" (one text turn) → calls a tool\n // (one tool turn) → emits final text (one text turn) → done.\n // Three turns per document, two documents.\n type Turn =\n | { readonly kind: \"text\"; readonly doc: string; readonly text: string }\n | { readonly kind: \"tool\"; readonly doc: string; readonly tool: string }\n type State = { readonly turn: number; readonly totalTurns: number }\n\n const result = await Effect.runPromise(\n Stream.fromIterable([\"doc1\", \"doc2\"]).pipe(\n loopFrom({ turn: 0, totalTurns: 0 } as State, (state, doc: string) => {\n // Each document runs three turns then stops.\n if (state.turn === 0) {\n return Stream.fromIterable([\n value<Turn>({ kind: \"text\", doc, text: \"thinking\" }),\n next({ turn: 1, totalTurns: state.totalTurns + 1 }),\n ])\n }\n if (state.turn === 1) {\n return Stream.fromIterable([\n value<Turn>({ kind: \"tool\", doc, tool: \"search\" }),\n next({ turn: 2, totalTurns: state.totalTurns + 1 }),\n ])\n }\n // Final turn — `stopWith` emits the final value, advances state\n // (reset turn to 0 for the next document, bump totalTurns), and\n // ends this document's inner loop in one shot.\n return Stream.fromIterable([\n value<Turn>({ kind: \"text\", doc, text: \"final\" }),\n stopWith({ turn: 0, totalTurns: state.totalTurns + 1 }),\n ])\n }),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\n { kind: \"text\", doc: \"doc1\", text: \"thinking\" },\n { kind: \"tool\", doc: \"doc1\", tool: \"search\" },\n { kind: \"text\", doc: \"doc1\", text: \"final\" },\n { kind: \"text\", doc: \"doc2\", text: \"thinking\" },\n { kind: \"tool\", doc: \"doc2\", tool: \"search\" },\n { kind: \"text\", doc: \"doc2\", text: \"final\" },\n ])\n })\n\n it(\"ends cleanly when the input stream ends mid-conversation\", async () => {\n // Single-input case: body advances via `next` then stops cleanly.\n const result = await Effect.runPromise(\n Stream.fromIterable([\"only\"]).pipe(\n loopFrom(0, (turns: number, input: string) =>\n turns >= 2\n ? Stream.fromIterable([stopEvent])\n : Stream.fromIterable([value(`${input}:${turns}`), next(turns + 1)]),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\"only:0\", \"only:1\"])\n })\n\n it(\"body's `stop` advances to the next input (does NOT halt the whole stream)\", async () => {\n // Three inputs, body always stops on its first emission. All three\n // are processed — `stop` is per-input, not global. To halt the whole\n // stream, end the INPUT stream upstream.\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3]).pipe(\n loopFrom(0, (_state: number, input: number) =>\n Stream.fromIterable([value(input * 10), stopEvent]),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([10, 20, 30])\n })\n\n it(\"data-first form (Function.dual) runs identically to data-last\", async () => {\n const inputs = Stream.fromIterable([1, 2])\n const result = await Effect.runPromise(\n Stream.runCollect(\n loopFrom(inputs, 0, (state: number, input: number) =>\n state >= input\n ? Stream.fromIterable([stopEvent])\n : Stream.fromIterable([value(state + input), next(state + 1)]),\n ),\n ),\n )\n\n // input=1: state=0 → emit 1, state→1; state=1≥1 → stop.\n // input=2: state=1 → emit 3, state→2; state=2≥2 → stop.\n expect(result).toEqual([1, 3])\n })\n\n it(\"supports Effect-returning bodies (parity with loop)\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([\"a\"]).pipe(\n loopFrom(0, (turns: number, input: string) =>\n Effect.gen(function* () {\n const cur = yield* Effect.succeed(turns)\n if (cur >= 2) return Stream.fromIterable([stopEvent])\n return Stream.fromIterable([value(`${input}:${cur}`), next(cur + 1)])\n }),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\"a:0\", \"a:1\"])\n })\n\n it(\"type: data-last (pipe) form preserves the body's E channel\", () => {\n const result = pipe(\n Stream.fromIterable([1]),\n loopFrom(0, (_state: number, _input: number) =>\n Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null })),\n ),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited>()\n })\n\n it(\"type: data-first form preserves the body's E channel and unifies with input's E\", () => {\n const input: Stream.Stream<number, Error> = Stream.fail(new Error(\"boom\"))\n const result = loopFrom(input, 0, (_state: number, _i: number) =>\n Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null })),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited | Error>()\n })\n})\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { t as AiError } from "../AiError-csR8Bhxx.mjs";
1
+ import { t as AiError } from "../AiError-CAX_48RU.mjs";
2
2
  import { n as AudioChunk } from "../Audio-BfCTGnH3.mjs";
3
3
  import { CommonGenerateMusicRequest, CommonStreamGenerateMusicRequest, MusicResult, MusicSessionInput, WeightedPrompt } from "../domain/Music.mjs";
4
4
  import { Context, Effect, Stream } from "effect";
@@ -35,7 +35,7 @@ type RatePoint<A> = {
35
35
  * The weight is the unit you care about - bytes, tokens, error count, etc.
36
36
  * For tokens-per-second on `TurnEvent`, pass:
37
37
  *
38
- * `(d) => d.type === "text_delta" ? countTokens(d.text) : 0`
38
+ * `(d) => d._tag === "TextDelta" ? countTokens(d.text) : 0`
39
39
  *
40
40
  * Use any tokenizer you like; the library does not ship one.
41
41
  */
@@ -31,7 +31,7 @@ const timeToFirst = (predicate) => (self) => withElapsed(self).pipe(Stream.filte
31
31
  * The weight is the unit you care about - bytes, tokens, error count, etc.
32
32
  * For tokens-per-second on `TurnEvent`, pass:
33
33
  *
34
- * `(d) => d.type === "text_delta" ? countTokens(d.text) : 0`
34
+ * `(d) => d._tag === "TextDelta" ? countTokens(d.text) : 0`
35
35
  *
36
36
  * Use any tokenizer you like; the library does not ship one.
37
37
  */
@@ -1 +1 @@
1
- {"version":3,"file":"Metrics.mjs","names":[],"sources":["../../src/observability/Metrics.ts"],"sourcesContent":["import { Clock, Duration, Effect, Option, Stream } from \"effect\"\n\n/**\n * Annotate every event in a stream with the elapsed `Duration` since the\n * stream started consuming. The first event reports its time-from-start,\n * which is also the conventional \"time to first ____\" metric.\n */\nexport const withElapsed = <A, E, R>(\n self: Stream.Stream<A, E, R>,\n): Stream.Stream<{ readonly value: A; readonly elapsed: Duration.Duration }, E, R> =>\n Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (start) =>\n self.pipe(\n Stream.mapEffect((value) =>\n Effect.map(Clock.currentTimeMillis, (now) => ({\n value,\n elapsed: Duration.millis(now - start),\n })),\n ),\n ),\n ),\n )\n\n/**\n * Compute the elapsed time until the first event matching the predicate.\n * Returns `Option.none()` if the stream completes without one.\n *\n * Consumes the stream. To track this *alongside* live consumption, use\n * `Stream.broadcast` to fan the source out and run `timeToFirst` on one\n * branch.\n */\nexport const timeToFirst =\n <A>(predicate: (a: A) => boolean) =>\n <E, R>(self: Stream.Stream<A, E, R>): Effect.Effect<Option.Option<Duration.Duration>, E, R> =>\n withElapsed(self).pipe(\n Stream.filter(({ value }) => predicate(value)),\n Stream.runHead,\n Effect.map(Option.map(({ elapsed }) => elapsed)),\n )\n\nexport type RatePoint<A> = {\n readonly value: A\n readonly total: number\n readonly ratePerSecond: number\n readonly elapsed: Duration.Duration\n}\n\n/**\n * Annotate every event with a running total and a rolling rate per second,\n * computed from a user-supplied weight function.\n *\n * The weight is the unit you care about - bytes, tokens, error count, etc.\n * For tokens-per-second on `TurnEvent`, pass:\n *\n * `(d) => d.type === \"text_delta\" ? countTokens(d.text) : 0`\n *\n * Use any tokenizer you like; the library does not ship one.\n */\nexport const withRate =\n <A>(weight: (a: A) => number) =>\n <E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<RatePoint<A>, E, R> =>\n Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (start) =>\n self.pipe(\n Stream.mapAccumEffect(\n () => ({ total: 0 }),\n (acc, value) =>\n Effect.map(Clock.currentTimeMillis, (now) => {\n const total = acc.total + weight(value)\n const elapsedMs = now - start\n const ratePerSecond = elapsedMs > 0 ? (total / elapsedMs) * 1000 : 0\n return [\n { total },\n [\n {\n value,\n total,\n ratePerSecond,\n elapsed: Duration.millis(elapsedMs),\n } satisfies RatePoint<A>,\n ],\n ] as const\n }),\n ),\n ),\n ),\n )\n"],"mappings":";;;;;;;;;;;;;AAOA,MAAa,eACX,SAEA,OAAO,OACL,OAAO,IAAI,MAAM,oBAAoB,UACnC,KAAK,KACH,OAAO,WAAW,UAChB,OAAO,IAAI,MAAM,oBAAoB,SAAS;CAC5C;CACA,SAAS,SAAS,OAAO,MAAM,MAAM;CACtC,EAAE,CACJ,CACF,CACF,CACF;;;;;;;;;AAUH,MAAa,eACP,eACG,SACL,YAAY,KAAK,CAAC,KAChB,OAAO,QAAQ,EAAE,YAAY,UAAU,MAAM,CAAC,EAC9C,OAAO,SACP,OAAO,IAAI,OAAO,KAAK,EAAE,cAAc,QAAQ,CAAC,CACjD;;;;;;;;;;;;AAoBL,MAAa,YACP,YACG,SACL,OAAO,OACL,OAAO,IAAI,MAAM,oBAAoB,UACnC,KAAK,KACH,OAAO,sBACE,EAAE,OAAO,GAAG,IAClB,KAAK,UACJ,OAAO,IAAI,MAAM,oBAAoB,QAAQ;CAC3C,MAAM,QAAQ,IAAI,QAAQ,OAAO,MAAM;CACvC,MAAM,YAAY,MAAM;CACxB,MAAM,gBAAgB,YAAY,IAAK,QAAQ,YAAa,MAAO;AACnE,QAAO,CACL,EAAE,OAAO,EACT,CACE;EACE;EACA;EACA;EACA,SAAS,SAAS,OAAO,UAAU;EACpC,CACF,CACF;EACD,CACL,CACF,CACF,CACF"}
1
+ {"version":3,"file":"Metrics.mjs","names":[],"sources":["../../src/observability/Metrics.ts"],"sourcesContent":["import { Clock, Duration, Effect, Option, Stream } from \"effect\"\n\n/**\n * Annotate every event in a stream with the elapsed `Duration` since the\n * stream started consuming. The first event reports its time-from-start,\n * which is also the conventional \"time to first ____\" metric.\n */\nexport const withElapsed = <A, E, R>(\n self: Stream.Stream<A, E, R>,\n): Stream.Stream<{ readonly value: A; readonly elapsed: Duration.Duration }, E, R> =>\n Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (start) =>\n self.pipe(\n Stream.mapEffect((value) =>\n Effect.map(Clock.currentTimeMillis, (now) => ({\n value,\n elapsed: Duration.millis(now - start),\n })),\n ),\n ),\n ),\n )\n\n/**\n * Compute the elapsed time until the first event matching the predicate.\n * Returns `Option.none()` if the stream completes without one.\n *\n * Consumes the stream. To track this *alongside* live consumption, use\n * `Stream.broadcast` to fan the source out and run `timeToFirst` on one\n * branch.\n */\nexport const timeToFirst =\n <A>(predicate: (a: A) => boolean) =>\n <E, R>(self: Stream.Stream<A, E, R>): Effect.Effect<Option.Option<Duration.Duration>, E, R> =>\n withElapsed(self).pipe(\n Stream.filter(({ value }) => predicate(value)),\n Stream.runHead,\n Effect.map(Option.map(({ elapsed }) => elapsed)),\n )\n\nexport type RatePoint<A> = {\n readonly value: A\n readonly total: number\n readonly ratePerSecond: number\n readonly elapsed: Duration.Duration\n}\n\n/**\n * Annotate every event with a running total and a rolling rate per second,\n * computed from a user-supplied weight function.\n *\n * The weight is the unit you care about - bytes, tokens, error count, etc.\n * For tokens-per-second on `TurnEvent`, pass:\n *\n * `(d) => d._tag === \"TextDelta\" ? countTokens(d.text) : 0`\n *\n * Use any tokenizer you like; the library does not ship one.\n */\nexport const withRate =\n <A>(weight: (a: A) => number) =>\n <E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<RatePoint<A>, E, R> =>\n Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (start) =>\n self.pipe(\n Stream.mapAccumEffect(\n () => ({ total: 0 }),\n (acc, value) =>\n Effect.map(Clock.currentTimeMillis, (now) => {\n const total = acc.total + weight(value)\n const elapsedMs = now - start\n const ratePerSecond = elapsedMs > 0 ? (total / elapsedMs) * 1000 : 0\n return [\n { total },\n [\n {\n value,\n total,\n ratePerSecond,\n elapsed: Duration.millis(elapsedMs),\n } satisfies RatePoint<A>,\n ],\n ] as const\n }),\n ),\n ),\n ),\n )\n"],"mappings":";;;;;;;;;;;;;AAOA,MAAa,eACX,SAEA,OAAO,OACL,OAAO,IAAI,MAAM,oBAAoB,UACnC,KAAK,KACH,OAAO,WAAW,UAChB,OAAO,IAAI,MAAM,oBAAoB,SAAS;CAC5C;CACA,SAAS,SAAS,OAAO,MAAM,MAAM;CACtC,EAAE,CACJ,CACF,CACF,CACF;;;;;;;;;AAUH,MAAa,eACP,eACG,SACL,YAAY,KAAK,CAAC,KAChB,OAAO,QAAQ,EAAE,YAAY,UAAU,MAAM,CAAC,EAC9C,OAAO,SACP,OAAO,IAAI,OAAO,KAAK,EAAE,cAAc,QAAQ,CAAC,CACjD;;;;;;;;;;;;AAoBL,MAAa,YACP,YACG,SACL,OAAO,OACL,OAAO,IAAI,MAAM,oBAAoB,UACnC,KAAK,KACH,OAAO,sBACE,EAAE,OAAO,GAAG,IAClB,KAAK,UACJ,OAAO,IAAI,MAAM,oBAAoB,QAAQ;CAC3C,MAAM,QAAQ,IAAI,QAAQ,OAAO,MAAM;CACvC,MAAM,YAAY,MAAM;CACxB,MAAM,gBAAgB,YAAY,IAAK,QAAQ,YAAa,MAAO;AACnE,QAAO,CACL,EAAE,OAAO,EACT,CACE;EACE;EACA;EACA;EACA,SAAS,SAAS,OAAO,UAAU;EACpC,CACF,CACF;EACD,CACL,CACF,CACF,CACF"}
@@ -1,4 +1,4 @@
1
- import { t as AiError } from "../AiError-csR8Bhxx.mjs";
1
+ import { t as AiError } from "../AiError-CAX_48RU.mjs";
2
2
  import { n as AudioChunk, r as AudioFormat, t as AudioBlob } from "../Audio-BfCTGnH3.mjs";
3
3
  import { Context, Effect, Stream } from "effect";
4
4
 
@@ -30,7 +30,7 @@ declare const parse: <A, I>(schema: Schema.Codec<A, I>) => <E, R>(self: Stream.S
30
30
  * non-JSON or partially-received frames silently rather than fail the
31
31
  * entire session over one bad frame.
32
32
  */
33
- declare const parseSafe: (raw: string) => Effect.Effect<unknown, never, never>;
33
+ declare const parseSafe: (raw: string) => Effect.Effect<unknown>;
34
34
  /**
35
35
  * Serialize a stream of values to JSONL bytes. Encodes each value via
36
36
  * `Schema.encodeUnknownSync`. Each line ends with `\n`.
@@ -1 +1 @@
1
- {"version":3,"file":"JSONL.d.mts","names":[],"sources":["../../src/streaming/JSONL.ts"],"mappings":";;;;;;;;cAAqD,mBAAA;;;cAExC,cAAA,SAAuB,mBAAA;EAAA,SACzB,IAAA;EAAA,SACA,KAAA;AAAA;;;;;cA4CE,SAAA,SACX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA;;;;;;cAaf,KAAA,SACJ,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,aACxB,IAAA,EAAM,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,cAAA,GAAiB,CAAA,EAAG,CAAA;;;;;;;cAsBrE,SAAA,GAAa,GAAA,aAAW,MAAA,CAAA,MAAA;;;;;cAYxB,OAAA,SACJ,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,aACxB,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA"}
1
+ {"version":3,"file":"JSONL.d.mts","names":[],"sources":["../../src/streaming/JSONL.ts"],"mappings":";;;;;;;;cAAqD,mBAAA;;;cAExC,cAAA,SAAuB,mBAAA;EAAA,SACzB,IAAA;EAAA,SACA,KAAA;AAAA;;;;;cA4CE,SAAA,SACX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA;;;;;;cAaf,KAAA,SAAe,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,aAEpC,IAAA,EAAM,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,cAAA,GAAiB,CAAA,EAAG,CAAA;;;;;;;cAgB5E,SAAA,GAAa,GAAA,aAAc,MAAA,CAAO,MAAA;;;;;cASlC,OAAA,SACJ,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,aACxB,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA"}
@@ -27,26 +27,21 @@ const fromBytes = (self) => self.pipe(decodeText, Stream.map((s) => s.replace(/\
27
27
  * decode errors both surface as a `JsonParseError` so callers can `catchTag`
28
28
  * uniformly.
29
29
  */
30
- const parse = (schema) => (self) => self.pipe(Stream.mapEffect((line) => Effect.try({
31
- try: () => JSON.parse(line),
32
- catch: (cause) => new JsonParseError({
30
+ const parse = (schema) => {
31
+ const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(schema));
32
+ return (self) => self.pipe(Stream.mapEffect((line) => decode(line).pipe(Effect.mapError((cause) => new JsonParseError({
33
33
  line,
34
34
  cause
35
- })
36
- }).pipe(Effect.flatMap((value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError((cause) => new JsonParseError({
37
- line,
38
- cause
39
- })))))));
35
+ })))));
36
+ };
37
+ const decodeUnknownFromJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
40
38
  /**
41
39
  * Best-effort parse of a single JSON frame. Returns the parsed value or
42
40
  * `undefined` on malformed input. Realtime WS adapters use this to skip
43
41
  * non-JSON or partially-received frames silently rather than fail the
44
42
  * entire session over one bad frame.
45
43
  */
46
- const parseSafe = (raw) => Effect.try({
47
- try: () => JSON.parse(raw),
48
- catch: () => void 0
49
- }).pipe(Effect.orElseSucceed(() => void 0));
44
+ const parseSafe = (raw) => decodeUnknownFromJson(raw).pipe(Effect.orElseSucceed(() => void 0));
50
45
  const encoder = new TextEncoder();
51
46
  /**
52
47
  * Serialize a stream of values to JSONL bytes. Encodes each value via
@@ -1 +1 @@
1
- {"version":3,"file":"JSONL.mjs","names":[],"sources":["../../src/streaming/JSONL.ts"],"sourcesContent":["import { Data, Effect, Schema, Stream } from \"effect\"\n\nexport class JsonParseError extends Data.TaggedError(\"JsonParseError\")<{\n readonly line: string\n readonly cause: unknown\n}> {}\n\n// ---------------------------------------------------------------------------\n// Generic stream helpers (kept module-local; see SSE.ts for the same shape).\n// ---------------------------------------------------------------------------\n\nconst decodeText = <E, R>(self: Stream.Stream<Uint8Array, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): TextDecoder => new TextDecoder(\"utf-8\"),\n (decoder, chunk: Uint8Array) => [decoder, [decoder.decode(chunk, { stream: true })]] as const,\n {\n onHalt: (decoder: TextDecoder) => {\n const tail = decoder.decode()\n return tail.length > 0 ? [tail] : []\n },\n },\n ),\n )\n\nconst splitOn =\n (separator: string) =>\n <E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): string => \"\",\n (buffer, chunk: string) => {\n const parts = (buffer + chunk).split(separator)\n const tail = parts[parts.length - 1] ?? \"\"\n return [tail, parts.slice(0, -1)] as const\n },\n { onHalt: (tail: string) => (tail.length > 0 ? [tail] : []) },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Decode a `Stream<Uint8Array>` into a `Stream<string>` of newline-delimited\n * lines. Empty lines are skipped. Buffers across chunk boundaries.\n */\nexport const fromBytes = <E, R>(\n self: Stream.Stream<Uint8Array, E, R>,\n): Stream.Stream<string, E, R> =>\n self.pipe(\n decodeText,\n Stream.map((s) => s.replace(/\\r/g, \"\")),\n splitOn(\"\\n\"),\n Stream.filter((line) => line.length > 0),\n )\n\n/**\n * Validate each JSONL line against a Schema. JSON parse errors and Schema\n * decode errors both surface as a `JsonParseError` so callers can `catchTag`\n * uniformly.\n */\nexport const parse =\n <A, I>(schema: Schema.Codec<A, I>) =>\n <E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<A, JsonParseError | E, R> =>\n self.pipe(\n Stream.mapEffect((line) =>\n Effect.try({\n try: () => JSON.parse(line) as unknown,\n catch: (cause) => new JsonParseError({ line, cause }),\n }).pipe(\n Effect.flatMap((value) =>\n Schema.decodeUnknownEffect(schema)(value).pipe(\n Effect.mapError((cause) => new JsonParseError({ line, cause })),\n ),\n ),\n ),\n ),\n )\n\n/**\n * Best-effort parse of a single JSON frame. Returns the parsed value or\n * `undefined` on malformed input. Realtime WS adapters use this to skip\n * non-JSON or partially-received frames silently rather than fail the\n * entire session over one bad frame.\n */\nexport const parseSafe = (raw: string) =>\n Effect.try({\n try: () => JSON.parse(raw) as unknown,\n catch: () => undefined,\n }).pipe(Effect.orElseSucceed(() => undefined))\n\nconst encoder = new TextEncoder()\n\n/**\n * Serialize a stream of values to JSONL bytes. Encodes each value via\n * `Schema.encodeUnknownSync`. Each line ends with `\\n`.\n */\nexport const toBytes =\n <A, I>(schema: Schema.Codec<A, I>) =>\n <E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<Uint8Array, E, R> =>\n self.pipe(\n Stream.map((value) => {\n const encoded = Schema.encodeUnknownSync(schema)(value)\n return encoder.encode(JSON.stringify(encoded) + \"\\n\")\n }),\n )\n"],"mappings":";;;;;;;;;;AAEA,IAAa,iBAAb,cAAoC,KAAK,YAAY,iBAAiB,CAGnE;AAMH,MAAM,cAAoB,SACxB,KAAK,KACH,OAAO,eACc,IAAI,YAAY,QAAQ,GAC1C,SAAS,UAAsB,CAAC,SAAS,CAAC,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC,CAAC,CAAC,EACpF,EACE,SAAS,YAAyB;CAChC,MAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAO,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE;GAEvC,CACF,CACF;AAEH,MAAM,WACH,eACM,SACL,KAAK,KACH,OAAO,eACS,KACb,QAAQ,UAAkB;CACzB,MAAM,SAAS,SAAS,OAAO,MAAM,UAAU;AAE/C,QAAO,CADM,MAAM,MAAM,SAAS,MAAM,IAC1B,MAAM,MAAM,GAAG,GAAG,CAAC;GAEnC,EAAE,SAAS,SAAkB,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE,EAAG,CAC9D,CACF;;;;;AAUL,MAAa,aACX,SAEA,KAAK,KACH,YACA,OAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,CAAC,EACvC,QAAQ,KAAK,EACb,OAAO,QAAQ,SAAS,KAAK,SAAS,EAAE,CACzC;;;;;;AAOH,MAAa,SACJ,YACA,SACL,KAAK,KACH,OAAO,WAAW,SAChB,OAAO,IAAI;CACT,WAAW,KAAK,MAAM,KAAK;CAC3B,QAAQ,UAAU,IAAI,eAAe;EAAE;EAAM;EAAO,CAAC;CACtD,CAAC,CAAC,KACD,OAAO,SAAS,UACd,OAAO,oBAAoB,OAAO,CAAC,MAAM,CAAC,KACxC,OAAO,UAAU,UAAU,IAAI,eAAe;CAAE;CAAM;CAAO,CAAC,CAAC,CAChE,CACF,CACF,CACF,CACF;;;;;;;AAQL,MAAa,aAAa,QACxB,OAAO,IAAI;CACT,WAAW,KAAK,MAAM,IAAI;CAC1B,aAAa,KAAA;CACd,CAAC,CAAC,KAAK,OAAO,oBAAoB,KAAA,EAAU,CAAC;AAEhD,MAAM,UAAU,IAAI,aAAa;;;;;AAMjC,MAAa,WACJ,YACA,SACL,KAAK,KACH,OAAO,KAAK,UAAU;CACpB,MAAM,UAAU,OAAO,kBAAkB,OAAO,CAAC,MAAM;AACvD,QAAO,QAAQ,OAAO,KAAK,UAAU,QAAQ,GAAG,KAAK;EACrD,CACH"}
1
+ {"version":3,"file":"JSONL.mjs","names":[],"sources":["../../src/streaming/JSONL.ts"],"sourcesContent":["import { Data, Effect, Schema, Stream } from \"effect\"\n\nexport class JsonParseError extends Data.TaggedError(\"JsonParseError\")<{\n readonly line: string\n readonly cause: unknown\n}> {}\n\n// ---------------------------------------------------------------------------\n// Generic stream helpers (kept module-local; see SSE.ts for the same shape).\n// ---------------------------------------------------------------------------\n\nconst decodeText = <E, R>(self: Stream.Stream<Uint8Array, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): TextDecoder => new TextDecoder(\"utf-8\"),\n (decoder, chunk: Uint8Array) => [decoder, [decoder.decode(chunk, { stream: true })]] as const,\n {\n onHalt: (decoder: TextDecoder) => {\n const tail = decoder.decode()\n return tail.length > 0 ? [tail] : []\n },\n },\n ),\n )\n\nconst splitOn =\n (separator: string) =>\n <E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): string => \"\",\n (buffer, chunk: string) => {\n const parts = (buffer + chunk).split(separator)\n const tail = parts[parts.length - 1] ?? \"\"\n return [tail, parts.slice(0, -1)] as const\n },\n { onHalt: (tail: string) => (tail.length > 0 ? [tail] : []) },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Decode a `Stream<Uint8Array>` into a `Stream<string>` of newline-delimited\n * lines. Empty lines are skipped. Buffers across chunk boundaries.\n */\nexport const fromBytes = <E, R>(\n self: Stream.Stream<Uint8Array, E, R>,\n): Stream.Stream<string, E, R> =>\n self.pipe(\n decodeText,\n Stream.map((s) => s.replace(/\\r/g, \"\")),\n splitOn(\"\\n\"),\n Stream.filter((line) => line.length > 0),\n )\n\n/**\n * Validate each JSONL line against a Schema. JSON parse errors and Schema\n * decode errors both surface as a `JsonParseError` so callers can `catchTag`\n * uniformly.\n */\nexport const parse = <A, I>(schema: Schema.Codec<A, I>) => {\n const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(schema))\n return <E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<A, JsonParseError | E, R> =>\n self.pipe(\n Stream.mapEffect((line) =>\n decode(line).pipe(Effect.mapError((cause) => new JsonParseError({ line, cause }))),\n ),\n )\n}\n\nconst decodeUnknownFromJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))\n\n/**\n * Best-effort parse of a single JSON frame. Returns the parsed value or\n * `undefined` on malformed input. Realtime WS adapters use this to skip\n * non-JSON or partially-received frames silently rather than fail the\n * entire session over one bad frame.\n */\nexport const parseSafe = (raw: string): Effect.Effect<unknown> =>\n decodeUnknownFromJson(raw).pipe(Effect.orElseSucceed(() => undefined))\n\nconst encoder = new TextEncoder()\n\n/**\n * Serialize a stream of values to JSONL bytes. Encodes each value via\n * `Schema.encodeUnknownSync`. Each line ends with `\\n`.\n */\nexport const toBytes =\n <A, I>(schema: Schema.Codec<A, I>) =>\n <E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<Uint8Array, E, R> =>\n self.pipe(\n Stream.map((value) => {\n const encoded = Schema.encodeUnknownSync(schema)(value)\n return encoder.encode(JSON.stringify(encoded) + \"\\n\")\n }),\n )\n"],"mappings":";;;;;;;;;;AAEA,IAAa,iBAAb,cAAoC,KAAK,YAAY,iBAAiB,CAGnE;AAMH,MAAM,cAAoB,SACxB,KAAK,KACH,OAAO,eACc,IAAI,YAAY,QAAQ,GAC1C,SAAS,UAAsB,CAAC,SAAS,CAAC,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC,CAAC,CAAC,EACpF,EACE,SAAS,YAAyB;CAChC,MAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAO,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE;GAEvC,CACF,CACF;AAEH,MAAM,WACH,eACM,SACL,KAAK,KACH,OAAO,eACS,KACb,QAAQ,UAAkB;CACzB,MAAM,SAAS,SAAS,OAAO,MAAM,UAAU;AAE/C,QAAO,CADM,MAAM,MAAM,SAAS,MAAM,IAC1B,MAAM,MAAM,GAAG,GAAG,CAAC;GAEnC,EAAE,SAAS,SAAkB,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE,EAAG,CAC9D,CACF;;;;;AAUL,MAAa,aACX,SAEA,KAAK,KACH,YACA,OAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,CAAC,EACvC,QAAQ,KAAK,EACb,OAAO,QAAQ,SAAS,KAAK,SAAS,EAAE,CACzC;;;;;;AAOH,MAAa,SAAe,WAA+B;CACzD,MAAM,SAAS,OAAO,oBAAoB,OAAO,eAAe,OAAO,CAAC;AACxE,SAAc,SACZ,KAAK,KACH,OAAO,WAAW,SAChB,OAAO,KAAK,CAAC,KAAK,OAAO,UAAU,UAAU,IAAI,eAAe;EAAE;EAAM;EAAO,CAAC,CAAC,CAAC,CACnF,CACF;;AAGL,MAAM,wBAAwB,OAAO,oBAAoB,OAAO,eAAe,OAAO,QAAQ,CAAC;;;;;;;AAQ/F,MAAa,aAAa,QACxB,sBAAsB,IAAI,CAAC,KAAK,OAAO,oBAAoB,KAAA,EAAU,CAAC;AAExE,MAAM,UAAU,IAAI,aAAa;;;;;AAMjC,MAAa,WACJ,YACA,SACL,KAAK,KACH,OAAO,KAAK,UAAU;CACpB,MAAM,UAAU,OAAO,kBAAkB,OAAO,CAAC,MAAM;AACvD,QAAO,QAAQ,OAAO,KAAK,UAAU,QAAQ,GAAG,KAAK;EACrD,CACH"}
@@ -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-Cl41C56K.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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"}
@@ -1 +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;;;;;;cAgFvD,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;;;AA1FnC;;;;;cA0Ha,uBAAA,GACX,MAAA,EAAQ,wBAAA;EAAA,SAEC,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,cAAA;EAAA,SACnB,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,0BAAA;AAAA"}
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"}
@@ -11,10 +11,10 @@ const makeService = (script, record) => Effect.gen(function* () {
11
11
  yield* record.generate(request);
12
12
  const i = yield* Ref.getAndUpdate(gCursor, (n) => n + 1);
13
13
  const scripted = script.results ?? [];
14
- if (i >= scripted.length) return yield* Effect.fail(new InvalidRequest({
14
+ if (i >= scripted.length) return yield* new InvalidRequest({
15
15
  provider: "mock",
16
16
  raw: `MockMusicGenerator exhausted: ${scripted.length} results scripted, but call ${i + 1} was made`
17
- }));
17
+ });
18
18
  return scripted[i];
19
19
  }),
20
20
  streamGeneration: (request) => Stream.unwrap(Effect.gen(function* () {
@@ -1 +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* Effect.fail(\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockMusicGenerator exhausted: ${scripted.length} results scripted, but call ${i + 1} was made`,\n }),\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":"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"}
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"}