@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.
Files changed (103) hide show
  1. package/README.md +1 -1
  2. package/dist/decision-model/Decision.d.mts +155 -0
  3. package/dist/decision-model/Decision.d.mts.map +1 -0
  4. package/dist/decision-model/Decision.mjs +118 -0
  5. package/dist/decision-model/Decision.mjs.map +1 -0
  6. package/dist/decision-model/Decision.test.d.mts +1 -0
  7. package/dist/decision-model/Decision.test.mjs +164 -0
  8. package/dist/decision-model/Decision.test.mjs.map +1 -0
  9. package/dist/decision-model/DecisionModel.d.mts +45 -0
  10. package/dist/decision-model/DecisionModel.d.mts.map +1 -0
  11. package/dist/decision-model/DecisionModel.mjs +25 -0
  12. package/dist/decision-model/DecisionModel.mjs.map +1 -0
  13. package/dist/dist-C8RmtoR8.mjs +172 -0
  14. package/dist/dist-C8RmtoR8.mjs.map +1 -0
  15. package/dist/dist-CorQTQL-.mjs +1533 -0
  16. package/dist/dist-CorQTQL-.mjs.map +1 -0
  17. package/dist/dist-DNeuh84d.mjs +7 -0
  18. package/dist/dist-DNeuh84d.mjs.map +1 -0
  19. package/dist/dist-DzeSyeYF.mjs +10253 -0
  20. package/dist/dist-DzeSyeYF.mjs.map +1 -0
  21. package/dist/domain/Transcript.test.mjs +3 -3
  22. package/dist/domain/Turn.test.mjs +2 -2
  23. package/dist/embedding-model/EmbeddingModel.test.mjs +2 -2
  24. package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
  25. package/dist/index.C4tKZ0yW-LuFnBtmu.mjs +7042 -0
  26. package/dist/index.C4tKZ0yW-LuFnBtmu.mjs.map +1 -0
  27. package/dist/index.d.mts +4 -1
  28. package/dist/index.mjs +4 -1
  29. package/dist/language-model/LanguageModel.test.mjs +2 -2
  30. package/dist/loop/Loop.mjs.map +1 -1
  31. package/dist/loop/Loop.test.mjs +3 -3
  32. package/dist/math/Probability.d.mts +21 -0
  33. package/dist/math/Probability.d.mts.map +1 -0
  34. package/dist/math/Probability.mjs +31 -0
  35. package/dist/math/Probability.mjs.map +1 -0
  36. package/dist/math/Probability.test.d.mts +1 -0
  37. package/dist/math/Probability.test.mjs +95 -0
  38. package/dist/math/Probability.test.mjs.map +1 -0
  39. package/dist/messenger/MessengerAdapter.test.mjs +3 -3
  40. package/dist/music-generator/MusicGenerator.test.mjs +3 -3
  41. package/dist/observability/Metrics.test.mjs +3 -3
  42. package/dist/observability/Telemetry.d.mts +1 -1
  43. package/dist/observability/Telemetry.mjs +1 -1
  44. package/dist/observability/Telemetry.mjs.map +1 -1
  45. package/dist/observability/Telemetry.test.mjs +4 -4
  46. package/dist/observability/Telemetry.test.mjs.map +1 -1
  47. package/dist/realtime/RealtimeSession.test.mjs +4 -4
  48. package/dist/retry/Retry.test.mjs +2 -2
  49. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +3 -3
  50. package/dist/streaming/JSONL.test.mjs +2 -2
  51. package/dist/streaming/Multipart.d.mts +1 -1
  52. package/dist/streaming/Multipart.mjs +1 -1
  53. package/dist/streaming/Multipart.mjs.map +1 -1
  54. package/dist/streaming/SSE.d.mts +1 -1
  55. package/dist/streaming/SSE.mjs +2 -2
  56. package/dist/streaming/SSE.mjs.map +1 -1
  57. package/dist/streaming/SSE.test.mjs +2 -2
  58. package/dist/streaming/Settle.test.mjs +3 -3
  59. package/dist/streaming/WebSocketSession.d.mts +5 -3
  60. package/dist/streaming/WebSocketSession.d.mts.map +1 -1
  61. package/dist/streaming/WebSocketSession.mjs +22 -10
  62. package/dist/streaming/WebSocketSession.mjs.map +1 -1
  63. package/dist/streaming/WebSocketSession.test.mjs +41 -5
  64. package/dist/streaming/WebSocketSession.test.mjs.map +1 -1
  65. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  66. package/dist/structured-format/StructuredFormat.test.mjs +2 -2
  67. package/dist/testing/FakeWebSocket.d.mts +5 -1
  68. package/dist/testing/FakeWebSocket.d.mts.map +1 -1
  69. package/dist/testing/FakeWebSocket.mjs +12 -6
  70. package/dist/testing/FakeWebSocket.mjs.map +1 -1
  71. package/dist/tool/Approval.test.mjs +2 -2
  72. package/dist/tool/Tool.mjs.map +1 -1
  73. package/dist/tool/Tool.test.mjs +3 -3
  74. package/dist/tool/Toolkit.test.mjs +3 -3
  75. package/dist/transcriber/Transcriber.test.mjs +3 -3
  76. package/package.json +15 -3
  77. package/src/decision-model/Decision.test.ts +146 -0
  78. package/src/decision-model/Decision.ts +273 -0
  79. package/src/decision-model/DecisionModel.ts +53 -0
  80. package/src/embedding-model/EmbeddingModel.test.ts +2 -2
  81. package/src/index.ts +3 -0
  82. package/src/loop/Loop.ts +1 -1
  83. package/src/math/Probability.test.ts +61 -0
  84. package/src/math/Probability.ts +44 -0
  85. package/src/observability/Telemetry.test.ts +1 -1
  86. package/src/observability/Telemetry.ts +2 -2
  87. package/src/streaming/Multipart.ts +1 -1
  88. package/src/streaming/SSE.ts +2 -2
  89. package/src/streaming/WebSocketSession.test.ts +54 -2
  90. package/src/streaming/WebSocketSession.ts +27 -6
  91. package/src/structured-format/StructuredFormat.ts +1 -1
  92. package/src/testing/FakeWebSocket.ts +21 -6
  93. package/src/tool/Tool.ts +1 -1
  94. package/dist/dist-BaenoU1A.mjs +0 -145
  95. package/dist/dist-BaenoU1A.mjs.map +0 -1
  96. package/dist/dist-nohcnkD6.mjs +0 -182
  97. package/dist/dist-nohcnkD6.mjs.map +0 -1
  98. package/dist/magic-string.es-CB2hOw9v.mjs +0 -1014
  99. package/dist/magic-string.es-CB2hOw9v.mjs.map +0 -1
  100. package/dist/test.DNmyFkvJ-YPzDR0Co.mjs +0 -10254
  101. package/dist/test.DNmyFkvJ-YPzDR0Co.mjs.map +0 -1
  102. package/dist/utils-CiH_DjFW.mjs +0 -3918
  103. package/dist/utils-CiH_DjFW.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { n as __exportAll } from "../rolldown-runtime-EAsCxrDo.mjs";
2
2
  import { Stream } from "effect";
3
- import { Sse } from "effect/unstable/encoding";
3
+ import { Sse } from "effect/encoding";
4
4
  //#region src/streaming/SSE.ts
