@effect-uai/core 0.12.0 → 0.13.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.
- package/dist/{AiError-Bf1pq4BB.d.mts → AiError-eLV9t1lU.d.mts} +12 -16
- package/dist/AiError-eLV9t1lU.d.mts.map +1 -0
- package/dist/{Audio-CR7CSljJ.d.mts → Audio-60lKKHxS.d.mts} +4 -5
- package/dist/Audio-60lKKHxS.d.mts.map +1 -0
- package/dist/{Image-C4vuBmu0.d.mts → Image-BK8-4H6s.d.mts} +12 -14
- package/dist/Image-BK8-4H6s.d.mts.map +1 -0
- package/dist/{Items-BKQgqJ8A.d.mts → Items-SfUabNuB.d.mts} +74 -76
- package/dist/Items-SfUabNuB.d.mts.map +1 -0
- package/dist/{Loop-D8nWpowc.d.mts → Loop-B54AB1ME.d.mts} +19 -21
- package/dist/Loop-B54AB1ME.d.mts.map +1 -0
- package/dist/{Media-D_CpcM1Z.d.mts → Media-DwgpifPD.d.mts} +1 -1
- package/dist/Media-DwgpifPD.d.mts.map +1 -0
- package/dist/{SandboxError-CIrVDOMV.d.mts → SandboxError-BN8b28Bh.d.mts} +11 -15
- package/dist/SandboxError-BN8b28Bh.d.mts.map +1 -0
- package/dist/{StructuredFormat-DHaUJBdd.d.mts → StructuredFormat-BlZhyOsi.d.mts} +3 -7
- package/dist/StructuredFormat-BlZhyOsi.d.mts.map +1 -0
- package/dist/{Tool-u5bag8SO.d.mts → Tool-DE-rnvkw.d.mts} +5 -8
- package/dist/Tool-DE-rnvkw.d.mts.map +1 -0
- package/dist/{ToolEvent-BBfOdhF_.d.mts → ToolEvent-D99MGQYH.d.mts} +56 -63
- package/dist/ToolEvent-D99MGQYH.d.mts.map +1 -0
- package/dist/{ToolResult-CRpmkBXR.d.mts → ToolResult-Cj8ChodL.d.mts} +49 -54
- package/dist/ToolResult-Cj8ChodL.d.mts.map +1 -0
- package/dist/{Toolkit-D4mQvhjq.d.mts → Toolkit-BEZrNPTI.d.mts} +13 -16
- package/dist/Toolkit-BEZrNPTI.d.mts.map +1 -0
- package/dist/browser/Browser.d.mts +11 -7
- package/dist/browser/Browser.d.mts.map +1 -1
- package/dist/browser/Browser.mjs +1 -1
- package/dist/browser/Browser.mjs.map +1 -1
- package/dist/browser/BrowserError.d.mts +9 -13
- package/dist/browser/BrowserError.d.mts.map +1 -1
- package/dist/browser/BrowserError.mjs +1 -1
- package/dist/browser/BrowserError.mjs.map +1 -1
- package/dist/browser/BrowserTool.d.mts +33 -36
- package/dist/browser/BrowserTool.d.mts.map +1 -1
- package/dist/browser/BrowserTool.mjs +1 -1
- package/dist/browser/BrowserTool.mjs.map +1 -1
- package/dist/capabilities/Capabilities.d.mts +8 -6
- package/dist/capabilities/Capabilities.d.mts.map +1 -1
- package/dist/capabilities/Capabilities.mjs +1 -1
- package/dist/capabilities/Capabilities.mjs.map +1 -1
- package/dist/chunker/Chunker.d.mts +32 -0
- package/dist/chunker/Chunker.d.mts.map +1 -0
- package/dist/chunker/Chunker.mjs +14 -0
- package/dist/chunker/Chunker.mjs.map +1 -0
- package/dist/dist-BaenoU1A.mjs +145 -0
- package/dist/dist-BaenoU1A.mjs.map +1 -0
- package/dist/{dist-C0m0QnUP.mjs → dist-nohcnkD6.mjs} +16 -15
- package/dist/dist-nohcnkD6.mjs.map +1 -0
- package/dist/domain/AiError.d.mts +1 -1
- package/dist/domain/AiError.mjs +1 -1
- package/dist/domain/AiError.mjs.map +1 -1
- package/dist/domain/Audio.d.mts +1 -1
- package/dist/domain/Audio.mjs +1 -1
- package/dist/domain/Citation.d.mts +0 -2
- package/dist/domain/Citation.d.mts.map +1 -1
- package/dist/domain/Citation.mjs +1 -1
- package/dist/domain/Image.d.mts +1 -1
- package/dist/domain/Image.mjs +1 -1
- package/dist/domain/Image.mjs.map +1 -1
- package/dist/domain/Items.d.mts +1 -1
- package/dist/domain/Items.mjs +1 -1
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Media.d.mts +1 -1
- package/dist/domain/Media.mjs +1 -1
- package/dist/domain/Music.d.mts +18 -11
- package/dist/domain/Music.d.mts.map +1 -1
- package/dist/domain/Music.mjs +1 -1
- package/dist/domain/Music.mjs.map +1 -1
- package/dist/domain/Research.d.mts +3 -4
- package/dist/domain/Research.d.mts.map +1 -1
- package/dist/domain/Research.mjs +1 -1
- package/dist/domain/Transcript.d.mts +2 -3
- package/dist/domain/Transcript.d.mts.map +1 -1
- package/dist/domain/Transcript.mjs +1 -1
- package/dist/domain/Turn.d.mts +143 -164
- package/dist/domain/Turn.d.mts.map +1 -1
- package/dist/domain/Turn.mjs +1 -1
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/domain/Turn.test.d.mts +1 -1
- package/dist/domain/Turn.test.mjs +28 -18
- package/dist/domain/Turn.test.mjs.map +1 -1
- package/dist/embedding-model/Embedding.d.mts +1 -3
- package/dist/embedding-model/Embedding.d.mts.map +1 -1
- package/dist/embedding-model/Embedding.mjs +1 -1
- package/dist/embedding-model/EmbeddingModel.d.mts +1 -3
- package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -1
- package/dist/embedding-model/EmbeddingModel.mjs +1 -1
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.test.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +14 -9
- package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
- package/dist/index.d.mts +16 -13
- package/dist/index.mjs +4 -1
- package/dist/job/Job.d.mts +4 -4
- package/dist/job/Job.d.mts.map +1 -1
- package/dist/job/Job.mjs +1 -1
- package/dist/job/Job.mjs.map +1 -1
- package/dist/language-model/LanguageModel.d.mts +4 -6
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +1 -1
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.d.mts +1 -1
- package/dist/language-model/LanguageModel.test.mjs +6 -3
- package/dist/language-model/LanguageModel.test.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +1 -1
- package/dist/loop/Loop.mjs +1 -1
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.d.mts +1 -1
- package/dist/loop/Loop.test.mjs +60 -34
- package/dist/loop/Loop.test.mjs.map +1 -1
- package/dist/{magic-string.es-Dk-MMA0F.mjs → magic-string.es-CB2hOw9v.mjs} +14 -13
- package/dist/{magic-string.es-Dk-MMA0F.mjs.map → magic-string.es-CB2hOw9v.mjs.map} +1 -1
- package/dist/math/Vector.d.mts +0 -2
- package/dist/math/Vector.d.mts.map +1 -1
- package/dist/math/Vector.mjs +1 -1
- package/dist/math/Vector.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.d.mts +2 -4
- package/dist/music-generator/MusicGenerator.d.mts.map +1 -1
- package/dist/music-generator/MusicGenerator.mjs +1 -1
- package/dist/music-generator/MusicGenerator.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.test.d.mts +1 -1
- package/dist/music-generator/MusicGenerator.test.mjs +19 -10
- package/dist/music-generator/MusicGenerator.test.mjs.map +1 -1
- package/dist/observability/Metrics.d.mts +10 -7
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +1 -1
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/observability/Metrics.test.d.mts +1 -1
- package/dist/observability/Metrics.test.mjs +50 -28
- package/dist/observability/Metrics.test.mjs.map +1 -1
- package/dist/observability/Telemetry.d.mts +2 -3
- package/dist/observability/Telemetry.d.mts.map +1 -1
- package/dist/observability/Telemetry.mjs +1 -1
- package/dist/observability/Telemetry.mjs.map +1 -1
- package/dist/observability/Telemetry.test.d.mts +1 -1
- package/dist/observability/Telemetry.test.mjs +15 -7
- package/dist/observability/Telemetry.test.mjs.map +1 -1
- package/dist/reranker/Reranker.d.mts +46 -0
- package/dist/reranker/Reranker.d.mts.map +1 -0
- package/dist/reranker/Reranker.mjs +14 -0
- package/dist/reranker/Reranker.mjs.map +1 -0
- package/dist/research/DeepResearch.d.mts +1 -3
- package/dist/research/DeepResearch.d.mts.map +1 -1
- package/dist/research/DeepResearch.mjs +1 -1
- package/dist/research/DeepResearch.mjs.map +1 -1
- package/dist/retry/Retry.d.mts +2 -6
- package/dist/retry/Retry.d.mts.map +1 -1
- package/dist/retry/Retry.mjs +1 -1
- package/dist/retry/Retry.mjs.map +1 -1
- package/dist/retry/Retry.test.d.mts +1 -1
- package/dist/retry/Retry.test.mjs +6 -3
- package/dist/retry/Retry.test.mjs.map +1 -1
- package/dist/sandbox/Memory.d.mts +0 -2
- package/dist/sandbox/Memory.d.mts.map +1 -1
- package/dist/sandbox/Memory.mjs +4 -4
- package/dist/sandbox/Memory.mjs.map +1 -1
- package/dist/sandbox/Sandbox.d.mts +144 -156
- package/dist/sandbox/Sandbox.d.mts.map +1 -1
- package/dist/sandbox/Sandbox.mjs +1 -1
- package/dist/sandbox/Sandbox.mjs.map +1 -1
- package/dist/sandbox/SandboxError.d.mts +1 -1
- package/dist/sandbox/SandboxError.mjs +1 -1
- package/dist/sandbox/SandboxError.mjs.map +1 -1
- package/dist/sandbox/SandboxImage.d.mts +0 -2
- package/dist/sandbox/SandboxImage.d.mts.map +1 -1
- package/dist/sandbox/SandboxImage.mjs +1 -1
- package/dist/sandbox/SandboxImage.mjs.map +1 -1
- package/dist/sandbox/SandboxNetwork.d.mts +0 -2
- package/dist/sandbox/SandboxNetwork.d.mts.map +1 -1
- package/dist/sandbox/SandboxNetwork.mjs +1 -1
- package/dist/sandbox/SandboxNetwork.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +8 -7
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.d.mts +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +31 -16
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
- package/dist/streaming/JSONL.d.mts +1 -5
- package/dist/streaming/JSONL.d.mts.map +1 -1
- package/dist/streaming/JSONL.mjs +1 -1
- package/dist/streaming/JSONL.mjs.map +1 -1
- package/dist/streaming/JSONL.test.d.mts +1 -1
- package/dist/streaming/JSONL.test.mjs +16 -8
- package/dist/streaming/JSONL.test.mjs.map +1 -1
- package/dist/streaming/Lines.d.mts +0 -2
- package/dist/streaming/Lines.d.mts.map +1 -1
- package/dist/streaming/Lines.mjs +1 -1
- package/dist/streaming/Lines.mjs.map +1 -1
- package/dist/streaming/SSE.d.mts +0 -2
- package/dist/streaming/SSE.d.mts.map +1 -1
- package/dist/streaming/SSE.mjs +1 -1
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.d.mts +1 -1
- package/dist/streaming/SSE.test.mjs +34 -17
- package/dist/streaming/SSE.test.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.mjs +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.test.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.test.mjs +7 -4
- package/dist/structured-format/StructuredFormat.test.mjs.map +1 -1
- package/dist/{dist-CcRV3-A5.mjs → test.DNmyFkvJ-YPzDR0Co.mjs} +1644 -5172
- package/dist/test.DNmyFkvJ-YPzDR0Co.mjs.map +1 -0
- package/dist/testing/MockMusicGenerator.d.mts +4 -3
- package/dist/testing/MockMusicGenerator.d.mts.map +1 -1
- package/dist/testing/MockMusicGenerator.mjs.map +1 -1
- package/dist/testing/MockProvider.d.mts +1 -2
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/testing/MockSandbox.d.mts +256 -290
- package/dist/testing/MockSandbox.d.mts.map +1 -1
- package/dist/testing/MockSandbox.mjs.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.d.mts +10 -6
- package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
- package/dist/testing/MockTranscriber.d.mts +3 -2
- package/dist/testing/MockTranscriber.d.mts.map +1 -1
- package/dist/testing/MockTranscriber.mjs.map +1 -1
- package/dist/tokenizer/Tokenizer.d.mts +22 -0
- package/dist/tokenizer/Tokenizer.d.mts.map +1 -0
- package/dist/tokenizer/Tokenizer.mjs +9 -0
- package/dist/tokenizer/Tokenizer.mjs.map +1 -0
- package/dist/tool/Approval.d.mts +31 -36
- package/dist/tool/Approval.d.mts.map +1 -1
- package/dist/tool/Approval.mjs +1 -1
- package/dist/tool/Approval.mjs.map +1 -1
- package/dist/tool/Approval.test.d.mts +1 -1
- package/dist/tool/Approval.test.mjs +37 -24
- package/dist/tool/Approval.test.mjs.map +1 -1
- package/dist/tool/HistoryCheck.d.mts +2 -4
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +1 -1
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Tool.d.mts +1 -1
- package/dist/tool/Tool.mjs +4 -4
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.d.mts +1 -1
- package/dist/tool/Tool.test.mjs +23 -11
- package/dist/tool/Tool.test.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +1 -1
- package/dist/tool/ToolEvent.mjs +1 -1
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/ToolResult.d.mts +1 -1
- package/dist/tool/ToolResult.mjs +1 -1
- package/dist/tool/ToolResult.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +1 -1
- package/dist/tool/Toolkit.mjs +4 -3
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.d.mts +1 -1
- package/dist/tool/Toolkit.test.mjs +40 -21
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.d.mts +6 -6
- package/dist/transcriber/Transcriber.d.mts.map +1 -1
- package/dist/transcriber/Transcriber.mjs +1 -1
- package/dist/transcriber/Transcriber.mjs.map +1 -1
- package/dist/transcriber/Transcriber.test.d.mts +1 -1
- package/dist/transcriber/Transcriber.test.mjs +20 -10
- package/dist/transcriber/Transcriber.test.mjs.map +1 -1
- package/dist/utils-CiH_DjFW.mjs +3918 -0
- package/dist/utils-CiH_DjFW.mjs.map +1 -0
- package/dist/web-read/WebRead.d.mts +15 -9
- package/dist/web-read/WebRead.d.mts.map +1 -1
- package/dist/web-read/WebRead.mjs +1 -1
- package/dist/web-read/WebRead.mjs.map +1 -1
- package/dist/web-read/WebReadTool.d.mts +4 -5
- package/dist/web-read/WebReadTool.d.mts.map +1 -1
- package/dist/web-read/WebReadTool.mjs +1 -1
- package/dist/web-read/WebReadTool.mjs.map +1 -1
- package/dist/web-search/WebSearch.d.mts +22 -13
- package/dist/web-search/WebSearch.d.mts.map +1 -1
- package/dist/web-search/WebSearch.mjs +1 -1
- package/dist/web-search/WebSearch.mjs.map +1 -1
- package/dist/web-search/WebSearchTool.d.mts +6 -6
- package/dist/web-search/WebSearchTool.d.mts.map +1 -1
- package/dist/web-search/WebSearchTool.mjs +1 -1
- package/dist/web-search/WebSearchTool.mjs.map +1 -1
- package/package.json +16 -4
- package/src/chunker/Chunker.ts +34 -0
- package/src/index.ts +3 -0
- package/src/loop/Loop.test.ts +13 -14
- package/src/loop/Loop.ts +74 -76
- package/src/reranker/Reranker.ts +51 -0
- package/src/testing/MockProvider.ts +8 -9
- package/src/tokenizer/Tokenizer.ts +19 -0
- package/src/tool/Tool.ts +13 -13
- package/dist/AiError-Bf1pq4BB.d.mts.map +0 -1
- package/dist/Audio-CR7CSljJ.d.mts.map +0 -1
- package/dist/Image-C4vuBmu0.d.mts.map +0 -1
- package/dist/Items-BKQgqJ8A.d.mts.map +0 -1
- package/dist/Loop-D8nWpowc.d.mts.map +0 -1
- package/dist/Media-D_CpcM1Z.d.mts.map +0 -1
- package/dist/SandboxError-CIrVDOMV.d.mts.map +0 -1
- package/dist/StructuredFormat-DHaUJBdd.d.mts.map +0 -1
- package/dist/Tool-u5bag8SO.d.mts.map +0 -1
- package/dist/ToolEvent-BBfOdhF_.d.mts.map +0 -1
- package/dist/ToolResult-CRpmkBXR.d.mts.map +0 -1
- package/dist/Toolkit-D4mQvhjq.d.mts.map +0 -1
- package/dist/dist-C0m0QnUP.mjs.map +0 -1
- package/dist/dist-CcRV3-A5.mjs.map +0 -1
- /package/dist/{chunk-uyGKjUfl.mjs → rolldown-runtime-EAsCxrDo.mjs} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LanguageModel.mjs","names":["Arr","AiError.IncompleteTurn"],"sources":["../../src/language-model/LanguageModel.ts"],"sourcesContent":["import { Array as Arr, Context, Effect, Option, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport type { HistoryItem } from \"../domain/Items.js\"\nimport type * as StructuredFormat from \"../structured-format/StructuredFormat.js\"\nimport type { Toolkit } from \"../tool/Toolkit.js\"\nimport { isTurnComplete, type Turn, type TurnEvent } from \"../domain/Turn.js\"\n\n/**\n * Cross-provider request shape. Every call carries its own `history` and\n * `model` - models are not bound at layer construction. Anything specific\n * to a single provider (reasoning effort, prompt caching, store flags,\n * ...) lives in that provider's own request interface, which extends this.\n */\nexport type CommonRequest = {\n readonly history: ReadonlyArray<HistoryItem>\n /**\n * Model identifier. Each provider narrows this to its typed literal union,\n * so code that yields a typed provider tag gets autocompletion.\n */\n readonly model: string\n /**\n * The model-visible tools for this request, as a `Toolkit`. The provider\n * renders it to wire descriptors (`Tool.descriptorsOf`); omit it for a turn\n * with no tools.\n */\n readonly tools?: Toolkit\n readonly toolChoice?:\n | \"auto\"\n | \"required\"\n | \"none\"\n | { readonly type: \"function\"; readonly name: string }\n readonly temperature?: number\n readonly topP?: number\n readonly maxOutputTokens?: number\n /**\n * Schema-bound JSON output. The provider constrains the wire to match the\n * schema; pair with `Turn.decodeStructured` for runtime validation. Supported\n * across all current providers (OpenAI Responses json_schema, Anthropic\n * `output_config`, Gemini `responseJsonSchema`).\n */\n readonly structured?: StructuredFormat.StructuredFormat<unknown>\n}\n\nexport type LanguageModelService = {\n readonly streamTurn: (request: CommonRequest) => Stream.Stream<TurnEvent, AiError.AiError>\n /**\n * Drain a single turn and return the assembled `Turn` from the\n * terminal `TurnComplete` event. Fails with `IncompleteTurn` if the\n * stream ends without one.\n *\n * Most providers derive this from `streamTurn` via\n * {@link turnFromStream}; providers with a native non-streaming\n * endpoint may override with a cheaper direct call.\n */\n readonly turn: (request: CommonRequest) => Effect.Effect<Turn, AiError.AiError>\n}\n\nexport class LanguageModel extends Context.Service<LanguageModel, LanguageModelService>()(\n \"@betalyra/effect-uai/LanguageModel\",\n) {}\n\n/**\n * Stream the deltas of a single turn.\n */\nexport const streamTurn = (\n request: CommonRequest,\n): Stream.Stream<TurnEvent, AiError.AiError, LanguageModel> =>\n Stream.unwrap(Effect.map(LanguageModel, (m) => m.streamTurn(request)))\n\n/**\n * Drain a single turn and return the assembled `Turn`. Delegates to the\n * service's `turn` method — providers with a native complete endpoint\n * can override; the rest get the default streamTurn-drain via\n * {@link turnFromStream}.\n */\nexport const turn = (request: CommonRequest): Effect.Effect<Turn, AiError.AiError, LanguageModel> =>\n Effect.flatMap(LanguageModel, (m) => m.turn(request))\n\n/**\n * Build a `turn` implementation from a `streamTurn` implementation.\n * Providers without a native non-streaming endpoint use this to\n * populate the service's `turn` field. Generic over the request type so\n * provider-typed services (with their narrowed request) can reuse it.\n */\nexport const turnFromStream =\n <Req>(streamTurn: (request: Req) => Stream.Stream<TurnEvent, AiError.AiError>) =>\n (request: Req): Effect.Effect<Turn, AiError.AiError> =>\n streamTurn(request).pipe(\n Stream.runCollect,\n Effect.flatMap((events) =>\n Arr.findLast(events, isTurnComplete).pipe(\n Option.match({\n onNone: () => Effect.fail(new AiError.IncompleteTurn({})),\n onSome: (e) => Effect.succeed(e.turn),\n }),\n ),\n ),\n )\n"],"mappings":";;;;;;;;;;;AAyDA,IAAa,gBAAb,cAAmC,QAAQ,
|
|
1
|
+
{"version":3,"file":"LanguageModel.mjs","names":["Arr","AiError.IncompleteTurn"],"sources":["../../src/language-model/LanguageModel.ts"],"sourcesContent":["import { Array as Arr, Context, Effect, Option, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport type { HistoryItem } from \"../domain/Items.js\"\nimport type * as StructuredFormat from \"../structured-format/StructuredFormat.js\"\nimport type { Toolkit } from \"../tool/Toolkit.js\"\nimport { isTurnComplete, type Turn, type TurnEvent } from \"../domain/Turn.js\"\n\n/**\n * Cross-provider request shape. Every call carries its own `history` and\n * `model` - models are not bound at layer construction. Anything specific\n * to a single provider (reasoning effort, prompt caching, store flags,\n * ...) lives in that provider's own request interface, which extends this.\n */\nexport type CommonRequest = {\n readonly history: ReadonlyArray<HistoryItem>\n /**\n * Model identifier. Each provider narrows this to its typed literal union,\n * so code that yields a typed provider tag gets autocompletion.\n */\n readonly model: string\n /**\n * The model-visible tools for this request, as a `Toolkit`. The provider\n * renders it to wire descriptors (`Tool.descriptorsOf`); omit it for a turn\n * with no tools.\n */\n readonly tools?: Toolkit\n readonly toolChoice?:\n | \"auto\"\n | \"required\"\n | \"none\"\n | { readonly type: \"function\"; readonly name: string }\n readonly temperature?: number\n readonly topP?: number\n readonly maxOutputTokens?: number\n /**\n * Schema-bound JSON output. The provider constrains the wire to match the\n * schema; pair with `Turn.decodeStructured` for runtime validation. Supported\n * across all current providers (OpenAI Responses json_schema, Anthropic\n * `output_config`, Gemini `responseJsonSchema`).\n */\n readonly structured?: StructuredFormat.StructuredFormat<unknown>\n}\n\nexport type LanguageModelService = {\n readonly streamTurn: (request: CommonRequest) => Stream.Stream<TurnEvent, AiError.AiError>\n /**\n * Drain a single turn and return the assembled `Turn` from the\n * terminal `TurnComplete` event. Fails with `IncompleteTurn` if the\n * stream ends without one.\n *\n * Most providers derive this from `streamTurn` via\n * {@link turnFromStream}; providers with a native non-streaming\n * endpoint may override with a cheaper direct call.\n */\n readonly turn: (request: CommonRequest) => Effect.Effect<Turn, AiError.AiError>\n}\n\nexport class LanguageModel extends Context.Service<LanguageModel, LanguageModelService>()(\n \"@betalyra/effect-uai/LanguageModel\",\n) {}\n\n/**\n * Stream the deltas of a single turn.\n */\nexport const streamTurn = (\n request: CommonRequest,\n): Stream.Stream<TurnEvent, AiError.AiError, LanguageModel> =>\n Stream.unwrap(Effect.map(LanguageModel, (m) => m.streamTurn(request)))\n\n/**\n * Drain a single turn and return the assembled `Turn`. Delegates to the\n * service's `turn` method — providers with a native complete endpoint\n * can override; the rest get the default streamTurn-drain via\n * {@link turnFromStream}.\n */\nexport const turn = (request: CommonRequest): Effect.Effect<Turn, AiError.AiError, LanguageModel> =>\n Effect.flatMap(LanguageModel, (m) => m.turn(request))\n\n/**\n * Build a `turn` implementation from a `streamTurn` implementation.\n * Providers without a native non-streaming endpoint use this to\n * populate the service's `turn` field. Generic over the request type so\n * provider-typed services (with their narrowed request) can reuse it.\n */\nexport const turnFromStream =\n <Req>(streamTurn: (request: Req) => Stream.Stream<TurnEvent, AiError.AiError>) =>\n (request: Req): Effect.Effect<Turn, AiError.AiError> =>\n streamTurn(request).pipe(\n Stream.runCollect,\n Effect.flatMap((events) =>\n Arr.findLast(events, isTurnComplete).pipe(\n Option.match({\n onNone: () => Effect.fail(new AiError.IncompleteTurn({})),\n onSome: (e) => Effect.succeed(e.turn),\n }),\n ),\n ),\n )\n"],"mappings":";;;;;;;;;;;AAyDA,IAAa,gBAAb,cAAmC,QAAQ,QAA6C,CAAC,CACvF,oCACF,CAAC,CAAC,CAAC;;;;AAKH,MAAa,cACX,YAEA,OAAO,OAAO,OAAO,IAAI,gBAAgB,MAAM,EAAE,WAAW,OAAO,CAAC,CAAC;;;;;;;AAQvE,MAAa,QAAQ,YACnB,OAAO,QAAQ,gBAAgB,MAAM,EAAE,KAAK,OAAO,CAAC;;;;;;;AAQtD,MAAa,kBACL,gBACL,YACC,WAAW,OAAO,CAAC,CAAC,KAClB,OAAO,YACP,OAAO,SAAS,WACdA,MAAI,SAAS,QAAQ,cAAc,CAAC,CAAC,KACnC,OAAO,MAAM;CACX,cAAc,OAAO,KAAK,IAAIC,eAAuB,CAAC,CAAC,CAAC;CACxD,SAAS,MAAM,OAAO,QAAQ,EAAE,IAAI;AACtC,CAAC,CACH,CACF,CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
|
@@ -2,7 +2,8 @@ import { IncompleteTurn, RateLimited } from "../domain/AiError.mjs";
|
|
|
2
2
|
import { assistantText, userText } from "../domain/Items.mjs";
|
|
3
3
|
import { TurnEvent } from "../domain/Turn.mjs";
|
|
4
4
|
import { LanguageModel, turn, turnFromStream } from "./LanguageModel.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { m as it, o as describe } from "../utils-CiH_DjFW.mjs";
|
|
6
|
+
import { n as globalExpect } from "../test.DNmyFkvJ-YPzDR0Co.mjs";
|
|
6
7
|
import { layer } from "../testing/MockProvider.mjs";
|
|
7
8
|
import { Cause, Effect, Exit, Layer, Option, Stream } from "effect";
|
|
8
9
|
//#region src/language-model/LanguageModel.test.ts
|
|
@@ -25,7 +26,8 @@ describe("LanguageModel.turn", () => {
|
|
|
25
26
|
history: [userText("hi")],
|
|
26
27
|
model: "mock"
|
|
27
28
|
});
|
|
28
|
-
|
|
29
|
+
const result = await Effect.runPromise(program.pipe(Effect.provide(layer([expected]))));
|
|
30
|
+
globalExpect(result).toEqual(expected);
|
|
29
31
|
});
|
|
30
32
|
it("fails with IncompleteTurn when the stream ends without TurnComplete", async () => {
|
|
31
33
|
const broken = Layer.succeed(LanguageModel, serviceFromStream(() => Stream.fromIterable([TurnEvent.TextDelta({ text: "partial" })])));
|
|
@@ -63,7 +65,8 @@ describe("LanguageModel.turn", () => {
|
|
|
63
65
|
history: [],
|
|
64
66
|
model: "mock"
|
|
65
67
|
});
|
|
66
|
-
|
|
68
|
+
const result = await Effect.runPromise(program.pipe(Effect.provide(weird)));
|
|
69
|
+
globalExpect(result).toEqual(second);
|
|
67
70
|
});
|
|
68
71
|
});
|
|
69
72
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LanguageModel.test.mjs","names":["Items.assistantText","Items.userText","MockProvider.layer","AiError.IncompleteTurn","AiError.RateLimited"],"sources":["../../src/language-model/LanguageModel.test.ts"],"sourcesContent":["import { Cause, Effect, Exit, Layer, Option, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport * as AiError from \"../domain/AiError.js\"\nimport * as Items from \"../domain/Items.js\"\nimport { type Turn, TurnEvent } from \"../domain/Turn.js\"\nimport { LanguageModel, type LanguageModelService, turn, turnFromStream } from \"./LanguageModel.js\"\n\nconst serviceFromStream = (\n streamTurn: LanguageModelService[\"streamTurn\"],\n): LanguageModelService => ({ streamTurn, turn: turnFromStream(streamTurn) })\nimport * as MockProvider from \"../testing/MockProvider.js\"\n\nconst oneTextTurn = (text: string): Turn => ({\n items: [Items.assistantText(text)],\n usage: { input_tokens: 1, output_tokens: 1 },\n stop_reason: \"stop\",\n})\n\ndescribe(\"LanguageModel.turn\", () => {\n it(\"returns the assembled Turn from the terminal TurnComplete event\", async () => {\n const expected = oneTextTurn(\"hello world\")\n const program = turn({ history: [Items.userText(\"hi\")], model: \"mock\" })\n\n const result = await Effect.runPromise(\n program.pipe(Effect.provide(MockProvider.layer([expected]))),\n )\n\n expect(result).toEqual(expected)\n })\n\n it(\"fails with IncompleteTurn when the stream ends without TurnComplete\", async () => {\n // Custom service whose stream emits a single TextDelta and then ends.\n const broken = Layer.succeed(\n LanguageModel,\n serviceFromStream(() =>\n Stream.fromIterable<TurnEvent>([TurnEvent.TextDelta({ text: \"partial\" })]),\n ),\n )\n\n const program = turn({ history: [Items.userText(\"hi\")], model: \"mock\" })\n const exit = await Effect.runPromise(Effect.exit(program.pipe(Effect.provide(broken))))\n\n expect(Exit.isFailure(exit)).toBe(true)\n if (Exit.isFailure(exit)) {\n const failure = Cause.findErrorOption(exit.cause)\n expect(Option.isSome(failure)).toBe(true)\n if (Option.isSome(failure)) {\n expect(failure.value).toBeInstanceOf(AiError.IncompleteTurn)\n }\n }\n })\n\n it(\"propagates an AiError raised by streamTurn\", async () => {\n const rateLimited = new AiError.RateLimited({ provider: \"mock\", raw: null })\n const failing = Layer.succeed(\n LanguageModel,\n serviceFromStream(() => Stream.fail<AiError.AiError>(rateLimited)),\n )\n\n const program = turn({ history: [], model: \"mock\" })\n const exit = await Effect.runPromise(Effect.exit(program.pipe(Effect.provide(failing))))\n\n expect(Exit.isFailure(exit)).toBe(true)\n if (Exit.isFailure(exit)) {\n expect(Cause.findErrorOption(exit.cause)).toEqual(Option.some(rateLimited))\n }\n })\n\n it(\"returns the LAST TurnComplete when the stream contains multiple (defensive)\", async () => {\n // A misbehaving provider might emit two TurnComplete events; turn\n // should pick the last one (the most recent assembled Turn).\n const first = oneTextTurn(\"first\")\n const second = oneTextTurn(\"second\")\n const weird = Layer.succeed(\n LanguageModel,\n serviceFromStream(() =>\n Stream.fromIterable<TurnEvent>([\n TurnEvent.TurnComplete({ turn: first }),\n TurnEvent.TurnComplete({ turn: second }),\n ]),\n ),\n )\n\n const program = turn({ history: [], model: \"mock\" })\n const result = await Effect.runPromise(program.pipe(Effect.provide(weird)))\n\n expect(result).toEqual(second)\n })\n})\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"LanguageModel.test.mjs","names":["Items.assistantText","Items.userText","MockProvider.layer","AiError.IncompleteTurn","AiError.RateLimited"],"sources":["../../src/language-model/LanguageModel.test.ts"],"sourcesContent":["import { Cause, Effect, Exit, Layer, Option, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport * as AiError from \"../domain/AiError.js\"\nimport * as Items from \"../domain/Items.js\"\nimport { type Turn, TurnEvent } from \"../domain/Turn.js\"\nimport { LanguageModel, type LanguageModelService, turn, turnFromStream } from \"./LanguageModel.js\"\n\nconst serviceFromStream = (\n streamTurn: LanguageModelService[\"streamTurn\"],\n): LanguageModelService => ({ streamTurn, turn: turnFromStream(streamTurn) })\nimport * as MockProvider from \"../testing/MockProvider.js\"\n\nconst oneTextTurn = (text: string): Turn => ({\n items: [Items.assistantText(text)],\n usage: { input_tokens: 1, output_tokens: 1 },\n stop_reason: \"stop\",\n})\n\ndescribe(\"LanguageModel.turn\", () => {\n it(\"returns the assembled Turn from the terminal TurnComplete event\", async () => {\n const expected = oneTextTurn(\"hello world\")\n const program = turn({ history: [Items.userText(\"hi\")], model: \"mock\" })\n\n const result = await Effect.runPromise(\n program.pipe(Effect.provide(MockProvider.layer([expected]))),\n )\n\n expect(result).toEqual(expected)\n })\n\n it(\"fails with IncompleteTurn when the stream ends without TurnComplete\", async () => {\n // Custom service whose stream emits a single TextDelta and then ends.\n const broken = Layer.succeed(\n LanguageModel,\n serviceFromStream(() =>\n Stream.fromIterable<TurnEvent>([TurnEvent.TextDelta({ text: \"partial\" })]),\n ),\n )\n\n const program = turn({ history: [Items.userText(\"hi\")], model: \"mock\" })\n const exit = await Effect.runPromise(Effect.exit(program.pipe(Effect.provide(broken))))\n\n expect(Exit.isFailure(exit)).toBe(true)\n if (Exit.isFailure(exit)) {\n const failure = Cause.findErrorOption(exit.cause)\n expect(Option.isSome(failure)).toBe(true)\n if (Option.isSome(failure)) {\n expect(failure.value).toBeInstanceOf(AiError.IncompleteTurn)\n }\n }\n })\n\n it(\"propagates an AiError raised by streamTurn\", async () => {\n const rateLimited = new AiError.RateLimited({ provider: \"mock\", raw: null })\n const failing = Layer.succeed(\n LanguageModel,\n serviceFromStream(() => Stream.fail<AiError.AiError>(rateLimited)),\n )\n\n const program = turn({ history: [], model: \"mock\" })\n const exit = await Effect.runPromise(Effect.exit(program.pipe(Effect.provide(failing))))\n\n expect(Exit.isFailure(exit)).toBe(true)\n if (Exit.isFailure(exit)) {\n expect(Cause.findErrorOption(exit.cause)).toEqual(Option.some(rateLimited))\n }\n })\n\n it(\"returns the LAST TurnComplete when the stream contains multiple (defensive)\", async () => {\n // A misbehaving provider might emit two TurnComplete events; turn\n // should pick the last one (the most recent assembled Turn).\n const first = oneTextTurn(\"first\")\n const second = oneTextTurn(\"second\")\n const weird = Layer.succeed(\n LanguageModel,\n serviceFromStream(() =>\n Stream.fromIterable<TurnEvent>([\n TurnEvent.TurnComplete({ turn: first }),\n TurnEvent.TurnComplete({ turn: second }),\n ]),\n ),\n )\n\n const program = turn({ history: [], model: \"mock\" })\n const result = await Effect.runPromise(program.pipe(Effect.provide(weird)))\n\n expect(result).toEqual(second)\n })\n})\n"],"mappings":";;;;;;;;;AAOA,MAAM,qBACJ,gBAC0B;CAAE;CAAY,MAAM,eAAe,UAAU;AAAE;AAG3E,MAAM,eAAe,UAAwB;CAC3C,OAAO,CAACA,cAAoB,IAAI,CAAC;CACjC,OAAO;EAAE,cAAc;EAAG,eAAe;CAAE;CAC3C,aAAa;AACf;AAEA,SAAS,4BAA4B;CACnC,GAAG,mEAAmE,YAAY;EAChF,MAAM,WAAW,YAAY,aAAa;EAC1C,MAAM,UAAU,KAAK;GAAE,SAAS,CAACC,SAAe,IAAI,CAAC;GAAG,OAAO;EAAO,CAAC;EAEvE,MAAM,SAAS,MAAM,OAAO,WAC1B,QAAQ,KAAK,OAAO,QAAQC,MAAmB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAC7D;EAEA,aAAO,MAAM,CAAC,CAAC,QAAQ,QAAQ;CACjC,CAAC;CAED,GAAG,uEAAuE,YAAY;EAEpF,MAAM,SAAS,MAAM,QACnB,eACA,wBACE,OAAO,aAAwB,CAAC,UAAU,UAAU,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,CAC3E,CACF;EAEA,MAAM,UAAU,KAAK;GAAE,SAAS,CAACD,SAAe,IAAI,CAAC;GAAG,OAAO;EAAO,CAAC;EACvE,MAAM,OAAO,MAAM,OAAO,WAAW,OAAO,KAAK,QAAQ,KAAK,OAAO,QAAQ,MAAM,CAAC,CAAC,CAAC;EAEtF,aAAO,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI;EACtC,IAAI,KAAK,UAAU,IAAI,GAAG;GACxB,MAAM,UAAU,MAAM,gBAAgB,KAAK,KAAK;GAChD,aAAO,OAAO,OAAO,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI;GACxC,IAAI,OAAO,OAAO,OAAO,GACvB,aAAO,QAAQ,KAAK,CAAC,CAAC,eAAeE,cAAsB;EAE/D;CACF,CAAC;CAED,GAAG,8CAA8C,YAAY;EAC3D,MAAM,cAAc,IAAIC,YAAoB;GAAE,UAAU;GAAQ,KAAK;EAAK,CAAC;EAC3E,MAAM,UAAU,MAAM,QACpB,eACA,wBAAwB,OAAO,KAAsB,WAAW,CAAC,CACnE;EAEA,MAAM,UAAU,KAAK;GAAE,SAAS,CAAC;GAAG,OAAO;EAAO,CAAC;EACnD,MAAM,OAAO,MAAM,OAAO,WAAW,OAAO,KAAK,QAAQ,KAAK,OAAO,QAAQ,OAAO,CAAC,CAAC,CAAC;EAEvF,aAAO,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI;EACtC,IAAI,KAAK,UAAU,IAAI,GACrB,aAAO,MAAM,gBAAgB,KAAK,KAAK,CAAC,CAAC,CAAC,QAAQ,OAAO,KAAK,WAAW,CAAC;CAE9E,CAAC;CAED,GAAG,+EAA+E,YAAY;EAG5F,MAAM,QAAQ,YAAY,OAAO;EACjC,MAAM,SAAS,YAAY,QAAQ;EACnC,MAAM,QAAQ,MAAM,QAClB,eACA,wBACE,OAAO,aAAwB,CAC7B,UAAU,aAAa,EAAE,MAAM,MAAM,CAAC,GACtC,UAAU,aAAa,EAAE,MAAM,OAAO,CAAC,CACzC,CAAC,CACH,CACF;EAEA,MAAM,UAAU,KAAK;GAAE,SAAS,CAAC;GAAG,OAAO;EAAO,CAAC;EACnD,MAAM,SAAS,MAAM,OAAO,WAAW,QAAQ,KAAK,OAAO,QAAQ,KAAK,CAAC,CAAC;EAE1E,aAAO,MAAM,CAAC,CAAC,QAAQ,MAAM;CAC/B,CAAC;AACH,CAAC"}
|
package/dist/loop/Loop.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as StopWith, c as loopOver, d as onTurnComplete, f as stop, i as Stop, l as loopWithState, n as Next, o as Value, p as value, r as Step, s as loop, u as next } from "../Loop-
|
|
1
|
+
import { a as StopWith, c as loopOver, d as onTurnComplete, f as stop, i as Stop, l as loopWithState, n as Next, o as Value, p as value, r as Step, s as loop, u as next } from "../Loop-B54AB1ME.mjs";
|
|
2
2
|
export { Next, Step, Stop, StopWith, Value, loop, loopOver, loopWithState, next, onTurnComplete, stop, value };
|
package/dist/loop/Loop.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as __exportAll } from "../
|
|
1
|
+
import { n as __exportAll } from "../rolldown-runtime-EAsCxrDo.mjs";
|
|
2
2
|
import { IncompleteTurn } from "../domain/AiError.mjs";
|
|
3
3
|
import { isTurnComplete } from "../domain/Turn.mjs";
|
|
4
4
|
import { Array, Cause, Channel, Data, Effect, Exit, Function, Match, Option, Ref, Scope, Stream, SubscriptionRef } from "effect";
|
package/dist/loop/Loop.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Loop.mjs","names":["Arr"],"sources":["../../src/loop/Loop.ts"],"sourcesContent":["/**\n * Pull-based `loop` for state-threaded sub-streams.\n *\n * Each iteration runs a body that returns a `Stream<Step<A, S>>`. The body\n * emits values via `Loop.value(a)` and signals iteration control via\n * `Loop.next(state)` (continue with new state) or `Loop.stop()` (terminate).\n * The loop unwraps `Value` steps back to `A` for downstream consumers, so\n * the resulting stream is a plain `Stream<A>`.\n *\n * The next body stream is only pulled when downstream pulls the outer\n * stream - no producer fiber, no queue buffering. Cancellation, failures,\n * scoped resources, and backpressure stay aligned with normal Stream\n * semantics.\n *\n * Convention: a `Next` or `Stop` step is the terminal element of a body's\n * iteration. Values emitted in the same chunk after one are discarded\n * (their producing side effects may already have run). The `Loop.next` /\n * `Loop.stop` helpers each emit a single terminal step, so concatenate\n * your values before them (`values.pipe(Stream.concat(Loop.next(state)))`).\n */\nimport {\n Array as Arr,\n Cause,\n Channel,\n Data,\n Effect,\n Exit,\n Function,\n Match,\n Option,\n Ref,\n Scope,\n Stream,\n SubscriptionRef,\n} from \"effect\"\nimport { IncompleteTurn } from \"../domain/AiError.js\"\nimport { isTurnComplete, type Turn, type TurnEvent } from \"../domain/Turn.js\"\n\n// ---------------------------------------------------------------------------\n// Step type - the body's emit shape\n// ---------------------------------------------------------------------------\n\n/**\n * The tagged union a body emits per pull. `Value` carries a payload that\n * flows downstream. `Next` ends the current iteration and continues with a\n * new state. `Stop` ends the loop entirely with no carried state.\n * `StopWith` also ends the loop but carries a final state that `loopOver`\n * will thread to the next input and `loopWithState` will write to its\n * `SubscriptionRef` before the loop ends. Plain `loop` has no next\n * iteration to apply it to and treats `StopWith` like `Stop`.\n *\n * `Stop` is intentionally `{}` so the bare `stop()` helper doesn't\n * constrain `S` from a body's stream type — every body has a `Stop`\n * variant in its union, and forcing `S` to flow through it would break\n * inference whenever the body never uses `next` / `stop(state)`.\n */\nexport type Step<A, S> = Data.TaggedEnum<{\n Value: { readonly value: A }\n Next: { readonly state: S }\n Stop: {}\n StopWith: { readonly state: S }\n}>\n\ninterface StepDef extends Data.TaggedEnum.WithGenerics<2> {\n readonly taggedEnum: Step<this[\"A\"], this[\"B\"]>\n}\n\nconst Step = Data.taggedEnum<StepDef>()\n\n/**\n * Bare step constructors, so a loop body can emit `Value(...)`, `Next(...)`,\n * `Stop()`, `StopWith(...)` directly inside a `Stream.make(...)` without the\n * `Step.` prefix. (The `Step` object itself can't be exported under that name\n * because it collides with the exported `Step` type.) For the common cases\n * prefer the `value` / `next` / `stop` helpers, which also handle the\n * single-element-stream wrapping for `next` / `stop`.\n */\nexport const { Value, Next, Stop, StopWith } = Step\n\n/** Wrap a value so it flows through the loop to downstream consumers. */\nexport const value = <A>(a: A): Step<A, never> => Step.Value({ value: a })\n\n/**\n * End the current iteration and continue with a new state. Emits a single\n * terminal `Next` step as a one-element stream, so it slots directly into a\n * loop body or after a run of values: `values.pipe(Stream.concat(next(s)))`.\n */\nexport const next = <S>(state: S): Stream.Stream<Step<never, S>> =>\n Stream.succeed(Step.Next({ state }))\n\n/**\n * End the loop. Called with no argument it emits a bare `Stop`; called with\n * a state it emits `StopWith(state)` — `loopOver` threads that final state\n * to the next input and `loopWithState` writes it to its `SubscriptionRef`\n * before the loop ends. Either way it returns a single-element stream, so\n * return it directly from a loop body.\n */\nexport const stop = <S = never>(state?: S): Stream.Stream<Step<never, S>> =>\n Stream.succeed<Step<never, S>>(state === undefined ? Step.Stop() : Step.StopWith({ state }))\n\n// ---------------------------------------------------------------------------\n// onTurnComplete - turn-aware stream operator for loop bodies\n// ---------------------------------------------------------------------------\n\n/**\n * Lift a provider's `Stream<TurnEvent>` into a loop body's `Stream<Step<TurnEvent | A, S>>`.\n * Each delta passes through as `value(delta)` (including the terminal\n * `TurnComplete`, so the consumer sees turn boundaries naturally). Once\n * the terminal arrives, `then(turn)` runs and its returned stream of loop\n * steps (typically tool outputs followed by `next(state)` or `stop()`) is\n * concatenated.\n *\n * Pre-pipe transforms (`Stream.tap` / `Stream.map` / `Stream.filter`) on\n * the raw delta stream cover anything an `emit`-style callback would do.\n *\n * `then` may return the step stream directly, or an `Effect` that produces\n * it (for branches that need to read a `Ref`, decode tool args, log, etc.).\n * Mirrors `loop`'s body: return a bare `stop()` / `next(s)` where no effect\n * is needed, lift to `Effect` only where one is.\n *\n * If the upstream ends without a `TurnComplete`, the resulting stream\n * fails with `AiError.IncompleteTurn`. Catch it via `Stream.catchTag` if\n * you want to recover.\n *\n * Dual: data-first `onTurnComplete(deltas, then)` and data-last\n * `deltas.pipe(onTurnComplete(then))` both work.\n */\ntype TurnContinuation<S, A, E2, R2> =\n | Stream.Stream<Step<A, S>, E2, R2>\n | Effect.Effect<Stream.Stream<Step<A, S>, E2, R2>, E2, R2>\n\nexport const onTurnComplete: {\n <S, A, E2 = never, R2 = never>(\n then: (turn: Turn) => TurnContinuation<S, A, E2, R2>,\n ): <E, R>(\n deltas: Stream.Stream<TurnEvent, E, R>,\n ) => Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>\n <S, A, E, R, E2 = never, R2 = never>(\n deltas: Stream.Stream<TurnEvent, E, R>,\n then: (turn: Turn) => TurnContinuation<S, A, E2, R2>,\n ): Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>\n} = Function.dual(\n 2,\n <S, A, E, R, E2, R2>(\n deltas: Stream.Stream<TurnEvent, E, R>,\n then: (turn: Turn) => TurnContinuation<S, A, E2, R2>,\n ): Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const turnRef = yield* Ref.make<Option.Option<Turn>>(Option.none())\n\n const events: Stream.Stream<Step<TurnEvent, S>, E, R> = deltas.pipe(\n Stream.tap((delta) =>\n isTurnComplete(delta) ? Ref.set(turnRef, Option.some(delta.turn)) : Effect.void,\n ),\n Stream.map(value),\n )\n\n const continuation = Stream.unwrap(\n Effect.gen(function* () {\n const opt = yield* Ref.get(turnRef)\n if (Option.isNone(opt)) return yield* new IncompleteTurn({})\n const result = then(opt.value)\n return Effect.isEffect(result) ? yield* result : result\n }),\n )\n\n return Stream.concat(events, continuation)\n }),\n ),\n)\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst isNonEmpty = <A>(array: ReadonlyArray<A>): array is readonly [A, ...Array<A>] =>\n array.length > 0\n\ntype CurrentBody<S, A, E, R> = {\n readonly scope: Scope.Closeable\n readonly pull: Effect.Effect<ReadonlyArray<Step<A, S>>, E | Cause.Done<void>, R>\n}\n\nconst closeBody = <S, A, E, R>(\n current: CurrentBody<S, A, E, R>,\n exit: Exit.Exit<unknown, unknown>,\n) => Scope.close(current.scope, exit)\n\n/**\n * Walk a chunk of `Step<A, S>` until a terminal `Next` or `Stop` is found.\n * Returns the unwrapped values seen so far and (optionally) the terminal\n * step. Anything in the chunk after the terminal is discarded - its\n * producing side effects may have run, but downstream never sees it.\n */\nconst partitionChunk = <A, S>(\n chunk: ReadonlyArray<Step<A, S>>,\n): {\n readonly values: ReadonlyArray<A>\n readonly decision: Option.Option<Step<A, S>>\n} => {\n const [valueSteps, rest] = Arr.span(\n chunk,\n (e): e is Step<A, S> & { _tag: \"Value\" } => e._tag === \"Value\",\n )\n return {\n values: valueSteps.map((e) => e.value),\n decision: Arr.head(rest),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\ntype LoopBody<S, A, E, R> = (\n state: S,\n) => Stream.Stream<Step<A, S>, E, R> | Effect.Effect<Stream.Stream<Step<A, S>, E, R>, E, R>\n\n/**\n * Drive a state-threaded loop body. Each iteration runs `body(state)` to get\n * a `Stream<Step<A, S>>`; values flow downstream, `next(s)` continues with\n * a new state, `stop()` ends the loop. See the file header for the full\n * pull-based execution model.\n *\n * Dual: data-first `loop(initial, body)` and data-last `loop(body)(initial)`\n * (or `pipe(initial, loop(body))`) both work.\n */\nexport const loop: {\n <S, A, E, R>(body: LoopBody<S, A, E, R>): (initial: S) => Stream.Stream<A, E, R>\n <S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>): Stream.Stream<A, E, R>\n} = Function.dual(\n 2,\n <S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>): Stream.Stream<A, E, R> =>\n Stream.scoped(\n Stream.fromPull(\n Effect.gen(function* () {\n const outerScope = yield* Effect.scope\n let state = initial\n let current: CurrentBody<S, A, E, R> | undefined\n let done = false\n\n const closeActive = (\n active: CurrentBody<S, A, E, R>,\n exit: Exit.Exit<unknown, unknown>,\n ) => {\n const isActive = current === active\n if (isActive) current = undefined\n // Scope.close is idempotent. Multiple paths can race to close the\n // active body during cancellation/failure, so closing twice is safe.\n return closeBody(active, exit)\n }\n\n yield* Scope.addFinalizerExit(outerScope, (exit) =>\n current === undefined ? Effect.void : closeActive(current, exit),\n )\n\n const pull = Effect.gen(function* () {\n while (true) {\n if (done) return yield* Cause.done()\n\n if (current === undefined) {\n const result = body(state)\n const stream = Effect.isEffect(result) ? Stream.unwrap(result) : result\n const bodyScope = yield* Scope.fork(outerScope)\n const bodyPull = yield* Channel.toPullScoped(\n Stream.toChannel(stream),\n bodyScope,\n ).pipe(Effect.onError((cause) => Scope.close(bodyScope, Exit.failCause(cause))))\n current = { scope: bodyScope, pull: bodyPull }\n }\n\n const active = current\n const chunk = yield* active.pull.pipe(\n Effect.catchIf(Cause.isDone, () =>\n closeActive(active, Exit.void).pipe(\n Effect.as(undefined as ReadonlyArray<Step<A, S>> | undefined),\n ),\n ),\n Effect.onError((cause) => closeActive(active, Exit.failCause(cause))),\n )\n\n if (chunk === undefined) {\n done = true\n return yield* Cause.done()\n }\n\n const { values, decision } = partitionChunk(chunk)\n\n if (Option.isSome(decision)) {\n yield* closeActive(active, Exit.void)\n if (decision.value._tag === \"Stop\" || decision.value._tag === \"StopWith\") {\n // `loop` has no next iteration to apply StopWith's state to;\n // the state lands in `loopOver`'s outer ref or\n // `loopWithState`'s SubscriptionRef via their taps.\n done = true\n } else if (decision.value._tag === \"Next\") {\n state = decision.value.state\n }\n }\n\n // Emit the values seen so far if any. Chunks from a Stream pull\n // are non-empty, so when `decision` is `None` every step was\n // a `Value` and `values` is non-empty here. With a decision and\n // no preceding values, fall through to the next iteration.\n if (isNonEmpty(values)) return values\n }\n })\n\n // `Stream.fromPull` expects the generator to RETURN the pull effect,\n // not run it; `return yield* pull` would execute a pull and break it.\n // @effect-diagnostics-next-line effect/returnEffectInGen:off\n return pull\n }),\n ),\n ),\n)\n\n// ---------------------------------------------------------------------------\n// loopOver - stream-driven sibling of loop. One input item runs a full\n// multi-turn inner loop.\n// ---------------------------------------------------------------------------\n\ntype LoopOverBody<S, I, A, E, R> = (\n state: S,\n input: I,\n) => Stream.Stream<Step<A, S>, E, R> | Effect.Effect<Stream.Stream<Step<A, S>, E, R>, E, R>\n\n/**\n * Input-driven sibling of `loop`. For each item pulled from the input\n * stream, runs an inner seed-driven `loop` whose body is\n * `(s) => body(s, item)`. State is threaded across input items.\n *\n * **Per-input semantics — the body emits standard `Step<A, S>`:**\n * - `value(a)`: emit `a` downstream\n * - `next(s)`: re-run the body with the SAME input and new state `s`\n * (multi-turn within one input — e.g. multiple model turns + tool\n * calls for one document)\n * - `stop()`: end this input's inner loop, advance to the next input\n * (state preserved)\n * - body stream ending without a decision: same as `stop()` (advance)\n *\n * **Outer termination:** the input stream ending. To halt programmatically\n * from within, end the input stream upstream (`Stream.takeWhile`, a\n * `SubscriptionRef` gate, etc.). Reserving `stop()` for per-item\n * advancement is what makes the common \"stream of documents, multi-turn\n * conversation per document\" shape readable.\n *\n * Dual: data-first `loopOver(input, initial, body)` and data-last\n * `input.pipe(loopOver(initial, body))` both work.\n */\nexport const loopOver: {\n <S, I, A, E, R>(\n initial: S,\n body: LoopOverBody<S, I, A, E, R>,\n ): <EI, RI>(input: Stream.Stream<I, EI, RI>) => Stream.Stream<A, E | EI, R | RI>\n <S, I, A, E, R, EI, RI>(\n input: Stream.Stream<I, EI, RI>,\n initial: S,\n body: LoopOverBody<S, I, A, E, R>,\n ): Stream.Stream<A, E | EI, R | RI>\n} = Function.dual(\n 3,\n <S, I, A, E, R, EI, RI>(\n input: Stream.Stream<I, EI, RI>,\n initial: S,\n body: LoopOverBody<S, I, A, E, R>,\n ): Stream.Stream<A, E | EI, R | RI> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const stateRef = yield* Ref.make<S>(initial)\n return input.pipe(\n Stream.flatMap((item) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const state = yield* Ref.get(stateRef)\n // Capture Next states (and stop(state)'s final state) into the\n // outer ref so the LAST state seen in this input's inner\n // loop is what the next input starts from.\n const wrappedBody = (s: S) => {\n const result = body(s, item)\n const stream = Effect.isEffect(result) ? Stream.unwrap(result) : result\n return stream.pipe(\n Stream.tap((step) =>\n Match.value(step).pipe(\n Match.tags({\n Next: (e) => Ref.set(stateRef, e.state),\n StopWith: (e) => Ref.set(stateRef, e.state),\n }),\n Match.orElse(() => Effect.void),\n ),\n ),\n )\n }\n return loop(state, wrappedBody)\n }),\n ),\n ),\n )\n }),\n ),\n)\n\n// ---------------------------------------------------------------------------\n// loopWithState - same body protocol, plus a live state observable.\n// ---------------------------------------------------------------------------\n\n/**\n * Like `loop`, but exposes the current loop state as a `SubscriptionRef`\n * alongside the value stream.\n *\n * Allocates one `SubscriptionRef<S>` seeded with `initial`, then runs the\n * loop with a wrapped body that taps every `Next(s)` step into the ref\n * before forwarding it. The caller decides how to consume both channels:\n *\n * - **Final state**: drain the stream, then `SubscriptionRef.get(state)`\n * - the ref holds the state from the last `Next` (or `initial` if the\n * loop ended without advancing).\n * - **Live transitions**: `SubscriptionRef.changes(state)` is a\n * `Stream<S>` of every state observed; subscribe alongside the value\n * stream.\n * - **Mid-iteration peek**: `SubscriptionRef.get(state)` at any time.\n *\n * The returned stream and ref are independent of each other - the ref\n * lives outside the stream's scope, so reading it after the stream\n * completes is safe.\n */\nexport const loopWithState = <S, A, E, R>(\n initial: S,\n body: LoopBody<S, A, E, R>,\n): Effect.Effect<{\n readonly stream: Stream.Stream<A, E, R>\n readonly state: SubscriptionRef.SubscriptionRef<S>\n}> =>\n Effect.gen(function* () {\n const stateRef = yield* SubscriptionRef.make(initial)\n\n const tap = (stream: Stream.Stream<Step<A, S>, E, R>): Stream.Stream<Step<A, S>, E, R> =>\n stream.pipe(\n Stream.tap((step) =>\n Match.value(step).pipe(\n Match.tags({\n Next: (e) => SubscriptionRef.set(stateRef, e.state),\n StopWith: (e) => SubscriptionRef.set(stateRef, e.state),\n }),\n Match.orElse(() => Effect.void),\n ),\n ),\n )\n\n const wrappedBody: LoopBody<S, A, E, R> = (s) => {\n const result = body(s)\n return Effect.isEffect(result) ? Effect.map(result, tap) : tap(result)\n }\n\n return {\n stream: loop(initial, wrappedBody),\n state: stateRef,\n }\n })\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Loop.mjs","names":["Arr"],"sources":["../../src/loop/Loop.ts"],"sourcesContent":["/**\n * Pull-based `loop` for state-threaded sub-streams.\n *\n * Each iteration runs a body that returns a `Stream<Step<A, S>>`. The body\n * emits values via `Loop.value(a)` and signals iteration control via\n * `Loop.next(state)` (continue with new state) or `Loop.stop()` (terminate).\n * The loop unwraps `Value` steps back to `A` for downstream consumers, so\n * the resulting stream is a plain `Stream<A>`.\n *\n * The next body stream is only pulled when downstream pulls the outer\n * stream - no producer fiber, no queue buffering. Cancellation, failures,\n * scoped resources, and backpressure stay aligned with normal Stream\n * semantics.\n *\n * Convention: a `Next` or `Stop` step is the terminal element of a body's\n * iteration. Values emitted in the same chunk after one are discarded\n * (their producing side effects may already have run). The `Loop.next` /\n * `Loop.stop` helpers each emit a single terminal step, so concatenate\n * your values before them (`values.pipe(Stream.concat(Loop.next(state)))`).\n */\nimport {\n Array as Arr,\n Cause,\n Channel,\n Data,\n Effect,\n Exit,\n Function,\n Match,\n Option,\n Ref,\n Scope,\n Stream,\n SubscriptionRef,\n} from \"effect\"\nimport { IncompleteTurn } from \"../domain/AiError.js\"\nimport { isTurnComplete, type Turn, type TurnEvent } from \"../domain/Turn.js\"\n\n// ---------------------------------------------------------------------------\n// Step type - the body's emit shape\n// ---------------------------------------------------------------------------\n\n/**\n * The tagged union a body emits per pull. `Value` carries a payload that\n * flows downstream. `Next` ends the current iteration and continues with a\n * new state. `Stop` ends the loop entirely with no carried state.\n * `StopWith` also ends the loop but carries a final state that `loopOver`\n * will thread to the next input and `loopWithState` will write to its\n * `SubscriptionRef` before the loop ends. Plain `loop` has no next\n * iteration to apply it to and treats `StopWith` like `Stop`.\n *\n * `Stop` is intentionally `{}` so the bare `stop()` helper doesn't\n * constrain `S` from a body's stream type — every body has a `Stop`\n * variant in its union, and forcing `S` to flow through it would break\n * inference whenever the body never uses `next` / `stop(state)`.\n */\nexport type Step<A, S> = Data.TaggedEnum<{\n Value: { readonly value: A }\n Next: { readonly state: S }\n Stop: {}\n StopWith: { readonly state: S }\n}>\n\ninterface StepDef extends Data.TaggedEnum.WithGenerics<2> {\n readonly taggedEnum: Step<this[\"A\"], this[\"B\"]>\n}\n\nconst Step = Data.taggedEnum<StepDef>()\n\n/**\n * Bare step constructors, so a loop body can emit `Value(...)`, `Next(...)`,\n * `Stop()`, `StopWith(...)` directly inside a `Stream.make(...)` without the\n * `Step.` prefix. (The `Step` object itself can't be exported under that name\n * because it collides with the exported `Step` type.) For the common cases\n * prefer the `value` / `next` / `stop` helpers, which also handle the\n * single-element-stream wrapping for `next` / `stop`.\n */\nexport const { Value, Next, Stop, StopWith } = Step\n\n/** Wrap a value so it flows through the loop to downstream consumers. */\nexport const value = <A>(a: A): Step<A, never> => Step.Value({ value: a })\n\n/**\n * End the current iteration and continue with a new state. Emits a single\n * terminal `Next` step as a one-element stream, so it slots directly into a\n * loop body or after a run of values: `values.pipe(Stream.concat(next(s)))`.\n */\nexport const next = <S>(state: S): Stream.Stream<Step<never, S>> =>\n Stream.succeed(Step.Next({ state }))\n\n/**\n * End the loop. Called with no argument it emits a bare `Stop`; called with\n * a state it emits `StopWith(state)` — `loopOver` threads that final state\n * to the next input and `loopWithState` writes it to its `SubscriptionRef`\n * before the loop ends. Either way it returns a single-element stream, so\n * return it directly from a loop body.\n */\nexport const stop = <S = never>(state?: S): Stream.Stream<Step<never, S>> =>\n Stream.succeed<Step<never, S>>(state === undefined ? Step.Stop() : Step.StopWith({ state }))\n\n// ---------------------------------------------------------------------------\n// onTurnComplete - turn-aware stream operator for loop bodies\n// ---------------------------------------------------------------------------\n\n/**\n * Lift a provider's `Stream<TurnEvent>` into a loop body's `Stream<Step<TurnEvent | A, S>>`.\n * Each delta passes through as `value(delta)` (including the terminal\n * `TurnComplete`, so the consumer sees turn boundaries naturally). Once\n * the terminal arrives, `then(turn)` runs and its returned stream of loop\n * steps (typically tool outputs followed by `next(state)` or `stop()`) is\n * concatenated.\n *\n * Pre-pipe transforms (`Stream.tap` / `Stream.map` / `Stream.filter`) on\n * the raw delta stream cover anything an `emit`-style callback would do.\n *\n * `then` may return the step stream directly, or an `Effect` that produces\n * it (for branches that need to read a `Ref`, decode tool args, log, etc.).\n * Mirrors `loop`'s body: return a bare `stop()` / `next(s)` where no effect\n * is needed, lift to `Effect` only where one is.\n *\n * If the upstream ends without a `TurnComplete`, the resulting stream\n * fails with `AiError.IncompleteTurn`. Catch it via `Stream.catchTag` if\n * you want to recover.\n *\n * Dual: data-first `onTurnComplete(deltas, then)` and data-last\n * `deltas.pipe(onTurnComplete(then))` both work.\n */\ntype TurnContinuation<S, A, E2, R2> =\n | Stream.Stream<Step<A, S>, E2, R2>\n | Effect.Effect<Stream.Stream<Step<A, S>, E2, R2>, E2, R2>\n\nexport const onTurnComplete: {\n <S, A, E2 = never, R2 = never>(\n then: (turn: Turn) => TurnContinuation<S, A, E2, R2>,\n ): <E, R>(\n deltas: Stream.Stream<TurnEvent, E, R>,\n ) => Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>\n <S, A, E, R, E2 = never, R2 = never>(\n deltas: Stream.Stream<TurnEvent, E, R>,\n then: (turn: Turn) => TurnContinuation<S, A, E2, R2>,\n ): Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>\n} = Function.dual(\n 2,\n <S, A, E, R, E2, R2>(\n deltas: Stream.Stream<TurnEvent, E, R>,\n then: (turn: Turn) => TurnContinuation<S, A, E2, R2>,\n ): Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const turnRef = yield* Ref.make<Option.Option<Turn>>(Option.none())\n\n const events: Stream.Stream<Step<TurnEvent, S>, E, R> = deltas.pipe(\n Stream.tap((delta) =>\n isTurnComplete(delta) ? Ref.set(turnRef, Option.some(delta.turn)) : Effect.void,\n ),\n Stream.map(value),\n )\n\n const continuation = Stream.unwrap(\n Effect.gen(function* () {\n const opt = yield* Ref.get(turnRef)\n if (Option.isNone(opt)) return yield* new IncompleteTurn({})\n const result = then(opt.value)\n return Effect.isEffect(result) ? yield* result : result\n }),\n )\n\n return Stream.concat(events, continuation)\n }),\n ),\n)\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst isNonEmpty = <A>(array: ReadonlyArray<A>): array is readonly [A, ...Array<A>] =>\n array.length > 0\n\ntype CurrentBody<S, A, E, R> = {\n readonly scope: Scope.Closeable\n readonly pull: Effect.Effect<ReadonlyArray<Step<A, S>>, E | Cause.Done<void>, R>\n}\n\nconst closeBody = <S, A, E, R>(\n current: CurrentBody<S, A, E, R>,\n exit: Exit.Exit<unknown, unknown>,\n) => Scope.close(current.scope, exit)\n\n/**\n * Walk a chunk of `Step<A, S>` until a terminal `Next` or `Stop` is found.\n * Returns the unwrapped values seen so far and (optionally) the terminal\n * step. Anything in the chunk after the terminal is discarded - its\n * producing side effects may have run, but downstream never sees it.\n */\nconst partitionChunk = <A, S>(\n chunk: ReadonlyArray<Step<A, S>>,\n): {\n readonly values: ReadonlyArray<A>\n readonly decision: Option.Option<Step<A, S>>\n} => {\n const [valueSteps, rest] = Arr.span(\n chunk,\n (e): e is Step<A, S> & { _tag: \"Value\" } => e._tag === \"Value\",\n )\n return {\n values: valueSteps.map((e) => e.value),\n decision: Arr.head(rest),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\ntype LoopBody<S, A, E, R> = (\n state: S,\n) => Stream.Stream<Step<A, S>, E, R> | Effect.Effect<Stream.Stream<Step<A, S>, E, R>, E, R>\n\n/**\n * Drive a state-threaded loop body. Each iteration runs `body(state)` to get\n * a `Stream<Step<A, S>>`; values flow downstream, `next(s)` continues with\n * a new state, `stop()` ends the loop. See the file header for the full\n * pull-based execution model.\n *\n * Dual: data-first `loop(initial, body)` and data-last `loop(body)(initial)`\n * (or `pipe(initial, loop(body))`) both work.\n */\nexport const loop: {\n <S, A, E, R>(body: LoopBody<S, A, E, R>): (initial: S) => Stream.Stream<A, E, R>\n <S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>): Stream.Stream<A, E, R>\n} = Function.dual(2, <S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>): Stream.Stream<A, E, R> =>\n Stream.scoped(\n Stream.fromPull(\n Effect.gen(function* () {\n const outerScope = yield* Effect.scope\n let state = initial\n let current: CurrentBody<S, A, E, R> | undefined\n let done = false\n\n const closeActive = (\n active: CurrentBody<S, A, E, R>,\n exit: Exit.Exit<unknown, unknown>,\n ) => {\n const isActive = current === active\n if (isActive) current = undefined\n // Scope.close is idempotent. Multiple paths can race to close the\n // active body during cancellation/failure, so closing twice is safe.\n return closeBody(active, exit)\n }\n\n yield* Scope.addFinalizerExit(outerScope, (exit) =>\n current === undefined ? Effect.void : closeActive(current, exit),\n )\n\n const pull = Effect.gen(function* () {\n while (true) {\n if (done) return yield* Cause.done()\n\n if (current === undefined) {\n const result = body(state)\n const stream = Effect.isEffect(result) ? Stream.unwrap(result) : result\n const bodyScope = yield* Scope.fork(outerScope)\n const bodyPull = yield* Channel.toPullScoped(\n Stream.toChannel(stream),\n bodyScope,\n ).pipe(Effect.onError((cause) => Scope.close(bodyScope, Exit.failCause(cause))))\n current = { scope: bodyScope, pull: bodyPull }\n }\n\n const active = current\n const chunk = yield* active.pull.pipe(\n Effect.catchIf(Cause.isDone, () =>\n closeActive(active, Exit.void).pipe(\n Effect.as(undefined as ReadonlyArray<Step<A, S>> | undefined),\n ),\n ),\n Effect.onError((cause) => closeActive(active, Exit.failCause(cause))),\n )\n\n if (chunk === undefined) {\n done = true\n return yield* Cause.done()\n }\n\n const { values, decision } = partitionChunk(chunk)\n\n if (Option.isSome(decision)) {\n yield* closeActive(active, Exit.void)\n if (decision.value._tag === \"Stop\" || decision.value._tag === \"StopWith\") {\n // `loop` has no next iteration to apply StopWith's state to;\n // the state lands in `loopOver`'s outer ref or\n // `loopWithState`'s SubscriptionRef via their taps.\n done = true\n } else if (decision.value._tag === \"Next\") {\n state = decision.value.state\n }\n }\n\n // Emit the values seen so far if any. Chunks from a Stream pull\n // are non-empty, so when `decision` is `None` every step was\n // a `Value` and `values` is non-empty here. With a decision and\n // no preceding values, fall through to the next iteration.\n if (isNonEmpty(values)) return values\n }\n })\n\n // `Stream.fromPull` expects the generator to RETURN the pull effect,\n // not run it; `return yield* pull` would execute a pull and break it.\n // @effect-diagnostics-next-line effect/returnEffectInGen:off\n return pull\n }),\n ),\n ),\n)\n\n// ---------------------------------------------------------------------------\n// loopOver - stream-driven sibling of loop. One input item runs a full\n// multi-turn inner loop.\n// ---------------------------------------------------------------------------\n\ntype LoopOverBody<S, I, A, E, R> = (\n state: S,\n input: I,\n) => Stream.Stream<Step<A, S>, E, R> | Effect.Effect<Stream.Stream<Step<A, S>, E, R>, E, R>\n\n/**\n * Input-driven sibling of `loop`. For each item pulled from the input\n * stream, runs an inner seed-driven `loop` whose body is\n * `(s) => body(s, item)`. State is threaded across input items.\n *\n * **Per-input semantics — the body emits standard `Step<A, S>`:**\n * - `value(a)`: emit `a` downstream\n * - `next(s)`: re-run the body with the SAME input and new state `s`\n * (multi-turn within one input — e.g. multiple model turns + tool\n * calls for one document)\n * - `stop()`: end this input's inner loop, advance to the next input\n * (state preserved)\n * - body stream ending without a decision: same as `stop()` (advance)\n *\n * **Outer termination:** the input stream ending. To halt programmatically\n * from within, end the input stream upstream (`Stream.takeWhile`, a\n * `SubscriptionRef` gate, etc.). Reserving `stop()` for per-item\n * advancement is what makes the common \"stream of documents, multi-turn\n * conversation per document\" shape readable.\n *\n * Dual: data-first `loopOver(input, initial, body)` and data-last\n * `input.pipe(loopOver(initial, body))` both work.\n */\nexport const loopOver: {\n <S, I, A, E, R>(\n initial: S,\n body: LoopOverBody<S, I, A, E, R>,\n ): <EI, RI>(input: Stream.Stream<I, EI, RI>) => Stream.Stream<A, E | EI, R | RI>\n <S, I, A, E, R, EI, RI>(\n input: Stream.Stream<I, EI, RI>,\n initial: S,\n body: LoopOverBody<S, I, A, E, R>,\n ): Stream.Stream<A, E | EI, R | RI>\n} = Function.dual(\n 3,\n <S, I, A, E, R, EI, RI>(\n input: Stream.Stream<I, EI, RI>,\n initial: S,\n body: LoopOverBody<S, I, A, E, R>,\n ): Stream.Stream<A, E | EI, R | RI> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const stateRef = yield* Ref.make<S>(initial)\n return input.pipe(\n Stream.flatMap((item) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const state = yield* Ref.get(stateRef)\n // Capture Next states (and stop(state)'s final state) into the\n // outer ref so the LAST state seen in this input's inner\n // loop is what the next input starts from.\n const wrappedBody = (s: S) => {\n const result = body(s, item)\n const stream = Effect.isEffect(result) ? Stream.unwrap(result) : result\n return stream.pipe(\n Stream.tap((step) =>\n Match.value(step).pipe(\n Match.tags({\n Next: (e) => Ref.set(stateRef, e.state),\n StopWith: (e) => Ref.set(stateRef, e.state),\n }),\n Match.orElse(() => Effect.void),\n ),\n ),\n )\n }\n return loop(state, wrappedBody)\n }),\n ),\n ),\n )\n }),\n ),\n)\n\n// ---------------------------------------------------------------------------\n// loopWithState - same body protocol, plus a live state observable.\n// ---------------------------------------------------------------------------\n\n/**\n * Like `loop`, but exposes the current loop state as a `SubscriptionRef`\n * alongside the value stream.\n *\n * Allocates one `SubscriptionRef<S>` seeded with `initial`, then runs the\n * loop with a wrapped body that taps every `Next(s)` step into the ref\n * before forwarding it. The caller decides how to consume both channels:\n *\n * - **Final state**: drain the stream, then `SubscriptionRef.get(state)`\n * - the ref holds the state from the last `Next` (or `initial` if the\n * loop ended without advancing).\n * - **Live transitions**: `SubscriptionRef.changes(state)` is a\n * `Stream<S>` of every state observed; subscribe alongside the value\n * stream.\n * - **Mid-iteration peek**: `SubscriptionRef.get(state)` at any time.\n *\n * The returned stream and ref are independent of each other - the ref\n * lives outside the stream's scope, so reading it after the stream\n * completes is safe.\n */\nexport const loopWithState = <S, A, E, R>(\n initial: S,\n body: LoopBody<S, A, E, R>,\n): Effect.Effect<{\n readonly stream: Stream.Stream<A, E, R>\n readonly state: SubscriptionRef.SubscriptionRef<S>\n}> =>\n Effect.gen(function* () {\n const stateRef = yield* SubscriptionRef.make(initial)\n\n const tap = (stream: Stream.Stream<Step<A, S>, E, R>): Stream.Stream<Step<A, S>, E, R> =>\n stream.pipe(\n Stream.tap((step) =>\n Match.value(step).pipe(\n Match.tags({\n Next: (e) => SubscriptionRef.set(stateRef, e.state),\n StopWith: (e) => SubscriptionRef.set(stateRef, e.state),\n }),\n Match.orElse(() => Effect.void),\n ),\n ),\n )\n\n const wrappedBody: LoopBody<S, A, E, R> = (s) => {\n const result = body(s)\n return Effect.isEffect(result) ? Effect.map(result, tap) : tap(result)\n }\n\n return {\n stream: loop(initial, wrappedBody),\n state: stateRef,\n }\n })\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
package/dist/loop/Loop.test.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { RateLimited } from "../domain/AiError.mjs";
|
|
2
2
|
import { TurnEvent } from "../domain/Turn.mjs";
|
|
3
3
|
import { loop, loopOver, loopWithState, next, onTurnComplete, stop, value } from "./Loop.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { m as it, o as describe } from "../utils-CiH_DjFW.mjs";
|
|
5
|
+
import { n as globalExpect } from "../test.DNmyFkvJ-YPzDR0Co.mjs";
|
|
6
|
+
import { t as import_dist } from "../dist-BaenoU1A.mjs";
|
|
5
7
|
import { Deferred, Effect, Fiber, Latch, Ref, Stream, SubscriptionRef, pipe } from "effect";
|
|
6
8
|
//#region src/loop/Loop.test.ts
|
|
7
9
|
describe("Loop.loop", () => {
|
|
8
10
|
it("threads state across iterations and emits each iteration's substream in order", async () => {
|
|
9
11
|
const stream = loop(0, (n) => n >= 3 ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop())) : Stream.fromIterable([n, n + .5]).pipe(Stream.map(value), Stream.concat(next(n + 1))));
|
|
10
|
-
|
|
12
|
+
const result = await Effect.runPromise(Stream.runCollect(stream));
|
|
13
|
+
globalExpect(result).toEqual([
|
|
11
14
|
0,
|
|
12
15
|
.5,
|
|
13
16
|
1,
|
|
@@ -19,14 +22,16 @@ describe("Loop.loop", () => {
|
|
|
19
22
|
});
|
|
20
23
|
it("supports iterations that emit zero values and only decide", async () => {
|
|
21
24
|
const stream = loop(0, (n) => n >= 5 ? stop() : next(n + 1));
|
|
22
|
-
|
|
25
|
+
const result = await Effect.runPromise(Stream.runCollect(stream));
|
|
26
|
+
globalExpect(result).toEqual([]);
|
|
23
27
|
});
|
|
24
28
|
it("supports Effect-returning bodies directly (no Stream.unwrap needed)", async () => {
|
|
25
29
|
const stream = loop(1, (n) => Effect.gen(function* () {
|
|
26
30
|
const doubled = yield* Effect.succeed(n * 2);
|
|
27
31
|
return doubled >= 16 ? Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(stop())) : Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(next(doubled)));
|
|
28
32
|
}));
|
|
29
|
-
|
|
33
|
+
const result = await Effect.runPromise(Stream.runCollect(stream));
|
|
34
|
+
globalExpect(result).toEqual([
|
|
30
35
|
2,
|
|
31
36
|
4,
|
|
32
37
|
8,
|
|
@@ -38,20 +43,24 @@ describe("Loop.loop", () => {
|
|
|
38
43
|
const doubled = yield* Effect.succeed(n * 2);
|
|
39
44
|
return doubled >= 4 ? Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(stop())) : Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(next(doubled)));
|
|
40
45
|
})));
|
|
41
|
-
|
|
46
|
+
const result = await Effect.runPromise(Stream.runCollect(stream));
|
|
47
|
+
globalExpect(result).toEqual([2, 4]);
|
|
42
48
|
});
|
|
43
49
|
it("propagates errors from the body's stream", async () => {
|
|
44
50
|
const boom = /* @__PURE__ */ new Error("boom");
|
|
45
51
|
const stream = loop(0, (n) => n === 2 ? Stream.fail(boom) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))));
|
|
46
|
-
|
|
52
|
+
const result = await Effect.runPromiseExit(Stream.runCollect(stream));
|
|
53
|
+
globalExpect(result._tag).toBe("Failure");
|
|
47
54
|
});
|
|
48
55
|
it("terminates silently if the body emits no Decision (mirrors paginate's silent stop)", async () => {
|
|
49
56
|
const stream = loop(0, (n) => Stream.fromIterable([value(n), value(n + 1)]));
|
|
50
|
-
|
|
57
|
+
const result = await Effect.runPromise(Stream.runCollect(stream));
|
|
58
|
+
globalExpect(result).toEqual([0, 1]);
|
|
51
59
|
});
|
|
52
60
|
it("short-circuits the body's stream when a Decision is seen", async () => {
|
|
53
61
|
const stream = loop(0, (n) => n >= 2 ? Stream.succeed(value(n)).pipe(Stream.concat(stop())) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1)), Stream.concat(Stream.succeed(value(n + 10)))));
|
|
54
|
-
|
|
62
|
+
const result = await Effect.runPromise(Stream.runCollect(stream));
|
|
63
|
+
globalExpect(result).toEqual([
|
|
55
64
|
0,
|
|
56
65
|
1,
|
|
57
66
|
2
|
|
@@ -118,7 +127,8 @@ describe("Loop.loop", () => {
|
|
|
118
127
|
it("is stack-safe and linear-time across many iterations", async () => {
|
|
119
128
|
const N = 1e5;
|
|
120
129
|
const stream = loop(0, (n) => n >= N ? Stream.succeed(value(n)).pipe(Stream.concat(stop())) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))));
|
|
121
|
-
|
|
130
|
+
const count = await Effect.runPromise(Stream.runFold(stream, () => 0, (acc) => acc + 1));
|
|
131
|
+
globalExpect(count).toBe(100001);
|
|
122
132
|
}, 1e4);
|
|
123
133
|
});
|
|
124
134
|
const scriptedModel = (script) => {
|
|
@@ -225,7 +235,8 @@ describe("Loop.loop - LLM-style scenarios", () => {
|
|
|
225
235
|
}],
|
|
226
236
|
model: m
|
|
227
237
|
};
|
|
228
|
-
|
|
238
|
+
const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)));
|
|
239
|
+
globalExpect(events).toEqual([
|
|
229
240
|
{
|
|
230
241
|
type: "text",
|
|
231
242
|
text: "hello"
|
|
@@ -292,7 +303,8 @@ describe("Loop.loop - LLM-style scenarios", () => {
|
|
|
292
303
|
}],
|
|
293
304
|
model: m1
|
|
294
305
|
};
|
|
295
|
-
|
|
306
|
+
const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)));
|
|
307
|
+
globalExpect(events).toEqual([
|
|
296
308
|
{
|
|
297
309
|
type: "text",
|
|
298
310
|
text: "Hard question."
|
|
@@ -324,27 +336,31 @@ describe("Loop.loop - LLM-style scenarios", () => {
|
|
|
324
336
|
});
|
|
325
337
|
describe("Loop.loop - pull-specific stream semantics", () => {
|
|
326
338
|
it("does not start the next iteration when downstream only takes the first value", async () => {
|
|
327
|
-
|
|
339
|
+
const bodyCalls = await Effect.runPromise(Effect.gen(function* () {
|
|
328
340
|
const callsRef = yield* Ref.make(0);
|
|
329
341
|
yield* loop(0, (n) => Stream.unwrap(Ref.update(callsRef, (calls) => calls + 1).pipe(Effect.as(n >= 10 ? Stream.succeed(value(n)).pipe(Stream.concat(stop())) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))))))).pipe(Stream.take(1), Stream.runCollect);
|
|
330
342
|
return yield* Ref.get(callsRef);
|
|
331
|
-
}))
|
|
343
|
+
}));
|
|
344
|
+
globalExpect(bodyCalls).toBe(1);
|
|
332
345
|
});
|
|
333
346
|
it("propagates defects from the body instead of leaving the consumer waiting", async () => {
|
|
334
347
|
const defect = /* @__PURE__ */ new Error("defect");
|
|
335
348
|
const stream = loop(0, () => Stream.die(defect));
|
|
336
|
-
|
|
349
|
+
const result = await Effect.runPromiseExit(Stream.runCollect(stream));
|
|
350
|
+
globalExpect(result._tag).toBe("Failure");
|
|
337
351
|
});
|
|
338
352
|
it("runs body finalizers when a Decision short-circuits the body", async () => {
|
|
339
|
-
|
|
353
|
+
const releases = await Effect.runPromise(Effect.gen(function* () {
|
|
340
354
|
const releasesRef = yield* Ref.make([]);
|
|
341
355
|
const stream = loop(0, (n) => (n >= 1 ? Stream.succeed(value(n)).pipe(Stream.concat(stop())) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1)), Stream.concat(Stream.succeed(value(n + 10))))).pipe(Stream.ensuring(Ref.update(releasesRef, (values) => [...values, n]))));
|
|
342
|
-
|
|
356
|
+
const values = yield* Stream.runCollect(stream);
|
|
357
|
+
globalExpect(values).toEqual([0, 1]);
|
|
343
358
|
return yield* Ref.get(releasesRef);
|
|
344
|
-
}))
|
|
359
|
+
}));
|
|
360
|
+
globalExpect(releases).toEqual([0, 1]);
|
|
345
361
|
});
|
|
346
362
|
it("runs the active body finalizer when the downstream consumer is interrupted", async () => {
|
|
347
|
-
|
|
363
|
+
const releases = await Effect.runPromise(Effect.gen(function* () {
|
|
348
364
|
const started = yield* Deferred.make();
|
|
349
365
|
const releasesRef = yield* Ref.make(0);
|
|
350
366
|
const body = () => Stream.concat(Stream.fromEffect(Deferred.succeed(started, void 0).pipe(Effect.as(value(0)))), Stream.never).pipe(Stream.ensuring(Ref.update(releasesRef, (n) => n + 1)));
|
|
@@ -353,13 +369,15 @@ describe("Loop.loop - pull-specific stream semantics", () => {
|
|
|
353
369
|
yield* Deferred.await(started);
|
|
354
370
|
yield* Fiber.interrupt(fiber);
|
|
355
371
|
return yield* Ref.get(releasesRef);
|
|
356
|
-
}))
|
|
372
|
+
}));
|
|
373
|
+
globalExpect(releases).toBe(1);
|
|
357
374
|
});
|
|
358
375
|
it("does not create a body scope if constructing the body stream defects", async () => {
|
|
359
376
|
const defect = /* @__PURE__ */ new Error("body construction failed");
|
|
360
|
-
|
|
377
|
+
const result = await Effect.runPromiseExit(Stream.runCollect(loop(0, () => {
|
|
361
378
|
throw defect;
|
|
362
|
-
})))
|
|
379
|
+
})));
|
|
380
|
+
globalExpect(result._tag).toBe("Failure");
|
|
363
381
|
});
|
|
364
382
|
});
|
|
365
383
|
describe("Loop.loopWithState", () => {
|
|
@@ -459,10 +477,11 @@ describe("Loop.loopWithState", () => {
|
|
|
459
477
|
});
|
|
460
478
|
describe("Loop.loopOver", () => {
|
|
461
479
|
it("runs a multi-turn inner loop per input until the body emits stop", async () => {
|
|
462
|
-
|
|
480
|
+
const result = await Effect.runPromise(Stream.fromIterable(["a", "b"]).pipe(loopOver(0, (turns, input) => {
|
|
463
481
|
if (turns >= 2 * (input === "a" ? 1 : 2)) return stop();
|
|
464
482
|
return Stream.succeed(value(`${input}:${turns}`)).pipe(Stream.concat(next(turns + 1)));
|
|
465
|
-
}), Stream.runCollect))
|
|
483
|
+
}), Stream.runCollect));
|
|
484
|
+
globalExpect(result).toEqual([
|
|
466
485
|
"a:0",
|
|
467
486
|
"a:1",
|
|
468
487
|
"b:2",
|
|
@@ -470,18 +489,19 @@ describe("Loop.loopOver", () => {
|
|
|
470
489
|
]);
|
|
471
490
|
});
|
|
472
491
|
it("threads state across inputs (audio-pipeline shape)", async () => {
|
|
473
|
-
|
|
492
|
+
const result = await Effect.runPromise(Stream.fromIterable([
|
|
474
493
|
"x",
|
|
475
494
|
"y",
|
|
476
495
|
"z"
|
|
477
|
-
]).pipe(loopOver([], (history, input) => Stream.succeed(value([...history, input].join(","))).pipe(Stream.concat(stop([...history, input])))), Stream.runCollect))
|
|
496
|
+
]).pipe(loopOver([], (history, input) => Stream.succeed(value([...history, input].join(","))).pipe(Stream.concat(stop([...history, input])))), Stream.runCollect));
|
|
497
|
+
globalExpect(result).toEqual([
|
|
478
498
|
"x",
|
|
479
499
|
"x,y",
|
|
480
500
|
"x,y,z"
|
|
481
501
|
]);
|
|
482
502
|
});
|
|
483
503
|
it("simulates a stream of documents with multi-turn tool calls per document", async () => {
|
|
484
|
-
|
|
504
|
+
const result = await Effect.runPromise(Stream.fromIterable(["doc1", "doc2"]).pipe(loopOver({
|
|
485
505
|
turn: 0,
|
|
486
506
|
totalTurns: 0
|
|
487
507
|
}, (state, doc) => {
|
|
@@ -509,7 +529,8 @@ describe("Loop.loopOver", () => {
|
|
|
509
529
|
turn: 0,
|
|
510
530
|
totalTurns: state.totalTurns + 1
|
|
511
531
|
})));
|
|
512
|
-
}), Stream.runCollect))
|
|
532
|
+
}), Stream.runCollect));
|
|
533
|
+
globalExpect(result).toEqual([
|
|
513
534
|
{
|
|
514
535
|
kind: "text",
|
|
515
536
|
doc: "doc1",
|
|
@@ -543,14 +564,16 @@ describe("Loop.loopOver", () => {
|
|
|
543
564
|
]);
|
|
544
565
|
});
|
|
545
566
|
it("ends cleanly when the input stream ends mid-conversation", async () => {
|
|
546
|
-
|
|
567
|
+
const result = await Effect.runPromise(Stream.fromIterable(["only"]).pipe(loopOver(0, (turns, input) => turns >= 2 ? stop() : Stream.succeed(value(`${input}:${turns}`)).pipe(Stream.concat(next(turns + 1)))), Stream.runCollect));
|
|
568
|
+
globalExpect(result).toEqual(["only:0", "only:1"]);
|
|
547
569
|
});
|
|
548
570
|
it("body's `stop()` advances to the next input (does NOT halt the whole stream)", async () => {
|
|
549
|
-
|
|
571
|
+
const result = await Effect.runPromise(Stream.fromIterable([
|
|
550
572
|
1,
|
|
551
573
|
2,
|
|
552
574
|
3
|
|
553
|
-
]).pipe(loopOver(0, (_state, input) => Stream.succeed(value(input * 10)).pipe(Stream.concat(stop()))), Stream.runCollect))
|
|
575
|
+
]).pipe(loopOver(0, (_state, input) => Stream.succeed(value(input * 10)).pipe(Stream.concat(stop()))), Stream.runCollect));
|
|
576
|
+
globalExpect(result).toEqual([
|
|
554
577
|
10,
|
|
555
578
|
20,
|
|
556
579
|
30
|
|
@@ -558,14 +581,16 @@ describe("Loop.loopOver", () => {
|
|
|
558
581
|
});
|
|
559
582
|
it("data-first form (Function.dual) runs identically to data-last", async () => {
|
|
560
583
|
const inputs = Stream.fromIterable([1, 2]);
|
|
561
|
-
|
|
584
|
+
const result = await Effect.runPromise(Stream.runCollect(loopOver(inputs, 0, (state, input) => state >= input ? stop() : Stream.succeed(value(state + input)).pipe(Stream.concat(next(state + 1))))));
|
|
585
|
+
globalExpect(result).toEqual([1, 3]);
|
|
562
586
|
});
|
|
563
587
|
it("supports Effect-returning bodies (parity with loop)", async () => {
|
|
564
|
-
|
|
588
|
+
const result = await Effect.runPromise(Stream.fromIterable(["a"]).pipe(loopOver(0, (turns, input) => Effect.gen(function* () {
|
|
565
589
|
const cur = yield* Effect.succeed(turns);
|
|
566
590
|
if (cur >= 2) return stop();
|
|
567
591
|
return Stream.succeed(value(`${input}:${cur}`)).pipe(Stream.concat(next(cur + 1)));
|
|
568
|
-
})), Stream.runCollect))
|
|
592
|
+
})), Stream.runCollect));
|
|
593
|
+
globalExpect(result).toEqual(["a:0", "a:1"]);
|
|
569
594
|
});
|
|
570
595
|
it("type: data-last (pipe) form preserves the body's E channel", () => {
|
|
571
596
|
pipe(Stream.fromIterable([1]), loopOver(0, (_state, _input) => Stream.fail(new RateLimited({
|
|
@@ -575,7 +600,8 @@ describe("Loop.loopOver", () => {
|
|
|
575
600
|
(0, import_dist.expectTypeOf)().toEqualTypeOf();
|
|
576
601
|
});
|
|
577
602
|
it("type: data-first form preserves the body's E channel and unifies with input's E", () => {
|
|
578
|
-
|
|
603
|
+
const input = Stream.fail(/* @__PURE__ */ new Error("boom"));
|
|
604
|
+
loopOver(input, 0, (_state, _i) => Stream.fail(new RateLimited({
|
|
579
605
|
provider: "test",
|
|
580
606
|
raw: null
|
|
581
607
|
})));
|