@effect-uai/core 0.16.0 → 0.18.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/README.md +1 -1
- package/dist/decision-model/Decision.d.mts +155 -0
- package/dist/decision-model/Decision.d.mts.map +1 -0
- package/dist/decision-model/Decision.mjs +118 -0
- package/dist/decision-model/Decision.mjs.map +1 -0
- package/dist/decision-model/Decision.test.d.mts +1 -0
- package/dist/decision-model/Decision.test.mjs +164 -0
- package/dist/decision-model/Decision.test.mjs.map +1 -0
- package/dist/decision-model/DecisionModel.d.mts +45 -0
- package/dist/decision-model/DecisionModel.d.mts.map +1 -0
- package/dist/decision-model/DecisionModel.mjs +25 -0
- package/dist/decision-model/DecisionModel.mjs.map +1 -0
- package/dist/dist-C8RmtoR8.mjs +172 -0
- package/dist/dist-C8RmtoR8.mjs.map +1 -0
- package/dist/dist-CorQTQL-.mjs +1533 -0
- package/dist/dist-CorQTQL-.mjs.map +1 -0
- package/dist/dist-DNeuh84d.mjs +7 -0
- package/dist/dist-DNeuh84d.mjs.map +1 -0
- package/dist/dist-DzeSyeYF.mjs +10253 -0
- package/dist/dist-DzeSyeYF.mjs.map +1 -0
- package/dist/domain/Transcript.test.mjs +3 -3
- package/dist/domain/Turn.test.mjs +2 -2
- package/dist/embedding-model/EmbeddingModel.test.mjs +2 -2
- package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
- package/dist/index.C4tKZ0yW-LuFnBtmu.mjs +7042 -0
- package/dist/index.C4tKZ0yW-LuFnBtmu.mjs.map +1 -0
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +4 -1
- package/dist/language-model/LanguageModel.test.mjs +2 -2
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +3 -3
- package/dist/math/Probability.d.mts +21 -0
- package/dist/math/Probability.d.mts.map +1 -0
- package/dist/math/Probability.mjs +31 -0
- package/dist/math/Probability.mjs.map +1 -0
- package/dist/math/Probability.test.d.mts +1 -0
- package/dist/math/Probability.test.mjs +95 -0
- package/dist/math/Probability.test.mjs.map +1 -0
- package/dist/messenger/MessengerAdapter.test.mjs +3 -3
- package/dist/music-generator/MusicGenerator.test.mjs +3 -3
- package/dist/observability/Metrics.test.mjs +3 -3
- package/dist/observability/Telemetry.d.mts +1 -1
- package/dist/observability/Telemetry.mjs +1 -1
- package/dist/observability/Telemetry.mjs.map +1 -1
- package/dist/observability/Telemetry.test.mjs +4 -4
- package/dist/observability/Telemetry.test.mjs.map +1 -1
- package/dist/realtime/RealtimeSession.test.mjs +4 -4
- package/dist/retry/Retry.test.mjs +2 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +3 -3
- package/dist/streaming/JSONL.test.mjs +2 -2
- package/dist/streaming/Multipart.d.mts +1 -1
- package/dist/streaming/Multipart.mjs +1 -1
- package/dist/streaming/Multipart.mjs.map +1 -1
- package/dist/streaming/SSE.d.mts +1 -1
- package/dist/streaming/SSE.mjs +2 -2
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.mjs +2 -2
- package/dist/streaming/Settle.test.mjs +3 -3
- package/dist/streaming/WebSocketSession.d.mts +5 -3
- package/dist/streaming/WebSocketSession.d.mts.map +1 -1
- package/dist/streaming/WebSocketSession.mjs +22 -10
- package/dist/streaming/WebSocketSession.mjs.map +1 -1
- package/dist/streaming/WebSocketSession.test.mjs +41 -5
- package/dist/streaming/WebSocketSession.test.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.test.mjs +2 -2
- package/dist/testing/FakeWebSocket.d.mts +5 -1
- package/dist/testing/FakeWebSocket.d.mts.map +1 -1
- package/dist/testing/FakeWebSocket.mjs +12 -6
- package/dist/testing/FakeWebSocket.mjs.map +1 -1
- package/dist/tool/Approval.test.mjs +2 -2
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.mjs +3 -3
- package/dist/tool/Toolkit.test.mjs +3 -3
- package/dist/transcriber/Transcriber.test.mjs +3 -3
- package/package.json +15 -3
- package/src/decision-model/Decision.test.ts +146 -0
- package/src/decision-model/Decision.ts +273 -0
- package/src/decision-model/DecisionModel.ts +53 -0
- package/src/embedding-model/EmbeddingModel.test.ts +2 -2
- package/src/index.ts +3 -0
- package/src/loop/Loop.ts +1 -1
- package/src/math/Probability.test.ts +61 -0
- package/src/math/Probability.ts +44 -0
- package/src/observability/Telemetry.test.ts +1 -1
- package/src/observability/Telemetry.ts +2 -2
- package/src/streaming/Multipart.ts +1 -1
- package/src/streaming/SSE.ts +2 -2
- package/src/streaming/WebSocketSession.test.ts +54 -2
- package/src/streaming/WebSocketSession.ts +27 -6
- package/src/structured-format/StructuredFormat.ts +1 -1
- package/src/testing/FakeWebSocket.ts +21 -6
- package/src/tool/Tool.ts +1 -1
- package/dist/dist-BaenoU1A.mjs +0 -145
- package/dist/dist-BaenoU1A.mjs.map +0 -1
- package/dist/dist-nohcnkD6.mjs +0 -182
- package/dist/dist-nohcnkD6.mjs.map +0 -1
- package/dist/magic-string.es-CB2hOw9v.mjs +0 -1014
- package/dist/magic-string.es-CB2hOw9v.mjs.map +0 -1
- package/dist/test.DNmyFkvJ-YPzDR0Co.mjs +0 -10254
- package/dist/test.DNmyFkvJ-YPzDR0Co.mjs.map +0 -1
- package/dist/utils-CiH_DjFW.mjs +0 -3918
- package/dist/utils-CiH_DjFW.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -8,6 +8,8 @@ import { t as BrowserTool_d_exports } from "./browser/BrowserTool.mjs";
|
|
|
8
8
|
import { t as Capabilities_d_exports } from "./capabilities/Capabilities.mjs";
|
|
9
9
|
import { n as AiError_d_exports } from "./AiError-DvhLIbmd.mjs";
|
|
10
10
|
import { t as Chunker_d_exports } from "./chunker/Chunker.mjs";
|
|
11
|
+
import { t as Decision_d_exports } from "./decision-model/Decision.mjs";
|
|
12
|
+
import { t as DecisionModel_d_exports } from "./decision-model/DecisionModel.mjs";
|
|
11
13
|
import { o as Audio_d_exports } from "./Audio-_nGR4V0X.mjs";
|
|
12
14
|
import { t as Citation_d_exports } from "./domain/Citation.mjs";
|
|
13
15
|
import { t as Music_d_exports } from "./domain/Music.mjs";
|
|
@@ -43,6 +45,7 @@ import { t as WebRead_d_exports } from "./web-read/WebRead.mjs";
|
|
|
43
45
|
import { t as WebReadTool_d_exports } from "./web-read/WebReadTool.mjs";
|
|
44
46
|
import { t as WebSearch_d_exports } from "./web-search/WebSearch.mjs";
|
|
45
47
|
import { t as WebSearchTool_d_exports } from "./web-search/WebSearchTool.mjs";
|
|
48
|
+
import { t as Probability_d_exports } from "./math/Probability.mjs";
|
|
46
49
|
import { t as Vector_d_exports } from "./math/Vector.mjs";
|
|
47
50
|
import { t as Tokenizer_d_exports } from "./tokenizer/Tokenizer.mjs";
|
|
48
51
|
import { t as Settle_d_exports } from "./streaming/Settle.mjs";
|
|
@@ -56,4 +59,4 @@ import { t as Multipart_d_exports } from "./streaming/Multipart.mjs";
|
|
|
56
59
|
import { t as SSE_d_exports } from "./streaming/SSE.mjs";
|
|
57
60
|
import { t as Metrics_d_exports } from "./observability/Metrics.mjs";
|
|
58
61
|
import { t as Telemetry_d_exports } from "./observability/Telemetry.mjs";
|
|
59
|
-
export { AiError_d_exports as AiError, Approval_d_exports as Approval, Audio_d_exports as Audio, Browser_d_exports as Browser, BrowserError_d_exports as BrowserError, BrowserTool_d_exports as BrowserTool, Capabilities_d_exports as Capabilities, Chunker_d_exports as Chunker, Citation_d_exports as Citation, DeepResearch_d_exports as DeepResearch, Embedding_d_exports as Embedding, EmbeddingModel_d_exports as EmbeddingModel, HistoryCheck_d_exports as HistoryCheck, Image_d_exports as Image, ImageGenerator_d_exports as ImageGenerator, Items_d_exports as Items, JSONL_d_exports as JSONL, Job_d_exports as Job, LanguageModel_d_exports as LanguageModel, Lines_d_exports as Lines, Loop_d_exports as Loop, Media_d_exports as Media, Memory_d_exports as Memory, Messenger_d_exports as Messenger, MessengerAdapter_d_exports as MessengerAdapter, MessengerError_d_exports as MessengerError, Metrics_d_exports as Metrics, Multipart_d_exports as Multipart, Music_d_exports as Music, MusicGenerator_d_exports as MusicGenerator, Realtime_d_exports as Realtime, RealtimeSession_d_exports as RealtimeSession, Reranker_d_exports as Reranker, Research_d_exports as Research, Retry_d_exports as Retry, SSE_d_exports as SSE, Sandbox_d_exports as Sandbox, SandboxError_d_exports as SandboxError, SandboxImage_d_exports as SandboxImage, SandboxNetwork_d_exports as SandboxNetwork, Settle_d_exports as Settle, SpeechSynthesizer_d_exports as SpeechSynthesizer, StructuredFormat_d_exports as StructuredFormat, Telemetry_d_exports as Telemetry, Tokenizer_d_exports as Tokenizer, Tool_d_exports as Tool, ToolEvent_d_exports as ToolEvent, ToolResult_d_exports as ToolResult, Toolkit_d_exports as Toolkit, Transcriber_d_exports as Transcriber, Transcript_d_exports as Transcript, Turn_d_exports as Turn, Vector_d_exports as Vector, WebRead_d_exports as WebRead, WebReadTool_d_exports as WebReadTool, WebSearch_d_exports as WebSearch, WebSearchTool_d_exports as WebSearchTool, WebSocketSession_d_exports as WebSocketSession };
|
|
62
|
+
export { AiError_d_exports as AiError, Approval_d_exports as Approval, Audio_d_exports as Audio, Browser_d_exports as Browser, BrowserError_d_exports as BrowserError, BrowserTool_d_exports as BrowserTool, Capabilities_d_exports as Capabilities, Chunker_d_exports as Chunker, Citation_d_exports as Citation, Decision_d_exports as Decision, DecisionModel_d_exports as DecisionModel, DeepResearch_d_exports as DeepResearch, Embedding_d_exports as Embedding, EmbeddingModel_d_exports as EmbeddingModel, HistoryCheck_d_exports as HistoryCheck, Image_d_exports as Image, ImageGenerator_d_exports as ImageGenerator, Items_d_exports as Items, JSONL_d_exports as JSONL, Job_d_exports as Job, LanguageModel_d_exports as LanguageModel, Lines_d_exports as Lines, Loop_d_exports as Loop, Media_d_exports as Media, Memory_d_exports as Memory, Messenger_d_exports as Messenger, MessengerAdapter_d_exports as MessengerAdapter, MessengerError_d_exports as MessengerError, Metrics_d_exports as Metrics, Multipart_d_exports as Multipart, Music_d_exports as Music, MusicGenerator_d_exports as MusicGenerator, Probability_d_exports as Probability, Realtime_d_exports as Realtime, RealtimeSession_d_exports as RealtimeSession, Reranker_d_exports as Reranker, Research_d_exports as Research, Retry_d_exports as Retry, SSE_d_exports as SSE, Sandbox_d_exports as Sandbox, SandboxError_d_exports as SandboxError, SandboxImage_d_exports as SandboxImage, SandboxNetwork_d_exports as SandboxNetwork, Settle_d_exports as Settle, SpeechSynthesizer_d_exports as SpeechSynthesizer, StructuredFormat_d_exports as StructuredFormat, Telemetry_d_exports as Telemetry, Tokenizer_d_exports as Tokenizer, Tool_d_exports as Tool, ToolEvent_d_exports as ToolEvent, ToolResult_d_exports as ToolResult, Toolkit_d_exports as Toolkit, Transcriber_d_exports as Transcriber, Transcript_d_exports as Transcript, Turn_d_exports as Turn, Vector_d_exports as Vector, WebRead_d_exports as WebRead, WebReadTool_d_exports as WebReadTool, WebSearch_d_exports as WebSearch, WebSearchTool_d_exports as WebSearchTool, WebSocketSession_d_exports as WebSocketSession };
|
package/dist/index.mjs
CHANGED
|
@@ -16,6 +16,9 @@ import { t as BrowserTool_exports } from "./browser/BrowserTool.mjs";
|
|
|
16
16
|
import { t as Capabilities_exports } from "./capabilities/Capabilities.mjs";
|
|
17
17
|
import { t as Chunker_exports } from "./chunker/Chunker.mjs";
|
|
18
18
|
import { t as Citation_exports } from "./domain/Citation.mjs";
|
|
19
|
+
import { t as Probability_exports } from "./math/Probability.mjs";
|
|
20
|
+
import { t as Decision_exports } from "./decision-model/Decision.mjs";
|
|
21
|
+
import { t as DecisionModel_exports } from "./decision-model/DecisionModel.mjs";
|
|
19
22
|
import { t as Job_exports } from "./job/Job.mjs";
|
|
20
23
|
import { t as DeepResearch_exports } from "./research/DeepResearch.mjs";
|
|
21
24
|
import { t as Music_exports } from "./domain/Music.mjs";
|
|
@@ -56,4 +59,4 @@ import { t as Multipart_exports } from "./streaming/Multipart.mjs";
|
|
|
56
59
|
import { t as SSE_exports } from "./streaming/SSE.mjs";
|
|
57
60
|
import { t as Metrics_exports } from "./observability/Metrics.mjs";
|
|
58
61
|
import { t as Telemetry_exports } from "./observability/Telemetry.mjs";
|
|
59
|
-
export { AiError_exports as AiError, Approval_exports as Approval, Audio_exports as Audio, Browser_exports as Browser, BrowserError_exports as BrowserError, BrowserTool_exports as BrowserTool, Capabilities_exports as Capabilities, Chunker_exports as Chunker, Citation_exports as Citation, DeepResearch_exports as DeepResearch, Embedding_exports as Embedding, EmbeddingModel_exports as EmbeddingModel, HistoryCheck_exports as HistoryCheck, Image_exports as Image, ImageGenerator_exports as ImageGenerator, Items_exports as Items, JSONL_exports as JSONL, Job_exports as Job, LanguageModel_exports as LanguageModel, Lines_exports as Lines, Loop_exports as Loop, Media_exports as Media, Memory_exports as Memory, Messenger_exports as Messenger, MessengerAdapter_exports as MessengerAdapter, MessengerError_exports as MessengerError, Metrics_exports as Metrics, Multipart_exports as Multipart, Music_exports as Music, MusicGenerator_exports as MusicGenerator, Realtime_exports as Realtime, RealtimeSession_exports as RealtimeSession, Reranker_exports as Reranker, Research_exports as Research, Retry_exports as Retry, SSE_exports as SSE, Sandbox_exports as Sandbox, SandboxError_exports as SandboxError, SandboxImage_exports as SandboxImage, SandboxNetwork_exports as SandboxNetwork, Settle_exports as Settle, SpeechSynthesizer_exports as SpeechSynthesizer, StructuredFormat_exports as StructuredFormat, Telemetry_exports as Telemetry, Tokenizer_exports as Tokenizer, Tool_exports as Tool, ToolEvent_exports as ToolEvent, ToolResult_exports as ToolResult, Toolkit_exports as Toolkit, Transcriber_exports as Transcriber, Transcript_exports as Transcript, Turn_exports as Turn, Vector_exports as Vector, WebRead_exports as WebRead, WebReadTool_exports as WebReadTool, WebSearch_exports as WebSearch, WebSearchTool_exports as WebSearchTool, WebSocketSession_exports as WebSocketSession };
|
|
62
|
+
export { AiError_exports as AiError, Approval_exports as Approval, Audio_exports as Audio, Browser_exports as Browser, BrowserError_exports as BrowserError, BrowserTool_exports as BrowserTool, Capabilities_exports as Capabilities, Chunker_exports as Chunker, Citation_exports as Citation, Decision_exports as Decision, DecisionModel_exports as DecisionModel, DeepResearch_exports as DeepResearch, Embedding_exports as Embedding, EmbeddingModel_exports as EmbeddingModel, HistoryCheck_exports as HistoryCheck, Image_exports as Image, ImageGenerator_exports as ImageGenerator, Items_exports as Items, JSONL_exports as JSONL, Job_exports as Job, LanguageModel_exports as LanguageModel, Lines_exports as Lines, Loop_exports as Loop, Media_exports as Media, Memory_exports as Memory, Messenger_exports as Messenger, MessengerAdapter_exports as MessengerAdapter, MessengerError_exports as MessengerError, Metrics_exports as Metrics, Multipart_exports as Multipart, Music_exports as Music, MusicGenerator_exports as MusicGenerator, Probability_exports as Probability, Realtime_exports as Realtime, RealtimeSession_exports as RealtimeSession, Reranker_exports as Reranker, Research_exports as Research, Retry_exports as Retry, SSE_exports as SSE, Sandbox_exports as Sandbox, SandboxError_exports as SandboxError, SandboxImage_exports as SandboxImage, SandboxNetwork_exports as SandboxNetwork, Settle_exports as Settle, SpeechSynthesizer_exports as SpeechSynthesizer, StructuredFormat_exports as StructuredFormat, Telemetry_exports as Telemetry, Tokenizer_exports as Tokenizer, Tool_exports as Tool, ToolEvent_exports as ToolEvent, ToolResult_exports as ToolResult, Toolkit_exports as Toolkit, Transcriber_exports as Transcriber, Transcript_exports as Transcript, Turn_exports as Turn, Vector_exports as Vector, WebRead_exports as WebRead, WebReadTool_exports as WebReadTool, WebSearch_exports as WebSearch, WebSearchTool_exports as WebSearchTool, WebSocketSession_exports as WebSocketSession };
|
|
@@ -2,8 +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 {
|
|
6
|
-
import { n as globalExpect } from "../
|
|
5
|
+
import { b as it, m as describe } from "../dist-DzeSyeYF.mjs";
|
|
6
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
7
7
|
import { layer } from "../testing/MockProvider.mjs";
|
|
8
8
|
import { Cause, Effect, Exit, Layer, Option, Stream } from "effect";
|
|
9
9
|
//#region src/language-model/LanguageModel.test.ts
|
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(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
|
+
{"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 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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"}
|
package/dist/loop/Loop.test.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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 {
|
|
5
|
-
import { n as globalExpect } from "../
|
|
6
|
-
import { t as import_dist } from "../dist-
|
|
4
|
+
import { b as it, m as describe } from "../dist-DzeSyeYF.mjs";
|
|
5
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
6
|
+
import { t as import_dist } from "../dist-DNeuh84d.mjs";
|
|
7
7
|
import { Deferred, Effect, Fiber, Latch, Ref, Stream, SubscriptionRef, pipe } from "effect";
|
|
8
8
|
//#region src/loop/Loop.test.ts
|
|
9
9
|
describe("Loop.loop", () => {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare namespace Probability_d_exports {
|
|
2
|
+
export { confidence, margin };
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* 1 minus normalized Shannon entropy: 1 when all mass sits on one outcome, 0
|
|
6
|
+
* when spread evenly. Fewer than two outcomes yields 1.
|
|
7
|
+
*
|
|
8
|
+
* Conservative on two outcomes, where `[0.94, 0.06]` scores ~0.67, so a
|
|
9
|
+
* threshold tuned on a long vector does not transfer. {@link margin} is the
|
|
10
|
+
* more stable gate across vectors of different length.
|
|
11
|
+
*/
|
|
12
|
+
declare const confidence: (probabilities: ReadonlyArray<number>) => number;
|
|
13
|
+
/**
|
|
14
|
+
* Top mass minus runner-up mass, which unlike {@link confidence} separates a
|
|
15
|
+
* close second from a flat spread. A single outcome yields its own
|
|
16
|
+
* probability; empty yields 0.
|
|
17
|
+
*/
|
|
18
|
+
declare const margin: (probabilities: ReadonlyArray<number>) => number;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { confidence, margin, Probability_d_exports as t };
|
|
21
|
+
//# sourceMappingURL=Probability.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Probability.d.mts","names":[],"sources":["../../src/math/Probability.ts"],"mappings":";;;;;;;;;;;cAaa,aAAU,eAAmB;;;;;;cAiB7B,SAAM,eAAmB"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { n as __exportAll } from "../rolldown-runtime-EAsCxrDo.mjs";
|
|
2
|
+
import { Array, Number, Option, Order, pipe } from "effect";
|
|
3
|
+
//#region src/math/Probability.ts
|
|
4
|
+
/** Certainty measures over a probability vector. */
|
|
5
|
+
var Probability_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
+
confidence: () => confidence,
|
|
7
|
+
margin: () => margin
|
|
8
|
+
});
|
|
9
|
+
const descending = Order.flip(Order.Number);
|
|
10
|
+
/**
|
|
11
|
+
* 1 minus normalized Shannon entropy: 1 when all mass sits on one outcome, 0
|
|
12
|
+
* when spread evenly. Fewer than two outcomes yields 1.
|
|
13
|
+
*
|
|
14
|
+
* Conservative on two outcomes, where `[0.94, 0.06]` scores ~0.67, so a
|
|
15
|
+
* threshold tuned on a long vector does not transfer. {@link margin} is the
|
|
16
|
+
* more stable gate across vectors of different length.
|
|
17
|
+
*/
|
|
18
|
+
const confidence = (probabilities) => probabilities.length < 2 ? 1 : 1 - pipe(probabilities, Array.filter((p) => p > 0), Array.map((p) => -p * Math.log(p)), Number.sumAll) / Math.log(probabilities.length);
|
|
19
|
+
/**
|
|
20
|
+
* Top mass minus runner-up mass, which unlike {@link confidence} separates a
|
|
21
|
+
* close second from a flat spread. A single outcome yields its own
|
|
22
|
+
* probability; empty yields 0.
|
|
23
|
+
*/
|
|
24
|
+
const margin = (probabilities) => {
|
|
25
|
+
const top = pipe(probabilities, Array.sort(descending), Array.take(2));
|
|
26
|
+
return pipe(Array.get(top, 0), Option.map((first) => pipe(Array.get(top, 1), Option.getOrElse(() => 0), (second) => first - second)), Option.getOrElse(() => 0));
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { confidence, margin, Probability_exports as t };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=Probability.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Probability.mjs","names":["A","N"],"sources":["../../src/math/Probability.ts"],"sourcesContent":["/** Certainty measures over a probability vector. */\nimport { Array as A, Number as N, Option, Order, pipe } from \"effect\"\n\nconst descending = Order.flip(Order.Number)\n\n/**\n * 1 minus normalized Shannon entropy: 1 when all mass sits on one outcome, 0\n * when spread evenly. Fewer than two outcomes yields 1.\n *\n * Conservative on two outcomes, where `[0.94, 0.06]` scores ~0.67, so a\n * threshold tuned on a long vector does not transfer. {@link margin} is the\n * more stable gate across vectors of different length.\n */\nexport const confidence = (probabilities: ReadonlyArray<number>): number =>\n probabilities.length < 2\n ? 1\n : 1 -\n pipe(\n probabilities,\n A.filter((p) => p > 0),\n A.map((p) => -p * Math.log(p)),\n N.sumAll,\n ) /\n Math.log(probabilities.length)\n\n/**\n * Top mass minus runner-up mass, which unlike {@link confidence} separates a\n * close second from a flat spread. A single outcome yields its own\n * probability; empty yields 0.\n */\nexport const margin = (probabilities: ReadonlyArray<number>): number => {\n const top = pipe(probabilities, A.sort(descending), A.take(2))\n return pipe(\n A.get(top, 0),\n Option.map((first) =>\n pipe(\n A.get(top, 1),\n Option.getOrElse(() => 0),\n (second) => first - second,\n ),\n ),\n Option.getOrElse(() => 0),\n )\n}\n"],"mappings":";;;;;;;;AAGA,MAAM,aAAa,MAAM,KAAK,MAAM,MAAM;;;;;;;;;AAU1C,MAAa,cAAc,kBACzB,cAAc,SAAS,IACnB,IACA,IACA,KACE,eACAA,MAAE,QAAQ,MAAM,IAAI,CAAC,GACrBA,MAAE,KAAK,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,GAC7BC,OAAE,MACJ,IACE,KAAK,IAAI,cAAc,MAAM;;;;;;AAOrC,MAAa,UAAU,kBAAiD;CACtE,MAAM,MAAM,KAAK,eAAeD,MAAE,KAAK,UAAU,GAAGA,MAAE,KAAK,CAAC,CAAC;CAC7D,OAAO,KACLA,MAAE,IAAI,KAAK,CAAC,GACZ,OAAO,KAAK,UACV,KACEA,MAAE,IAAI,KAAK,CAAC,GACZ,OAAO,gBAAgB,CAAC,IACvB,WAAW,QAAQ,MACtB,CACF,GACA,OAAO,gBAAgB,CAAC,CAC1B;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { confidence, margin } from "./Probability.mjs";
|
|
2
|
+
import { b as it, m as describe } from "../dist-DzeSyeYF.mjs";
|
|
3
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
4
|
+
//#region src/math/Probability.test.ts
|
|
5
|
+
describe("confidence", () => {
|
|
6
|
+
it("is 1 when all mass sits on one outcome", () => {
|
|
7
|
+
globalExpect(confidence([
|
|
8
|
+
1,
|
|
9
|
+
0,
|
|
10
|
+
0
|
|
11
|
+
])).toBeCloseTo(1, 10);
|
|
12
|
+
});
|
|
13
|
+
it("is 0 when mass is spread evenly, at any length", () => {
|
|
14
|
+
globalExpect(confidence([.5, .5])).toBeCloseTo(0, 10);
|
|
15
|
+
globalExpect(confidence([
|
|
16
|
+
.25,
|
|
17
|
+
.25,
|
|
18
|
+
.25,
|
|
19
|
+
.25
|
|
20
|
+
])).toBeCloseTo(0, 10);
|
|
21
|
+
});
|
|
22
|
+
it("is 1 for fewer than two outcomes, which have nothing to be uncertain between", () => {
|
|
23
|
+
globalExpect(confidence([])).toBe(1);
|
|
24
|
+
globalExpect(confidence([1])).toBe(1);
|
|
25
|
+
});
|
|
26
|
+
it("rises as mass concentrates", () => {
|
|
27
|
+
const spread = confidence([
|
|
28
|
+
.4,
|
|
29
|
+
.35,
|
|
30
|
+
.25
|
|
31
|
+
]);
|
|
32
|
+
const leaning = confidence([
|
|
33
|
+
.6,
|
|
34
|
+
.25,
|
|
35
|
+
.15
|
|
36
|
+
]);
|
|
37
|
+
const decided = confidence([
|
|
38
|
+
.9,
|
|
39
|
+
.07,
|
|
40
|
+
.03
|
|
41
|
+
]);
|
|
42
|
+
globalExpect(spread).toBeLessThan(leaning);
|
|
43
|
+
globalExpect(leaning).toBeLessThan(decided);
|
|
44
|
+
});
|
|
45
|
+
it("is conservative on two outcomes, so a threshold does not transfer across lengths", () => {
|
|
46
|
+
globalExpect(confidence([.94, .06])).toBeCloseTo(.67, 2);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe("margin", () => {
|
|
50
|
+
it("is the gap between the top two, whatever order they arrive in", () => {
|
|
51
|
+
globalExpect(margin([
|
|
52
|
+
.6,
|
|
53
|
+
.3,
|
|
54
|
+
.1
|
|
55
|
+
])).toBeCloseTo(.3, 10);
|
|
56
|
+
globalExpect(margin([
|
|
57
|
+
.1,
|
|
58
|
+
.6,
|
|
59
|
+
.3
|
|
60
|
+
])).toBeCloseTo(.3, 10);
|
|
61
|
+
});
|
|
62
|
+
it("is 0 on a tie and 0 when empty", () => {
|
|
63
|
+
globalExpect(margin([.5, .5])).toBeCloseTo(0, 10);
|
|
64
|
+
globalExpect(margin([])).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
it("yields the lone probability for a single outcome", () => {
|
|
67
|
+
globalExpect(margin([.7])).toBeCloseTo(.7, 10);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("confidence and margin disagree, which is why both exist", () => {
|
|
71
|
+
const closeSecond = [
|
|
72
|
+
.45,
|
|
73
|
+
.42,
|
|
74
|
+
.13
|
|
75
|
+
];
|
|
76
|
+
const flatSpread = [
|
|
77
|
+
.45,
|
|
78
|
+
.11,
|
|
79
|
+
.11,
|
|
80
|
+
.11,
|
|
81
|
+
.11,
|
|
82
|
+
.11
|
|
83
|
+
];
|
|
84
|
+
it("confidence cannot separate them", () => {
|
|
85
|
+
globalExpect(Math.abs(confidence(closeSecond) - confidence(flatSpread))).toBeLessThan(.05);
|
|
86
|
+
});
|
|
87
|
+
it("margin separates them sharply", () => {
|
|
88
|
+
globalExpect(margin(closeSecond)).toBeCloseTo(.03, 10);
|
|
89
|
+
globalExpect(margin(flatSpread)).toBeCloseTo(.34, 10);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
//#endregion
|
|
93
|
+
export {};
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=Probability.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Probability.test.mjs","names":[],"sources":["../../src/math/Probability.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\"\nimport { confidence, margin } from \"./Probability.js\"\n\ndescribe(\"confidence\", () => {\n it(\"is 1 when all mass sits on one outcome\", () => {\n expect(confidence([1, 0, 0])).toBeCloseTo(1, 10)\n })\n\n it(\"is 0 when mass is spread evenly, at any length\", () => {\n expect(confidence([0.5, 0.5])).toBeCloseTo(0, 10)\n expect(confidence([0.25, 0.25, 0.25, 0.25])).toBeCloseTo(0, 10)\n })\n\n it(\"is 1 for fewer than two outcomes, which have nothing to be uncertain between\", () => {\n expect(confidence([])).toBe(1)\n expect(confidence([1])).toBe(1)\n })\n\n it(\"rises as mass concentrates\", () => {\n const spread = confidence([0.4, 0.35, 0.25])\n const leaning = confidence([0.6, 0.25, 0.15])\n const decided = confidence([0.9, 0.07, 0.03])\n expect(spread).toBeLessThan(leaning)\n expect(leaning).toBeLessThan(decided)\n })\n\n it(\"is conservative on two outcomes, so a threshold does not transfer across lengths\", () => {\n expect(confidence([0.94, 0.06])).toBeCloseTo(0.67, 2)\n })\n})\n\ndescribe(\"margin\", () => {\n it(\"is the gap between the top two, whatever order they arrive in\", () => {\n expect(margin([0.6, 0.3, 0.1])).toBeCloseTo(0.3, 10)\n expect(margin([0.1, 0.6, 0.3])).toBeCloseTo(0.3, 10)\n })\n\n it(\"is 0 on a tie and 0 when empty\", () => {\n expect(margin([0.5, 0.5])).toBeCloseTo(0, 10)\n expect(margin([])).toBe(0)\n })\n\n it(\"yields the lone probability for a single outcome\", () => {\n expect(margin([0.7])).toBeCloseTo(0.7, 10)\n })\n})\n\ndescribe(\"confidence and margin disagree, which is why both exist\", () => {\n // Two plausible answers versus no idea. Same winner, same top mass.\n const closeSecond = [0.45, 0.42, 0.13]\n const flatSpread = [0.45, 0.11, 0.11, 0.11, 0.11, 0.11]\n\n it(\"confidence cannot separate them\", () => {\n expect(Math.abs(confidence(closeSecond) - confidence(flatSpread))).toBeLessThan(0.05)\n })\n\n it(\"margin separates them sharply\", () => {\n expect(margin(closeSecond)).toBeCloseTo(0.03, 10)\n expect(margin(flatSpread)).toBeCloseTo(0.34, 10)\n })\n})\n"],"mappings":";;;;AAGA,SAAS,oBAAoB;CAC3B,GAAG,gDAAgD;EACjD,aAAO,WAAW;GAAC;GAAG;GAAG;EAAC,CAAC,CAAC,CAAC,CAAC,YAAY,GAAG,EAAE;CACjD,CAAC;CAED,GAAG,wDAAwD;EACzD,aAAO,WAAW,CAAC,IAAK,EAAG,CAAC,CAAC,CAAC,CAAC,YAAY,GAAG,EAAE;EAChD,aAAO,WAAW;GAAC;GAAM;GAAM;GAAM;EAAI,CAAC,CAAC,CAAC,CAAC,YAAY,GAAG,EAAE;CAChE,CAAC;CAED,GAAG,sFAAsF;EACvF,aAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;EAC7B,aAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;CAChC,CAAC;CAED,GAAG,oCAAoC;EACrC,MAAM,SAAS,WAAW;GAAC;GAAK;GAAM;EAAI,CAAC;EAC3C,MAAM,UAAU,WAAW;GAAC;GAAK;GAAM;EAAI,CAAC;EAC5C,MAAM,UAAU,WAAW;GAAC;GAAK;GAAM;EAAI,CAAC;EAC5C,aAAO,MAAM,CAAC,CAAC,aAAa,OAAO;EACnC,aAAO,OAAO,CAAC,CAAC,aAAa,OAAO;CACtC,CAAC;CAED,GAAG,0FAA0F;EAC3F,aAAO,WAAW,CAAC,KAAM,GAAI,CAAC,CAAC,CAAC,CAAC,YAAY,KAAM,CAAC;CACtD,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB;CACvB,GAAG,uEAAuE;EACxE,aAAO,OAAO;GAAC;GAAK;GAAK;EAAG,CAAC,CAAC,CAAC,CAAC,YAAY,IAAK,EAAE;EACnD,aAAO,OAAO;GAAC;GAAK;GAAK;EAAG,CAAC,CAAC,CAAC,CAAC,YAAY,IAAK,EAAE;CACrD,CAAC;CAED,GAAG,wCAAwC;EACzC,aAAO,OAAO,CAAC,IAAK,EAAG,CAAC,CAAC,CAAC,CAAC,YAAY,GAAG,EAAE;EAC5C,aAAO,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;CAC3B,CAAC;CAED,GAAG,0DAA0D;EAC3D,aAAO,OAAO,CAAC,EAAG,CAAC,CAAC,CAAC,CAAC,YAAY,IAAK,EAAE;CAC3C,CAAC;AACH,CAAC;AAED,SAAS,iEAAiE;CAExE,MAAM,cAAc;EAAC;EAAM;EAAM;CAAI;CACrC,MAAM,aAAa;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;CAAI;CAEtD,GAAG,yCAAyC;EAC1C,aAAO,KAAK,IAAI,WAAW,WAAW,IAAI,WAAW,UAAU,CAAC,CAAC,CAAC,CAAC,aAAa,GAAI;CACtF,CAAC;CAED,GAAG,uCAAuC;EACxC,aAAO,OAAO,WAAW,CAAC,CAAC,CAAC,YAAY,KAAM,EAAE;EAChD,aAAO,OAAO,UAAU,CAAC,CAAC,CAAC,YAAY,KAAM,EAAE;CACjD,CAAC;AACH,CAAC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ChannelId, MessageId, inConversation } from "./Messenger.mjs";
|
|
2
2
|
import { splitForLimit } from "./MessengerAdapter.mjs";
|
|
3
3
|
import { MessengerRateLimited } from "./MessengerError.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { n as globalExpect } from "../
|
|
6
|
-
import { t as it } from "../dist-
|
|
4
|
+
import { m as describe } from "../dist-DzeSyeYF.mjs";
|
|
5
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
6
|
+
import { t as it } from "../dist-C8RmtoR8.mjs";
|
|
7
7
|
import { make } from "../testing/MockMessenger.mjs";
|
|
8
8
|
import { Array, Duration, Effect, Option, Result, Stream } from "effect";
|
|
9
9
|
import { TestClock } from "effect/testing";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { audioEvent, controlInput, promptsInput, singleVariant } from "../domain/Music.mjs";
|
|
2
2
|
import { generate, streamGeneration, streamGenerationFrom } from "./MusicGenerator.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { n as globalExpect } from "../
|
|
5
|
-
import { t as import_dist } from "../dist-
|
|
3
|
+
import { b as it, m as describe } from "../dist-DzeSyeYF.mjs";
|
|
4
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
5
|
+
import { t as import_dist } from "../dist-DNeuh84d.mjs";
|
|
6
6
|
import { layer, layerWithoutInteractive } from "../testing/MockMusicGenerator.mjs";
|
|
7
7
|
import { Duration, Effect, Stream } from "effect";
|
|
8
8
|
//#region src/music-generator/MusicGenerator.test.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { TurnEvent } from "../domain/Turn.mjs";
|
|
2
2
|
import { computeThroughputTick, isMetricEvent, makeEvent, metricEvents, throughput, timeToCompletion, timeToFirstToken, tokenTotals } from "./Metrics.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { n as globalExpect } from "../
|
|
5
|
-
import { t as it } from "../dist-
|
|
3
|
+
import { m as describe } from "../dist-DzeSyeYF.mjs";
|
|
4
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
5
|
+
import { t as it } from "../dist-C8RmtoR8.mjs";
|
|
6
6
|
import { Array, Duration, Effect, Fiber, Option, Stream } from "effect";
|
|
7
7
|
import * as TestClock from "effect/testing/TestClock";
|
|
8
8
|
//#region src/observability/Metrics.test.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as __exportAll } from "../rolldown-runtime-EAsCxrDo.mjs";
|
|
2
2
|
import { isMetricEvent } from "./Metrics.mjs";
|
|
3
3
|
import { Effect, Layer, Match, Metric, Stream } from "effect";
|
|
4
|
-
import { OtlpMetrics, OtlpSerialization } from "effect/
|
|
4
|
+
import { OtlpMetrics, OtlpSerialization } from "effect/observability";
|
|
5
5
|
//#region src/observability/Telemetry.ts
|
|
6
6
|
var Telemetry_exports = /* @__PURE__ */ __exportAll({
|
|
7
7
|
layerOtlp: () => layerOtlp,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Telemetry.mjs","names":[],"sources":["../../src/observability/Telemetry.ts"],"sourcesContent":["import { Duration, Effect, Layer, Match, Metric, Stream } from \"effect\"\nimport type { HttpClient } from \"effect/
|
|
1
|
+
{"version":3,"file":"Telemetry.mjs","names":[],"sources":["../../src/observability/Telemetry.ts"],"sourcesContent":["import { Duration, Effect, Layer, Match, Metric, Stream } from \"effect\"\nimport type { HttpClient } from \"effect/http\"\nimport { OtlpMetrics, OtlpSerialization } from \"effect/observability\"\nimport { isMetricEvent, type Measurement, type MetricEvent } from \"./Metrics.js\"\n\n// ---------------------------------------------------------------------------\n// The agnostic recorder\n// ---------------------------------------------------------------------------\n\n/**\n * Histogram bucket boundaries used for every histogram measurement (the only\n * built-in one is throughput). Custom histogram metrics share these; a metric\n * that needs bespoke boundaries can update its own instrument directly instead\n * of routing through `record`.\n */\nconst histogramBoundaries = Metric.exponentialBoundaries({ start: 1, factor: 2, count: 16 })\n\n/**\n * Build the effect `Metric` instrument a measurement maps to. Dispatches on\n * `kind`, never on the event `_tag`, so a new built-in or custom metric needs\n * no change here. Instruments dedupe by name in the global registry, so a fresh\n * instance per call accumulates onto the same series (no cache needed).\n */\nconst buildInstrument = (m: Measurement): Metric.Metric<any, any> =>\n Match.value(m.kind).pipe(\n Match.when(\"counter\", () => Metric.counter(m.name, { incremental: true })),\n Match.when(\"histogram\", () => Metric.histogram(m.name, { boundaries: histogramBoundaries })),\n Match.when(\"timer\", () => Metric.timer(m.name)),\n Match.when(\"gauge\", () => Metric.gauge(m.name)),\n Match.exhaustive,\n )\n\nconst mergeAttributes = (\n base: Readonly<Record<string, string>> | undefined,\n extra: Readonly<Record<string, string>> | undefined,\n): Readonly<Record<string, string>> | undefined =>\n base === undefined ? extra : extra === undefined ? base : { ...base, ...extra }\n\nconst recordEvent = (\n ev: MetricEvent,\n baseAttributes: Readonly<Record<string, string>> | undefined,\n): Effect.Effect<void> =>\n Effect.forEach(\n ev.measurements,\n (m) => {\n const attributes = mergeAttributes(baseAttributes, m.attributes)\n const base = buildInstrument(m)\n const instrument = attributes === undefined ? base : Metric.withAttributes(base, attributes)\n return Metric.update(instrument, m.value)\n },\n { discard: true },\n )\n\nexport type RecordOptions = {\n /** Attributes applied to every recorded metric, e.g. `{ model, provider }`. */\n readonly attributes?: Readonly<Record<string, string>>\n}\n\n/**\n * Record every measurement on every metric event into effect `Metric`\n * instruments, then pass events through unchanged. Matches events structurally\n * (`isMetricEvent`), so it records built-in and custom metrics alike, and\n * dispatches on `measurement.kind`, never on the event tag. Sits mid-pipeline\n * before the transport. Provide an OTLP (or Prometheus) layer to export the\n * instruments it populates.\n */\nexport const record =\n (options?: RecordOptions) =>\n <A, E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<A, E, R> =>\n self.pipe(\n Stream.tap((ev) => (isMetricEvent(ev) ? recordEvent(ev, options?.attributes) : Effect.void)),\n )\n\n// ---------------------------------------------------------------------------\n// OTLP wiring\n// ---------------------------------------------------------------------------\n\nexport type LayerOtlpOptions = {\n readonly url: string\n readonly resource?: {\n readonly serviceName?: string\n readonly serviceVersion?: string\n }\n readonly exportInterval?: Duration.Input\n}\n\n/**\n * Convenience layer that exports recorded metrics over OTLP. Provides the JSON\n * serializer and leaves `HttpClient` as a requirement, so the platform's client\n * (FetchHttpClient, NodeHttpClient, ...) is provided at the edge. This keeps\n * the library platform-independent.\n */\nexport const layerOtlp = (\n options: LayerOtlpOptions,\n): Layer.Layer<never, never, HttpClient.HttpClient> =>\n OtlpMetrics.layer(options).pipe(Layer.provide(OtlpSerialization.layerJson))\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAM,sBAAsB,OAAO,sBAAsB;CAAE,OAAO;CAAG,QAAQ;CAAG,OAAO;AAAG,CAAC;;;;;;;AAQ3F,MAAM,mBAAmB,MACvB,MAAM,MAAM,EAAE,IAAI,CAAC,CAAC,KAClB,MAAM,KAAK,iBAAiB,OAAO,QAAQ,EAAE,MAAM,EAAE,aAAa,KAAK,CAAC,CAAC,GACzE,MAAM,KAAK,mBAAmB,OAAO,UAAU,EAAE,MAAM,EAAE,YAAY,oBAAoB,CAAC,CAAC,GAC3F,MAAM,KAAK,eAAe,OAAO,MAAM,EAAE,IAAI,CAAC,GAC9C,MAAM,KAAK,eAAe,OAAO,MAAM,EAAE,IAAI,CAAC,GAC9C,MAAM,UACR;AAEF,MAAM,mBACJ,MACA,UAEA,SAAS,KAAA,IAAY,QAAQ,UAAU,KAAA,IAAY,OAAO;CAAE,GAAG;CAAM,GAAG;AAAM;AAEhF,MAAM,eACJ,IACA,mBAEA,OAAO,QACL,GAAG,eACF,MAAM;CACL,MAAM,aAAa,gBAAgB,gBAAgB,EAAE,UAAU;CAC/D,MAAM,OAAO,gBAAgB,CAAC;CAC9B,MAAM,aAAa,eAAe,KAAA,IAAY,OAAO,OAAO,eAAe,MAAM,UAAU;CAC3F,OAAO,OAAO,OAAO,YAAY,EAAE,KAAK;AAC1C,GACA,EAAE,SAAS,KAAK,CAClB;;;;;;;;;AAeF,MAAa,UACV,aACS,SACR,KAAK,KACH,OAAO,KAAK,OAAQ,cAAc,EAAE,IAAI,YAAY,IAAI,SAAS,UAAU,IAAI,OAAO,IAAK,CAC7F;;;;;;;AAqBJ,MAAa,aACX,YAEA,YAAY,MAAM,OAAO,CAAC,CAAC,KAAK,MAAM,QAAQ,kBAAkB,SAAS,CAAC"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { makeEvent } from "./Metrics.mjs";
|
|
2
2
|
import { layerOtlp, record } from "./Telemetry.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { n as globalExpect } from "../
|
|
5
|
-
import { t as it } from "../dist-
|
|
3
|
+
import { m as describe } from "../dist-DzeSyeYF.mjs";
|
|
4
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
5
|
+
import { t as it } from "../dist-C8RmtoR8.mjs";
|
|
6
6
|
import { Array, Duration, Effect, Layer, Metric, Option, Ref, Stream } from "effect";
|
|
7
|
-
import { HttpClient, HttpClientResponse } from "effect/
|
|
7
|
+
import { HttpClient, HttpClientResponse } from "effect/http";
|
|
8
8
|
//#region src/observability/Telemetry.test.ts
|
|
9
9
|
const event = (measurements) => makeEvent({
|
|
10
10
|
_tag: "Test",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Telemetry.test.mjs","names":["Arr"],"sources":["../../src/observability/Telemetry.test.ts"],"sourcesContent":["import { describe, it } from \"@effect/vitest\"\nimport { Array as Arr, Duration, Effect, Layer, Metric, Option, Ref, Stream } from \"effect\"\nimport { type HttpBody, HttpClient, HttpClientResponse } from \"effect/
|
|
1
|
+
{"version":3,"file":"Telemetry.test.mjs","names":["Arr"],"sources":["../../src/observability/Telemetry.test.ts"],"sourcesContent":["import { describe, it } from \"@effect/vitest\"\nimport { Array as Arr, Duration, Effect, Layer, Metric, Option, Ref, Stream } from \"effect\"\nimport { type HttpBody, HttpClient, HttpClientResponse } from \"effect/http\"\nimport { expect } from \"vitest\"\nimport { type Measurement, makeEvent } from \"./Metrics.js\"\nimport { layerOtlp, record } from \"./Telemetry.js\"\n\nconst event = (measurements: ReadonlyArray<Measurement>) =>\n makeEvent({ _tag: \"Test\", turnIndex: 0, measurements })\n\n// Each test gets a fresh registry so instruments do not leak across tests.\nconst isolated = <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>\n Effect.provideService(self, Metric.MetricRegistry, new Map())\n\nconst findById = (snap: ReadonlyArray<Metric.Metric.Snapshot>, id: string) =>\n Option.getOrThrow(Arr.findFirst(snap, (s) => s.id === id))\n\ndescribe(\"record\", () => {\n it.effect(\"accumulates a counter measurement across events\", () =>\n isolated(\n Effect.gen(function* () {\n const ev = event([{ name: \"rec_counter\", kind: \"counter\", value: 5 }])\n yield* Stream.make(ev, ev).pipe(record(), Stream.runDrain)\n const snap = yield* Metric.snapshot\n const c = findById(snap, \"rec_counter\")\n expect(c.type).toBe(\"Counter\")\n expect((c.state as { readonly count: number }).count).toBe(10)\n }),\n ),\n )\n\n it.effect(\"records a timer measurement as a histogram observation\", () =>\n isolated(\n Effect.gen(function* () {\n const ev = event([{ name: \"rec_timer\", kind: \"timer\", value: Duration.millis(100) }])\n yield* Stream.make(ev).pipe(record(), Stream.runDrain)\n const snap = yield* Metric.snapshot\n const h = findById(snap, \"rec_timer\")\n expect(h.type).toBe(\"Histogram\")\n expect((h.state as { readonly count: number }).count).toBe(1)\n }),\n ),\n )\n\n it.effect(\"applies base attributes to every recorded metric\", () =>\n isolated(\n Effect.gen(function* () {\n const ev = event([{ name: \"rec_attr\", kind: \"counter\", value: 1 }])\n yield* Stream.make(ev).pipe(\n record({ attributes: { model: \"test-model\" } }),\n Stream.runDrain,\n )\n const snap = yield* Metric.snapshot\n const c = findById(snap, \"rec_attr\")\n expect(c.attributes).toMatchObject({ model: \"test-model\" })\n }),\n ),\n )\n\n it.effect(\"records a custom metric with a novel name, unchanged recorder\", () =>\n isolated(\n Effect.gen(function* () {\n // A user-defined event the recorder has never seen: still exported.\n const custom = makeEvent({\n _tag: \"ToolLatency\",\n turnIndex: 0,\n measurements: [{ name: \"myapp_tool_latency_total\", kind: \"counter\", value: 3 }],\n })\n yield* Stream.make(custom).pipe(record(), Stream.runDrain)\n const snap = yield* Metric.snapshot\n const c = findById(snap, \"myapp_tool_latency_total\")\n expect((c.state as { readonly count: number }).count).toBe(3)\n }),\n ),\n )\n\n it.effect(\"passes non-metric elements through untouched\", () =>\n isolated(\n Effect.gen(function* () {\n const out = yield* Stream.make(\"a\", \"b\", \"c\").pipe(record(), Stream.runCollect)\n expect(out).toEqual([\"a\", \"b\", \"c\"])\n }),\n ),\n )\n})\n\nconst bodyToString = (body: HttpBody.HttpBody): string =>\n body._tag === \"Uint8Array\" ? new TextDecoder().decode(body.body) : \"\"\n\ndescribe(\"layerOtlp\", () => {\n it.effect(\"exports recorded metrics over OTLP via a mock HttpClient\", () =>\n isolated(\n Effect.gen(function* () {\n const bodies = yield* Ref.make<ReadonlyArray<string>>([])\n const mockClient = HttpClient.make((request) =>\n Ref.update(bodies, (b) => [...b, bodyToString(request.body)]).pipe(\n Effect.as(HttpClientResponse.fromWeb(request, new Response(\"{}\", { status: 200 }))),\n ),\n )\n const clientLayer = Layer.succeed(HttpClient.HttpClient, mockClient)\n const ev = event([{ name: \"otlp_export_counter\", kind: \"counter\", value: 3 }])\n\n // Provide the OTLP layer scoped to the recording program. The exporter\n // flushes on scope close (interval is long, so we rely on the flush).\n yield* Stream.make(ev).pipe(\n record(),\n Stream.runDrain,\n Effect.provide(\n layerOtlp({\n url: \"http://localhost:4318/v1/metrics\",\n resource: { serviceName: \"test-service\" },\n exportInterval: \"10 minutes\",\n }).pipe(Layer.provide(clientLayer)),\n ),\n )\n\n const captured = yield* Ref.get(bodies)\n expect(captured.length).toBeGreaterThanOrEqual(1)\n expect(captured.join(\"\\n\")).toContain(\"otlp_export_counter\")\n }),\n ),\n )\n})\n"],"mappings":"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"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { imageBase64 } from "../domain/Image.mjs";
|
|
2
2
|
import { RealtimeEvent, RealtimeInput } from "../domain/Realtime.mjs";
|
|
3
3
|
import { open, sendVideoFrame, toolCalls } from "./RealtimeSession.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { n as globalExpect } from "../
|
|
6
|
-
import { t as import_dist } from "../dist-
|
|
7
|
-
import { t as it } from "../dist-
|
|
4
|
+
import { m as describe } from "../dist-DzeSyeYF.mjs";
|
|
5
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
6
|
+
import { t as import_dist } from "../dist-DNeuh84d.mjs";
|
|
7
|
+
import { t as it } from "../dist-C8RmtoR8.mjs";
|
|
8
8
|
import { layer, layerAudioOnly } from "../testing/MockRealtimeSession.mjs";
|
|
9
9
|
import { Effect, Stream } from "effect";
|
|
10
10
|
//#region src/realtime/RealtimeSession.test.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ContentFiltered, RateLimited, Timeout, Unavailable } from "../domain/AiError.mjs";
|
|
2
2
|
import { effect, stream } from "./Retry.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { n as globalExpect } from "../
|
|
3
|
+
import { b as it, m as describe } from "../dist-DzeSyeYF.mjs";
|
|
4
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
5
5
|
import { Cause, Effect, Exit, Option, Ref, Schedule, Stream } from "effect";
|
|
6
6
|
//#region src/retry/Retry.test.ts
|
|
7
7
|
const planned = (attempts, plan) => Stream.unwrap(Ref.getAndUpdate(attempts, (n) => n + 1).pipe(Effect.map((n) => plan[Math.min(n, plan.length - 1)])));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { streamSynthesis, streamSynthesisFrom, streamSynthesizeDialogue, synthesize, synthesizeDialogue } from "./SpeechSynthesizer.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as globalExpect } from "../
|
|
4
|
-
import { t as import_dist } from "../dist-
|
|
2
|
+
import { b as it, m as describe } from "../dist-DzeSyeYF.mjs";
|
|
3
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
4
|
+
import { t as import_dist } from "../dist-DNeuh84d.mjs";
|
|
5
5
|
import { layer, layerWithoutIncremental, layerWithoutMultiSpeaker } from "../testing/MockSpeechSynthesizer.mjs";
|
|
6
6
|
import { Duration, Effect, Stream } from "effect";
|
|
7
7
|
//#region src/speech-synthesizer/SpeechSynthesizer.test.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fromBytes, parse, toBytes } from "./JSONL.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as globalExpect } from "../
|
|
2
|
+
import { b as it, m as describe } from "../dist-DzeSyeYF.mjs";
|
|
3
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
4
4
|
import { Effect, Result, Schema, Stream } from "effect";
|
|
5
5
|
//#region src/streaming/JSONL.test.ts
|
|
6
6
|
const enc = new TextEncoder();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as __exportAll } from "../rolldown-runtime-EAsCxrDo.mjs";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
-
import { HttpClientRequest } from "effect/
|
|
3
|
+
import { HttpClientRequest } from "effect/http";
|
|
4
4
|
//#region src/streaming/Multipart.ts
|
|
5
5
|
var Multipart_exports = /* @__PURE__ */ __exportAll({ bodyMultipart: () => bodyMultipart });
|
|
6
6
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Multipart.mjs","names":[],"sources":["../../src/streaming/Multipart.ts"],"sourcesContent":["import { Effect } from \"effect\"\nimport { HttpClientRequest } from \"effect/
|
|
1
|
+
{"version":3,"file":"Multipart.mjs","names":[],"sources":["../../src/streaming/Multipart.ts"],"sourcesContent":["import { Effect } from \"effect\"\nimport { HttpClientRequest } from \"effect/http\"\n\n/**\n * Multipart body for an `HttpClientRequest`, encoded to bytes.\n *\n * Use this over `HttpClientRequest.bodyFormData`: the Undici client\n * passes the `FormData` object straight to `dispatcher.request`, which\n * cannot serialise it, so the request is never sent and the effect\n * hangs with no error. Encoding here produces the same bytes every\n * other client derives internally, so it works on all of them.\n *\n * The failure is whatever `Response.arrayBuffer` rejects with; map it\n * to your provider's transport error at the call site.\n */\nexport const bodyMultipart = (\n form: FormData,\n): Effect.Effect<\n (request: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest,\n unknown\n> =>\n Effect.gen(function* () {\n const encoded = new Response(form)\n const buffer = yield* Effect.tryPromise(() => encoded.arrayBuffer())\n return HttpClientRequest.bodyUint8Array(\n new Uint8Array(buffer),\n encoded.headers.get(\"content-type\") ?? \"multipart/form-data\",\n )\n })\n"],"mappings":";;;;;;;;;;;;;;;;;AAeA,MAAa,iBACX,SAKA,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,IAAI,SAAS,IAAI;CACjC,MAAM,SAAS,OAAO,OAAO,iBAAiB,QAAQ,YAAY,CAAC;CACnE,OAAO,kBAAkB,eACvB,IAAI,WAAW,MAAM,GACrB,QAAQ,QAAQ,IAAI,cAAc,KAAK,qBACzC;AACF,CAAC"}
|
package/dist/streaming/SSE.d.mts
CHANGED
|
@@ -15,7 +15,7 @@ type Event = {
|
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
17
|
* Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a
|
|
18
|
-
* `Stream<SSE.Event>`, using the spec parser from `effect/
|
|
18
|
+
* `Stream<SSE.Event>`, using the spec parser from `effect/encoding`.
|
|
19
19
|
* An event is dispatched only once its terminating blank line arrives, so a
|
|
20
20
|
* partial event at stream end (clean or failed) is discarded per the spec
|
|
21
21
|
* rather than surfaced as a truncated frame.
|