5
5
  var SSE_exports = /* @__PURE__ */ __exportAll({
6
6
  fromBytes: () => fromBytes,
@@ -22,7 +22,7 @@ const makeParserState = () => {
22
22
  };
23
23
  /**
24
24
  * Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a
25
- * `Stream<SSE.Event>`, using the spec parser from `effect/unstable/encoding`.
25
+ * `Stream<SSE.Event>`, using the spec parser from `effect/encoding`.
26
26
  * An event is dispatched only once its terminating blank line arrives, so a
27
27
  * partial event at stream end (clean or failed) is discarded per the spec
28
28
  * rather than surfaced as a truncated frame.
@@ -1 +1 @@
1
- {"version":3,"file":"SSE.mjs","names":[],"sources":["../../src/streaming/SSE.ts"],"sourcesContent":["import { Stream } from \"effect\"\nimport { Sse } from \"effect/unstable/encoding\"\n\n/**\n * One Server-Sent Event. Fields per the WHATWG spec:\n * - `event`: optional event name (absent for the default \"message\")\n * - `data`: payload, with multiple `data:` lines joined by `\\n`\n * - `id`: optional last-event id\n */\nexport type Event = {\n readonly event?: string\n readonly data: string\n readonly id?: string\n}\n\nconst toEvent = (wire: Sse.Event): Event => ({\n data: wire.data,\n ...(wire.event !== \"message\" && { event: wire.event }),\n ...(wire.id !== undefined && { id: wire.id }),\n})\n\ntype ParserState = {\n readonly parser: Sse.Parser\n readonly out: Array<Event>\n}\n\nconst makeParserState = (): ParserState => {\n const out: Array<Event> = []\n const parser = Sse.makeParser((event) => {\n // `retry:` reconnection hints have no representation in `Event`\n if (event._tag === \"Event\") out.push(toEvent(event))\n })\n return { parser, out }\n}\n\n/**\n * Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a\n * `Stream<SSE.Event>`, using the spec parser from `effect/unstable/encoding`.\n * An event is dispatched only once its terminating blank line arrives, so a\n * partial event at stream end (clean or failed) is discarded per the spec\n * rather than surfaced as a truncated frame.\n */\nexport const fromBytes = <E, R>(\n self: Stream.Stream<Uint8Array, E, R>,\n): Stream.Stream<Event, E, R> =>\n self.pipe(\n Stream.decodeText,\n Stream.mapAccum(makeParserState, (state, text: string) => {\n state.parser.feed(text)\n return [state, state.out.splice(0)] as const\n }),\n )\n\nconst encoder = new TextEncoder()\n\n/**\n * Encode a `Stream<Event>` as `Stream<Uint8Array>` ready to send on an\n * HTTP response with `Content-Type: text/event-stream`.\n */\nexport const toBytes = <E, R>(self: Stream.Stream<Event, E, R>): Stream.Stream<Uint8Array, E, R> =>\n Stream.map(self, (ev) =>\n encoder.encode(\n Sse.encoder.write({\n _tag: \"Event\",\n event: ev.event ?? \"message\",\n id: ev.id,\n data: ev.data,\n }),\n ),\n )\n"],"mappings":";;;;;;;;AAeA,MAAM,WAAW,UAA4B;CAC3C,MAAM,KAAK;CACX,GAAI,KAAK,UAAU,aAAa,EAAE,OAAO,KAAK,MAAM;CACpD,GAAI,KAAK,OAAO,KAAA,KAAa,EAAE,IAAI,KAAK,GAAG;AAC7C;AAOA,MAAM,wBAAqC;CACzC,MAAM,MAAoB,CAAC;CAK3B,OAAO;EAAE,QAJM,IAAI,YAAY,UAAU;GAEvC,IAAI,MAAM,SAAS,SAAS,IAAI,KAAK,QAAQ,KAAK,CAAC;EACrD,CACc;EAAG;CAAI;AACvB;;;;;;;;AASA,MAAa,aACX,SAEA,KAAK,KACH,OAAO,YACP,OAAO,SAAS,kBAAkB,OAAO,SAAiB;CACxD,MAAM,OAAO,KAAK,IAAI;CACtB,OAAO,CAAC,OAAO,MAAM,IAAI,OAAO,CAAC,CAAC;AACpC,CAAC,CACH;AAEF,MAAM,UAAU,IAAI,YAAY;;;;;AAMhC,MAAa,WAAiB,SAC5B,OAAO,IAAI,OAAO,OAChB,QAAQ,OACN,IAAI,QAAQ,MAAM;CAChB,MAAM;CACN,OAAO,GAAG,SAAS;CACnB,IAAI,GAAG;CACP,MAAM,GAAG;AACX,CAAC,CACH,CACF"}
1
+ {"version":3,"file":"SSE.mjs","names":[],"sources":["../../src/streaming/SSE.ts"],"sourcesContent":["import { Stream } from \"effect\"\nimport { Sse } from \"effect/encoding\"\n\n/**\n * One Server-Sent Event. Fields per the WHATWG spec:\n * - `event`: optional event name (absent for the default \"message\")\n * - `data`: payload, with multiple `data:` lines joined by `\\n`\n * - `id`: optional last-event id\n */\nexport type Event = {\n readonly event?: string\n readonly data: string\n readonly id?: string\n}\n\nconst toEvent = (wire: Sse.Event): Event => ({\n data: wire.data,\n ...(wire.event !== \"message\" && { event: wire.event }),\n ...(wire.id !== undefined && { id: wire.id }),\n})\n\ntype ParserState = {\n readonly parser: Sse.Parser\n readonly out: Array<Event>\n}\n\nconst makeParserState = (): ParserState => {\n const out: Array<Event> = []\n const parser = Sse.makeParser((event) => {\n // `retry:` reconnection hints have no representation in `Event`\n if (event._tag === \"Event\") out.push(toEvent(event))\n })\n return { parser, out }\n}\n\n/**\n * Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a\n * `Stream<SSE.Event>`, using the spec parser from `effect/encoding`.\n * An event is dispatched only once its terminating blank line arrives, so a\n * partial event at stream end (clean or failed) is discarded per the spec\n * rather than surfaced as a truncated frame.\n */\nexport const fromBytes = <E, R>(\n self: Stream.Stream<Uint8Array, E, R>,\n): Stream.Stream<Event, E, R> =>\n self.pipe(\n Stream.decodeText,\n Stream.mapAccum(makeParserState, (state, text: string) => {\n state.parser.feed(text)\n return [state, state.out.splice(0)] as const\n }),\n )\n\nconst encoder = new TextEncoder()\n\n/**\n * Encode a `Stream<Event>` as `Stream<Uint8Array>` ready to send on an\n * HTTP response with `Content-Type: text/event-stream`.\n */\nexport const toBytes = <E, R>(self: Stream.Stream<Event, E, R>): Stream.Stream<Uint8Array, E, R> =>\n Stream.map(self, (ev) =>\n encoder.encode(\n Sse.encoder.write({\n _tag: \"Event\",\n event: ev.event ?? \"message\",\n id: ev.id,\n data: ev.data,\n }),\n ),\n )\n"],"mappings":";;;;;;;;AAeA,MAAM,WAAW,UAA4B;CAC3C,MAAM,KAAK;CACX,GAAI,KAAK,UAAU,aAAa,EAAE,OAAO,KAAK,MAAM;CACpD,GAAI,KAAK,OAAO,KAAA,KAAa,EAAE,IAAI,KAAK,GAAG;AAC7C;AAOA,MAAM,wBAAqC;CACzC,MAAM,MAAoB,CAAC;CAK3B,OAAO;EAAE,QAJM,IAAI,YAAY,UAAU;GAEvC,IAAI,MAAM,SAAS,SAAS,IAAI,KAAK,QAAQ,KAAK,CAAC;EACrD,CACc;EAAG;CAAI;AACvB;;;;;;;;AASA,MAAa,aACX,SAEA,KAAK,KACH,OAAO,YACP,OAAO,SAAS,kBAAkB,OAAO,SAAiB;CACxD,MAAM,OAAO,KAAK,IAAI;CACtB,OAAO,CAAC,OAAO,MAAM,IAAI,OAAO,CAAC,CAAC;AACpC,CAAC,CACH;AAEF,MAAM,UAAU,IAAI,YAAY;;;;;AAMhC,MAAa,WAAiB,SAC5B,OAAO,IAAI,OAAO,OAChB,QAAQ,OACN,IAAI,QAAQ,MAAM;CAChB,MAAM;CACN,OAAO,GAAG,SAAS;CACnB,IAAI,GAAG;CACP,MAAM,GAAG;AACX,CAAC,CACH,CACF"}
@@ -1,6 +1,6 @@
1
1
  import { fromBytes, toBytes } from "./SSE.mjs";
2
- import { m as it, o as describe } from "../utils-CiH_DjFW.mjs";
3
- import { n as globalExpect } from "../test.DNmyFkvJ-YPzDR0Co.mjs";
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 { Cause, Deferred, Effect, Exit, Fiber, Result, Stream } from "effect";
5
5
  //#region src/streaming/SSE.test.ts
6
6
  const enc = new TextEncoder();
@@ -1,7 +1,7 @@
1
1
  import { drainBurst, onQuiet, settleBurst } from "./Settle.mjs";
2
- import { o as describe } from "../utils-CiH_DjFW.mjs";
3
- import { n as globalExpect } from "../test.DNmyFkvJ-YPzDR0Co.mjs";
4
- import { t as it } from "../dist-nohcnkD6.mjs";
2
+ import { m as describe } from "../dist-DzeSyeYF.mjs";
3
+ import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
4
+ import { t as it } from "../dist-C8RmtoR8.mjs";
5
5
  import { Effect, Fiber, Option, Queue, Ref, Stream } from "effect";
6
6
  import { TestClock } from "effect/testing";
7
7
  //#region src/streaming/Settle.test.ts
@@ -1,8 +1,8 @@
1
1
  import { t as AiError } from "../AiError-DvhLIbmd.mjs";
2
2
  import { Cause, Duration, Effect, Queue, Schema, Scope } from "effect";
3
- import * as Socket from "effect/unstable/socket/Socket";
3
+ import * as Socket from "effect/socket/Socket";
4
4
  declare namespace WebSocketSession_d_exports {
5
- export { OpenOptions, WebSocketSession, open, redactUrl, toAiError };
5
+ export { OpenOptions, WebSocketSession, isCleanClose, open, redactUrl, toAiError };
6
6
  }
7
7
  type WebSocketSession<A> = {
8
8
  /** Fails `Unavailable` once the socket is closed. */
@@ -25,6 +25,8 @@ type OpenOptions<A, I> = {
25
25
  */
26
26
  declare const redactUrl: (text: string) => string;
27
27
  declare const toAiError: (provider: string) => (error: Socket.SocketError) => AiError;
28
+ /** Effect's `Socket` fails on every close, clean or not. */
29
+ declare const isCleanClose: (error: Socket.SocketError) => boolean;
28
30
  /**
29
31
  * Connect for the lifetime of the surrounding `Scope`. The reader runs in a
30
32
  * forked fiber; a dirty close reaches the consumer as a failed queue rather
@@ -32,5 +34,5 @@ declare const toAiError: (provider: string) => (error: Socket.SocketError) => Ai
32
34
  */
33
35
  declare const open: <A, I>(options: OpenOptions<A, I>) => Effect.Effect<WebSocketSession<A>, AiError, Scope.Scope | Socket.WebSocketConstructor>;
34
36
  //#endregion
35
- export { OpenOptions, WebSocketSession, open, redactUrl, WebSocketSession_d_exports as t, toAiError };
37
+ export { OpenOptions, WebSocketSession, isCleanClose, open, redactUrl, WebSocketSession_d_exports as t, toAiError };
36
38
  //# sourceMappingURL=WebSocketSession.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"WebSocketSession.d.mts","names":[],"sources":["../../src/streaming/WebSocketSession.ts"],"mappings":";;;;;;KAuBY,iBAAiB;;WAElB,OAAO,kBAAkB,OAAO,aAAa;;WAE7C,QAAQ,MAAM,QAAQ,GAAG,UAAkB,MAAM;;KAGhD,YAAY,GAAG;WAChB;WACA;;WAEA,QAAQ,OAAO,MAAM,GAAG;WACxB,cAAc,SAAS;WACvB;;;;;;;cAiBE,YAAS;cAeT,YAAS,sBACH,OACT,OAAO,gBAAc;;;;;;cAsBlB,OAAQ,GAAG,GAAC,SACd,YAAY,GAAG,OACvB,OAAO,OAAO,iBAAiB,IAAI,SAAiB,MAAM,QAAQ,OAAO"}
1
+ {"version":3,"file":"WebSocketSession.d.mts","names":[],"sources":["../../src/streaming/WebSocketSession.ts"],"mappings":";;;;;;KAyBY,iBAAiB;;WAElB,OAAO,kBAAkB,OAAO,aAAa;;WAE7C,QAAQ,MAAM,QAAQ,GAAG,UAAkB,MAAM;;KAGhD,YAAY,GAAG;WAChB;WACA;;WAEA,QAAQ,OAAO,MAAM,GAAG;WACxB,cAAc,SAAS;WACvB;;;;;;;cAiBE,YAAS;cAeT,YAAS,sBACH,OACT,OAAO,gBAAc;;cAqBlB,eAAY,OAAW,OAAO;;;;;;cAQ9B,OAAQ,GAAG,GAAC,SACd,YAAY,GAAG,OACvB,OAAO,OAAO,iBAAiB,IAAI,SAAiB,MAAM,QAAQ,OAAO"}
@@ -1,8 +1,8 @@
1
1
  import { n as __exportAll } from "../rolldown-runtime-EAsCxrDo.mjs";
2
2
  import { AuthFailed, Timeout, Unavailable } from "../domain/AiError.mjs";
3
3
  import { parseSafe } from "./JSONL.mjs";
4
- import { Cause, Effect, Match, Option, Predicate, Queue, Schema } from "effect";
5
- import * as Socket from "effect/unstable/socket/Socket";
4
+ import { Cause, Effect, Match, Option, Predicate, Queue, Ref, Schema, Stream } from "effect";
5
+ import * as Socket from "effect/socket/Socket";
6
6
  //#region src/streaming/WebSocketSession.ts
7
7
  /**
8
8
  * One JSON-over-WebSocket session: connect, read decoded frames off a queue,
@@ -13,6 +13,7 @@ import * as Socket from "effect/unstable/socket/Socket";
13
13
  * own copies of this.
14
14
  */
15
15
  var WebSocketSession_exports = /* @__PURE__ */ __exportAll({
16
+ isCleanClose: () => isCleanClose,
16
17
  open: () => open,
17
18
  redactUrl: () => redactUrl,
18
19
  toAiError: () => toAiError
@@ -50,6 +51,14 @@ const toAiError = (provider) => (error) => {
50
51
  raw
51
52
  })));
52
53
  };
54
+ /** Normal closure, going away, and no status code. */
55
+ const cleanCloseCodes = /* @__PURE__ */ new Set([
56
+ 1e3,
57
+ 1001,
58
+ 1005
59
+ ]);
60
+ /** Effect's `Socket` fails on every close, clean or not. */
61
+ const isCleanClose = (error) => error.reason._tag === "SocketCloseError" && cleanCloseCodes.has(error.reason.code);
53
62
  /**
54
63
  * Connect for the lifetime of the surrounding `Scope`. The reader runs in a
55
64
  * forked fiber; a dirty close reaches the consumer as a failed queue rather
@@ -58,12 +67,14 @@ const toAiError = (provider) => (error) => {
58
67
  const open = (options) => Effect.gen(function* () {
59
68
  const asAiError = toAiError(options.provider);
60
69
  const decode = Schema.decodeUnknownEffect(options.schema);
61
- const socket = yield* Socket.makeWebSocket(options.url, {
62
- closeCodeIsError: (code) => code !== 1e3 && code !== 1001 && code !== 1005,
63
- ...options.openTimeout !== void 0 && { openTimeout: options.openTimeout }
64
- });
70
+ const socket = yield* Socket.makeWebSocket(options.url, { ...options.openTimeout !== void 0 && { openTimeout: options.openTimeout } });
65
71
  const frames = yield* Queue.bounded(options.capacity ?? 64);
66
- const write = yield* socket.writer;
72
+ const { write } = yield* socket.writer;
73
+ const ended = yield* Ref.make(false);
74
+ const closed = new Unavailable({
75
+ provider: options.provider,
76
+ raw: "socket closed"
77
+ });
67
78
  const onFrame = (raw) => Effect.gen(function* () {
68
79
  const json = yield* parseSafe(raw);
69
80
  if (json === void 0) return;
@@ -74,16 +85,17 @@ const open = (options) => Effect.gen(function* () {
74
85
  }
75
86
  yield* Queue.offer(frames, decoded.value);
76
87
  });
77
- yield* socket.runString(onFrame).pipe(Effect.mapError(asAiError), Effect.matchCauseEffect({
88
+ yield* Stream.fromPull(Socket.readerString(socket)).pipe(Stream.scoped, Stream.catchIf(isCleanClose, () => Stream.empty), Stream.runForEach(onFrame), Effect.mapError(asAiError), Effect.ensuring(Ref.set(ended, true)), Effect.matchCauseEffect({
78
89
  onFailure: (cause) => Cause.hasFails(cause) ? Queue.failCause(frames, cause) : Queue.end(frames),
79
90
  onSuccess: () => Queue.end(frames)
80
91
  }), Effect.forkScoped);
92
+ const send = (frame) => Effect.flatMap(Ref.get(ended), (isEnded) => isEnded ? Effect.fail(closed) : Effect.mapError(write(frame), asAiError));
81
93
  return {
82
- send: (frame) => write(frame).pipe(Effect.mapError(asAiError)),
94
+ send,
83
95
  frames
84
96
  };
85
97
  });
86
98
  //#endregion
87
- export { open, redactUrl, WebSocketSession_exports as t, toAiError };
99
+ export { isCleanClose, open, redactUrl, WebSocketSession_exports as t, toAiError };
88
100
 
89
101
  //# sourceMappingURL=WebSocketSession.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"WebSocketSession.mjs","names":["AiError.Timeout","AiError.AuthFailed","AiError.Unavailable","JSONL.parseSafe"],"sources":["../../src/streaming/WebSocketSession.ts"],"sourcesContent":["/**\n * One JSON-over-WebSocket session: connect, read decoded frames off a queue,\n * write frames back.\n *\n * For duplex protocols where the caller drives the conversation frame by\n * frame. The stream-shaped adapters (realtime STT and TTS) still carry their\n * own copies of this.\n */\nimport {\n Cause,\n type Duration,\n Effect,\n Match,\n Option,\n Predicate,\n Queue,\n Schema,\n type Scope,\n} from \"effect\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as AiError from \"../domain/AiError.js\"\nimport * as JSONL from \"./JSONL.js\"\n\nexport type WebSocketSession<A> = {\n /** Fails `Unavailable` once the socket is closed. */\n readonly send: (frame: string) => Effect.Effect<void, AiError.AiError>\n /** Ends when the socket closes cleanly, fails when it does not. */\n readonly frames: Queue.Dequeue<A, AiError.AiError | Cause.Done>\n}\n\nexport type OpenOptions<A, I> = {\n readonly url: string\n readonly provider: string\n /** Frames that do not decode are dropped, and logged at debug level. */\n readonly schema: Schema.Codec<A, I>\n readonly openTimeout?: Duration.Input\n readonly capacity?: number\n}\n\n/**\n * The cause of a failed open is a DOM `ErrorEvent`, not an `Error`, and its\n * `message` is a prototype getter, so a structural decode does not see it.\n */\nconst describeCause = (cause: unknown): string =>\n Predicate.hasProperty(cause, \"message\") && Predicate.isString(cause.message)\n ? cause.message\n : String(cause)\n\n/**\n * Some wires authenticate with a key in the query string, and some runtimes\n * name the socket URL in their open error (Bun does). Everything taken off a\n * socket error goes through this.\n */\nexport const redactUrl = (text: string): string =>\n text.replace(/(\\b(?:wss?|https?):\\/\\/[^\\s\"']*?)\\?[^\\s\"']*/gi, \"$1?<redacted>\")\n\nconst detailOf = Match.type<Socket.SocketError[\"reason\"]>().pipe(\n Match.tag(\"SocketCloseError\", (reason) => reason.message),\n Match.orElse((reason) => describeCause(reason.cause)),\n)\n\n/** Never the `SocketError` itself: callers print `raw` with `Cause.pretty`. */\nconst rawOf = (error: Socket.SocketError): string =>\n redactUrl(`${error.reason._tag}: ${detailOf(error.reason)}`)\n\n/** A rejected WS upgrade surfaces only as prose, so the status is read from it. */\nconst isAuthRejection = (text: string): boolean => /\\b40[13]\\b/.test(text)\n\nexport const toAiError =\n (provider: string) =>\n (error: Socket.SocketError): AiError.AiError => {\n const raw = rawOf(error)\n return Match.value(error.reason).pipe(\n Match.tag(\"SocketOpenError\", (reason) =>\n Match.value(reason.kind).pipe(\n Match.when(\"Timeout\", (): AiError.AiError => new AiError.Timeout({ provider, raw })),\n Match.orElse((): AiError.AiError =>\n isAuthRejection(describeCause(reason.cause))\n ? new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n : new AiError.Unavailable({ provider, raw }),\n ),\n ),\n ),\n Match.orElse((): AiError.AiError => new AiError.Unavailable({ provider, raw })),\n )\n }\n\n/**\n * Connect for the lifetime of the surrounding `Scope`. The reader runs in a\n * forked fiber; a dirty close reaches the consumer as a failed queue rather\n * than a clean end.\n */\nexport const open = <A, I>(\n options: OpenOptions<A, I>,\n): Effect.Effect<WebSocketSession<A>, AiError.AiError, Scope.Scope | Socket.WebSocketConstructor> =>\n Effect.gen(function* () {\n const asAiError = toAiError(options.provider)\n const decode = Schema.decodeUnknownEffect(options.schema)\n const socket = yield* Socket.makeWebSocket(options.url, {\n // Effect's Socket treats every close code as an error by default.\n closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,\n ...(options.openTimeout !== undefined && { openTimeout: options.openTimeout }),\n })\n const frames = yield* Queue.bounded<A, AiError.AiError | Cause.Done>(options.capacity ?? 64)\n const write = yield* socket.writer\n\n const onFrame = (raw: string) =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return\n const decoded = yield* decode(json).pipe(Effect.option)\n if (Option.isNone(decoded)) {\n yield* Effect.logDebug(`[${options.provider}] dropped frame`, { frame: json })\n return\n }\n yield* Queue.offer(frames, decoded.value)\n })\n\n yield* socket.runString(onFrame).pipe(\n Effect.mapError(asAiError),\n // `Queue.end` on a clean close so the consumer drains what is queued;\n // a failure must not arrive looking like the end of the conversation.\n Effect.matchCauseEffect({\n onFailure: (cause) =>\n Cause.hasFails(cause) ? Queue.failCause(frames, cause) : Queue.end(frames),\n onSuccess: () => Queue.end(frames),\n }),\n Effect.forkScoped,\n )\n\n return { send: (frame: string) => write(frame).pipe(Effect.mapError(asAiError)), frames }\n })\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAM,iBAAiB,UACrB,UAAU,YAAY,OAAO,SAAS,KAAK,UAAU,SAAS,MAAM,OAAO,IACvE,MAAM,UACN,OAAO,KAAK;;;;;;AAOlB,MAAa,aAAa,SACxB,KAAK,QAAQ,iDAAiD,eAAe;AAE/E,MAAM,WAAW,MAAM,KAAmC,CAAC,CAAC,KAC1D,MAAM,IAAI,qBAAqB,WAAW,OAAO,OAAO,GACxD,MAAM,QAAQ,WAAW,cAAc,OAAO,KAAK,CAAC,CACtD;;AAGA,MAAM,SAAS,UACb,UAAU,GAAG,MAAM,OAAO,KAAK,IAAI,SAAS,MAAM,MAAM,GAAG;;AAG7D,MAAM,mBAAmB,SAA0B,aAAa,KAAK,IAAI;AAEzE,MAAa,aACV,cACA,UAA+C;CAC9C,MAAM,MAAM,MAAM,KAAK;CACvB,OAAO,MAAM,MAAM,MAAM,MAAM,CAAC,CAAC,KAC/B,MAAM,IAAI,oBAAoB,WAC5B,MAAM,MAAM,OAAO,IAAI,CAAC,CAAC,KACvB,MAAM,KAAK,iBAAkC,IAAIA,QAAgB;EAAE;EAAU;CAAI,CAAC,CAAC,GACnF,MAAM,aACJ,gBAAgB,cAAc,OAAO,KAAK,CAAC,IACvC,IAAIC,WAAmB;EAAE;EAAU,SAAS;EAAQ;CAAI,CAAC,IACzD,IAAIC,YAAoB;EAAE;EAAU;CAAI,CAAC,CAC/C,CACF,CACF,GACA,MAAM,aAA8B,IAAIA,YAAoB;EAAE;EAAU;CAAI,CAAC,CAAC,CAChF;AACF;;;;;;AAOF,MAAa,QACX,YAEA,OAAO,IAAI,aAAa;CACtB,MAAM,YAAY,UAAU,QAAQ,QAAQ;CAC5C,MAAM,SAAS,OAAO,oBAAoB,QAAQ,MAAM;CACxD,MAAM,SAAS,OAAO,OAAO,cAAc,QAAQ,KAAK;EAEtD,mBAAmB,SAAS,SAAS,OAAQ,SAAS,QAAQ,SAAS;EACvE,GAAI,QAAQ,gBAAgB,KAAA,KAAa,EAAE,aAAa,QAAQ,YAAY;CAC9E,CAAC;CACD,MAAM,SAAS,OAAO,MAAM,QAAyC,QAAQ,YAAY,EAAE;CAC3F,MAAM,QAAQ,OAAO,OAAO;CAE5B,MAAM,WAAW,QACf,OAAO,IAAI,aAAa;EACtB,MAAM,OAAO,OAAOC,UAAgB,GAAG;EACvC,IAAI,SAAS,KAAA,GAAW;EACxB,MAAM,UAAU,OAAO,OAAO,IAAI,CAAC,CAAC,KAAK,OAAO,MAAM;EACtD,IAAI,OAAO,OAAO,OAAO,GAAG;GAC1B,OAAO,OAAO,SAAS,IAAI,QAAQ,SAAS,kBAAkB,EAAE,OAAO,KAAK,CAAC;GAC7E;EACF;EACA,OAAO,MAAM,MAAM,QAAQ,QAAQ,KAAK;CAC1C,CAAC;CAEH,OAAO,OAAO,UAAU,OAAO,CAAC,CAAC,KAC/B,OAAO,SAAS,SAAS,GAGzB,OAAO,iBAAiB;EACtB,YAAY,UACV,MAAM,SAAS,KAAK,IAAI,MAAM,UAAU,QAAQ,KAAK,IAAI,MAAM,IAAI,MAAM;EAC3E,iBAAiB,MAAM,IAAI,MAAM;CACnC,CAAC,GACD,OAAO,UACT;CAEA,OAAO;EAAE,OAAO,UAAkB,MAAM,KAAK,CAAC,CAAC,KAAK,OAAO,SAAS,SAAS,CAAC;EAAG;CAAO;AAC1F,CAAC"}
1
+ {"version":3,"file":"WebSocketSession.mjs","names":["AiError.Timeout","AiError.AuthFailed","AiError.Unavailable","JSONL.parseSafe"],"sources":["../../src/streaming/WebSocketSession.ts"],"sourcesContent":["/**\n * One JSON-over-WebSocket session: connect, read decoded frames off a queue,\n * write frames back.\n *\n * For duplex protocols where the caller drives the conversation frame by\n * frame. The stream-shaped adapters (realtime STT and TTS) still carry their\n * own copies of this.\n */\nimport {\n Cause,\n type Duration,\n Effect,\n Match,\n Option,\n Predicate,\n Queue,\n Ref,\n Schema,\n type Scope,\n Stream,\n} from \"effect\"\nimport * as Socket from \"effect/socket/Socket\"\nimport * as AiError from \"../domain/AiError.js\"\nimport * as JSONL from \"./JSONL.js\"\n\nexport type WebSocketSession<A> = {\n /** Fails `Unavailable` once the socket is closed. */\n readonly send: (frame: string) => Effect.Effect<void, AiError.AiError>\n /** Ends when the socket closes cleanly, fails when it does not. */\n readonly frames: Queue.Dequeue<A, AiError.AiError | Cause.Done>\n}\n\nexport type OpenOptions<A, I> = {\n readonly url: string\n readonly provider: string\n /** Frames that do not decode are dropped, and logged at debug level. */\n readonly schema: Schema.Codec<A, I>\n readonly openTimeout?: Duration.Input\n readonly capacity?: number\n}\n\n/**\n * The cause of a failed open is a DOM `ErrorEvent`, not an `Error`, and its\n * `message` is a prototype getter, so a structural decode does not see it.\n */\nconst describeCause = (cause: unknown): string =>\n Predicate.hasProperty(cause, \"message\") && Predicate.isString(cause.message)\n ? cause.message\n : String(cause)\n\n/**\n * Some wires authenticate with a key in the query string, and some runtimes\n * name the socket URL in their open error (Bun does). Everything taken off a\n * socket error goes through this.\n */\nexport const redactUrl = (text: string): string =>\n text.replace(/(\\b(?:wss?|https?):\\/\\/[^\\s\"']*?)\\?[^\\s\"']*/gi, \"$1?<redacted>\")\n\nconst detailOf = Match.type<Socket.SocketError[\"reason\"]>().pipe(\n Match.tag(\"SocketCloseError\", (reason) => reason.message),\n Match.orElse((reason) => describeCause(reason.cause)),\n)\n\n/** Never the `SocketError` itself: callers print `raw` with `Cause.pretty`. */\nconst rawOf = (error: Socket.SocketError): string =>\n redactUrl(`${error.reason._tag}: ${detailOf(error.reason)}`)\n\n/** A rejected WS upgrade surfaces only as prose, so the status is read from it. */\nconst isAuthRejection = (text: string): boolean => /\\b40[13]\\b/.test(text)\n\nexport const toAiError =\n (provider: string) =>\n (error: Socket.SocketError): AiError.AiError => {\n const raw = rawOf(error)\n return Match.value(error.reason).pipe(\n Match.tag(\"SocketOpenError\", (reason) =>\n Match.value(reason.kind).pipe(\n Match.when(\"Timeout\", (): AiError.AiError => new AiError.Timeout({ provider, raw })),\n Match.orElse((): AiError.AiError =>\n isAuthRejection(describeCause(reason.cause))\n ? new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n : new AiError.Unavailable({ provider, raw }),\n ),\n ),\n ),\n Match.orElse((): AiError.AiError => new AiError.Unavailable({ provider, raw })),\n )\n }\n\n/** Normal closure, going away, and no status code. */\nconst cleanCloseCodes: ReadonlySet<number> = new Set([1000, 1001, 1005])\n\n/** Effect's `Socket` fails on every close, clean or not. */\nexport const isCleanClose = (error: Socket.SocketError): boolean =>\n error.reason._tag === \"SocketCloseError\" && cleanCloseCodes.has(error.reason.code)\n\n/**\n * Connect for the lifetime of the surrounding `Scope`. The reader runs in a\n * forked fiber; a dirty close reaches the consumer as a failed queue rather\n * than a clean end.\n */\nexport const open = <A, I>(\n options: OpenOptions<A, I>,\n): Effect.Effect<WebSocketSession<A>, AiError.AiError, Scope.Scope | Socket.WebSocketConstructor> =>\n Effect.gen(function* () {\n const asAiError = toAiError(options.provider)\n const decode = Schema.decodeUnknownEffect(options.schema)\n const socket = yield* Socket.makeWebSocket(options.url, {\n ...(options.openTimeout !== undefined && { openTimeout: options.openTimeout }),\n })\n const frames = yield* Queue.bounded<A, AiError.AiError | Cause.Done>(options.capacity ?? 64)\n const { write } = yield* socket.writer\n // The writer waits for the next connection rather than failing, so a send\n // after the reader ends has to be refused here.\n const ended = yield* Ref.make(false)\n const closed = new AiError.Unavailable({ provider: options.provider, raw: \"socket closed\" })\n\n const onFrame = (raw: string) =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return\n const decoded = yield* decode(json).pipe(Effect.option)\n if (Option.isNone(decoded)) {\n yield* Effect.logDebug(`[${options.provider}] dropped frame`, { frame: json })\n return\n }\n yield* Queue.offer(frames, decoded.value)\n })\n\n yield* Stream.fromPull(Socket.readerString(socket)).pipe(\n Stream.scoped,\n Stream.catchIf(isCleanClose, () => Stream.empty),\n Stream.runForEach(onFrame),\n Effect.mapError(asAiError),\n // Set before `frames` ends, so a send after the last frame already fails.\n Effect.ensuring(Ref.set(ended, true)),\n // `Queue.end` on a clean close so the consumer drains what is queued;\n // a failure must not arrive looking like the end of the conversation.\n Effect.matchCauseEffect({\n onFailure: (cause) =>\n Cause.hasFails(cause) ? Queue.failCause(frames, cause) : Queue.end(frames),\n onSuccess: () => Queue.end(frames),\n }),\n Effect.forkScoped,\n )\n\n const send = (frame: string): Effect.Effect<void, AiError.AiError> =>\n Effect.flatMap(Ref.get(ended), (isEnded) =>\n isEnded ? Effect.fail(closed) : Effect.mapError(write(frame), asAiError),\n )\n\n return { send, frames }\n })\n"],"mappings":"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"}
@@ -1,9 +1,11 @@
1
1
  import { describe } from "../domain/AiError.mjs";
2
- import { toAiError } from "./WebSocketSession.mjs";
3
- import { o as describe$1 } from "../utils-CiH_DjFW.mjs";
4
- import { n as globalExpect } from "../test.DNmyFkvJ-YPzDR0Co.mjs";
5
- import { t as it } from "../dist-nohcnkD6.mjs";
6
- import * as Socket from "effect/unstable/socket/Socket";
2
+ import { open, toAiError } from "./WebSocketSession.mjs";
3
+ import { m as describe$1 } from "../dist-DzeSyeYF.mjs";
4
+ import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
5
+ import { t as it } from "../dist-C8RmtoR8.mjs";
6
+ import { make } from "../testing/FakeWebSocket.mjs";
7
+ import { Effect, Exit, Pull, Queue, Schema } from "effect";
8
+ import * as Socket from "effect/socket/Socket";
7
9
  //#region src/streaming/WebSocketSession.test.ts
8
10
  const KEY = "AIzaSyEXAMPLEKEY";
9
11
  const URL = `wss://generativelanguage.googleapis.com/ws/BidiGenerateContent?key=${KEY}`;
@@ -45,6 +47,40 @@ describe$1("WebSocketSession errors", () => {
45
47
  globalExpect(ended._tag).toBe("Unavailable");
46
48
  });
47
49
  });
50
+ const Frame = Schema.Struct({ n: Schema.Number });
51
+ const session = (server) => open({
52
+ url: "wss://example.test",
53
+ provider: "test",
54
+ schema: Frame
55
+ }).pipe(Effect.provide(server.layer));
56
+ describe$1("WebSocketSession lifetime", () => {
57
+ it.live("writes a reply to a frame before the next one arrives", () => Effect.gen(function* () {
58
+ const server = yield* make({ greeting: [{ n: 1 }] });
59
+ const { send, frames } = yield* session(server);
60
+ const first = yield* Queue.take(frames);
61
+ yield* send(JSON.stringify({ n: first.n + 1 }));
62
+ yield* Effect.sleep("5 millis");
63
+ globalExpect(yield* server.sent).toEqual([{ n: 2 }]);
64
+ }).pipe(Effect.scoped));
65
+ it.live("ends the frames on a clean close, and refuses a send after it", () => Effect.gen(function* () {
66
+ const server = yield* make({ greeting: [{ n: 1 }] });
67
+ const { send, frames } = yield* session(server);
68
+ yield* Queue.take(frames);
69
+ yield* server.close(1e3);
70
+ const end = yield* Effect.exit(Queue.take(frames));
71
+ const late = yield* Effect.exit(send(JSON.stringify({ n: 2 })));
72
+ globalExpect(Exit.isFailure(end) && Pull.isDoneCause(end.cause)).toBe(true);
73
+ globalExpect(Exit.isFailure(late) && JSON.stringify(late)).toContain("Unavailable");
74
+ }).pipe(Effect.scoped));
75
+ it.live("fails the frames on a dirty close", () => Effect.gen(function* () {
76
+ const server = yield* make({ greeting: [{ n: 1 }] });
77
+ const { frames } = yield* session(server);
78
+ yield* Queue.take(frames);
79
+ yield* server.close(1011);
80
+ const end = yield* Effect.exit(Queue.take(frames));
81
+ globalExpect(JSON.stringify(end)).toContain("Unavailable");
82
+ }).pipe(Effect.scoped));
83
+ });
48
84
  //#endregion
49
85
  export {};
50
86
 
@@ -1 +1 @@
1
- {"version":3,"file":"WebSocketSession.test.mjs","names":["describe","AiError.describe"],"sources":["../../src/streaming/WebSocketSession.test.ts"],"sourcesContent":["import { describe, it } from \"@effect/vitest\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport { expect } from \"vitest\"\nimport * as AiError from \"../domain/AiError.js\"\nimport { toAiError } from \"./WebSocketSession.js\"\n\nconst KEY = \"AIzaSyEXAMPLEKEY\"\nconst URL = `wss://generativelanguage.googleapis.com/ws/BidiGenerateContent?key=${KEY}`\n\n/**\n * What Bun hands the `error` listener when the upgrade is rejected: the whole\n * socket URL, in prose, on a prototype getter. Node and Deno say less, but the\n * adapter cannot know which runtime it is on.\n */\nclass BunErrorEvent {\n get message(): string {\n return `WebSocket connection to '${URL}' failed: Connection ended`\n }\n}\n\n/** Deno names the status but not the URL, also on a prototype getter. */\nclass DenoErrorEvent {\n constructor(private readonly detail: string) {}\n get message(): string {\n return `NetworkError: failed to connect to WebSocket: ${this.detail}`\n }\n}\n\nconst openFailure = (cause: unknown) =>\n new Socket.SocketError({ reason: new Socket.SocketOpenError({ kind: \"Unknown\", cause }) })\n\ndescribe(\"WebSocketSession errors\", () => {\n it(\"keeps the key out of the error a rejected upgrade produces\", () => {\n const error = toAiError(\"google\")(openFailure(new BunErrorEvent()))\n\n expect(JSON.stringify(error)).not.toContain(KEY)\n expect(AiError.describe(error)).not.toContain(KEY)\n // The host is still there: an error that says nothing is its own problem.\n expect(JSON.stringify(error)).toContain(\"generativelanguage.googleapis.com\")\n })\n\n it(\"reads the rejected status out of the prose, wherever the runtime put it\", () => {\n // Deno's wording. The status is the only hint a rejected upgrade gives.\n const forbidden = toAiError(\"google\")(\n openFailure(new DenoErrorEvent(\"Invalid status code: 403\")),\n )\n const ended = toAiError(\"google\")(openFailure(new BunErrorEvent()))\n\n expect(forbidden._tag).toBe(\"AuthFailed\")\n expect(ended._tag).toBe(\"Unavailable\")\n })\n})\n"],"mappings":";;;;;;;AAMA,MAAM,MAAM;AACZ,MAAM,MAAM,sEAAsE;;;;;;AAOlF,IAAM,gBAAN,MAAoB;CAClB,IAAI,UAAkB;EACpB,OAAO,4BAA4B,IAAI;CACzC;AACF;;AAGA,IAAM,iBAAN,MAAqB;CACU;CAA7B,YAAY,QAAiC;EAAhB,KAAA,SAAA;CAAiB;CAC9C,IAAI,UAAkB;EACpB,OAAO,iDAAiD,KAAK;CAC/D;AACF;AAEA,MAAM,eAAe,UACnB,IAAI,OAAO,YAAY,EAAE,QAAQ,IAAI,OAAO,gBAAgB;CAAE,MAAM;CAAW;AAAM,CAAC,EAAE,CAAC;AAE3FA,WAAS,iCAAiC;CACxC,GAAG,oEAAoE;EACrE,MAAM,QAAQ,UAAU,QAAQ,CAAC,CAAC,YAAY,IAAI,cAAc,CAAC,CAAC;EAElE,aAAO,KAAK,UAAU,KAAK,CAAC,CAAC,CAAC,IAAI,UAAU,GAAG;EAC/C,aAAOC,SAAiB,KAAK,CAAC,CAAC,CAAC,IAAI,UAAU,GAAG;EAEjD,aAAO,KAAK,UAAU,KAAK,CAAC,CAAC,CAAC,UAAU,mCAAmC;CAC7E,CAAC;CAED,GAAG,iFAAiF;EAElF,MAAM,YAAY,UAAU,QAAQ,CAAC,CACnC,YAAY,IAAI,eAAe,0BAA0B,CAAC,CAC5D;EACA,MAAM,QAAQ,UAAU,QAAQ,CAAC,CAAC,YAAY,IAAI,cAAc,CAAC,CAAC;EAElE,aAAO,UAAU,IAAI,CAAC,CAAC,KAAK,YAAY;EACxC,aAAO,MAAM,IAAI,CAAC,CAAC,KAAK,aAAa;CACvC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"WebSocketSession.test.mjs","names":["describe","AiError.describe","FakeWebSocket.make"],"sources":["../../src/streaming/WebSocketSession.test.ts"],"sourcesContent":["import { describe, it } from \"@effect/vitest\"\nimport { Effect, Exit, Pull, Queue, Schema } from \"effect\"\nimport * as Socket from \"effect/socket/Socket\"\nimport { expect } from \"vitest\"\nimport * as AiError from \"../domain/AiError.js\"\nimport * as FakeWebSocket from \"../testing/FakeWebSocket.js\"\nimport { open, toAiError } from \"./WebSocketSession.js\"\n\nconst KEY = \"AIzaSyEXAMPLEKEY\"\nconst URL = `wss://generativelanguage.googleapis.com/ws/BidiGenerateContent?key=${KEY}`\n\n/**\n * What Bun hands the `error` listener when the upgrade is rejected: the whole\n * socket URL, in prose, on a prototype getter. Node and Deno say less, but the\n * adapter cannot know which runtime it is on.\n */\nclass BunErrorEvent {\n get message(): string {\n return `WebSocket connection to '${URL}' failed: Connection ended`\n }\n}\n\n/** Deno names the status but not the URL, also on a prototype getter. */\nclass DenoErrorEvent {\n constructor(private readonly detail: string) {}\n get message(): string {\n return `NetworkError: failed to connect to WebSocket: ${this.detail}`\n }\n}\n\nconst openFailure = (cause: unknown) =>\n new Socket.SocketError({ reason: new Socket.SocketOpenError({ kind: \"Unknown\", cause }) })\n\ndescribe(\"WebSocketSession errors\", () => {\n it(\"keeps the key out of the error a rejected upgrade produces\", () => {\n const error = toAiError(\"google\")(openFailure(new BunErrorEvent()))\n\n expect(JSON.stringify(error)).not.toContain(KEY)\n expect(AiError.describe(error)).not.toContain(KEY)\n // The host is still there: an error that says nothing is its own problem.\n expect(JSON.stringify(error)).toContain(\"generativelanguage.googleapis.com\")\n })\n\n it(\"reads the rejected status out of the prose, wherever the runtime put it\", () => {\n // Deno's wording. The status is the only hint a rejected upgrade gives.\n const forbidden = toAiError(\"google\")(\n openFailure(new DenoErrorEvent(\"Invalid status code: 403\")),\n )\n const ended = toAiError(\"google\")(openFailure(new BunErrorEvent()))\n\n expect(forbidden._tag).toBe(\"AuthFailed\")\n expect(ended._tag).toBe(\"Unavailable\")\n })\n})\n\nconst Frame = Schema.Struct({ n: Schema.Number })\n\nconst session = (server: FakeWebSocket.FakeWebSocketServer) =>\n open({ url: \"wss://example.test\", provider: \"test\", schema: Frame }).pipe(\n Effect.provide(server.layer),\n )\n\ndescribe(\"WebSocketSession lifetime\", () => {\n it.live(\"writes a reply to a frame before the next one arrives\", () =>\n Effect.gen(function* () {\n const server = yield* FakeWebSocket.make({ greeting: [{ n: 1 }] })\n const { send, frames } = yield* session(server)\n\n const first = yield* Queue.take(frames)\n yield* send(JSON.stringify({ n: first.n + 1 }))\n yield* Effect.sleep(\"5 millis\")\n\n expect(yield* server.sent).toEqual([{ n: 2 }])\n }).pipe(Effect.scoped),\n )\n\n it.live(\"ends the frames on a clean close, and refuses a send after it\", () =>\n Effect.gen(function* () {\n const server = yield* FakeWebSocket.make({ greeting: [{ n: 1 }] })\n const { send, frames } = yield* session(server)\n\n yield* Queue.take(frames)\n yield* server.close(1000)\n const end = yield* Effect.exit(Queue.take(frames))\n const late = yield* Effect.exit(send(JSON.stringify({ n: 2 })))\n\n expect(Exit.isFailure(end) && Pull.isDoneCause(end.cause)).toBe(true)\n expect(Exit.isFailure(late) && JSON.stringify(late)).toContain(\"Unavailable\")\n }).pipe(Effect.scoped),\n )\n\n it.live(\"fails the frames on a dirty close\", () =>\n Effect.gen(function* () {\n const server = yield* FakeWebSocket.make({ greeting: [{ n: 1 }] })\n const { frames } = yield* session(server)\n\n yield* Queue.take(frames)\n yield* server.close(1011)\n const end = yield* Effect.exit(Queue.take(frames))\n\n expect(JSON.stringify(end)).toContain(\"Unavailable\")\n }).pipe(Effect.scoped),\n )\n})\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"StructuredFormat.mjs","names":[],"sources":["../../src/structured-format/StructuredFormat.ts"],"sourcesContent":["import type { StandardJSONSchemaV1, StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { Data, Effect, Match, Result, Schema, Stream, pipe } from \"effect\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Cross-validator schema constraint for structured outputs. Any schema\n * implementing both Standard Schema (runtime validation) and Standard\n * JSON Schema (wire encoding) works directly: Zod 4+, Valibot, ArkType,\n * and Effect Schema after `fromEffectSchema`.\n */\nexport type StructuredSchema<Output = unknown> = StandardSchemaV1<unknown, Output> &\n StandardJSONSchemaV1<unknown, Output>\n\n/**\n * A schema-bound output the user wants the model to produce. Pairs the\n * cross-validator schema with metadata providers need (name, description,\n * strict-mode flag).\n */\nexport type StructuredFormat<A> = {\n readonly name: string\n readonly description?: string\n readonly schema: StructuredSchema<A>\n /**\n * Provider strict-mode flag. OpenAI, Anthropic, and Mistral honour it\n * (constrained decoding); other providers ignore.\n */\n readonly strict?: boolean\n}\n\n/** A single path-scoped validation problem. Library-agnostic shape. */\nexport type DecodeIssue = {\n readonly path: ReadonlyArray<string | number>\n readonly message: string\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/**\n * Schema validation failed. `raw` is the original text (or stringified\n * value) that failed; `issues` is a flat list of per-field problems.\n */\nexport class StructuredDecodeError extends Data.TaggedError(\"StructuredDecodeError\")<{\n readonly raw: string\n readonly issues: ReadonlyArray<DecodeIssue>\n}> {}\n\n/**\n * `JSON.parse` threw on a string that was supposed to be JSON. Distinct\n * from `StructuredDecodeError`: the bytes weren't even JSON.\n */\nexport class JsonParseError extends Data.TaggedError(\"StructuredJsonParseError\")<{\n readonly raw: string\n readonly cause: unknown\n}> {}\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap an Effect `Schema` as a `StructuredFormat`. Effect Schema doesn't\n * natively implement Standard Schema; this helper installs the\n * `~standard` and JSON Schema interfaces.\n */\nexport const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(\n schema: S,\n options?: {\n readonly name?: string\n readonly description?: string\n readonly strict?: boolean\n },\n): StructuredFormat<S[\"Type\"]> => ({\n name: options?.name ?? \"output\",\n schema: Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema)),\n ...(options?.description !== undefined && {\n description: options.description,\n }),\n ...(options?.strict !== undefined && { strict: options.strict }),\n})\n\n// ---------------------------------------------------------------------------\n// Standard Schema → DecodeIssue\n// ---------------------------------------------------------------------------\n\nconst propertyKeyToScalar = Match.type<PropertyKey>().pipe(\n Match.when(Match.string, (s) => s),\n Match.when(Match.number, (n) => n),\n Match.when(Match.symbol, (s) => s.toString()),\n Match.exhaustive,\n)\n\nconst segmentToKey = Match.type<PropertyKey | StandardSchemaV1.PathSegment>().pipe(\n Match.when(Match.string, (s) => s),\n Match.when(Match.number, (n) => n),\n Match.when(Match.symbol, (s) => s.toString()),\n Match.orElse((segment) => propertyKeyToScalar(segment.key)),\n)\n\nconst issueToDecode = (issue: StandardSchemaV1.Issue): DecodeIssue => ({\n path: (issue.path ?? []).map(segmentToKey),\n message: issue.message,\n})\n\n// ---------------------------------------------------------------------------\n// Decoding\n// ---------------------------------------------------------------------------\n\n/**\n * Validate an `unknown` against the format's schema. Returns the typed\n * value or a `StructuredDecodeError`. Standard Schema's `validate` may\n * be async; this function handles both sync and async results.\n */\nexport const decode =\n <A>(format: StructuredFormat<A>) =>\n (raw: unknown): Effect.Effect<A, StructuredDecodeError> =>\n pipe(\n Effect.promise(async () => format.schema[\"~standard\"].validate(raw)),\n Effect.flatMap((result) =>\n result.issues === undefined\n ? Effect.succeed(result.value)\n : Effect.fail(\n new StructuredDecodeError({\n raw: typeof raw === \"string\" ? raw : JSON.stringify(raw),\n issues: result.issues.map(issueToDecode),\n }),\n ),\n ),\n )\n\n/**\n * Parse a JSON string then validate against the format's schema. Two\n * failure modes: `JsonParseError` (bytes weren't JSON) and\n * `StructuredDecodeError` (JSON didn't match the schema).\n */\nexport const parseJson =\n <A>(format: StructuredFormat<A>) =>\n (raw: string): Effect.Effect<A, JsonParseError | StructuredDecodeError> =>\n pipe(\n Effect.try({\n // Validation runs through Standard Schema (Zod / Valibot / Effect), not\n // Effect Schema, so its JSON codecs don't apply here; and JSON.parse is\n // a separate step on purpose, to surface JsonParseError distinctly.\n // @effect-diagnostics-next-line effect/preferSchemaOverJson:off\n try: () => JSON.parse(raw),\n catch: (cause) => new JsonParseError({ raw, cause }),\n }),\n Effect.flatMap(decode(format)),\n )\n\n/**\n * Stream operator: each input string is JSON-parsed and validated.\n * Failures surface in the stream's failure channel, distinguished by tag.\n */\nexport const decodeJsonLines =\n <A>(format: StructuredFormat<A>) =>\n <E, R>(\n self: Stream.Stream<string, E, R>,\n ): Stream.Stream<A, E | JsonParseError | StructuredDecodeError, R> =>\n self.pipe(Stream.mapEffect(parseJson(format)))\n\n/**\n * Like {@link decodeJsonLines}, but each line yields a `Result` instead of\n * failing the stream. Use when one bad line shouldn't abort the rest —\n * log-and-continue, or partial-recovery with a corrective re-prompt.\n * Upstream errors (the input stream's own `E`) still propagate normally.\n */\nexport const decodeJsonLinesRecoverable =\n <A>(format: StructuredFormat<A>) =>\n <E, R>(\n self: Stream.Stream<string, E, R>,\n ): Stream.Stream<Result.Result<A, JsonParseError | StructuredDecodeError>, E, R> =>\n self.pipe(Stream.mapEffect((line) => Effect.result(parseJson(format)(line))))\n"],"mappings":";;;;;;;;;;;;;;;;AA8CA,IAAa,wBAAb,cAA2C,KAAK,YAAY,uBAAuB,CAAC,CAGjF,CAAC;;;;;AAMJ,IAAa,iBAAb,cAAoC,KAAK,YAAY,0BAA0B,CAAC,CAG7E,CAAC;;;;;;AAWJ,MAAa,oBACX,QACA,aAKiC;CACjC,MAAM,SAAS,QAAQ;CACvB,QAAQ,OAAO,uBAAuB,OAAO,mBAAmB,MAAM,CAAC;CACvE,GAAI,SAAS,gBAAgB,KAAA,KAAa,EACxC,aAAa,QAAQ,YACvB;CACA,GAAI,SAAS,WAAW,KAAA,KAAa,EAAE,QAAQ,QAAQ,OAAO;AAChE;AAMA,MAAM,sBAAsB,MAAM,KAAkB,CAAC,CAAC,KACpD,MAAM,KAAK,MAAM,SAAS,MAAM,CAAC,GACjC,MAAM,KAAK,MAAM,SAAS,MAAM,CAAC,GACjC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,SAAS,CAAC,GAC5C,MAAM,UACR;AAEA,MAAM,eAAe,MAAM,KAAiD,CAAC,CAAC,KAC5E,MAAM,KAAK,MAAM,SAAS,MAAM,CAAC,GACjC,MAAM,KAAK,MAAM,SAAS,MAAM,CAAC,GACjC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,SAAS,CAAC,GAC5C,MAAM,QAAQ,YAAY,oBAAoB,QAAQ,GAAG,CAAC,CAC5D;AAEA,MAAM,iBAAiB,WAAgD;CACrE,OAAO,MAAM,QAAQ,CAAC,EAAA,CAAG,IAAI,YAAY;CACzC,SAAS,MAAM;AACjB;;;;;;AAWA,MAAa,UACP,YACH,QACC,KACE,OAAO,QAAQ,YAAY,OAAO,OAAO,YAAY,CAAC,SAAS,GAAG,CAAC,GACnE,OAAO,SAAS,WACd,OAAO,WAAW,KAAA,IACd,OAAO,QAAQ,OAAO,KAAK,IAC3B,OAAO,KACL,IAAI,sBAAsB;CACxB,KAAK,OAAO,QAAQ,WAAW,MAAM,KAAK,UAAU,GAAG;CACvD,QAAQ,OAAO,OAAO,IAAI,aAAa;AACzC,CAAC,CACH,CACN,CACF;;;;;;AAOJ,MAAa,aACP,YACH,QACC,KACE,OAAO,IAAI;CAKT,WAAW,KAAK,MAAM,GAAG;CACzB,QAAQ,UAAU,IAAI,eAAe;EAAE;EAAK;CAAM,CAAC;AACrD,CAAC,GACD,OAAO,QAAQ,OAAO,MAAM,CAAC,CAC/B;;;;;AAMJ,MAAa,mBACP,YAEF,SAEA,KAAK,KAAK,OAAO,UAAU,UAAU,MAAM,CAAC,CAAC;;;;;;;AAQjD,MAAa,8BACP,YAEF,SAEA,KAAK,KAAK,OAAO,WAAW,SAAS,OAAO,OAAO,UAAU,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"StructuredFormat.mjs","names":[],"sources":["../../src/structured-format/StructuredFormat.ts"],"sourcesContent":["import type { StandardJSONSchemaV1, StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { Data, Effect, Match, Result, Schema, Stream, pipe } from \"effect\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Cross-validator schema constraint for structured outputs. Any schema\n * implementing both Standard Schema (runtime validation) and Standard\n * JSON Schema (wire encoding) works directly: Zod 4+, Valibot, ArkType,\n * and Effect Schema after `fromEffectSchema`.\n */\nexport type StructuredSchema<Output = unknown> = StandardSchemaV1<unknown, Output> &\n StandardJSONSchemaV1<unknown, Output>\n\n/**\n * A schema-bound output the user wants the model to produce. Pairs the\n * cross-validator schema with metadata providers need (name, description,\n * strict-mode flag).\n */\nexport type StructuredFormat<A> = {\n readonly name: string\n readonly description?: string\n readonly schema: StructuredSchema<A>\n /**\n * Provider strict-mode flag. OpenAI, Anthropic, and Mistral honour it\n * (constrained decoding); other providers ignore.\n */\n readonly strict?: boolean\n}\n\n/** A single path-scoped validation problem. Library-agnostic shape. */\nexport type DecodeIssue = {\n readonly path: ReadonlyArray<string | number>\n readonly message: string\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/**\n * Schema validation failed. `raw` is the original text (or stringified\n * value) that failed; `issues` is a flat list of per-field problems.\n */\nexport class StructuredDecodeError extends Data.TaggedError(\"StructuredDecodeError\")<{\n readonly raw: string\n readonly issues: ReadonlyArray<DecodeIssue>\n}> {}\n\n/**\n * `JSON.parse` threw on a string that was supposed to be JSON. Distinct\n * from `StructuredDecodeError`: the bytes weren't even JSON.\n */\nexport class JsonParseError extends Data.TaggedError(\"StructuredJsonParseError\")<{\n readonly raw: string\n readonly cause: unknown\n}> {}\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap an Effect `Schema` as a `StructuredFormat`. Effect Schema doesn't\n * natively implement Standard Schema; this helper installs the\n * `~standard` and JSON Schema interfaces.\n */\nexport const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(\n schema: S,\n options?: {\n readonly name?: string\n readonly description?: string\n readonly strict?: boolean\n },\n): StructuredFormat<S[\"Type\"]> => ({\n name: options?.name ?? \"output\",\n schema: Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema)),\n ...(options?.description !== undefined && {\n description: options.description,\n }),\n ...(options?.strict !== undefined && { strict: options.strict }),\n})\n\n// ---------------------------------------------------------------------------\n// Standard Schema → DecodeIssue\n// ---------------------------------------------------------------------------\n\nconst propertyKeyToScalar = Match.type<PropertyKey>().pipe(\n Match.when(Match.string, (s) => s),\n Match.when(Match.number, (n) => n),\n Match.when(Match.symbol, (s) => s.toString()),\n Match.exhaustive,\n)\n\nconst segmentToKey = Match.type<PropertyKey | StandardSchemaV1.PathSegment>().pipe(\n Match.when(Match.string, (s) => s),\n Match.when(Match.number, (n) => n),\n Match.when(Match.symbol, (s) => s.toString()),\n Match.orElse((segment) => propertyKeyToScalar(segment.key)),\n)\n\nconst issueToDecode = (issue: StandardSchemaV1.Issue): DecodeIssue => ({\n path: (issue.path ?? []).map(segmentToKey),\n message: issue.message,\n})\n\n// ---------------------------------------------------------------------------\n// Decoding\n// ---------------------------------------------------------------------------\n\n/**\n * Validate an `unknown` against the format's schema. Returns the typed\n * value or a `StructuredDecodeError`. Standard Schema's `validate` may\n * be async; this function handles both sync and async results.\n */\nexport const decode =\n <A>(format: StructuredFormat<A>) =>\n (raw: unknown): Effect.Effect<A, StructuredDecodeError> =>\n pipe(\n Effect.promise(async () => format.schema[\"~standard\"].validate(raw)),\n Effect.flatMap((result) =>\n result.issues === undefined\n ? Effect.succeed(result.value)\n : Effect.fail(\n new StructuredDecodeError({\n raw: typeof raw === \"string\" ? raw : JSON.stringify(raw),\n issues: result.issues.map(issueToDecode),\n }),\n ),\n ),\n )\n\n/**\n * Parse a JSON string then validate against the format's schema. Two\n * failure modes: `JsonParseError` (bytes weren't JSON) and\n * `StructuredDecodeError` (JSON didn't match the schema).\n */\nexport const parseJson =\n <A>(format: StructuredFormat<A>) =>\n (raw: string): Effect.Effect<A, JsonParseError | StructuredDecodeError> =>\n pipe(\n Effect.try({\n // Validation runs through Standard Schema (Zod / Valibot / Effect), not\n // Effect Schema, so its JSON codecs don't apply here; and JSON.parse is\n // a separate step on purpose, to surface JsonParseError distinctly.\n // @effect-diagnostics-next-linepreferSchemaOverJson:off\n try: () => JSON.parse(raw),\n catch: (cause) => new JsonParseError({ raw, cause }),\n }),\n Effect.flatMap(decode(format)),\n )\n\n/**\n * Stream operator: each input string is JSON-parsed and validated.\n * Failures surface in the stream's failure channel, distinguished by tag.\n */\nexport const decodeJsonLines =\n <A>(format: StructuredFormat<A>) =>\n <E, R>(\n self: Stream.Stream<string, E, R>,\n ): Stream.Stream<A, E | JsonParseError | StructuredDecodeError, R> =>\n self.pipe(Stream.mapEffect(parseJson(format)))\n\n/**\n * Like {@link decodeJsonLines}, but each line yields a `Result` instead of\n * failing the stream. Use when one bad line shouldn't abort the rest —\n * log-and-continue, or partial-recovery with a corrective re-prompt.\n * Upstream errors (the input stream's own `E`) still propagate normally.\n */\nexport const decodeJsonLinesRecoverable =\n <A>(format: StructuredFormat<A>) =>\n <E, R>(\n self: Stream.Stream<string, E, R>,\n ): Stream.Stream<Result.Result<A, JsonParseError | StructuredDecodeError>, E, R> =>\n self.pipe(Stream.mapEffect((line) => Effect.result(parseJson(format)(line))))\n"],"mappings":";;;;;;;;;;;;;;;;AA8CA,IAAa,wBAAb,cAA2C,KAAK,YAAY,uBAAuB,CAAC,CAGjF,CAAC;;;;;AAMJ,IAAa,iBAAb,cAAoC,KAAK,YAAY,0BAA0B,CAAC,CAG7E,CAAC;;;;;;AAWJ,MAAa,oBACX,QACA,aAKiC;CACjC,MAAM,SAAS,QAAQ;CACvB,QAAQ,OAAO,uBAAuB,OAAO,mBAAmB,MAAM,CAAC;CACvE,GAAI,SAAS,gBAAgB,KAAA,KAAa,EACxC,aAAa,QAAQ,YACvB;CACA,GAAI,SAAS,WAAW,KAAA,KAAa,EAAE,QAAQ,QAAQ,OAAO;AAChE;AAMA,MAAM,sBAAsB,MAAM,KAAkB,CAAC,CAAC,KACpD,MAAM,KAAK,MAAM,SAAS,MAAM,CAAC,GACjC,MAAM,KAAK,MAAM,SAAS,MAAM,CAAC,GACjC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,SAAS,CAAC,GAC5C,MAAM,UACR;AAEA,MAAM,eAAe,MAAM,KAAiD,CAAC,CAAC,KAC5E,MAAM,KAAK,MAAM,SAAS,MAAM,CAAC,GACjC,MAAM,KAAK,MAAM,SAAS,MAAM,CAAC,GACjC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,SAAS,CAAC,GAC5C,MAAM,QAAQ,YAAY,oBAAoB,QAAQ,GAAG,CAAC,CAC5D;AAEA,MAAM,iBAAiB,WAAgD;CACrE,OAAO,MAAM,QAAQ,CAAC,EAAA,CAAG,IAAI,YAAY;CACzC,SAAS,MAAM;AACjB;;;;;;AAWA,MAAa,UACP,YACH,QACC,KACE,OAAO,QAAQ,YAAY,OAAO,OAAO,YAAY,CAAC,SAAS,GAAG,CAAC,GACnE,OAAO,SAAS,WACd,OAAO,WAAW,KAAA,IACd,OAAO,QAAQ,OAAO,KAAK,IAC3B,OAAO,KACL,IAAI,sBAAsB;CACxB,KAAK,OAAO,QAAQ,WAAW,MAAM,KAAK,UAAU,GAAG;CACvD,QAAQ,OAAO,OAAO,IAAI,aAAa;AACzC,CAAC,CACH,CACN,CACF;;;;;;AAOJ,MAAa,aACP,YACH,QACC,KACE,OAAO,IAAI;CAKT,WAAW,KAAK,MAAM,GAAG;CACzB,QAAQ,UAAU,IAAI,eAAe;EAAE;EAAK;CAAM,CAAC;AACrD,CAAC,GACD,OAAO,QAAQ,OAAO,MAAM,CAAC,CAC/B;;;;;AAMJ,MAAa,mBACP,YAEF,SAEA,KAAK,KAAK,OAAO,UAAU,UAAU,MAAM,CAAC,CAAC;;;;;;;AAQjD,MAAa,8BACP,YAEF,SAEA,KAAK,KAAK,OAAO,WAAW,SAAS,OAAO,OAAO,UAAU,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { JsonParseError, StructuredDecodeError, decodeJsonLines, decodeJsonLinesRecoverable, fromEffectSchema } from "./StructuredFormat.mjs";
2
- import { m as it, o as describe } from "../utils-CiH_DjFW.mjs";
3
- import { n as globalExpect } from "../test.DNmyFkvJ-YPzDR0Co.mjs";
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, Exit, Filter, Result, Schema, Stream } from "effect";
5
5
  //#region src/structured-format/StructuredFormat.test.ts
6
6
  const Item = Schema.Struct({
@@ -1,9 +1,13 @@
1
1
  import { Effect, Layer, Scope } from "effect";
2
- import * as Socket from "effect/unstable/socket/Socket";
2
+ import * as Socket from "effect/socket/Socket";
3
3
  //#region src/testing/FakeWebSocket.d.ts
4
4
  type FakeWebSocketServer = {
5
5
  /** Every frame the client has sent, JSON-parsed, oldest first. */
6
6
  readonly sent: Effect.Effect<ReadonlyArray<unknown>>;
7
+ /** Wait until the client has sent at least `count` frames. */
8
+ readonly awaitSent: (count: number) => Effect.Effect<void>;
9
+ /** Wait until the client has subscribed and the greeting has landed. */
10
+ readonly greeted: Effect.Effect<void>;
7
11
  /** Deliver one frame to the client. */
8
12
  readonly push: (frame: unknown) => Effect.Effect<void>;
9
13
  /** Close the socket. 1000, 1001 and 1005 are the clean codes. */
@@ -1 +1 @@
1
- {"version":3,"file":"FakeWebSocket.d.mts","names":[],"sources":["../../src/testing/FakeWebSocket.ts"],"mappings":";;;KAeY;;WAED,MAAM,OAAO,OAAO;;WAEpB,OAAO,mBAAmB,OAAO;;WAEjC,QAAQ,kBAAkB,OAAO;;WAEjC,SAAS,OAAO;;WAEhB,OAAO,MAAM,MAAM,OAAO;;KAGzB;;WAED,WAAW;;WAEX,SAAS,mBAAmB,OAAO,OAAO;;cAWxC,OAAI,UACL,yBACT,OAAO,OAAO,4BAA4B,MAAM"}
1
+ {"version":3,"file":"FakeWebSocket.d.mts","names":[],"sources":["../../src/testing/FakeWebSocket.ts"],"mappings":";;;KAeY;;WAED,MAAM,OAAO,OAAO;;WAEpB,YAAY,kBAAkB,OAAO;;WAErC,SAAS,OAAO;;WAEhB,OAAO,mBAAmB,OAAO;;WAEjC,QAAQ,kBAAkB,OAAO;;WAEjC,SAAS,OAAO;;WAEhB,OAAO,MAAM,MAAM,OAAO;;KAGzB;;WAED,WAAW;;WAEX,SAAS,mBAAmB,OAAO,OAAO;;cAWxC,OAAI,UACL,yBACT,OAAO,OAAO,4BAA4B,MAAM"}
@@ -1,5 +1,5 @@
1
- import { Effect, Layer, Queue, Ref, Stream } from "effect";
2
- import * as Socket from "effect/unstable/socket/Socket";
1
+ import { Deferred, Effect, Layer, Queue, Stream, SubscriptionRef } from "effect";
2
+ import * as Socket from "effect/socket/Socket";
3
3
  //#region src/testing/FakeWebSocket.ts
4
4
  /**
5
5
  * An in-memory WebSocket for testing socket adapters without a server.
@@ -19,7 +19,8 @@ const CLOSED = 3;
19
19
  const soon = (run) => void Promise.resolve().then(run);
20
20
  const make = (options) => Effect.gen(function* () {
21
21
  const inbound = yield* Queue.unbounded();
22
- const sent = yield* Ref.make([]);
22
+ const sent = yield* SubscriptionRef.make([]);
23
+ const greeted = yield* Deferred.make();
23
24
  const wiring = {
24
25
  listeners: {},
25
26
  readyState: OPEN,
@@ -37,7 +38,7 @@ const make = (options) => Effect.gen(function* () {
37
38
  });
38
39
  };
39
40
  yield* Stream.fromQueue(inbound).pipe(Stream.runForEach((frame) => Effect.gen(function* () {
40
- yield* Ref.update(sent, (frames) => [...frames, frame]);
41
+ yield* SubscriptionRef.update(sent, (frames) => [...frames, frame]);
41
42
  const answer = yield* options?.reply?.(frame) ?? Effect.succeed([]);
42
43
  yield* deliver(answer);
43
44
  })), Effect.forkScoped);
@@ -52,7 +53,10 @@ const make = (options) => Effect.gen(function* () {
52
53
  };
53
54
  if (type === "message" && !wiring.greeted) {
54
55
  wiring.greeted = true;
55
- soon(() => deliverSync(options?.greeting ?? []));
56
+ soon(() => {
57
+ deliverSync(options?.greeting ?? []);
58
+ Deferred.doneUnsafe(greeted, Effect.void);
59
+ });
56
60
  }
57
61
  },
58
62
  removeEventListener: (type, fn) => {
@@ -66,7 +70,9 @@ const make = (options) => Effect.gen(function* () {
66
70
  };
67
71
  const connect = () => socket;
68
72
  return {
69
- sent: Ref.get(sent),
73
+ sent: SubscriptionRef.get(sent),
74
+ awaitSent: (count) => SubscriptionRef.changes(sent).pipe(Stream.filter((frames) => frames.length >= count), Stream.take(1), Stream.runDrain),
75
+ greeted: Deferred.await(greeted),
70
76
  push: (frame) => deliver([frame]),
71
77
  close: (code = 1e3) => Effect.sync(() => shutdown(code)),
72
78
  connect,
@@ -1 +1 @@
1
- {"version":3,"file":"FakeWebSocket.mjs","names":[],"sources":["../../src/testing/FakeWebSocket.ts"],"sourcesContent":["/**\n * An in-memory WebSocket for testing socket adapters without a server.\n *\n * Provide `layer` in place of the real `Socket.WebSocketConstructor` and the\n * adapter runs end to end: its own connect, handshake, decode and teardown.\n * The test plays the other end with `reply`, `push`, `close` and `sent`.\n *\n * Client frames land on a queue, so the scripted server is a forked fiber and\n * `reply` is a real Effect. What stays imperative is the listener table the\n * shim registers, since `Socket.makeWebSocket` drives it with plain callbacks.\n * The shim is a plain object, so nothing here touches a platform global.\n */\nimport { Effect, Layer, Queue, Ref, type Scope, Stream } from \"effect\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\n\nexport type FakeWebSocketServer = {\n /** Every frame the client has sent, JSON-parsed, oldest first. */\n readonly sent: Effect.Effect<ReadonlyArray<unknown>>\n /** Deliver one frame to the client. */\n readonly push: (frame: unknown) => Effect.Effect<void>\n /** Close the socket. 1000, 1001 and 1005 are the clean codes. */\n readonly close: (code?: number) => Effect.Effect<void>\n /** Hand this to an adapter that takes a socket constructor directly. */\n readonly connect: Socket.WebSocketConstructor[\"Service\"]\n /** Provide this in place of the real constructor. */\n readonly layer: Layer.Layer<Socket.WebSocketConstructor>\n}\n\nexport type FakeWebSocketOptions = {\n /** Pushed once the client subscribes, for a protocol whose server speaks first. */\n readonly greeting?: ReadonlyArray<unknown>\n /** How the server answers each client frame. */\n readonly reply?: (frame: unknown) => Effect.Effect<ReadonlyArray<unknown>>\n}\n\ntype Listener = (event: any) => void\n\nconst OPEN = 1\nconst CLOSED = 3\n\n/** Run after the current call unwinds, so the client is never re-entered. */\nconst soon = (run: () => void): void => void Promise.resolve().then(run)\n\nexport const make = (\n options?: FakeWebSocketOptions,\n): Effect.Effect<FakeWebSocketServer, never, Scope.Scope> =>\n Effect.gen(function* () {\n const inbound = yield* Queue.unbounded<unknown>()\n const sent = yield* Ref.make<ReadonlyArray<unknown>>([])\n\n const wiring = {\n listeners: {} as Record<string, ReadonlyArray<Listener>>,\n readyState: OPEN,\n greeted: false,\n }\n\n const emit = (type: string, event: unknown): void =>\n (wiring.listeners[type] ?? []).forEach((listener) => listener(event))\n\n const deliverSync = (frames: ReadonlyArray<unknown>): void =>\n frames.forEach((f) => emit(\"message\", { data: JSON.stringify(f) }))\n\n const deliver = (frames: ReadonlyArray<unknown>): Effect.Effect<void> =>\n Effect.sync(() => deliverSync(frames))\n\n const shutdown = (code: number): void => {\n if (wiring.readyState === CLOSED) return\n wiring.readyState = CLOSED\n emit(\"close\", { code, reason: \"\" })\n }\n\n // The scripted server. Answers land on their own turn, like a real one.\n yield* Stream.fromQueue(inbound).pipe(\n Stream.runForEach((frame) =>\n Effect.gen(function* () {\n yield* Ref.update(sent, (frames) => [...frames, frame])\n const answer = yield* options?.reply?.(frame) ?? Effect.succeed([])\n yield* deliver(answer)\n }),\n ),\n Effect.forkScoped,\n )\n\n const socket = {\n get readyState() {\n return wiring.readyState\n },\n addEventListener: (type: string, fn: Listener): void => {\n wiring.listeners = {\n ...wiring.listeners,\n [type]: [...(wiring.listeners[type] ?? []), fn],\n }\n // The greeting waits for a subscriber before it lands.\n if (type === \"message\" && !wiring.greeted) {\n wiring.greeted = true\n soon(() => deliverSync(options?.greeting ?? []))\n }\n },\n removeEventListener: (type: string, fn: Listener): void => {\n wiring.listeners = {\n ...wiring.listeners,\n [type]: (wiring.listeners[type] ?? []).filter((l) => l !== fn),\n }\n },\n // Safe from a synchronous callback, and no runtime needed.\n send: (data: string): void => void Queue.offerUnsafe(inbound, JSON.parse(data)),\n close: (code = 1000): void => shutdown(code),\n }\n\n const connect: Socket.WebSocketConstructor[\"Service\"] = () =>\n socket as unknown as globalThis.WebSocket\n\n return {\n sent: Ref.get(sent),\n push: (frame) => deliver([frame]),\n close: (code = 1000) => Effect.sync(() => shutdown(code)),\n connect,\n layer: Layer.succeed(Socket.WebSocketConstructor, connect),\n }\n })\n"],"mappings":";;;;;;;;;;;;;;;AAqCA,MAAM,OAAO;AACb,MAAM,SAAS;;AAGf,MAAM,QAAQ,QAA0B,KAAK,QAAQ,QAAQ,CAAC,CAAC,KAAK,GAAG;AAEvE,MAAa,QACX,YAEA,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,OAAO,MAAM,UAAmB;CAChD,MAAM,OAAO,OAAO,IAAI,KAA6B,CAAC,CAAC;CAEvD,MAAM,SAAS;EACb,WAAW,CAAC;EACZ,YAAY;EACZ,SAAS;CACX;CAEA,MAAM,QAAQ,MAAc,WACzB,OAAO,UAAU,SAAS,CAAC,EAAA,CAAG,SAAS,aAAa,SAAS,KAAK,CAAC;CAEtE,MAAM,eAAe,WACnB,OAAO,SAAS,MAAM,KAAK,WAAW,EAAE,MAAM,KAAK,UAAU,CAAC,EAAE,CAAC,CAAC;CAEpE,MAAM,WAAW,WACf,OAAO,WAAW,YAAY,MAAM,CAAC;CAEvC,MAAM,YAAY,SAAuB;EACvC,IAAI,OAAO,eAAe,QAAQ;EAClC,OAAO,aAAa;EACpB,KAAK,SAAS;GAAE;GAAM,QAAQ;EAAG,CAAC;CACpC;CAGA,OAAO,OAAO,UAAU,OAAO,CAAC,CAAC,KAC/B,OAAO,YAAY,UACjB,OAAO,IAAI,aAAa;EACtB,OAAO,IAAI,OAAO,OAAO,WAAW,CAAC,GAAG,QAAQ,KAAK,CAAC;EACtD,MAAM,SAAS,OAAO,SAAS,QAAQ,KAAK,KAAK,OAAO,QAAQ,CAAC,CAAC;EAClE,OAAO,QAAQ,MAAM;CACvB,CAAC,CACH,GACA,OAAO,UACT;CAEA,MAAM,SAAS;EACb,IAAI,aAAa;GACf,OAAO,OAAO;EAChB;EACA,mBAAmB,MAAc,OAAuB;GACtD,OAAO,YAAY;IACjB,GAAG,OAAO;KACT,OAAO,CAAC,GAAI,OAAO,UAAU,SAAS,CAAC,GAAI,EAAE;GAChD;GAEA,IAAI,SAAS,aAAa,CAAC,OAAO,SAAS;IACzC,OAAO,UAAU;IACjB,WAAW,YAAY,SAAS,YAAY,CAAC,CAAC,CAAC;GACjD;EACF;EACA,sBAAsB,MAAc,OAAuB;GACzD,OAAO,YAAY;IACjB,GAAG,OAAO;KACT,QAAQ,OAAO,UAAU,SAAS,CAAC,EAAA,CAAG,QAAQ,MAAM,MAAM,EAAE;GAC/D;EACF;EAEA,OAAO,SAAuB,KAAK,MAAM,YAAY,SAAS,KAAK,MAAM,IAAI,CAAC;EAC9E,QAAQ,OAAO,QAAe,SAAS,IAAI;CAC7C;CAEA,MAAM,gBACJ;CAEF,OAAO;EACL,MAAM,IAAI,IAAI,IAAI;EAClB,OAAO,UAAU,QAAQ,CAAC,KAAK,CAAC;EAChC,QAAQ,OAAO,QAAS,OAAO,WAAW,SAAS,IAAI,CAAC;EACxD;EACA,OAAO,MAAM,QAAQ,OAAO,sBAAsB,OAAO;CAC3D;AACF,CAAC"}
1
+ {"version":3,"file":"FakeWebSocket.mjs","names":[],"sources":["../../src/testing/FakeWebSocket.ts"],"sourcesContent":["/**\n * An in-memory WebSocket for testing socket adapters without a server.\n *\n * Provide `layer` in place of the real `Socket.WebSocketConstructor` and the\n * adapter runs end to end: its own connect, handshake, decode and teardown.\n * The test plays the other end with `reply`, `push`, `close` and `sent`.\n *\n * Client frames land on a queue, so the scripted server is a forked fiber and\n * `reply` is a real Effect. What stays imperative is the listener table the\n * shim registers, since `Socket.makeWebSocket` drives it with plain callbacks.\n * The shim is a plain object, so nothing here touches a platform global.\n */\nimport { Deferred, Effect, Layer, Queue, type Scope, Stream, SubscriptionRef } from \"effect\"\nimport * as Socket from \"effect/socket/Socket\"\n\nexport type FakeWebSocketServer = {\n /** Every frame the client has sent, JSON-parsed, oldest first. */\n readonly sent: Effect.Effect<ReadonlyArray<unknown>>\n /** Wait until the client has sent at least `count` frames. */\n readonly awaitSent: (count: number) => Effect.Effect<void>\n /** Wait until the client has subscribed and the greeting has landed. */\n readonly greeted: Effect.Effect<void>\n /** Deliver one frame to the client. */\n readonly push: (frame: unknown) => Effect.Effect<void>\n /** Close the socket. 1000, 1001 and 1005 are the clean codes. */\n readonly close: (code?: number) => Effect.Effect<void>\n /** Hand this to an adapter that takes a socket constructor directly. */\n readonly connect: Socket.WebSocketConstructor[\"Service\"]\n /** Provide this in place of the real constructor. */\n readonly layer: Layer.Layer<Socket.WebSocketConstructor>\n}\n\nexport type FakeWebSocketOptions = {\n /** Pushed once the client subscribes, for a protocol whose server speaks first. */\n readonly greeting?: ReadonlyArray<unknown>\n /** How the server answers each client frame. */\n readonly reply?: (frame: unknown) => Effect.Effect<ReadonlyArray<unknown>>\n}\n\ntype Listener = (event: any) => void\n\nconst OPEN = 1\nconst CLOSED = 3\n\n/** Run after the current call unwinds, so the client is never re-entered. */\nconst soon = (run: () => void): void => void Promise.resolve().then(run)\n\nexport const make = (\n options?: FakeWebSocketOptions,\n): Effect.Effect<FakeWebSocketServer, never, Scope.Scope> =>\n Effect.gen(function* () {\n const inbound = yield* Queue.unbounded<unknown>()\n const sent = yield* SubscriptionRef.make<ReadonlyArray<unknown>>([])\n const greeted = yield* Deferred.make<void>()\n\n const wiring = {\n listeners: {} as Record<string, ReadonlyArray<Listener>>,\n readyState: OPEN,\n greeted: false,\n }\n\n const emit = (type: string, event: unknown): void =>\n (wiring.listeners[type] ?? []).forEach((listener) => listener(event))\n\n const deliverSync = (frames: ReadonlyArray<unknown>): void =>\n frames.forEach((f) => emit(\"message\", { data: JSON.stringify(f) }))\n\n const deliver = (frames: ReadonlyArray<unknown>): Effect.Effect<void> =>\n Effect.sync(() => deliverSync(frames))\n\n const shutdown = (code: number): void => {\n if (wiring.readyState === CLOSED) return\n wiring.readyState = CLOSED\n emit(\"close\", { code, reason: \"\" })\n }\n\n // The scripted server. Answers land on their own turn, like a real one.\n yield* Stream.fromQueue(inbound).pipe(\n Stream.runForEach((frame) =>\n Effect.gen(function* () {\n yield* SubscriptionRef.update(sent, (frames) => [...frames, frame])\n const answer = yield* options?.reply?.(frame) ?? Effect.succeed([])\n yield* deliver(answer)\n }),\n ),\n Effect.forkScoped,\n )\n\n const socket = {\n get readyState() {\n return wiring.readyState\n },\n addEventListener: (type: string, fn: Listener): void => {\n wiring.listeners = {\n ...wiring.listeners,\n [type]: [...(wiring.listeners[type] ?? []), fn],\n }\n // The greeting waits for a subscriber before it lands.\n if (type === \"message\" && !wiring.greeted) {\n wiring.greeted = true\n soon(() => {\n deliverSync(options?.greeting ?? [])\n Deferred.doneUnsafe(greeted, Effect.void)\n })\n }\n },\n removeEventListener: (type: string, fn: Listener): void => {\n wiring.listeners = {\n ...wiring.listeners,\n [type]: (wiring.listeners[type] ?? []).filter((l) => l !== fn),\n }\n },\n // Safe from a synchronous callback, and no runtime needed.\n send: (data: string): void => void Queue.offerUnsafe(inbound, JSON.parse(data)),\n close: (code = 1000): void => shutdown(code),\n }\n\n const connect: Socket.WebSocketConstructor[\"Service\"] = () =>\n socket as unknown as globalThis.WebSocket\n\n return {\n sent: SubscriptionRef.get(sent),\n awaitSent: (count) =>\n SubscriptionRef.changes(sent).pipe(\n Stream.filter((frames) => frames.length >= count),\n Stream.take(1),\n Stream.runDrain,\n ),\n greeted: Deferred.await(greeted),\n push: (frame) => deliver([frame]),\n close: (code = 1000) => Effect.sync(() => shutdown(code)),\n connect,\n layer: Layer.succeed(Socket.WebSocketConstructor, connect),\n }\n })\n"],"mappings":";;;;;;;;;;;;;;;AAyCA,MAAM,OAAO;AACb,MAAM,SAAS;;AAGf,MAAM,QAAQ,QAA0B,KAAK,QAAQ,QAAQ,CAAC,CAAC,KAAK,GAAG;AAEvE,MAAa,QACX,YAEA,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,OAAO,MAAM,UAAmB;CAChD,MAAM,OAAO,OAAO,gBAAgB,KAA6B,CAAC,CAAC;CACnE,MAAM,UAAU,OAAO,SAAS,KAAW;CAE3C,MAAM,SAAS;EACb,WAAW,CAAC;EACZ,YAAY;EACZ,SAAS;CACX;CAEA,MAAM,QAAQ,MAAc,WACzB,OAAO,UAAU,SAAS,CAAC,EAAA,CAAG,SAAS,aAAa,SAAS,KAAK,CAAC;CAEtE,MAAM,eAAe,WACnB,OAAO,SAAS,MAAM,KAAK,WAAW,EAAE,MAAM,KAAK,UAAU,CAAC,EAAE,CAAC,CAAC;CAEpE,MAAM,WAAW,WACf,OAAO,WAAW,YAAY,MAAM,CAAC;CAEvC,MAAM,YAAY,SAAuB;EACvC,IAAI,OAAO,eAAe,QAAQ;EAClC,OAAO,aAAa;EACpB,KAAK,SAAS;GAAE;GAAM,QAAQ;EAAG,CAAC;CACpC;CAGA,OAAO,OAAO,UAAU,OAAO,CAAC,CAAC,KAC/B,OAAO,YAAY,UACjB,OAAO,IAAI,aAAa;EACtB,OAAO,gBAAgB,OAAO,OAAO,WAAW,CAAC,GAAG,QAAQ,KAAK,CAAC;EAClE,MAAM,SAAS,OAAO,SAAS,QAAQ,KAAK,KAAK,OAAO,QAAQ,CAAC,CAAC;EAClE,OAAO,QAAQ,MAAM;CACvB,CAAC,CACH,GACA,OAAO,UACT;CAEA,MAAM,SAAS;EACb,IAAI,aAAa;GACf,OAAO,OAAO;EAChB;EACA,mBAAmB,MAAc,OAAuB;GACtD,OAAO,YAAY;IACjB,GAAG,OAAO;KACT,OAAO,CAAC,GAAI,OAAO,UAAU,SAAS,CAAC,GAAI,EAAE;GAChD;GAEA,IAAI,SAAS,aAAa,CAAC,OAAO,SAAS;IACzC,OAAO,UAAU;IACjB,WAAW;KACT,YAAY,SAAS,YAAY,CAAC,CAAC;KACnC,SAAS,WAAW,SAAS,OAAO,IAAI;IAC1C,CAAC;GACH;EACF;EACA,sBAAsB,MAAc,OAAuB;GACzD,OAAO,YAAY;IACjB,GAAG,OAAO;KACT,QAAQ,OAAO,UAAU,SAAS,CAAC,EAAA,CAAG,QAAQ,MAAM,MAAM,EAAE;GAC/D;EACF;EAEA,OAAO,SAAuB,KAAK,MAAM,YAAY,SAAS,KAAK,MAAM,IAAI,CAAC;EAC9E,QAAQ,OAAO,QAAe,SAAS,IAAI;CAC7C;CAEA,MAAM,gBACJ;CAEF,OAAO;EACL,MAAM,gBAAgB,IAAI,IAAI;EAC9B,YAAY,UACV,gBAAgB,QAAQ,IAAI,CAAC,CAAC,KAC5B,OAAO,QAAQ,WAAW,OAAO,UAAU,KAAK,GAChD,OAAO,KAAK,CAAC,GACb,OAAO,QACT;EACF,SAAS,SAAS,MAAM,OAAO;EAC/B,OAAO,UAAU,QAAQ,CAAC,KAAK,CAAC;EAChC,QAAQ,OAAO,QAAS,OAAO,WAAW,SAAS,IAAI,CAAC;EACxD;EACA,OAAO,MAAM,QAAQ,OAAO,sBAAsB,OAAO;CAC3D;AACF,CAAC"}
@@ -5,8 +5,8 @@ import { ToolEvent, isApprovalRequested, isOutput, isProgress } from "./ToolEven
5
5
  import { make as make$1, run } from "./Toolkit.mjs";
6
6
  import { fromMap, fromQueue } from "./Approval.mjs";
7
7
  import { cancelAllPending, findUnansweredCalls, isReconciled } from "./HistoryCheck.mjs";
8
- import { m as it, o as describe } from "../utils-CiH_DjFW.mjs";
9
- import { n as globalExpect } from "../test.DNmyFkvJ-YPzDR0Co.mjs";
8
+ import { b as it, m as describe } from "../dist-DzeSyeYF.mjs";
9
+ import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
10
10
  import { Effect, Queue, Schema, Stream } from "effect";
11
11
  //#region src/tool/Approval.test.ts
12
12
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"Tool.mjs","names":[],"sources":["../../src/tool/Tool.ts"],"sourcesContent":["import type { StandardJSONSchemaV1, StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { Effect, Match, Schema } from \"effect\"\nimport type { ToolCall, ToolCallOutput } from \"../domain/Items.js\"\nimport { toolCallOutput } from \"../domain/Items.js\"\nimport { serializeValue } from \"./ToolResult.js\"\n\nexport class ToolError extends Schema.TaggedError<ToolError>(\"@betalyra/effect-uai/ToolError\")(\n \"ToolError\",\n {\n call_id: Schema.String,\n tool: Schema.String,\n message: Schema.String,\n cause: Schema.optional(Schema.Unknown),\n },\n) {}\n\n/**\n * One Standard Schema validation issue, carried on `ToolValidationError`.\n * Mirrors `StandardSchemaV1.Issue`; `path` is left as `unknown` elements\n * because Standard Schema path segments may be symbols or `{ key }` objects.\n */\nconst ToolValidationIssue = Schema.Struct({\n message: Schema.String,\n path: Schema.optional(Schema.Array(Schema.Unknown)),\n})\n\n/**\n * The model's `arguments` for a call failed the tool's `inputSchema`. Distinct\n * from `ToolError` (malformed JSON, runtime crash) so callers can tell a\n * contract violation apart from a parse or execution failure, and so the\n * structured `issues` survive instead of being flattened into a message string.\n */\nexport class ToolValidationError extends Schema.TaggedError<ToolValidationError>(\n \"@betalyra/effect-uai/ToolValidationError\",\n)(\"ToolValidationError\", {\n call_id: Schema.String,\n tool: Schema.String,\n issues: Schema.Array(ToolValidationIssue),\n}) {}\n\n/**\n * The deliberate \"speak to the model\" failure sentinel. A tool's `run` that\n * fails with a `ToolFailed` (or a bare `string`, its sugar) is absorbed by\n * `Toolkit.run` into a `ToolResult.Failure` the model reads and adapts to,\n * instead of ending the run. Every other error type propagates typed on the\n * executor's stream. `kind` overrides the synthesized `Failure.kind`\n * (default `\"tool_failed\"`) so a tool can hand the model a distinguishable\n * category (`\"not_found\"`, `\"rate_limited\"`) without a new result variant.\n */\nexport class ToolFailed extends Schema.TaggedError<ToolFailed>(\"@betalyra/effect-uai/ToolFailed\")(\n \"ToolFailed\",\n {\n message: Schema.String,\n kind: Schema.optional(Schema.String),\n },\n) {}\n\n/**\n * Fail a tool's `run` with a model-visible message (optionally a `kind`).\n * `yield* Tool.fail(\"page not found\", { kind: \"not_found\" })` inside a\n * `run`'s `Effect.gen`; the executor absorbs it as `ToolResult.Failure`.\n */\nexport const fail = (\n message: string,\n options?: { readonly kind?: string },\n): Effect.Effect<never, ToolFailed> =>\n Effect.fail(\n new ToolFailed({ message, ...(options?.kind !== undefined ? { kind: options.kind } : {}) }),\n )\n\n/**\n * Schemas accepted on `Tool.inputSchema`. Must implement both Standard\n * Schema validation and JSON Schema conversion (for rendering tool\n * descriptors to provider request bodies).\n *\n * Any Standard-Schema-compliant library that exposes both interfaces\n * works directly: Zod 4+, Valibot, ArkType, Effect Schema (after\n * `fromEffectSchema`), etc.\n */\nexport type ToolInputSchema<Input = unknown> = StandardSchemaV1<unknown, Input> &\n StandardJSONSchemaV1<unknown, Input>\n\n/**\n * Convenience wrapper for Effect Schema users - adds both the\n * `validate` and `jsonSchema` extensions to a plain Effect Schema so it\n * can be used as a `Tool.inputSchema`.\n */\nexport const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(\n schema: S,\n): S & ToolInputSchema<S[\"Type\"]> =>\n Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema))\n\n/**\n * Use any schema library that implements both Standard Schema (validation)\n * and Standard JSON Schema (JSON Schema generation) as a `Tool.inputSchema`.\n * Covers Zod 4.2+, Valibot 1.2+, and ArkType 2.1.28+ in one helper.\n *\n * Effect Schema doesn't implement Standard JSON Schema natively — use\n * `fromEffectSchema` for those.\n *\n * The intersection constraint catches missing interfaces at compile time:\n * a Zod v3 schema (no Standard JSON Schema) produces a precise type error\n * pointing at the missing interface rather than a runtime surprise. The\n * helper itself is a thin type-narrowing identity — schemas that satisfy\n * both standards already structurally satisfy `ToolInputSchema`; the\n * helper makes the input type inference explicit at the call site.\n */\nexport const fromStandardSchema = <S extends StandardSchemaV1 & StandardJSONSchemaV1>(\n schema: S,\n): S & ToolInputSchema<StandardSchemaV1.InferOutput<S>> =>\n schema as S & ToolInputSchema<StandardSchemaV1.InferOutput<S>>\n\n/**\n * Input schema for a tool that takes no arguments. Renders a clean\n * `{ type: \"object\", properties: {} }`; validation accepts anything and\n * yields `{}`. Use it instead of `fromEffectSchema(Schema.Struct({}))`,\n * which renders a degenerate `anyOf` (object-or-array) that strict\n * providers like Gemini reject.\n */\nexport const noInput: ToolInputSchema<Record<string, never>> = {\n \"~standard\": {\n version: 1,\n vendor: \"effect-uai\",\n validate: () => ({ value: {} }),\n jsonSchema: {\n input: () => ({ type: \"object\", properties: {} }),\n output: () => ({ type: \"object\", properties: {} }),\n },\n },\n}\n\n/**\n * Emit one intermediate `Event` from inside a tool's `run`. Backed by the\n * executor's per-call queue, so it returns an `Effect` (backpressure) and\n * composes with `Stream.runForEach(emit)`.\n */\nexport type Emit<Event> = (event: Event) => Effect.Effect<void>\n\n/**\n * A model-visible tool with a local `Effect` executor. `run` computes the\n * model-facing `Output`; emitting progress is a side channel via `emit`. A\n * plain tool never calls `emit` (its `Event` is irrelevant); a streaming tool\n * emits intermediate `Event`s that flow to the consumer as\n * `ToolEvent.Progress` in real time.\n *\n * This is one of four tool *kinds*, discriminated by `_tag`. The other three\n * (`ProviderTool`, `SignalTool`, `InteractionTool`) are model-visible but have\n * no local executor — see their constructors below. `Tool` keeps its name (and\n * the `Tool.make` constructor) because the local kind is by far the common one.\n */\nexport type Tool<Name extends string, Input, Event, Output, E = unknown, R = never> = {\n readonly _tag: \"LocalTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n readonly run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output, E, R>\n /**\n * Bound on this tool's emit queue. Unbounded when omitted. Per-tool because\n * backpressure depends on how a given tool emits, unlike `concurrency`\n * (cross-tool, on `Toolkit.run`'s options).\n */\n readonly emitBufferSize?: number\n /**\n * Whether the provider should render this tool with its strict-mode\n * flag (OpenAI's `strict: true`, etc). Default: true. The framework\n * never rewrites the schema; if the rendered JSON Schema isn't\n * compatible, the provider returns an error.\n */\n readonly strict?: boolean\n}\n\n/**\n * A provider-hosted tool: visible to the model and executed by the *provider*\n * (native web search, code execution, RAG grounding), never by this process.\n * It has no local `run`; `provider`/`config` tell the provider adapter how to\n * render it. Passing it to `Toolkit.run` yields a `non_local_tool` result.\n */\nexport type ProviderTool<Name extends string, Input, Provider extends string, Config> = {\n readonly _tag: \"ProviderTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n readonly provider: Provider\n readonly config: Config\n readonly strict?: boolean\n}\n\n/**\n * A typed request for the agent loop to change control flow (escalate, pause,\n * schedule, hand off). Model-visible and decodable (`decodeArgs`) but never\n * locally executed — the loop intercepts the call in `onTurnComplete` and acts\n * on it, so there is no fake `run`. `Toolkit.run` reports it as `non_local_tool`.\n */\nexport type SignalTool<Name extends string, Input> = {\n readonly _tag: \"SignalTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/**\n * A typed request for an external actor (user, channel, frontend) to respond\n * before the loop resumes. Like `SignalTool` it is decode-only, but its\n * lifecycle differs: the loop usually stops/pauses and resumes later by\n * appending a `function_call_output` once the actor replies.\n */\nexport type InteractionTool<Name extends string, Input> = {\n readonly _tag: \"InteractionTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/** A local (executable) tool of any shape. Readability alias. */\nexport type AnyLocalTool<E = any, R = any> = Tool<string, any, any, any, E, R>\n\n/** Any of the four model-visible tool kinds. */\nexport type AnyTool<E = any, R = any> =\n | Tool<string, any, any, any, E, R>\n | ProviderTool<string, any, string, any>\n | SignalTool<string, any>\n | InteractionTool<string, any>\n\n/** A model-visible tool that carries a decodable input schema (every kind). */\nexport type DecodableTool<Input> = {\n readonly name: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/** Extract the `R` of a local tool; non-local kinds contribute `never`. */\nexport type ToolR<T> = T extends Tool<string, any, any, any, any, infer R> ? R : never\n\n/** Extract the `E` of a local tool; non-local kinds contribute `never`. */\nexport type ToolE<T> = T extends Tool<string, any, any, any, infer E, any> ? E : never\n\n/**\n * Provider-agnostic tool descriptor. Each provider maps `inputSchema`\n * to its own wire field (OpenAI → `parameters`, Anthropic →\n * `input_schema`). Built from a `Tool` by `Tool.toDescriptors`.\n */\nexport type ToolDescriptor = {\n readonly name: string\n readonly description: string\n readonly inputSchema: Record<string, unknown>\n readonly strict?: boolean\n}\n\nconst resolveInputSchema = (\n schema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>,\n): ToolInputSchema<any> =>\n Schema.isSchema(schema)\n ? fromEffectSchema(schema as Schema.Codec<any, any, never, any>)\n : (schema as ToolInputSchema<any>)\n\n/** A model-visible tool with a local `Effect` executor. */\nexport const make: {\n <\n Name extends string,\n S extends Schema.Codec<any, any, never, any>,\n Event,\n Output,\n E = never,\n R = never,\n >(\n spec: Omit<Tool<Name, S[\"Type\"], Event, Output, E, R>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): Tool<Name, S[\"Type\"], Event, Output, E, R>\n <Name extends string, Input, Event, Output, E = never, R = never>(\n spec: Omit<Tool<Name, Input, Event, Output, E, R>, \"_tag\">,\n ): Tool<Name, Input, Event, Output, E, R>\n} = (\n spec: Omit<AnyLocalTool, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): AnyLocalTool => ({\n _tag: \"LocalTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A provider-hosted tool (native web search, code execution, RAG grounding).\n * No local executor — `provider`/`config` drive the provider adapter's\n * rendering. The model sees it; `Toolkit.run` reports it as `non_local_tool`.\n */\nexport const provider: {\n <\n Name extends string,\n S extends Schema.Codec<any, any, never, any>,\n Provider extends string,\n Config,\n >(\n spec: Omit<ProviderTool<Name, S[\"Type\"], Provider, Config>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): ProviderTool<Name, S[\"Type\"], Provider, Config>\n <Name extends string, Input, Provider extends string, Config>(\n spec: Omit<ProviderTool<Name, Input, Provider, Config>, \"_tag\">,\n ): ProviderTool<Name, Input, Provider, Config>\n} = (\n spec: Omit<ProviderTool<string, any, string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): ProviderTool<string, any, string, any> => ({\n _tag: \"ProviderTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A typed control-flow signal to the agent loop (escalate, pause, schedule,\n * hand off). Decode-only: the loop interprets the call rather than executing a\n * handler, so there is no `run`.\n */\nexport const signal: {\n <Name extends string, S extends Schema.Codec<any, any, never, any>>(\n spec: Omit<SignalTool<Name, S[\"Type\"]>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): SignalTool<Name, S[\"Type\"]>\n <Name extends string, Input>(spec: Omit<SignalTool<Name, Input>, \"_tag\">): SignalTool<Name, Input>\n} = (\n spec: Omit<SignalTool<string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): SignalTool<string, any> => ({\n _tag: \"SignalTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A typed request for an external actor to respond before the loop resumes\n * (frontend, Telegram, CLI prompt). Decode-only; the loop stops/pauses and\n * resumes by appending the actor's `function_call_output`.\n */\nexport const interaction: {\n <Name extends string, S extends Schema.Codec<any, any, never, any>>(\n spec: Omit<InteractionTool<Name, S[\"Type\"]>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): InteractionTool<Name, S[\"Type\"]>\n <Name extends string, Input>(\n spec: Omit<InteractionTool<Name, Input>, \"_tag\">,\n ): InteractionTool<Name, Input>\n} = (\n spec: Omit<InteractionTool<string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): InteractionTool<string, any> => ({\n _tag: \"InteractionTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * Return a copy of a tool under a new `name`. Useful for resolving a name\n * clash before combining tools into one `Toolkit`. Literal-preserving, so the\n * renamed tool keys the toolkit record under the new name statically.\n */\nexport const withName = <N extends string, T extends AnyTool>(\n tool: T,\n name: N,\n): Omit<T, \"name\"> & { readonly name: N } => ({ ...tool, name })\n\n/**\n * Replace a local tool's `run`, keeping its model-facing definition (name,\n * description, schema, `_tag`) identical. The new `run`'s `input` is typed from\n * the original tool, so override/mock need no annotation:\n *\n * const safe = { ...toolkit, send_email: Tool.withRun(toolkit.send_email,\n * ({ to }) => Effect.succeed({ status: \"dry-run\", to })) }\n *\n * Descriptors stay identical to production, so a mocked toolkit can't drift\n * from the contract the model sees.\n */\nexport const withRun = <\n Name extends string,\n Input,\n Event,\n Output,\n E,\n R,\n Output2,\n E2 = never,\n R2 = never,\n>(\n tool: Tool<Name, Input, Event, Output, E, R>,\n run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output2, E2, R2>,\n): Tool<Name, Input, Event, Output2, E2, R2> => ({ ...tool, run })\n\n/** True for provider-hosted tools — the kind a provider renders natively. */\nexport const isProviderTool = (tool: AnyTool): tool is ProviderTool<string, any, string, any> =>\n tool._tag === \"ProviderTool\"\n\n/**\n * The `ProviderTool`s in a toolkit, for a provider adapter to render as its\n * native hosted-tool entries (web search, code execution, grounding). The other\n * kinds render as function declarations via `descriptorsOf`.\n */\nexport const providerToolsOf = (\n toolkit?: Readonly<Record<string, AnyTool>>,\n): ReadonlyArray<ProviderTool<string, any, string, any>> =>\n toolkit === undefined ? [] : Object.values(toolkit).filter(isProviderTool)\n\n/**\n * Render tools to provider-agnostic descriptors. `ProviderTool`s are excluded:\n * they are provider-hosted and must never leak into the function-declaration\n * list — each adapter renders them natively via `providerToolsOf`. Every other\n * kind is model-visible as a declaration.\n */\nexport const toDescriptors = (tools: ReadonlyArray<AnyTool>): ReadonlyArray<ToolDescriptor> =>\n tools\n .filter((tool) => !isProviderTool(tool))\n .map((tool) => {\n const inputSchema = tool.inputSchema[\"~standard\"].jsonSchema.input({\n target: \"draft-2020-12\",\n })\n // `strict` only exists on local/provider kinds; signals/interactions omit it.\n const strict = \"strict\" in tool ? tool.strict : undefined\n return strict !== undefined\n ? { name: tool.name, description: tool.description, inputSchema, strict }\n : { name: tool.name, description: tool.description, inputSchema }\n })\n\n/**\n * Render a `Toolkit` (a name-indexed record of tools) to descriptors, treating\n * an absent toolkit as no tools. The normalization point the `LanguageModel`\n * boundary uses so a request can carry the toolkit itself instead of a\n * pre-rendered descriptor array. Lives here (not on `Toolkit`) to keep providers\n * free of a dependency on the toolkit module — it only needs the tool values.\n */\nexport const descriptorsOf = (\n toolkit?: Readonly<Record<string, AnyTool>>,\n): ReadonlyArray<ToolDescriptor> =>\n toolkit === undefined ? [] : toDescriptors(Object.values(toolkit))\n\n/**\n * Outcome of decoding a call's `arguments` against a tool's `inputSchema`,\n * as data so both the typed (`decodeArgs`) and result-synthesizing\n * (`Toolkit.run`) callers can translate it their own way.\n */\nexport type DecodeResult<Input> =\n | { readonly _tag: \"ok\"; readonly input: Input }\n | { readonly _tag: \"parseError\" }\n | { readonly _tag: \"invalid\"; readonly issues: ReadonlyArray<StandardSchemaV1.Issue> }\n\nconst VALIDATION_THREW: ReadonlyArray<StandardSchemaV1.Issue> = [\n { message: \"input schema validation threw\" },\n]\n\n/**\n * Parse and validate a call's `arguments`. Empty arguments normalize to\n * `{}` (some models emit `\"\"` for zero-arg tools); a throwing validator is\n * captured, not propagated. Never fails.\n */\nexport const decodeCallInput = <Input>(\n tool: DecodableTool<Input>,\n call: ToolCall,\n): Effect.Effect<DecodeResult<Input>> => {\n const raw = call.arguments.trim() === \"\" ? \"{}\" : call.arguments\n return Effect.try({\n // @effect-diagnostics-next-line effect/preferSchemaOverJson:off\n try: () => JSON.parse(raw) as unknown,\n catch: (): DecodeResult<Input> => ({ _tag: \"parseError\" }),\n }).pipe(\n Effect.flatMap((parsed) =>\n Effect.tryPromise({\n try: () => Promise.resolve(tool.inputSchema[\"~standard\"].validate(parsed)),\n catch: (): DecodeResult<Input> => ({ _tag: \"invalid\", issues: VALIDATION_THREW }),\n }).pipe(\n Effect.map((result): DecodeResult<Input> =>\n result.issues !== undefined\n ? { _tag: \"invalid\", issues: result.issues }\n : { _tag: \"ok\", input: result.value },\n ),\n ),\n ),\n Effect.catch((captured: DecodeResult<Input>) => Effect.succeed(captured)),\n )\n}\n\n/**\n * Decode and validate a function_call's JSON `arguments` against the tool's\n * own `inputSchema`, yielding the typed input. The decode-only half of\n * `execute` - reach for it when you intercept a call to translate it into a\n * control-flow decision rather than running the tool's `run`.\n *\n * Malformed JSON fails with `ToolError`; a parsed body that violates\n * `inputSchema` fails with `ToolValidationError` carrying the issues.\n */\nexport const decodeArgs = <Input>(\n tool: DecodableTool<Input>,\n call: ToolCall,\n): Effect.Effect<Input, ToolError | ToolValidationError> =>\n decodeCallInput(tool, call).pipe(\n Effect.flatMap(\n Match.type<DecodeResult<Input>>().pipe(\n Match.tag(\"ok\", ({ input }) => Effect.succeed(input)),\n Match.tag(\"parseError\", () =>\n Effect.fail(\n new ToolError({\n call_id: call.call_id,\n tool: tool.name,\n message: \"Failed to parse JSON arguments\",\n }),\n ),\n ),\n Match.tag(\"invalid\", ({ issues }) =>\n Effect.fail(new ToolValidationError({ call_id: call.call_id, tool: tool.name, issues })),\n ),\n Match.exhaustive,\n ),\n ),\n )\n\n/**\n * Decode and validate the JSON arguments of a function_call against the\n * tool's input schema, run the tool, and serialize the output into a\n * function_call_output item. Single-shot: intermediate events emitted by\n * `run` are discarded (use `Toolkit.run` to stream them).\n */\nexport const execute = <Name extends string, Input, Event, Output, E, R>(\n tool: Tool<Name, Input, Event, Output, E, R>,\n call: ToolCall,\n): Effect.Effect<ToolCallOutput, E | ToolError | ToolValidationError, R> =>\n decodeArgs(tool, call).pipe(\n Effect.flatMap((input) => tool.run(input, () => Effect.void)),\n Effect.map((output) => toolCallOutput(call.call_id, serializeValue(output))),\n )\n"],"mappings":"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"}
1
+ {"version":3,"file":"Tool.mjs","names":[],"sources":["../../src/tool/Tool.ts"],"sourcesContent":["import type { StandardJSONSchemaV1, StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { Effect, Match, Schema } from \"effect\"\nimport type { ToolCall, ToolCallOutput } from \"../domain/Items.js\"\nimport { toolCallOutput } from \"../domain/Items.js\"\nimport { serializeValue } from \"./ToolResult.js\"\n\nexport class ToolError extends Schema.TaggedError<ToolError>(\"@betalyra/effect-uai/ToolError\")(\n \"ToolError\",\n {\n call_id: Schema.String,\n tool: Schema.String,\n message: Schema.String,\n cause: Schema.optional(Schema.Unknown),\n },\n) {}\n\n/**\n * One Standard Schema validation issue, carried on `ToolValidationError`.\n * Mirrors `StandardSchemaV1.Issue`; `path` is left as `unknown` elements\n * because Standard Schema path segments may be symbols or `{ key }` objects.\n */\nconst ToolValidationIssue = Schema.Struct({\n message: Schema.String,\n path: Schema.optional(Schema.Array(Schema.Unknown)),\n})\n\n/**\n * The model's `arguments` for a call failed the tool's `inputSchema`. Distinct\n * from `ToolError` (malformed JSON, runtime crash) so callers can tell a\n * contract violation apart from a parse or execution failure, and so the\n * structured `issues` survive instead of being flattened into a message string.\n */\nexport class ToolValidationError extends Schema.TaggedError<ToolValidationError>(\n \"@betalyra/effect-uai/ToolValidationError\",\n)(\"ToolValidationError\", {\n call_id: Schema.String,\n tool: Schema.String,\n issues: Schema.Array(ToolValidationIssue),\n}) {}\n\n/**\n * The deliberate \"speak to the model\" failure sentinel. A tool's `run` that\n * fails with a `ToolFailed` (or a bare `string`, its sugar) is absorbed by\n * `Toolkit.run` into a `ToolResult.Failure` the model reads and adapts to,\n * instead of ending the run. Every other error type propagates typed on the\n * executor's stream. `kind` overrides the synthesized `Failure.kind`\n * (default `\"tool_failed\"`) so a tool can hand the model a distinguishable\n * category (`\"not_found\"`, `\"rate_limited\"`) without a new result variant.\n */\nexport class ToolFailed extends Schema.TaggedError<ToolFailed>(\"@betalyra/effect-uai/ToolFailed\")(\n \"ToolFailed\",\n {\n message: Schema.String,\n kind: Schema.optional(Schema.String),\n },\n) {}\n\n/**\n * Fail a tool's `run` with a model-visible message (optionally a `kind`).\n * `yield* Tool.fail(\"page not found\", { kind: \"not_found\" })` inside a\n * `run`'s `Effect.gen`; the executor absorbs it as `ToolResult.Failure`.\n */\nexport const fail = (\n message: string,\n options?: { readonly kind?: string },\n): Effect.Effect<never, ToolFailed> =>\n Effect.fail(\n new ToolFailed({ message, ...(options?.kind !== undefined ? { kind: options.kind } : {}) }),\n )\n\n/**\n * Schemas accepted on `Tool.inputSchema`. Must implement both Standard\n * Schema validation and JSON Schema conversion (for rendering tool\n * descriptors to provider request bodies).\n *\n * Any Standard-Schema-compliant library that exposes both interfaces\n * works directly: Zod 4+, Valibot, ArkType, Effect Schema (after\n * `fromEffectSchema`), etc.\n */\nexport type ToolInputSchema<Input = unknown> = StandardSchemaV1<unknown, Input> &\n StandardJSONSchemaV1<unknown, Input>\n\n/**\n * Convenience wrapper for Effect Schema users - adds both the\n * `validate` and `jsonSchema` extensions to a plain Effect Schema so it\n * can be used as a `Tool.inputSchema`.\n */\nexport const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(\n schema: S,\n): S & ToolInputSchema<S[\"Type\"]> =>\n Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema))\n\n/**\n * Use any schema library that implements both Standard Schema (validation)\n * and Standard JSON Schema (JSON Schema generation) as a `Tool.inputSchema`.\n * Covers Zod 4.2+, Valibot 1.2+, and ArkType 2.1.28+ in one helper.\n *\n * Effect Schema doesn't implement Standard JSON Schema natively — use\n * `fromEffectSchema` for those.\n *\n * The intersection constraint catches missing interfaces at compile time:\n * a Zod v3 schema (no Standard JSON Schema) produces a precise type error\n * pointing at the missing interface rather than a runtime surprise. The\n * helper itself is a thin type-narrowing identity — schemas that satisfy\n * both standards already structurally satisfy `ToolInputSchema`; the\n * helper makes the input type inference explicit at the call site.\n */\nexport const fromStandardSchema = <S extends StandardSchemaV1 & StandardJSONSchemaV1>(\n schema: S,\n): S & ToolInputSchema<StandardSchemaV1.InferOutput<S>> =>\n schema as S & ToolInputSchema<StandardSchemaV1.InferOutput<S>>\n\n/**\n * Input schema for a tool that takes no arguments. Renders a clean\n * `{ type: \"object\", properties: {} }`; validation accepts anything and\n * yields `{}`. Use it instead of `fromEffectSchema(Schema.Struct({}))`,\n * which renders a degenerate `anyOf` (object-or-array) that strict\n * providers like Gemini reject.\n */\nexport const noInput: ToolInputSchema<Record<string, never>> = {\n \"~standard\": {\n version: 1,\n vendor: \"effect-uai\",\n validate: () => ({ value: {} }),\n jsonSchema: {\n input: () => ({ type: \"object\", properties: {} }),\n output: () => ({ type: \"object\", properties: {} }),\n },\n },\n}\n\n/**\n * Emit one intermediate `Event` from inside a tool's `run`. Backed by the\n * executor's per-call queue, so it returns an `Effect` (backpressure) and\n * composes with `Stream.runForEach(emit)`.\n */\nexport type Emit<Event> = (event: Event) => Effect.Effect<void>\n\n/**\n * A model-visible tool with a local `Effect` executor. `run` computes the\n * model-facing `Output`; emitting progress is a side channel via `emit`. A\n * plain tool never calls `emit` (its `Event` is irrelevant); a streaming tool\n * emits intermediate `Event`s that flow to the consumer as\n * `ToolEvent.Progress` in real time.\n *\n * This is one of four tool *kinds*, discriminated by `_tag`. The other three\n * (`ProviderTool`, `SignalTool`, `InteractionTool`) are model-visible but have\n * no local executor — see their constructors below. `Tool` keeps its name (and\n * the `Tool.make` constructor) because the local kind is by far the common one.\n */\nexport type Tool<Name extends string, Input, Event, Output, E = unknown, R = never> = {\n readonly _tag: \"LocalTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n readonly run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output, E, R>\n /**\n * Bound on this tool's emit queue. Unbounded when omitted. Per-tool because\n * backpressure depends on how a given tool emits, unlike `concurrency`\n * (cross-tool, on `Toolkit.run`'s options).\n */\n readonly emitBufferSize?: number\n /**\n * Whether the provider should render this tool with its strict-mode\n * flag (OpenAI's `strict: true`, etc). Default: true. The framework\n * never rewrites the schema; if the rendered JSON Schema isn't\n * compatible, the provider returns an error.\n */\n readonly strict?: boolean\n}\n\n/**\n * A provider-hosted tool: visible to the model and executed by the *provider*\n * (native web search, code execution, RAG grounding), never by this process.\n * It has no local `run`; `provider`/`config` tell the provider adapter how to\n * render it. Passing it to `Toolkit.run` yields a `non_local_tool` result.\n */\nexport type ProviderTool<Name extends string, Input, Provider extends string, Config> = {\n readonly _tag: \"ProviderTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n readonly provider: Provider\n readonly config: Config\n readonly strict?: boolean\n}\n\n/**\n * A typed request for the agent loop to change control flow (escalate, pause,\n * schedule, hand off). Model-visible and decodable (`decodeArgs`) but never\n * locally executed — the loop intercepts the call in `onTurnComplete` and acts\n * on it, so there is no fake `run`. `Toolkit.run` reports it as `non_local_tool`.\n */\nexport type SignalTool<Name extends string, Input> = {\n readonly _tag: \"SignalTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/**\n * A typed request for an external actor (user, channel, frontend) to respond\n * before the loop resumes. Like `SignalTool` it is decode-only, but its\n * lifecycle differs: the loop usually stops/pauses and resumes later by\n * appending a `function_call_output` once the actor replies.\n */\nexport type InteractionTool<Name extends string, Input> = {\n readonly _tag: \"InteractionTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/** A local (executable) tool of any shape. Readability alias. */\nexport type AnyLocalTool<E = any, R = any> = Tool<string, any, any, any, E, R>\n\n/** Any of the four model-visible tool kinds. */\nexport type AnyTool<E = any, R = any> =\n | Tool<string, any, any, any, E, R>\n | ProviderTool<string, any, string, any>\n | SignalTool<string, any>\n | InteractionTool<string, any>\n\n/** A model-visible tool that carries a decodable input schema (every kind). */\nexport type DecodableTool<Input> = {\n readonly name: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/** Extract the `R` of a local tool; non-local kinds contribute `never`. */\nexport type ToolR<T> = T extends Tool<string, any, any, any, any, infer R> ? R : never\n\n/** Extract the `E` of a local tool; non-local kinds contribute `never`. */\nexport type ToolE<T> = T extends Tool<string, any, any, any, infer E, any> ? E : never\n\n/**\n * Provider-agnostic tool descriptor. Each provider maps `inputSchema`\n * to its own wire field (OpenAI → `parameters`, Anthropic →\n * `input_schema`). Built from a `Tool` by `Tool.toDescriptors`.\n */\nexport type ToolDescriptor = {\n readonly name: string\n readonly description: string\n readonly inputSchema: Record<string, unknown>\n readonly strict?: boolean\n}\n\nconst resolveInputSchema = (\n schema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>,\n): ToolInputSchema<any> =>\n Schema.isSchema(schema)\n ? fromEffectSchema(schema as Schema.Codec<any, any, never, any>)\n : (schema as ToolInputSchema<any>)\n\n/** A model-visible tool with a local `Effect` executor. */\nexport const make: {\n <\n Name extends string,\n S extends Schema.Codec<any, any, never, any>,\n Event,\n Output,\n E = never,\n R = never,\n >(\n spec: Omit<Tool<Name, S[\"Type\"], Event, Output, E, R>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): Tool<Name, S[\"Type\"], Event, Output, E, R>\n <Name extends string, Input, Event, Output, E = never, R = never>(\n spec: Omit<Tool<Name, Input, Event, Output, E, R>, \"_tag\">,\n ): Tool<Name, Input, Event, Output, E, R>\n} = (\n spec: Omit<AnyLocalTool, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): AnyLocalTool => ({\n _tag: \"LocalTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A provider-hosted tool (native web search, code execution, RAG grounding).\n * No local executor — `provider`/`config` drive the provider adapter's\n * rendering. The model sees it; `Toolkit.run` reports it as `non_local_tool`.\n */\nexport const provider: {\n <\n Name extends string,\n S extends Schema.Codec<any, any, never, any>,\n Provider extends string,\n Config,\n >(\n spec: Omit<ProviderTool<Name, S[\"Type\"], Provider, Config>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): ProviderTool<Name, S[\"Type\"], Provider, Config>\n <Name extends string, Input, Provider extends string, Config>(\n spec: Omit<ProviderTool<Name, Input, Provider, Config>, \"_tag\">,\n ): ProviderTool<Name, Input, Provider, Config>\n} = (\n spec: Omit<ProviderTool<string, any, string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): ProviderTool<string, any, string, any> => ({\n _tag: \"ProviderTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A typed control-flow signal to the agent loop (escalate, pause, schedule,\n * hand off). Decode-only: the loop interprets the call rather than executing a\n * handler, so there is no `run`.\n */\nexport const signal: {\n <Name extends string, S extends Schema.Codec<any, any, never, any>>(\n spec: Omit<SignalTool<Name, S[\"Type\"]>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): SignalTool<Name, S[\"Type\"]>\n <Name extends string, Input>(spec: Omit<SignalTool<Name, Input>, \"_tag\">): SignalTool<Name, Input>\n} = (\n spec: Omit<SignalTool<string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): SignalTool<string, any> => ({\n _tag: \"SignalTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A typed request for an external actor to respond before the loop resumes\n * (frontend, Telegram, CLI prompt). Decode-only; the loop stops/pauses and\n * resumes by appending the actor's `function_call_output`.\n */\nexport const interaction: {\n <Name extends string, S extends Schema.Codec<any, any, never, any>>(\n spec: Omit<InteractionTool<Name, S[\"Type\"]>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): InteractionTool<Name, S[\"Type\"]>\n <Name extends string, Input>(\n spec: Omit<InteractionTool<Name, Input>, \"_tag\">,\n ): InteractionTool<Name, Input>\n} = (\n spec: Omit<InteractionTool<string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): InteractionTool<string, any> => ({\n _tag: \"InteractionTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * Return a copy of a tool under a new `name`. Useful for resolving a name\n * clash before combining tools into one `Toolkit`. Literal-preserving, so the\n * renamed tool keys the toolkit record under the new name statically.\n */\nexport const withName = <N extends string, T extends AnyTool>(\n tool: T,\n name: N,\n): Omit<T, \"name\"> & { readonly name: N } => ({ ...tool, name })\n\n/**\n * Replace a local tool's `run`, keeping its model-facing definition (name,\n * description, schema, `_tag`) identical. The new `run`'s `input` is typed from\n * the original tool, so override/mock need no annotation:\n *\n * const safe = { ...toolkit, send_email: Tool.withRun(toolkit.send_email,\n * ({ to }) => Effect.succeed({ status: \"dry-run\", to })) }\n *\n * Descriptors stay identical to production, so a mocked toolkit can't drift\n * from the contract the model sees.\n */\nexport const withRun = <\n Name extends string,\n Input,\n Event,\n Output,\n E,\n R,\n Output2,\n E2 = never,\n R2 = never,\n>(\n tool: Tool<Name, Input, Event, Output, E, R>,\n run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output2, E2, R2>,\n): Tool<Name, Input, Event, Output2, E2, R2> => ({ ...tool, run })\n\n/** True for provider-hosted tools — the kind a provider renders natively. */\nexport const isProviderTool = (tool: AnyTool): tool is ProviderTool<string, any, string, any> =>\n tool._tag === \"ProviderTool\"\n\n/**\n * The `ProviderTool`s in a toolkit, for a provider adapter to render as its\n * native hosted-tool entries (web search, code execution, grounding). The other\n * kinds render as function declarations via `descriptorsOf`.\n */\nexport const providerToolsOf = (\n toolkit?: Readonly<Record<string, AnyTool>>,\n): ReadonlyArray<ProviderTool<string, any, string, any>> =>\n toolkit === undefined ? [] : Object.values(toolkit).filter(isProviderTool)\n\n/**\n * Render tools to provider-agnostic descriptors. `ProviderTool`s are excluded:\n * they are provider-hosted and must never leak into the function-declaration\n * list — each adapter renders them natively via `providerToolsOf`. Every other\n * kind is model-visible as a declaration.\n */\nexport const toDescriptors = (tools: ReadonlyArray<AnyTool>): ReadonlyArray<ToolDescriptor> =>\n tools\n .filter((tool) => !isProviderTool(tool))\n .map((tool) => {\n const inputSchema = tool.inputSchema[\"~standard\"].jsonSchema.input({\n target: \"draft-2020-12\",\n })\n // `strict` only exists on local/provider kinds; signals/interactions omit it.\n const strict = \"strict\" in tool ? tool.strict : undefined\n return strict !== undefined\n ? { name: tool.name, description: tool.description, inputSchema, strict }\n : { name: tool.name, description: tool.description, inputSchema }\n })\n\n/**\n * Render a `Toolkit` (a name-indexed record of tools) to descriptors, treating\n * an absent toolkit as no tools. The normalization point the `LanguageModel`\n * boundary uses so a request can carry the toolkit itself instead of a\n * pre-rendered descriptor array. Lives here (not on `Toolkit`) to keep providers\n * free of a dependency on the toolkit module — it only needs the tool values.\n */\nexport const descriptorsOf = (\n toolkit?: Readonly<Record<string, AnyTool>>,\n): ReadonlyArray<ToolDescriptor> =>\n toolkit === undefined ? [] : toDescriptors(Object.values(toolkit))\n\n/**\n * Outcome of decoding a call's `arguments` against a tool's `inputSchema`,\n * as data so both the typed (`decodeArgs`) and result-synthesizing\n * (`Toolkit.run`) callers can translate it their own way.\n */\nexport type DecodeResult<Input> =\n | { readonly _tag: \"ok\"; readonly input: Input }\n | { readonly _tag: \"parseError\" }\n | { readonly _tag: \"invalid\"; readonly issues: ReadonlyArray<StandardSchemaV1.Issue> }\n\nconst VALIDATION_THREW: ReadonlyArray<StandardSchemaV1.Issue> = [\n { message: \"input schema validation threw\" },\n]\n\n/**\n * Parse and validate a call's `arguments`. Empty arguments normalize to\n * `{}` (some models emit `\"\"` for zero-arg tools); a throwing validator is\n * captured, not propagated. Never fails.\n */\nexport const decodeCallInput = <Input>(\n tool: DecodableTool<Input>,\n call: ToolCall,\n): Effect.Effect<DecodeResult<Input>> => {\n const raw = call.arguments.trim() === \"\" ? \"{}\" : call.arguments\n return Effect.try({\n // @effect-diagnostics-next-linepreferSchemaOverJson:off\n try: () => JSON.parse(raw) as unknown,\n catch: (): DecodeResult<Input> => ({ _tag: \"parseError\" }),\n }).pipe(\n Effect.flatMap((parsed) =>\n Effect.tryPromise({\n try: () => Promise.resolve(tool.inputSchema[\"~standard\"].validate(parsed)),\n catch: (): DecodeResult<Input> => ({ _tag: \"invalid\", issues: VALIDATION_THREW }),\n }).pipe(\n Effect.map((result): DecodeResult<Input> =>\n result.issues !== undefined\n ? { _tag: \"invalid\", issues: result.issues }\n : { _tag: \"ok\", input: result.value },\n ),\n ),\n ),\n Effect.catch((captured: DecodeResult<Input>) => Effect.succeed(captured)),\n )\n}\n\n/**\n * Decode and validate a function_call's JSON `arguments` against the tool's\n * own `inputSchema`, yielding the typed input. The decode-only half of\n * `execute` - reach for it when you intercept a call to translate it into a\n * control-flow decision rather than running the tool's `run`.\n *\n * Malformed JSON fails with `ToolError`; a parsed body that violates\n * `inputSchema` fails with `ToolValidationError` carrying the issues.\n */\nexport const decodeArgs = <Input>(\n tool: DecodableTool<Input>,\n call: ToolCall,\n): Effect.Effect<Input, ToolError | ToolValidationError> =>\n decodeCallInput(tool, call).pipe(\n Effect.flatMap(\n Match.type<DecodeResult<Input>>().pipe(\n Match.tag(\"ok\", ({ input }) => Effect.succeed(input)),\n Match.tag(\"parseError\", () =>\n Effect.fail(\n new ToolError({\n call_id: call.call_id,\n tool: tool.name,\n message: \"Failed to parse JSON arguments\",\n }),\n ),\n ),\n Match.tag(\"invalid\", ({ issues }) =>\n Effect.fail(new ToolValidationError({ call_id: call.call_id, tool: tool.name, issues })),\n ),\n Match.exhaustive,\n ),\n ),\n )\n\n/**\n * Decode and validate the JSON arguments of a function_call against the\n * tool's input schema, run the tool, and serialize the output into a\n * function_call_output item. Single-shot: intermediate events emitted by\n * `run` are discarded (use `Toolkit.run` to stream them).\n */\nexport const execute = <Name extends string, Input, Event, Output, E, R>(\n tool: Tool<Name, Input, Event, Output, E, R>,\n call: ToolCall,\n): Effect.Effect<ToolCallOutput, E | ToolError | ToolValidationError, R> =>\n decodeArgs(tool, call).pipe(\n Effect.flatMap((input) => tool.run(input, () => Effect.void)),\n Effect.map((output) => toolCallOutput(call.call_id, serializeValue(output))),\n )\n"],"mappings":"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"}