@effect-uai/core 0.9.0 → 0.11.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 (196) hide show
  1. package/dist/{AiError-CrdR1EQi.d.mts → AiError-Bf1pq4BB.d.mts} +8 -3
  2. package/dist/{AiError-CrdR1EQi.d.mts.map → AiError-Bf1pq4BB.d.mts.map} +1 -1
  3. package/dist/{Audio-DDSKCkSe.d.mts → Audio-CR7CSljJ.d.mts} +2 -2
  4. package/dist/{Audio-DDSKCkSe.d.mts.map → Audio-CR7CSljJ.d.mts.map} +1 -1
  5. package/dist/{Image-C_Zjpwhc.d.mts → Image-C4vuBmu0.d.mts} +12 -12
  6. package/dist/{Image-C_Zjpwhc.d.mts.map → Image-C4vuBmu0.d.mts.map} +1 -1
  7. package/dist/{Items-CgKD6Npw.d.mts → Items-BWNu6VGp.d.mts} +118 -92
  8. package/dist/Items-BWNu6VGp.d.mts.map +1 -0
  9. package/dist/{Loop-B5_cqumi.d.mts → Loop-D8nWpowc.d.mts} +2 -2
  10. package/dist/{Loop-B5_cqumi.d.mts.map → Loop-D8nWpowc.d.mts.map} +1 -1
  11. package/dist/{Media-DOLyaygI.d.mts → Media-D_CpcM1Z.d.mts} +1 -1
  12. package/dist/{Media-DOLyaygI.d.mts.map → Media-D_CpcM1Z.d.mts.map} +1 -1
  13. package/dist/{StructuredFormat-CMJFD3sn.d.mts → StructuredFormat-DHaUJBdd.d.mts} +1 -1
  14. package/dist/{StructuredFormat-CMJFD3sn.d.mts.map → StructuredFormat-DHaUJBdd.d.mts.map} +1 -1
  15. package/dist/{Tool-0XUIT2A2.d.mts → Tool-CXFHCQrT.d.mts} +101 -16
  16. package/dist/Tool-CXFHCQrT.d.mts.map +1 -0
  17. package/dist/{ToolEvent-DYrAUJGs.d.mts → ToolEvent-nVijCrvm.d.mts} +2 -2
  18. package/dist/ToolEvent-nVijCrvm.d.mts.map +1 -0
  19. package/dist/{ToolResult-BZd-cEWW.d.mts → ToolResult-CQqKkcDa.d.mts} +10 -4
  20. package/dist/ToolResult-CQqKkcDa.d.mts.map +1 -0
  21. package/dist/{Toolkit-BKC8b8Xo.d.mts → Toolkit-BYPwwu-j.d.mts} +65 -24
  22. package/dist/Toolkit-BYPwwu-j.d.mts.map +1 -0
  23. package/dist/browser/Browser.d.mts +190 -0
  24. package/dist/browser/Browser.d.mts.map +1 -0
  25. package/dist/browser/Browser.mjs +34 -0
  26. package/dist/browser/Browser.mjs.map +1 -0
  27. package/dist/browser/BrowserError.d.mts +141 -0
  28. package/dist/browser/BrowserError.d.mts.map +1 -0
  29. package/dist/browser/BrowserError.mjs +89 -0
  30. package/dist/browser/BrowserError.mjs.map +1 -0
  31. package/dist/browser/BrowserTool.d.mts +59 -0
  32. package/dist/browser/BrowserTool.d.mts.map +1 -0
  33. package/dist/browser/BrowserTool.mjs +86 -0
  34. package/dist/browser/BrowserTool.mjs.map +1 -0
  35. package/dist/{dist-DWgRbDN3.mjs → dist-C0m0QnUP.mjs} +71 -30
  36. package/dist/dist-C0m0QnUP.mjs.map +1 -0
  37. package/dist/{dist-B04dlFHg.mjs → dist-CcRV3-A5.mjs} +8 -8
  38. package/dist/{dist-B04dlFHg.mjs.map → dist-CcRV3-A5.mjs.map} +1 -1
  39. package/dist/domain/AiError.d.mts +2 -2
  40. package/dist/domain/AiError.mjs +22 -3
  41. package/dist/domain/AiError.mjs.map +1 -1
  42. package/dist/domain/Audio.d.mts +1 -1
  43. package/dist/domain/Citation.d.mts +60 -0
  44. package/dist/domain/Citation.d.mts.map +1 -0
  45. package/dist/domain/Citation.mjs +7 -0
  46. package/dist/domain/Citation.mjs.map +1 -0
  47. package/dist/domain/Image.d.mts +1 -1
  48. package/dist/domain/Items.d.mts +1 -1
  49. package/dist/domain/Items.mjs +13 -3
  50. package/dist/domain/Items.mjs.map +1 -1
  51. package/dist/domain/Media.d.mts +1 -1
  52. package/dist/domain/Music.d.mts +1 -1
  53. package/dist/domain/Research.d.mts +37 -0
  54. package/dist/domain/Research.d.mts.map +1 -0
  55. package/dist/domain/Research.mjs +7 -0
  56. package/dist/domain/Research.mjs.map +1 -0
  57. package/dist/domain/Turn.d.mts +92 -10
  58. package/dist/domain/Turn.d.mts.map +1 -1
  59. package/dist/domain/Turn.mjs +8 -1
  60. package/dist/domain/Turn.mjs.map +1 -1
  61. package/dist/domain/Turn.test.mjs +1 -1
  62. package/dist/embedding-model/Embedding.d.mts +1 -1
  63. package/dist/embedding-model/EmbeddingModel.d.mts +1 -1
  64. package/dist/embedding-model/EmbeddingModel.mjs +2 -2
  65. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
  66. package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
  67. package/dist/index.d.mts +22 -13
  68. package/dist/index.mjs +18 -9
  69. package/dist/job/Job.d.mts +80 -0
  70. package/dist/job/Job.d.mts.map +1 -0
  71. package/dist/job/Job.mjs +49 -0
  72. package/dist/job/Job.mjs.map +1 -0
  73. package/dist/language-model/LanguageModel.d.mts +4 -4
  74. package/dist/language-model/LanguageModel.mjs +2 -2
  75. package/dist/language-model/LanguageModel.mjs.map +1 -1
  76. package/dist/language-model/LanguageModel.test.mjs +1 -1
  77. package/dist/loop/Loop.d.mts +1 -1
  78. package/dist/loop/Loop.test.mjs +1 -1
  79. package/dist/music-generator/MusicGenerator.d.mts +2 -2
  80. package/dist/music-generator/MusicGenerator.mjs +4 -4
  81. package/dist/music-generator/MusicGenerator.mjs.map +1 -1
  82. package/dist/music-generator/MusicGenerator.test.mjs +1 -1
  83. package/dist/observability/Metrics.d.mts +1 -1
  84. package/dist/observability/Metrics.test.mjs +2 -2
  85. package/dist/observability/Telemetry.test.mjs +2 -2
  86. package/dist/research/DeepResearch.d.mts +71 -0
  87. package/dist/research/DeepResearch.d.mts.map +1 -0
  88. package/dist/research/DeepResearch.mjs +61 -0
  89. package/dist/research/DeepResearch.mjs.map +1 -0
  90. package/dist/retry/Retry.d.mts +1 -1
  91. package/dist/retry/Retry.test.mjs +1 -1
  92. package/dist/sandbox/Sandbox.mjs +18 -18
  93. package/dist/sandbox/Sandbox.mjs.map +1 -1
  94. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +2 -2
  95. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +8 -8
  96. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
  97. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
  98. package/dist/streaming/JSONL.d.mts +3 -1
  99. package/dist/streaming/JSONL.d.mts.map +1 -1
  100. package/dist/streaming/JSONL.mjs +5 -11
  101. package/dist/streaming/JSONL.mjs.map +1 -1
  102. package/dist/streaming/JSONL.test.mjs +10 -1
  103. package/dist/streaming/JSONL.test.mjs.map +1 -1
  104. package/dist/streaming/SSE.d.mts +5 -3
  105. package/dist/streaming/SSE.d.mts.map +1 -1
  106. package/dist/streaming/SSE.mjs +28 -38
  107. package/dist/streaming/SSE.mjs.map +1 -1
  108. package/dist/streaming/SSE.test.mjs +54 -4
  109. package/dist/streaming/SSE.test.mjs.map +1 -1
  110. package/dist/structured-format/StructuredFormat.d.mts +1 -1
  111. package/dist/structured-format/StructuredFormat.test.mjs +1 -1
  112. package/dist/testing/MockMusicGenerator.d.mts +1 -1
  113. package/dist/testing/MockProvider.d.mts +1 -1
  114. package/dist/testing/MockSpeechSynthesizer.d.mts +1 -1
  115. package/dist/tool/Approval.d.mts +19 -7
  116. package/dist/tool/Approval.d.mts.map +1 -1
  117. package/dist/tool/Approval.mjs +18 -11
  118. package/dist/tool/Approval.mjs.map +1 -1
  119. package/dist/tool/Approval.test.mjs +47 -8
  120. package/dist/tool/Approval.test.mjs.map +1 -1
  121. package/dist/tool/HistoryCheck.d.mts +2 -2
  122. package/dist/tool/Tool.d.mts +2 -2
  123. package/dist/tool/Tool.mjs +112 -28
  124. package/dist/tool/Tool.mjs.map +1 -1
  125. package/dist/tool/Tool.test.mjs +127 -3
  126. package/dist/tool/Tool.test.mjs.map +1 -1
  127. package/dist/tool/ToolEvent.d.mts +1 -1
  128. package/dist/tool/ToolEvent.mjs +5 -0
  129. package/dist/tool/ToolEvent.mjs.map +1 -1
  130. package/dist/tool/ToolResult.d.mts +2 -2
  131. package/dist/tool/ToolResult.mjs +15 -6
  132. package/dist/tool/ToolResult.mjs.map +1 -1
  133. package/dist/tool/Toolkit.d.mts +2 -2
  134. package/dist/tool/Toolkit.mjs +62 -22
  135. package/dist/tool/Toolkit.mjs.map +1 -1
  136. package/dist/tool/Toolkit.test.mjs +207 -9
  137. package/dist/tool/Toolkit.test.mjs.map +1 -1
  138. package/dist/transcriber/Transcriber.d.mts +2 -2
  139. package/dist/transcriber/Transcriber.mjs +3 -3
  140. package/dist/transcriber/Transcriber.mjs.map +1 -1
  141. package/dist/transcriber/Transcriber.test.mjs +1 -1
  142. package/dist/web-read/WebRead.d.mts +44 -0
  143. package/dist/web-read/WebRead.d.mts.map +1 -0
  144. package/dist/web-read/WebRead.mjs +18 -0
  145. package/dist/web-read/WebRead.mjs.map +1 -0
  146. package/dist/web-read/WebReadTool.d.mts +57 -0
  147. package/dist/web-read/WebReadTool.d.mts.map +1 -0
  148. package/dist/web-read/WebReadTool.mjs +59 -0
  149. package/dist/web-read/WebReadTool.mjs.map +1 -0
  150. package/dist/web-search/WebSearch.d.mts +1 -1
  151. package/dist/web-search/WebSearch.mjs +1 -1
  152. package/dist/web-search/WebSearch.mjs.map +1 -1
  153. package/dist/web-search/WebSearchTool.d.mts +12 -6
  154. package/dist/web-search/WebSearchTool.d.mts.map +1 -1
  155. package/dist/web-search/WebSearchTool.mjs +10 -5
  156. package/dist/web-search/WebSearchTool.mjs.map +1 -1
  157. package/package.json +39 -3
  158. package/src/browser/Browser.ts +268 -0
  159. package/src/browser/BrowserError.ts +145 -0
  160. package/src/browser/BrowserTool.ts +161 -0
  161. package/src/domain/AiError.ts +27 -1
  162. package/src/domain/Citation.ts +64 -0
  163. package/src/domain/Items.ts +14 -4
  164. package/src/domain/Research.ts +42 -0
  165. package/src/domain/Turn.ts +26 -0
  166. package/src/embedding-model/EmbeddingModel.ts +2 -2
  167. package/src/index.ts +9 -0
  168. package/src/job/Job.ts +125 -0
  169. package/src/language-model/LanguageModel.ts +2 -2
  170. package/src/music-generator/MusicGenerator.ts +4 -4
  171. package/src/research/DeepResearch.ts +137 -0
  172. package/src/sandbox/Sandbox.ts +18 -18
  173. package/src/speech-synthesizer/SpeechSynthesizer.ts +8 -8
  174. package/src/streaming/JSONL.test.ts +19 -0
  175. package/src/streaming/JSONL.ts +6 -38
  176. package/src/streaming/SSE.test.ts +86 -3
  177. package/src/streaming/SSE.ts +36 -80
  178. package/src/tool/Approval.test.ts +55 -5
  179. package/src/tool/Approval.ts +72 -27
  180. package/src/tool/Tool.test.ts +151 -1
  181. package/src/tool/Tool.ts +263 -66
  182. package/src/tool/ToolEvent.ts +5 -0
  183. package/src/tool/ToolResult.ts +14 -8
  184. package/src/tool/Toolkit.test.ts +231 -2
  185. package/src/tool/Toolkit.ts +175 -62
  186. package/src/transcriber/Transcriber.ts +3 -3
  187. package/src/web-read/WebRead.ts +70 -0
  188. package/src/web-read/WebReadTool.ts +96 -0
  189. package/src/web-search/WebSearch.ts +1 -1
  190. package/src/web-search/WebSearchTool.ts +11 -5
  191. package/dist/Items-CgKD6Npw.d.mts.map +0 -1
  192. package/dist/Tool-0XUIT2A2.d.mts.map +0 -1
  193. package/dist/ToolEvent-DYrAUJGs.d.mts.map +0 -1
  194. package/dist/ToolResult-BZd-cEWW.d.mts.map +0 -1
  195. package/dist/Toolkit-BKC8b8Xo.d.mts.map +0 -1
  196. package/dist/dist-DWgRbDN3.mjs.map +0 -1
@@ -1,10 +1,14 @@
1
1
  import { fromBytes, parse, toBytes } from "./JSONL.mjs";
2
- import { a as describe, n as globalExpect, o as it } from "../dist-B04dlFHg.mjs";
2
+ import { c as it, n as globalExpect, o as describe } from "../dist-CcRV3-A5.mjs";
3
3
  import { Effect, Result, Schema, Stream } from "effect";
4
4
  //#region src/streaming/JSONL.test.ts
5
5
  const enc = new TextEncoder();
6
6
  const bytesOf = (...chunks) => Stream.fromIterable(chunks.map((c) => enc.encode(c)));
7
7
  const collect = (s) => Effect.runPromise(Stream.runCollect(s));
8
+ const runUntilEnd = (s) => Effect.runPromise(s.pipe(Stream.result, Stream.runCollect, Effect.map((results) => ({
9
+ seen: results.filter(Result.isSuccess).map((r) => r.success),
10
+ failures: results.filter(Result.isFailure).map((r) => r.failure)
11
+ }))));
8
12
  const collectResult = (s) => Effect.runPromise(Effect.result(Stream.runCollect(s)));
9
13
  const Patch = Schema.Struct({
10
14
  op: Schema.String,
@@ -27,6 +31,11 @@ describe("JSONL.fromBytes", () => {
27
31
  it("ignores blank lines", async () => {
28
32
  globalExpect(await collect(fromBytes(bytesOf("a\n\n\nb\n")))).toEqual(["a", "b"]);
29
33
  });
34
+ it("discards a buffered partial line when the upstream fails mid-stream", async () => {
35
+ const { seen, failures } = await runUntilEnd(fromBytes(Stream.concat(bytesOf("{\"n\":1}\n{\"n\":2"), Stream.fail("boom"))));
36
+ globalExpect(seen).toEqual(["{\"n\":1}"]);
37
+ globalExpect(failures).toEqual(["boom"]);
38
+ });
30
39
  });
31
40
  describe("JSONL.parse", () => {
32
41
  it("decodes well-formed JSON lines through a Schema", async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"JSONL.test.mjs","names":["JSONL.fromBytes","JSONL.parse","JSONL.toBytes"],"sources":["../../src/streaming/JSONL.test.ts"],"sourcesContent":["import { Effect, Result, Schema, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport * as JSONL from \"./JSONL.js\"\n\nconst enc = new TextEncoder()\nconst bytesOf = (...chunks: ReadonlyArray<string>) =>\n Stream.fromIterable(chunks.map((c) => enc.encode(c)))\n\nconst collect = <A, E>(s: Stream.Stream<A, E>) => Effect.runPromise(Stream.runCollect(s))\n\nconst collectResult = <A, E>(s: Stream.Stream<A, E>) =>\n Effect.runPromise(Effect.result(Stream.runCollect(s)))\n\nconst Patch = Schema.Struct({ op: Schema.String, value: Schema.Number })\n\ndescribe(\"JSONL.fromBytes\", () => {\n it(\"emits one string per line\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\nb\\nc\\n\")))\n expect(out).toEqual([\"a\", \"b\", \"c\"])\n })\n\n it(\"buffers lines across chunk boundaries\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"ab\", \"c\\nde\", \"f\\n\")))\n expect(out).toEqual([\"abc\", \"def\"])\n })\n\n it(\"flushes a trailing line without a final newline\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\nb\")))\n expect(out).toEqual([\"a\", \"b\"])\n })\n\n it(\"ignores blank lines\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\n\\n\\nb\\n\")))\n expect(out).toEqual([\"a\", \"b\"])\n })\n})\n\ndescribe(\"JSONL.parse\", () => {\n it(\"decodes well-formed JSON lines through a Schema\", async () => {\n const out = await collect(\n bytesOf(`{\"op\":\"add\",\"value\":1}\\n{\"op\":\"sub\",\"value\":2}\\n`).pipe(\n JSONL.fromBytes,\n JSONL.parse(Patch),\n ),\n )\n expect(out).toEqual([\n { op: \"add\", value: 1 },\n { op: \"sub\", value: 2 },\n ])\n })\n\n it(\"fails with JsonParseError on malformed JSON\", async () => {\n const result = await collectResult(\n bytesOf(`{\"op\":\"add\",\"value\":1}\\nNOT_JSON\\n`).pipe(JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(Result.isFailure(result)).toBe(true)\n if (Result.isFailure(result)) {\n expect(result.failure._tag).toBe(\"JsonParseError\")\n expect(result.failure.line).toBe(\"NOT_JSON\")\n }\n })\n\n it(\"fails with JsonParseError on schema mismatch\", async () => {\n const result = await collectResult(\n bytesOf(`{\"op\":\"add\",\"value\":\"not a number\"}\\n`).pipe(JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(Result.isFailure(result)).toBe(true)\n if (Result.isFailure(result)) {\n expect(result.failure._tag).toBe(\"JsonParseError\")\n }\n })\n})\n\ndescribe(\"JSONL round-trip\", () => {\n it(\"toBytes then fromBytes/parse recovers the values\", async () => {\n const values = [\n { op: \"a\", value: 1 },\n { op: \"b\", value: 2 },\n ]\n const out = await collect(\n Stream.fromIterable(values).pipe(JSONL.toBytes(Patch), JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(out).toEqual(values)\n })\n})\n"],"mappings":";;;;AAIA,MAAM,MAAM,IAAI,aAAa;AAC7B,MAAM,WAAW,GAAG,WAClB,OAAO,aAAa,OAAO,KAAK,MAAM,IAAI,OAAO,EAAE,CAAC,CAAC;AAEvD,MAAM,WAAiB,MAA2B,OAAO,WAAW,OAAO,WAAW,EAAE,CAAC;AAEzF,MAAM,iBAAuB,MAC3B,OAAO,WAAW,OAAO,OAAO,OAAO,WAAW,EAAE,CAAC,CAAC;AAExD,MAAM,QAAQ,OAAO,OAAO;CAAE,IAAI,OAAO;CAAQ,OAAO,OAAO;CAAQ,CAAC;AAExE,SAAS,yBAAyB;AAChC,IAAG,6BAA6B,YAAY;AAE1C,eAAO,MADW,QAAQA,UAAgB,QAAQ,YAAY,CAAC,CAAC,CACrD,CAAC,QAAQ;GAAC;GAAK;GAAK;GAAI,CAAC;GACpC;AAEF,IAAG,yCAAyC,YAAY;AAEtD,eAAO,MADW,QAAQA,UAAgB,QAAQ,MAAM,SAAS,MAAM,CAAC,CAAC,CAC9D,CAAC,QAAQ,CAAC,OAAO,MAAM,CAAC;GACnC;AAEF,IAAG,mDAAmD,YAAY;AAEhE,eAAO,MADW,QAAQA,UAAgB,QAAQ,OAAO,CAAC,CAAC,CAChD,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;GAC/B;AAEF,IAAG,uBAAuB,YAAY;AAEpC,eAAO,MADW,QAAQA,UAAgB,QAAQ,aAAa,CAAC,CAAC,CACtD,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;GAC/B;EACF;AAEF,SAAS,qBAAqB;AAC5B,IAAG,mDAAmD,YAAY;AAOhE,eAAO,MANW,QAChB,QAAQ,mDAAmD,CAAC,KAC1DA,WACAC,MAAY,MAAM,CACnB,CACF,CACU,CAAC,QAAQ,CAClB;GAAE,IAAI;GAAO,OAAO;GAAG,EACvB;GAAE,IAAI;GAAO,OAAO;GAAG,CACxB,CAAC;GACF;AAEF,IAAG,+CAA+C,YAAY;EAC5D,MAAM,SAAS,MAAM,cACnB,QAAQ,qCAAqC,CAAC,KAAKD,WAAiBC,MAAY,MAAM,CAAC,CACxF;AACD,eAAO,OAAO,UAAU,OAAO,CAAC,CAAC,KAAK,KAAK;AAC3C,MAAI,OAAO,UAAU,OAAO,EAAE;AAC5B,gBAAO,OAAO,QAAQ,KAAK,CAAC,KAAK,iBAAiB;AAClD,gBAAO,OAAO,QAAQ,KAAK,CAAC,KAAK,WAAW;;GAE9C;AAEF,IAAG,gDAAgD,YAAY;EAC7D,MAAM,SAAS,MAAM,cACnB,QAAQ,wCAAwC,CAAC,KAAKD,WAAiBC,MAAY,MAAM,CAAC,CAC3F;AACD,eAAO,OAAO,UAAU,OAAO,CAAC,CAAC,KAAK,KAAK;AAC3C,MAAI,OAAO,UAAU,OAAO,CAC1B,cAAO,OAAO,QAAQ,KAAK,CAAC,KAAK,iBAAiB;GAEpD;EACF;AAEF,SAAS,0BAA0B;AACjC,IAAG,oDAAoD,YAAY;EACjE,MAAM,SAAS,CACb;GAAE,IAAI;GAAK,OAAO;GAAG,EACrB;GAAE,IAAI;GAAK,OAAO;GAAG,CACtB;AAID,eAAO,MAHW,QAChB,OAAO,aAAa,OAAO,CAAC,KAAKC,QAAc,MAAM,EAAEF,WAAiBC,MAAY,MAAM,CAAC,CAC5F,CACU,CAAC,QAAQ,OAAO;GAC3B;EACF"}
1
+ {"version":3,"file":"JSONL.test.mjs","names":["JSONL.fromBytes","JSONL.parse","JSONL.toBytes"],"sources":["../../src/streaming/JSONL.test.ts"],"sourcesContent":["import { Effect, Result, Schema, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport * as JSONL from \"./JSONL.js\"\n\nconst enc = new TextEncoder()\nconst bytesOf = (...chunks: ReadonlyArray<string>) =>\n Stream.fromIterable(chunks.map((c) => enc.encode(c)))\n\nconst collect = <A, E>(s: Stream.Stream<A, E>) => Effect.runPromise(Stream.runCollect(s))\n\nconst runUntilEnd = <A, E>(s: Stream.Stream<A, E>) =>\n Effect.runPromise(\n s.pipe(\n Stream.result,\n Stream.runCollect,\n Effect.map((results) => ({\n seen: results.filter(Result.isSuccess).map((r) => r.success),\n failures: results.filter(Result.isFailure).map((r) => r.failure),\n })),\n ),\n )\n\nconst collectResult = <A, E>(s: Stream.Stream<A, E>) =>\n Effect.runPromise(Effect.result(Stream.runCollect(s)))\n\nconst Patch = Schema.Struct({ op: Schema.String, value: Schema.Number })\n\ndescribe(\"JSONL.fromBytes\", () => {\n it(\"emits one string per line\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\nb\\nc\\n\")))\n expect(out).toEqual([\"a\", \"b\", \"c\"])\n })\n\n it(\"buffers lines across chunk boundaries\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"ab\", \"c\\nde\", \"f\\n\")))\n expect(out).toEqual([\"abc\", \"def\"])\n })\n\n it(\"flushes a trailing line without a final newline\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\nb\")))\n expect(out).toEqual([\"a\", \"b\"])\n })\n\n it(\"ignores blank lines\", async () => {\n const out = await collect(JSONL.fromBytes(bytesOf(\"a\\n\\n\\nb\\n\")))\n expect(out).toEqual([\"a\", \"b\"])\n })\n\n it(\"discards a buffered partial line when the upstream fails mid-stream\", async () => {\n const failing = Stream.concat(bytesOf('{\"n\":1}\\n{\"n\":2'), Stream.fail(\"boom\" as const))\n const { seen, failures } = await runUntilEnd(JSONL.fromBytes(failing))\n expect(seen).toEqual(['{\"n\":1}'])\n expect(failures).toEqual([\"boom\"])\n })\n})\n\ndescribe(\"JSONL.parse\", () => {\n it(\"decodes well-formed JSON lines through a Schema\", async () => {\n const out = await collect(\n bytesOf(`{\"op\":\"add\",\"value\":1}\\n{\"op\":\"sub\",\"value\":2}\\n`).pipe(\n JSONL.fromBytes,\n JSONL.parse(Patch),\n ),\n )\n expect(out).toEqual([\n { op: \"add\", value: 1 },\n { op: \"sub\", value: 2 },\n ])\n })\n\n it(\"fails with JsonParseError on malformed JSON\", async () => {\n const result = await collectResult(\n bytesOf(`{\"op\":\"add\",\"value\":1}\\nNOT_JSON\\n`).pipe(JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(Result.isFailure(result)).toBe(true)\n if (Result.isFailure(result)) {\n expect(result.failure._tag).toBe(\"JsonParseError\")\n expect(result.failure.line).toBe(\"NOT_JSON\")\n }\n })\n\n it(\"fails with JsonParseError on schema mismatch\", async () => {\n const result = await collectResult(\n bytesOf(`{\"op\":\"add\",\"value\":\"not a number\"}\\n`).pipe(JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(Result.isFailure(result)).toBe(true)\n if (Result.isFailure(result)) {\n expect(result.failure._tag).toBe(\"JsonParseError\")\n }\n })\n})\n\ndescribe(\"JSONL round-trip\", () => {\n it(\"toBytes then fromBytes/parse recovers the values\", async () => {\n const values = [\n { op: \"a\", value: 1 },\n { op: \"b\", value: 2 },\n ]\n const out = await collect(\n Stream.fromIterable(values).pipe(JSONL.toBytes(Patch), JSONL.fromBytes, JSONL.parse(Patch)),\n )\n expect(out).toEqual(values)\n })\n})\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ declare namespace SSE_d_exports {
6
6
  }
7
7
  /**
8
8
  * One Server-Sent Event. Fields per the WHATWG spec:
9
- * - `event`: optional event name (default "message" on the wire)
9
+ * - `event`: optional event name (absent for the default "message")
10
10
  * - `data`: payload, with multiple `data:` lines joined by `\n`
11
11
  * - `id`: optional last-event id
12
12
  */
@@ -17,8 +17,10 @@ type Event = {
17
17
  };
18
18
  /**
19
19
  * Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a
20
- * `Stream<SSE.Event>`. Handles partial UTF-8 sequences, CRLF/LF line
21
- * endings, and events split across chunk boundaries.
20
+ * `Stream<SSE.Event>`, using the spec parser from `effect/unstable/encoding`.
21
+ * An event is dispatched only once its terminating blank line arrives, so a
22
+ * partial event at stream end (clean or failed) is discarded per the spec
23
+ * rather than surfaced as a truncated frame.
22
24
  */
23
25
  declare const fromBytes: <E, R>(self: Stream.Stream<Uint8Array, E, R>) => Stream.Stream<Event, E, R>;
24
26
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"SSE.d.mts","names":[],"sources":["../../src/streaming/SSE.ts"],"mappings":";;;;;;;;;;;;KAQY,KAAA;EAAA,SACD,KAAA;EAAA,SACA,IAAA;EAAA,SACA,EAAA;AAAA;;;;;;cA4EE,SAAA,SACX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,CAAO,KAAA,EAAO,CAAA,EAAG,CAAA;AAF3B;;;;AAAA,cAyBa,OAAA,SAAiB,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,KAAA,EAAO,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA"}
1
+ {"version":3,"file":"SSE.d.mts","names":[],"sources":["../../src/streaming/SSE.ts"],"mappings":";;;;;;;;;;;;KASY,KAAA;EAAA,SACD,KAAA;EAAA,SACA,IAAA;EAAA,SACA,EAAA;AAAA;;;;;;;AA8BX;cAAa,SAAA,SACX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,CAAO,KAAA,EAAO,CAAA,EAAG,CAAA;;;;;cAed,OAAA,SAAiB,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,KAAA,EAAO,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA"}
@@ -1,57 +1,47 @@
1
1
  import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
2
2
  import { Stream } from "effect";
3
+ import { Sse } from "effect/unstable/encoding";
3
4
  //#region src/streaming/SSE.ts
4
5
  var SSE_exports = /* @__PURE__ */ __exportAll({
5
6
  fromBytes: () => fromBytes,
6
7
  toBytes: () => toBytes
7
8
  });
8
- /** Decode `Uint8Array` chunks as UTF-8, handling multi-byte boundaries. */
9
- const decodeText = (self) => self.pipe(Stream.mapAccum(() => new TextDecoder("utf-8"), (decoder, chunk) => [decoder, [decoder.decode(chunk, { stream: true })]], { onHalt: (decoder) => {
10
- const tail = decoder.decode();
11
- return tail.length > 0 ? [tail] : [];
12
- } }));
13
- /** Split a text stream on a separator, buffering across chunk boundaries. */
14
- const splitOn = (separator) => (self) => self.pipe(Stream.mapAccum(() => "", (buffer, chunk) => {
15
- const parts = (buffer + chunk).split(separator);
16
- return [parts[parts.length - 1] ?? "", parts.slice(0, -1)];
17
- }, { onHalt: (tail) => tail.length > 0 ? [tail] : [] }));
18
- const parseField = (line) => {
19
- const colon = line.indexOf(":");
20
- if (colon < 0) return [line, ""];
21
- const value = line.slice(colon + 1);
22
- return [line.slice(0, colon), value.startsWith(" ") ? value.slice(1) : value];
23
- };
24
- const parseBlock = (block) => {
25
- const lines = block.split("\n").filter((l) => l.length > 0 && !l.startsWith(":"));
26
- if (lines.length === 0) return null;
27
- const fields = lines.map(parseField);
28
- const dataLines = fields.filter(([f]) => f === "data").map(([, v]) => v);
29
- const event = fields.find(([f]) => f === "event")?.[1];
30
- const id = fields.find(([f]) => f === "id")?.[1];
31
- const out = { data: dataLines.join("\n") };
32
- if (event !== void 0) out.event = event;
33
- if (id !== void 0) out.id = id;
34
- return out;
9
+ const toEvent = (wire) => ({
10
+ data: wire.data,
11
+ ...wire.event !== "message" && { event: wire.event },
12
+ ...wire.id !== void 0 && { id: wire.id }
13
+ });
14
+ const makeParserState = () => {
15
+ const out = [];
16
+ return {
17
+ parser: Sse.makeParser((event) => {
18
+ if (event._tag === "Event") out.push(toEvent(event));
19
+ }),
20
+ out
21
+ };
35
22
  };
36
23
  /**
37
24
  * Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a
38
- * `Stream<SSE.Event>`. Handles partial UTF-8 sequences, CRLF/LF line
39
- * endings, and events split across chunk boundaries.
25
+ * `Stream<SSE.Event>`, using the spec parser from `effect/unstable/encoding`.
26
+ * An event is dispatched only once its terminating blank line arrives, so a
27
+ * partial event at stream end (clean or failed) is discarded per the spec
28
+ * rather than surfaced as a truncated frame.
40
29
  */
41
- const fromBytes = (self) => self.pipe(decodeText, Stream.map((s) => s.replace(/\r/g, "")), splitOn("\n\n"), Stream.map(parseBlock), Stream.filter((ev) => ev !== null));
42
- const eventToString = (ev) => {
43
- const parts = [];
44
- if (ev.event !== void 0) parts.push(`event: ${ev.event}`);
45
- if (ev.id !== void 0) parts.push(`id: ${ev.id}`);
46
- for (const line of ev.data.split("\n")) parts.push(`data: ${line}`);
47
- return parts.join("\n") + "\n\n";
48
- };
30
+ const fromBytes = (self) => self.pipe(Stream.decodeText, Stream.mapAccum(makeParserState, (state, text) => {
31
+ state.parser.feed(text);
32
+ return [state, state.out.splice(0)];
33
+ }));
49
34
  const encoder = new TextEncoder();
50
35
  /**
51
36
  * Encode a `Stream<Event>` as `Stream<Uint8Array>` ready to send on an
52
37
  * HTTP response with `Content-Type: text/event-stream`.
53
38
  */
54
- const toBytes = (self) => Stream.map(self, (ev) => encoder.encode(eventToString(ev)));
39
+ const toBytes = (self) => Stream.map(self, (ev) => encoder.encode(Sse.encoder.write({
40
+ _tag: "Event",
41
+ event: ev.event ?? "message",
42
+ id: ev.id,
43
+ data: ev.data
44
+ })));
55
45
  //#endregion
56
46
  export { fromBytes, SSE_exports as t, toBytes };
57
47
 
@@ -1 +1 @@
1
- {"version":3,"file":"SSE.mjs","names":[],"sources":["../../src/streaming/SSE.ts"],"sourcesContent":["import { Stream } from \"effect\"\n\n/**\n * One Server-Sent Event. Fields per the WHATWG spec:\n * - `event`: optional event name (default \"message\" on the wire)\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\n// ---------------------------------------------------------------------------\n// Generic stream helpers (kept module-local for now; promote to a shared\n// Stream module once a third caller appears).\n// ---------------------------------------------------------------------------\n\n/** Decode `Uint8Array` chunks as UTF-8, handling multi-byte boundaries. */\nconst decodeText = <E, R>(self: Stream.Stream<Uint8Array, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): TextDecoder => new TextDecoder(\"utf-8\"),\n (decoder, chunk: Uint8Array) => [decoder, [decoder.decode(chunk, { stream: true })]] as const,\n {\n onHalt: (decoder: TextDecoder) => {\n const tail = decoder.decode()\n return tail.length > 0 ? [tail] : []\n },\n },\n ),\n )\n\n/** Split a text stream on a separator, buffering across chunk boundaries. */\nconst splitOn =\n (separator: string) =>\n <E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): string => \"\",\n (buffer, chunk: string) => {\n const parts = (buffer + chunk).split(separator)\n const tail = parts[parts.length - 1] ?? \"\"\n return [tail, parts.slice(0, -1)] as const\n },\n { onHalt: (tail: string) => (tail.length > 0 ? [tail] : []) },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Parser\n// ---------------------------------------------------------------------------\n\nconst parseField = (line: string): readonly [string, string] => {\n const colon = line.indexOf(\":\")\n if (colon < 0) return [line, \"\"]\n const value = line.slice(colon + 1)\n return [line.slice(0, colon), value.startsWith(\" \") ? value.slice(1) : value]\n}\n\nconst parseBlock = (block: string): Event | null => {\n const lines = block.split(\"\\n\").filter((l) => l.length > 0 && !l.startsWith(\":\"))\n if (lines.length === 0) return null\n\n const fields = lines.map(parseField)\n const dataLines = fields.filter(([f]) => f === \"data\").map(([, v]) => v)\n const event = fields.find(([f]) => f === \"event\")?.[1]\n const id = fields.find(([f]) => f === \"id\")?.[1]\n\n const out: { event?: string; data: string; id?: string } = {\n data: dataLines.join(\"\\n\"),\n }\n if (event !== undefined) out.event = event\n if (id !== undefined) out.id = id\n return out as Event\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a\n * `Stream<SSE.Event>`. Handles partial UTF-8 sequences, CRLF/LF line\n * endings, and events split across chunk boundaries.\n */\nexport const fromBytes = <E, R>(\n self: Stream.Stream<Uint8Array, E, R>,\n): Stream.Stream<Event, E, R> =>\n self.pipe(\n decodeText,\n Stream.map((s) => s.replace(/\\r/g, \"\")), // SSE allows CRLF; normalize to LF\n splitOn(\"\\n\\n\"),\n Stream.map(parseBlock),\n Stream.filter((ev): ev is Event => ev !== null),\n )\n\nconst eventToString = (ev: Event): string => {\n const parts: string[] = []\n if (ev.event !== undefined) parts.push(`event: ${ev.event}`)\n if (ev.id !== undefined) parts.push(`id: ${ev.id}`)\n for (const line of ev.data.split(\"\\n\")) parts.push(`data: ${line}`)\n return parts.join(\"\\n\") + \"\\n\\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) => encoder.encode(eventToString(ev)))\n"],"mappings":";;;;;;;;AAoBA,MAAM,cAAoB,SACxB,KAAK,KACH,OAAO,eACc,IAAI,YAAY,QAAQ,GAC1C,SAAS,UAAsB,CAAC,SAAS,CAAC,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC,CAAC,CAAC,EACpF,EACE,SAAS,YAAyB;CAChC,MAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAO,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE;GAEvC,CACF,CACF;;AAGH,MAAM,WACH,eACM,SACL,KAAK,KACH,OAAO,eACS,KACb,QAAQ,UAAkB;CACzB,MAAM,SAAS,SAAS,OAAO,MAAM,UAAU;AAE/C,QAAO,CADM,MAAM,MAAM,SAAS,MAAM,IAC1B,MAAM,MAAM,GAAG,GAAG,CAAC;GAEnC,EAAE,SAAS,SAAkB,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE,EAAG,CAC9D,CACF;AAML,MAAM,cAAc,SAA4C;CAC9D,MAAM,QAAQ,KAAK,QAAQ,IAAI;AAC/B,KAAI,QAAQ,EAAG,QAAO,CAAC,MAAM,GAAG;CAChC,MAAM,QAAQ,KAAK,MAAM,QAAQ,EAAE;AACnC,QAAO,CAAC,KAAK,MAAM,GAAG,MAAM,EAAE,MAAM,WAAW,IAAI,GAAG,MAAM,MAAM,EAAE,GAAG,MAAM;;AAG/E,MAAM,cAAc,UAAgC;CAClD,MAAM,QAAQ,MAAM,MAAM,KAAK,CAAC,QAAQ,MAAM,EAAE,SAAS,KAAK,CAAC,EAAE,WAAW,IAAI,CAAC;AACjF,KAAI,MAAM,WAAW,EAAG,QAAO;CAE/B,MAAM,SAAS,MAAM,IAAI,WAAW;CACpC,MAAM,YAAY,OAAO,QAAQ,CAAC,OAAO,MAAM,OAAO,CAAC,KAAK,GAAG,OAAO,EAAE;CACxE,MAAM,QAAQ,OAAO,MAAM,CAAC,OAAO,MAAM,QAAQ,GAAG;CACpD,MAAM,KAAK,OAAO,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG;CAE9C,MAAM,MAAqD,EACzD,MAAM,UAAU,KAAK,KAAK,EAC3B;AACD,KAAI,UAAU,KAAA,EAAW,KAAI,QAAQ;AACrC,KAAI,OAAO,KAAA,EAAW,KAAI,KAAK;AAC/B,QAAO;;;;;;;AAYT,MAAa,aACX,SAEA,KAAK,KACH,YACA,OAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,CAAC,EACvC,QAAQ,OAAO,EACf,OAAO,IAAI,WAAW,EACtB,OAAO,QAAQ,OAAoB,OAAO,KAAK,CAChD;AAEH,MAAM,iBAAiB,OAAsB;CAC3C,MAAM,QAAkB,EAAE;AAC1B,KAAI,GAAG,UAAU,KAAA,EAAW,OAAM,KAAK,UAAU,GAAG,QAAQ;AAC5D,KAAI,GAAG,OAAO,KAAA,EAAW,OAAM,KAAK,OAAO,GAAG,KAAK;AACnD,MAAK,MAAM,QAAQ,GAAG,KAAK,MAAM,KAAK,CAAE,OAAM,KAAK,SAAS,OAAO;AACnE,QAAO,MAAM,KAAK,KAAK,GAAG;;AAG5B,MAAM,UAAU,IAAI,aAAa;;;;;AAMjC,MAAa,WAAiB,SAC5B,OAAO,IAAI,OAAO,OAAO,QAAQ,OAAO,cAAc,GAAG,CAAC,CAAC"}
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,OAAO;CACrD,GAAI,KAAK,OAAO,KAAA,KAAa,EAAE,IAAI,KAAK,IAAI;CAC7C;AAOD,MAAM,wBAAqC;CACzC,MAAM,MAAoB,EAAE;AAK5B,QAAO;EAAE,QAJM,IAAI,YAAY,UAAU;AAEvC,OAAI,MAAM,SAAS,QAAS,KAAI,KAAK,QAAQ,MAAM,CAAC;IAEvC;EAAE;EAAK;;;;;;;;;AAUxB,MAAa,aACX,SAEA,KAAK,KACH,OAAO,YACP,OAAO,SAAS,kBAAkB,OAAO,SAAiB;AACxD,OAAM,OAAO,KAAK,KAAK;AACvB,QAAO,CAAC,OAAO,MAAM,IAAI,OAAO,EAAE,CAAC;EACnC,CACH;AAEH,MAAM,UAAU,IAAI,aAAa;;;;;AAMjC,MAAa,WAAiB,SAC5B,OAAO,IAAI,OAAO,OAChB,QAAQ,OACN,IAAI,QAAQ,MAAM;CAChB,MAAM;CACN,OAAO,GAAG,SAAS;CACnB,IAAI,GAAG;CACP,MAAM,GAAG;CACV,CAAC,CACH,CACF"}
@@ -1,10 +1,15 @@
1
1
  import { fromBytes, toBytes } from "./SSE.mjs";
2
- import { a as describe, n as globalExpect, o as it } from "../dist-B04dlFHg.mjs";
3
- import { Effect, Stream } from "effect";
2
+ import { c as it, n as globalExpect, o as describe } from "../dist-CcRV3-A5.mjs";
3
+ import { Cause, Deferred, Effect, Exit, Fiber, Result, Stream } from "effect";
4
4
  //#region src/streaming/SSE.test.ts
5
5
  const enc = new TextEncoder();
6
6
  const bytesOf = (...chunks) => Stream.fromIterable(chunks.map((c) => enc.encode(c)));
7
7
  const collect = (s) => Effect.runPromise(Stream.runCollect(s));
8
+ const runUntilEnd = (s) => Effect.runPromise(s.pipe(Stream.result, Stream.runCollect, Effect.map((results) => ({
9
+ seen: results.filter(Result.isSuccess).map((r) => r.success),
10
+ failures: results.filter(Result.isFailure).map((r) => r.failure)
11
+ }))));
12
+ const failuresOf = (exit) => Exit.isFailure(exit) ? exit.cause.reasons.filter(Cause.isFailReason).map((r) => r.error) : [];
8
13
  describe("SSE.fromBytes", () => {
9
14
  it("parses a single complete event", async () => {
10
15
  globalExpect(await collect(fromBytes(bytesOf("event: foo\ndata: hello\n\n")))).toEqual([{
@@ -36,8 +41,53 @@ describe("SSE.fromBytes", () => {
36
41
  data: "x"
37
42
  }]);
38
43
  });
39
- it("flushes a trailing event without a closing blank line", async () => {
40
- globalExpect(await collect(fromBytes(bytesOf("data: tail")))).toEqual([{ data: "tail" }]);
44
+ it("parses a data field with no space after the colon", async () => {
45
+ globalExpect(await collect(fromBytes(bytesOf("data:x\n\n")))).toEqual([{ data: "x" }]);
46
+ });
47
+ it("ignores an id containing a NUL character", async () => {
48
+ globalExpect(await collect(fromBytes(bytesOf("id: a\0b\ndata: x\n\n")))).toEqual([{ data: "x" }]);
49
+ });
50
+ it("discards a trailing event without a closing blank line", async () => {
51
+ globalExpect(await collect(fromBytes(bytesOf("data: tail")))).toEqual([]);
52
+ });
53
+ it("discards buffered partial data when the upstream fails mid-stream", async () => {
54
+ const { seen, failures } = await runUntilEnd(fromBytes(Stream.concat(bytesOf("data: good\n\ndata: partial-tr"), Stream.fail("boom"))));
55
+ globalExpect(seen).toEqual([{ data: "good" }]);
56
+ globalExpect(failures).toEqual(["boom"]);
57
+ });
58
+ it("discards a partial multi-byte char in the text decoder when the upstream fails", async () => {
59
+ const partialSquid = enc.encode("data: 🦑\n\n").slice(0, 8);
60
+ const { seen, failures } = await runUntilEnd(fromBytes(Stream.concat(Stream.fromIterable([enc.encode("data: ok\n\n"), partialSquid]), Stream.fail("boom"))));
61
+ globalExpect(seen).toEqual([{ data: "ok" }]);
62
+ globalExpect(failures).toEqual(["boom"]);
63
+ });
64
+ it("discards buffered partial data when the stream is interrupted", async () => {
65
+ const { seen, exit } = await Effect.runPromise(Effect.gen(function* () {
66
+ const seen = [];
67
+ const latch = yield* Deferred.make();
68
+ const hanging = Stream.concat(bytesOf("data: good\n\ndata: partial"), Stream.never);
69
+ const fiber = yield* Stream.runForEach(fromBytes(hanging), (e) => Effect.sync(() => {
70
+ seen.push(e);
71
+ }).pipe(Effect.andThen(Deferred.succeed(latch, void 0)))).pipe(Effect.forkChild);
72
+ yield* Deferred.await(latch);
73
+ yield* Fiber.interrupt(fiber);
74
+ return {
75
+ seen,
76
+ exit: yield* Fiber.await(fiber)
77
+ };
78
+ }));
79
+ globalExpect(seen).toEqual([{ data: "good" }]);
80
+ globalExpect(Exit.isFailure(exit)).toBe(true);
81
+ globalExpect(failuresOf(exit)).toEqual([]);
82
+ });
83
+ it("ignores retry directives", async () => {
84
+ globalExpect(await collect(fromBytes(bytesOf("retry: 3000\n\ndata: a\n\n")))).toEqual([{ data: "a" }]);
85
+ });
86
+ it("treats an explicit 'event: message' as the default event", async () => {
87
+ globalExpect(await collect(fromBytes(bytesOf("event: message\ndata: x\n\n")))).toEqual([{ data: "x" }]);
88
+ });
89
+ it("does not dispatch events with no data", async () => {
90
+ globalExpect(await collect(fromBytes(bytesOf("event: ping\n\ndata: x\n\n")))).toEqual([{ data: "x" }]);
41
91
  });
42
92
  it("ignores empty blocks between events", async () => {
43
93
  globalExpect(await collect(fromBytes(bytesOf("data: a\n\n\n\ndata: b\n\n")))).toEqual([{ data: "a" }, { data: "b" }]);
@@ -1 +1 @@
1
- {"version":3,"file":"SSE.test.mjs","names":["SSE.fromBytes","SSE.toBytes"],"sources":["../../src/streaming/SSE.test.ts"],"sourcesContent":["import { Effect, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport * as SSE from \"./SSE.js\"\n\nconst enc = new TextEncoder()\nconst bytesOf = (...chunks: ReadonlyArray<string>) =>\n Stream.fromIterable(chunks.map((c) => enc.encode(c)))\n\nconst collect = <A, E>(s: Stream.Stream<A, E>) => Effect.runPromise(Stream.runCollect(s))\n\ndescribe(\"SSE.fromBytes\", () => {\n it(\"parses a single complete event\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"event: foo\\ndata: hello\\n\\n\")))\n expect(out).toEqual([{ event: \"foo\", data: \"hello\" }])\n })\n\n it(\"joins multiple data lines with \\\\n\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"data: line1\\ndata: line2\\ndata: line3\\n\\n\")))\n expect(out).toEqual([{ data: \"line1\\nline2\\nline3\" }])\n })\n\n it(\"handles events split across chunk boundaries\", async () => {\n const out = await collect(\n SSE.fromBytes(bytesOf(\"event: split\\nda\", \"ta: hi\\n\", \"\\nevent: next\\ndata: x\\n\\n\")),\n )\n expect(out).toEqual([\n { event: \"split\", data: \"hi\" },\n { event: \"next\", data: \"x\" },\n ])\n })\n\n it(\"handles CRLF line endings\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"event: a\\r\\ndata: b\\r\\n\\r\\n\")))\n expect(out).toEqual([{ event: \"a\", data: \"b\" }])\n })\n\n it(\"preserves id and skips comment lines\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\": ping\\nid: 42\\ndata: x\\n\\n\")))\n expect(out).toEqual([{ id: \"42\", data: \"x\" }])\n })\n\n it(\"flushes a trailing event without a closing blank line\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"data: tail\")))\n expect(out).toEqual([{ data: \"tail\" }])\n })\n\n it(\"ignores empty blocks between events\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"data: a\\n\\n\\n\\ndata: b\\n\\n\")))\n expect(out).toEqual([{ data: \"a\" }, { data: \"b\" }])\n })\n\n it(\"handles a UTF-8 multi-byte char split across chunks\", async () => {\n // \"🦑\" is 0xF0 0x9F 0xA6 0x91. Split between bytes 2 and 3.\n const squidBytes = enc.encode(\"data: 🦑\\n\\n\")\n const a = squidBytes.slice(0, 8) // \"data: \" + first 2 bytes of squid\n const b = squidBytes.slice(8) // remaining squid bytes + \"\\n\\n\"\n const out = await collect(SSE.fromBytes(Stream.fromIterable([a, b])))\n expect(out).toEqual([{ data: \"🦑\" }])\n })\n})\n\ndescribe(\"SSE.toBytes round-trip\", () => {\n it(\"re-parses what it serializes\", async () => {\n const events: ReadonlyArray<SSE.Event> = [\n { event: \"a\", data: \"hello\" },\n { data: \"multi\\nline\" },\n { event: \"b\", id: \"7\", data: \"x\" },\n ]\n const reparsed = await collect(Stream.fromIterable(events).pipe(SSE.toBytes, SSE.fromBytes))\n expect(reparsed).toEqual(events)\n })\n})\n"],"mappings":";;;;AAIA,MAAM,MAAM,IAAI,aAAa;AAC7B,MAAM,WAAW,GAAG,WAClB,OAAO,aAAa,OAAO,KAAK,MAAM,IAAI,OAAO,EAAE,CAAC,CAAC;AAEvD,MAAM,WAAiB,MAA2B,OAAO,WAAW,OAAO,WAAW,EAAE,CAAC;AAEzF,SAAS,uBAAuB;AAC9B,IAAG,kCAAkC,YAAY;AAE/C,eAAO,MADW,QAAQA,UAAc,QAAQ,8BAA8B,CAAC,CAAC,CACrE,CAAC,QAAQ,CAAC;GAAE,OAAO;GAAO,MAAM;GAAS,CAAC,CAAC;GACtD;AAEF,IAAG,sCAAsC,YAAY;AAEnD,eAAO,MADW,QAAQA,UAAc,QAAQ,4CAA4C,CAAC,CAAC,CACnF,CAAC,QAAQ,CAAC,EAAE,MAAM,uBAAuB,CAAC,CAAC;GACtD;AAEF,IAAG,gDAAgD,YAAY;AAI7D,eAAO,MAHW,QAChBA,UAAc,QAAQ,oBAAoB,YAAY,6BAA6B,CAAC,CACrF,CACU,CAAC,QAAQ,CAClB;GAAE,OAAO;GAAS,MAAM;GAAM,EAC9B;GAAE,OAAO;GAAQ,MAAM;GAAK,CAC7B,CAAC;GACF;AAEF,IAAG,6BAA6B,YAAY;AAE1C,eAAO,MADW,QAAQA,UAAc,QAAQ,8BAA8B,CAAC,CAAC,CACrE,CAAC,QAAQ,CAAC;GAAE,OAAO;GAAK,MAAM;GAAK,CAAC,CAAC;GAChD;AAEF,IAAG,wCAAwC,YAAY;AAErD,eAAO,MADW,QAAQA,UAAc,QAAQ,8BAA8B,CAAC,CAAC,CACrE,CAAC,QAAQ,CAAC;GAAE,IAAI;GAAM,MAAM;GAAK,CAAC,CAAC;GAC9C;AAEF,IAAG,yDAAyD,YAAY;AAEtE,eAAO,MADW,QAAQA,UAAc,QAAQ,aAAa,CAAC,CAAC,CACpD,CAAC,QAAQ,CAAC,EAAE,MAAM,QAAQ,CAAC,CAAC;GACvC;AAEF,IAAG,uCAAuC,YAAY;AAEpD,eAAO,MADW,QAAQA,UAAc,QAAQ,6BAA6B,CAAC,CAAC,CACpE,CAAC,QAAQ,CAAC,EAAE,MAAM,KAAK,EAAE,EAAE,MAAM,KAAK,CAAC,CAAC;GACnD;AAEF,IAAG,uDAAuD,YAAY;EAEpE,MAAM,aAAa,IAAI,OAAO,eAAe;EAC7C,MAAM,IAAI,WAAW,MAAM,GAAG,EAAE;EAChC,MAAM,IAAI,WAAW,MAAM,EAAE;AAE7B,eAAO,MADW,QAAQA,UAAc,OAAO,aAAa,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAC1D,CAAC,QAAQ,CAAC,EAAE,MAAM,MAAM,CAAC,CAAC;GACrC;EACF;AAEF,SAAS,gCAAgC;AACvC,IAAG,gCAAgC,YAAY;EAC7C,MAAM,SAAmC;GACvC;IAAE,OAAO;IAAK,MAAM;IAAS;GAC7B,EAAE,MAAM,eAAe;GACvB;IAAE,OAAO;IAAK,IAAI;IAAK,MAAM;IAAK;GACnC;AAED,eAAO,MADgB,QAAQ,OAAO,aAAa,OAAO,CAAC,KAAKC,SAAaD,UAAc,CAAC,CAC5E,CAAC,QAAQ,OAAO;GAChC;EACF"}
1
+ {"version":3,"file":"SSE.test.mjs","names":["SSE.fromBytes","SSE.toBytes"],"sources":["../../src/streaming/SSE.test.ts"],"sourcesContent":["import { Cause, Deferred, Effect, Exit, Fiber, Result, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport * as SSE from \"./SSE.js\"\n\nconst enc = new TextEncoder()\nconst bytesOf = (...chunks: ReadonlyArray<string>) =>\n Stream.fromIterable(chunks.map((c) => enc.encode(c)))\n\nconst collect = <A, E>(s: Stream.Stream<A, E>) => Effect.runPromise(Stream.runCollect(s))\n\nconst runUntilEnd = <A, E>(s: Stream.Stream<A, E>) =>\n Effect.runPromise(\n s.pipe(\n Stream.result,\n Stream.runCollect,\n Effect.map((results) => ({\n seen: results.filter(Result.isSuccess).map((r) => r.success),\n failures: results.filter(Result.isFailure).map((r) => r.failure),\n })),\n ),\n )\n\nconst failuresOf = (exit: Exit.Exit<unknown, unknown>): ReadonlyArray<unknown> =>\n Exit.isFailure(exit) ? exit.cause.reasons.filter(Cause.isFailReason).map((r) => r.error) : []\n\ndescribe(\"SSE.fromBytes\", () => {\n it(\"parses a single complete event\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"event: foo\\ndata: hello\\n\\n\")))\n expect(out).toEqual([{ event: \"foo\", data: \"hello\" }])\n })\n\n it(\"joins multiple data lines with \\\\n\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"data: line1\\ndata: line2\\ndata: line3\\n\\n\")))\n expect(out).toEqual([{ data: \"line1\\nline2\\nline3\" }])\n })\n\n it(\"handles events split across chunk boundaries\", async () => {\n const out = await collect(\n SSE.fromBytes(bytesOf(\"event: split\\nda\", \"ta: hi\\n\", \"\\nevent: next\\ndata: x\\n\\n\")),\n )\n expect(out).toEqual([\n { event: \"split\", data: \"hi\" },\n { event: \"next\", data: \"x\" },\n ])\n })\n\n it(\"handles CRLF line endings\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"event: a\\r\\ndata: b\\r\\n\\r\\n\")))\n expect(out).toEqual([{ event: \"a\", data: \"b\" }])\n })\n\n it(\"preserves id and skips comment lines\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\": ping\\nid: 42\\ndata: x\\n\\n\")))\n expect(out).toEqual([{ id: \"42\", data: \"x\" }])\n })\n\n it(\"parses a data field with no space after the colon\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"data:x\\n\\n\")))\n expect(out).toEqual([{ data: \"x\" }])\n })\n\n it(\"ignores an id containing a NUL character\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"id: a\\u0000b\\ndata: x\\n\\n\")))\n expect(out).toEqual([{ data: \"x\" }])\n })\n\n it(\"discards a trailing event without a closing blank line\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"data: tail\")))\n expect(out).toEqual([])\n })\n\n it(\"discards buffered partial data when the upstream fails mid-stream\", async () => {\n const failing = Stream.concat(\n bytesOf(\"data: good\\n\\ndata: partial-tr\"),\n Stream.fail(\"boom\" as const),\n )\n const { seen, failures } = await runUntilEnd(SSE.fromBytes(failing))\n expect(seen).toEqual([{ data: \"good\" }])\n expect(failures).toEqual([\"boom\"])\n })\n\n it(\"discards a partial multi-byte char in the text decoder when the upstream fails\", async () => {\n const partialSquid = enc.encode(\"data: 🦑\\n\\n\").slice(0, 8)\n const failing = Stream.concat(\n Stream.fromIterable([enc.encode(\"data: ok\\n\\n\"), partialSquid]),\n Stream.fail(\"boom\" as const),\n )\n const { seen, failures } = await runUntilEnd(SSE.fromBytes(failing))\n expect(seen).toEqual([{ data: \"ok\" }])\n expect(failures).toEqual([\"boom\"])\n })\n\n it(\"discards buffered partial data when the stream is interrupted\", async () => {\n const { seen, exit } = await Effect.runPromise(\n Effect.gen(function* () {\n const seen: Array<SSE.Event> = []\n const latch = yield* Deferred.make<void>()\n const hanging = Stream.concat(bytesOf(\"data: good\\n\\ndata: partial\"), Stream.never)\n const fiber = yield* Stream.runForEach(SSE.fromBytes(hanging), (e) =>\n Effect.sync(() => {\n seen.push(e)\n }).pipe(Effect.andThen(Deferred.succeed(latch, void 0))),\n ).pipe(Effect.forkChild)\n yield* Deferred.await(latch)\n yield* Fiber.interrupt(fiber)\n const exit = yield* Fiber.await(fiber)\n return { seen, exit }\n }),\n )\n expect(seen).toEqual([{ data: \"good\" }])\n expect(Exit.isFailure(exit)).toBe(true)\n expect(failuresOf(exit)).toEqual([])\n })\n\n it(\"ignores retry directives\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"retry: 3000\\n\\ndata: a\\n\\n\")))\n expect(out).toEqual([{ data: \"a\" }])\n })\n\n it(\"treats an explicit 'event: message' as the default event\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"event: message\\ndata: x\\n\\n\")))\n expect(out).toEqual([{ data: \"x\" }])\n })\n\n it(\"does not dispatch events with no data\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"event: ping\\n\\ndata: x\\n\\n\")))\n expect(out).toEqual([{ data: \"x\" }])\n })\n\n it(\"ignores empty blocks between events\", async () => {\n const out = await collect(SSE.fromBytes(bytesOf(\"data: a\\n\\n\\n\\ndata: b\\n\\n\")))\n expect(out).toEqual([{ data: \"a\" }, { data: \"b\" }])\n })\n\n it(\"handles a UTF-8 multi-byte char split across chunks\", async () => {\n // \"🦑\" is 0xF0 0x9F 0xA6 0x91. Split between bytes 2 and 3.\n const squidBytes = enc.encode(\"data: 🦑\\n\\n\")\n const a = squidBytes.slice(0, 8) // \"data: \" + first 2 bytes of squid\n const b = squidBytes.slice(8) // remaining squid bytes + \"\\n\\n\"\n const out = await collect(SSE.fromBytes(Stream.fromIterable([a, b])))\n expect(out).toEqual([{ data: \"🦑\" }])\n })\n})\n\ndescribe(\"SSE.toBytes round-trip\", () => {\n it(\"re-parses what it serializes\", async () => {\n const events: ReadonlyArray<SSE.Event> = [\n { event: \"a\", data: \"hello\" },\n { data: \"multi\\nline\" },\n { event: \"b\", id: \"7\", data: \"x\" },\n ]\n const reparsed = await collect(Stream.fromIterable(events).pipe(SSE.toBytes, SSE.fromBytes))\n expect(reparsed).toEqual(events)\n })\n})\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { c as decodeJsonLines, d as parseJson, i as StructuredFormat, l as decodeJsonLinesRecoverable, n as JsonParseError, o as StructuredSchema, r as StructuredDecodeError, s as decode, t as DecodeIssue, u as fromEffectSchema } from "../StructuredFormat-CMJFD3sn.mjs";
1
+ import { c as decodeJsonLines, d as parseJson, i as StructuredFormat, l as decodeJsonLinesRecoverable, n as JsonParseError, o as StructuredSchema, r as StructuredDecodeError, s as decode, t as DecodeIssue, u as fromEffectSchema } from "../StructuredFormat-DHaUJBdd.mjs";
2
2
  export { DecodeIssue, JsonParseError, StructuredDecodeError, StructuredFormat, StructuredSchema, decode, decodeJsonLines, decodeJsonLinesRecoverable, fromEffectSchema, parseJson };
@@ -1,5 +1,5 @@
1
1
  import { JsonParseError, StructuredDecodeError, decodeJsonLines, decodeJsonLinesRecoverable, fromEffectSchema } from "./StructuredFormat.mjs";
2
- import { a as describe, n as globalExpect, o as it } from "../dist-B04dlFHg.mjs";
2
+ import { c as it, n as globalExpect, o as describe } from "../dist-CcRV3-A5.mjs";
3
3
  import { Effect, Exit, Filter, Result, Schema, Stream } from "effect";
4
4
  //#region src/structured-format/StructuredFormat.test.ts
5
5
  const itemFormat = fromEffectSchema(Schema.Struct({
@@ -1,4 +1,4 @@
1
- import { n as AudioChunk } from "../Audio-DDSKCkSe.mjs";
1
+ import { n as AudioChunk } from "../Audio-CR7CSljJ.mjs";
2
2
  import { CommonGenerateMusicRequest, CommonStreamGenerateMusicRequest, GenerateResult, MusicStreamEvent, audioEvent } from "../domain/Music.mjs";
3
3
  import { MusicGenerator, MusicInteractiveSession } from "../music-generator/MusicGenerator.mjs";
4
4
  import { Effect, Layer } from "effect";
@@ -1,4 +1,4 @@
1
- import { o as HistoryItem } from "../Items-CgKD6Npw.mjs";
1
+ import { o as HistoryItem } from "../Items-BWNu6VGp.mjs";
2
2
  import { Turn } from "../domain/Turn.mjs";
3
3
  import { LanguageModel, LanguageModelService } from "../language-model/LanguageModel.mjs";
4
4
  import { Duration, Effect, Layer } from "effect";
@@ -1,4 +1,4 @@
1
- import { n as AudioChunk, t as AudioBlob } from "../Audio-DDSKCkSe.mjs";
1
+ import { n as AudioChunk, t as AudioBlob } from "../Audio-CR7CSljJ.mjs";
2
2
  import { CommonStreamSynthesizeRequest, CommonSynthesizeDialogueRequest, CommonSynthesizeRequest, MultiSpeakerTts, SpeechSynthesizer, TtsIncrementalText } from "../speech-synthesizer/SpeechSynthesizer.mjs";
3
3
  import { Effect, Layer } from "effect";
4
4
 
@@ -1,12 +1,12 @@
1
- import { v as ToolCall } from "../Items-CgKD6Npw.mjs";
2
- import { t as ToolResult } from "../ToolResult-BZd-cEWW.mjs";
3
- import { t as ToolEvent } from "../ToolEvent-DYrAUJGs.mjs";
4
- import { Data, Effect, Queue, Scope, Stream } from "effect";
1
+ import { v as ToolCall } from "../Items-BWNu6VGp.mjs";
2
+ import { t as ToolResult } from "../ToolResult-CQqKkcDa.mjs";
3
+ import { t as ToolEvent } from "../ToolEvent-nVijCrvm.mjs";
4
+ import { Data, Duration, Effect, Queue, Scope, Stream } from "effect";
5
5
  import * as _$effect_Unify0 from "effect/Unify";
6
6
 
7
7
  //#region src/tool/Approval.d.ts
8
8
  declare namespace Approval_d_exports {
9
- export { ApprovalDecision, ApprovalMapEntry, ToolCallPlan, Verdict, approvalRequested, approve, fromMap, fromQueue, reject, splitApprovalDecisions };
9
+ export { ApprovalDecision, ApprovalMapEntry, FromQueueOptions, ToolCallPlan, Verdict, approvalRequested, approve, fromMap, fromQueue, reject, splitApprovalDecisions };
10
10
  }
11
11
  type ToolCallPlan = {
12
12
  readonly approved: ReadonlyArray<ToolCall>;
@@ -84,12 +84,24 @@ type Verdict = {
84
84
  readonly decision: "approve" | "deny";
85
85
  readonly reason?: string;
86
86
  };
87
+ type FromQueueOptions = {
88
+ /**
89
+ * Resolve any still-pending gated call as `cancelled` once this elapses,
90
+ * so a lost or never-sent verdict cannot hang the decisions stream
91
+ * forever. Omit to wait indefinitely.
92
+ */
93
+ readonly timeout?: Duration.Input;
94
+ };
87
95
  /**
88
96
  * Queue-backed approval planner. Safe calls are returned immediately in
89
97
  * `approved`; gated calls emit `ApprovalRequested` events and later produce
90
98
  * one `ApprovalDecision` when their matching verdict arrives.
99
+ *
100
+ * The verdict queue is a single-consumer transport: run one approval round
101
+ * at a time (the router retires once every gated call of the round is
102
+ * resolved). For overlapping rounds, use one queue per round.
91
103
  */
92
- declare const fromQueue: (predicate: (call: ToolCall) => boolean, verdicts: Queue.Dequeue<Verdict>) => (calls: ReadonlyArray<ToolCall>) => Effect.Effect<{
104
+ declare const fromQueue: (predicate: (call: ToolCall) => boolean, verdicts: Queue.Dequeue<Verdict>, options?: FromQueueOptions) => (calls: ReadonlyArray<ToolCall>) => Effect.Effect<{
93
105
  readonly approved: ReadonlyArray<ToolCall>;
94
106
  readonly decisions: Stream.Stream<ApprovalDecision>;
95
107
  readonly approvalRequests: Stream.Stream<ToolEvent>;
@@ -102,5 +114,5 @@ type ApprovalMapEntry = {
102
114
  };
103
115
  declare const fromMap: (predicate: (call: ToolCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) => (calls: ReadonlyArray<ToolCall>) => ToolCallPlan;
104
116
  //#endregion
105
- export { ApprovalDecision, ApprovalMapEntry, ToolCallPlan, Verdict, approvalRequested, approve, fromMap, fromQueue, reject, splitApprovalDecisions, Approval_d_exports as t };
117
+ export { ApprovalDecision, ApprovalMapEntry, FromQueueOptions, ToolCallPlan, Verdict, approvalRequested, approve, fromMap, fromQueue, reject, splitApprovalDecisions, Approval_d_exports as t };
106
118
  //# sourceMappingURL=Approval.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Approval.d.mts","names":[],"sources":["../../src/tool/Approval.ts"],"mappings":";;;;;;;;;;KAYY,YAAA;EAAA,SACD,QAAA,EAAU,aAAA,CAAc,QAAA;EAAA,SACxB,QAAA,EAAU,aAAA,CAAc,UAAA;AAAA;AAAA,KAGvB,gBAAA,GAAmB,IAAA,CAAK,UAAA;EAClC,QAAA;IAAA,SAAqB,IAAA,EAAM,QAAA;EAAA;EAC3B,QAAA;IAAA,SAAqB,MAAA,EAAQ,UAAA;EAAA;AAAA;AAAA,cAGlB,gBAAA;EAAA;;mBAJgB,QAAA;EAAA;EAAA;;qBACE,UAAA;EAAA;EAAA;;mBADF,QAAA;EAAA;IAAA;;;qBACE,UAAA;EAAA;IAAA;;;;;;uBADF,QAAA;MAAA;MAAA;;yBACE,UAAA;MAAA;IAAA;;qBADF,QAAA;IAAA;MAAA;uBACE,UAAA;IAAA;;;;uBADF,QAAA;MAAA;MAAA;;yBACE,UAAA;MAAA;IAAA;;qBADF,QAAA;IAAA;MAAA;uBACE,UAAA;IAAA;;;cAKlB,OAAA,GAAW,IAAA,EAAM,QAAA,KAAW,gBAAA;AAAA,cAE5B,MAAA,GAAU,MAAA,EAAQ,UAAA,KAAa,gBAAA;AAAA,cAG/B,sBAAA,GAA0B,SAAA,EAAW,aAAA,CAAc,gBAAA,MAAoB,YAAA;AAAA,cASvE,iBAAA,GAAqB,IAAA,EAAM,QAAA,KAAW,SAAA;AAAA,KAWvC,OAAA;EAAA,SACD,OAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA;AAAA;;;;;;cAQE,SAAA,GACV,SAAA,GAAY,IAAA,EAAM,QAAA,cAAsB,QAAA,EAAU,KAAA,CAAM,OAAA,CAAQ,OAAA,OAE/D,KAAA,EAAO,aAAA,CAAc,QAAA,MACpB,MAAA,CAAO,MAAA;EAAA,SAEG,QAAA,EAAU,aAAA,CAAc,QAAA;EAAA,SACxB,SAAA,EAAW,MAAA,CAAO,MAAA,CAAO,gBAAA;EAAA,SACzB,gBAAA,EAAkB,MAAA,CAAO,MAAA,CAAO,SAAA;AAAA,UAG3C,KAAA,CAAM,KAAA;AAAA,KAkDE,gBAAA;EAAA,SACG,QAAA;AAAA;EAAA,SACA,QAAA;EAAA,SAA2B,MAAA;AAAA;AAAA,cAE7B,OAAA,GACV,SAAA,GAAY,IAAA,EAAM,QAAA,cAAsB,SAAA,EAAW,WAAA,SAAoB,gBAAA,OACvE,KAAA,EAAO,aAAA,CAAc,QAAA,MAAY,YAAA"}
1
+ {"version":3,"file":"Approval.d.mts","names":[],"sources":["../../src/tool/Approval.ts"],"mappings":";;;;;;;;;;KAYY,YAAA;EAAA,SACD,QAAA,EAAU,aAAA,CAAc,QAAA;EAAA,SACxB,QAAA,EAAU,aAAA,CAAc,UAAA;AAAA;AAAA,KAGvB,gBAAA,GAAmB,IAAA,CAAK,UAAA;EAClC,QAAA;IAAA,SAAqB,IAAA,EAAM,QAAA;EAAA;EAC3B,QAAA;IAAA,SAAqB,MAAA,EAAQ,UAAA;EAAA;AAAA;AAAA,cAGlB,gBAAA;EAAA;;mBAJgB,QAAA;EAAA;EAAA;;qBACE,UAAA;EAAA;EAAA;;mBADF,QAAA;EAAA;IAAA;;;qBACE,UAAA;EAAA;IAAA;;;;;;uBADF,QAAA;MAAA;MAAA;;yBACE,UAAA;MAAA;IAAA;;qBADF,QAAA;IAAA;MAAA;uBACE,UAAA;IAAA;;;;uBADF,QAAA;MAAA;MAAA;;yBACE,UAAA;MAAA;IAAA;;qBADF,QAAA;IAAA;MAAA;uBACE,UAAA;IAAA;;;cAKlB,OAAA,GAAW,IAAA,EAAM,QAAA,KAAW,gBAAA;AAAA,cAE5B,MAAA,GAAU,MAAA,EAAQ,UAAA,KAAa,gBAAA;AAAA,cAG/B,sBAAA,GAA0B,SAAA,EAAW,aAAA,CAAc,gBAAA,MAAoB,YAAA;AAAA,cASvE,iBAAA,GAAqB,IAAA,EAAM,QAAA,KAAW,SAAA;AAAA,KAWvC,OAAA;EAAA,SACD,OAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA;AAAA;AAAA,KAGC,gBAAA;;;;;;WAMD,OAAA,GAAU,QAAA,CAAS,KAAA;AAAA;;;;;;;;;;cAYjB,SAAA,GAET,SAAA,GAAY,IAAA,EAAM,QAAA,cAClB,QAAA,EAAU,KAAA,CAAM,OAAA,CAAQ,OAAA,GACxB,OAAA,GAAU,gBAAA,MAGV,KAAA,EAAO,aAAA,CAAc,QAAA,MACpB,MAAA,CAAO,MAAA;EAAA,SAEG,QAAA,EAAU,aAAA,CAAc,QAAA;EAAA,SACxB,SAAA,EAAW,MAAA,CAAO,MAAA,CAAO,gBAAA;EAAA,SACzB,gBAAA,EAAkB,MAAA,CAAO,MAAA,CAAO,SAAA;AAAA,UAG3C,KAAA,CAAM,KAAA;AAAA,KA8EE,gBAAA;EAAA,SACG,QAAA;AAAA;EAAA,SACA,QAAA;EAAA,SAA2B,MAAA;AAAA;AAAA,cAE7B,OAAA,GACV,SAAA,GAAY,IAAA,EAAM,QAAA,cAAsB,SAAA,EAAW,WAAA,SAAoB,gBAAA,OACvE,KAAA,EAAO,aAAA,CAAc,QAAA,MAAY,YAAA"}
@@ -1,7 +1,7 @@
1
1
  import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
2
2
  import { cancelled, denied } from "./ToolResult.mjs";
3
3
  import { ToolEvent } from "./ToolEvent.mjs";
4
- import { Data, Deferred, Effect, Queue, Stream } from "effect";
4
+ import { Data, Deferred, Effect, Queue, Ref, Stream } from "effect";
5
5
  //#region src/tool/Approval.ts
6
6
  /**
7
7
  * Approval helpers for the two transport flavors.
@@ -41,23 +41,30 @@ const approvalRequested = (call) => ToolEvent.ApprovalRequested({
41
41
  * Queue-backed approval planner. Safe calls are returned immediately in
42
42
  * `approved`; gated calls emit `ApprovalRequested` events and later produce
43
43
  * one `ApprovalDecision` when their matching verdict arrives.
44
+ *
45
+ * The verdict queue is a single-consumer transport: run one approval round
46
+ * at a time (the router retires once every gated call of the round is
47
+ * resolved). For overlapping rounds, use one queue per round.
44
48
  */
45
- const fromQueue = (predicate, verdicts) => (calls) => Effect.gen(function* () {
49
+ const fromQueue = (predicate, verdicts, options) => (calls) => Effect.gen(function* () {
46
50
  const gated = calls.filter(predicate);
47
51
  const approved = calls.filter((call) => !predicate(call));
48
52
  const entries = yield* Effect.forEach(gated, (call) => Deferred.make().pipe(Effect.map((d) => [call.call_id, d])));
49
53
  const deferreds = new Map(entries);
50
- yield* Effect.forkScoped(Effect.forever(Effect.gen(function* () {
51
- const v = yield* Queue.take(verdicts);
52
- const d = deferreds.get(v.call_id);
53
- if (d !== void 0) yield* Deferred.succeed(d, v);
54
- })));
54
+ const callById = new Map(gated.map((c) => [c.call_id, c]));
55
+ const outstanding = yield* Ref.make(gated.length);
56
+ const resolve = (call, decision) => Deferred.succeed(deferreds.get(call.call_id), decision).pipe(Effect.flatMap((won) => won ? Ref.update(outstanding, (n) => n - 1) : Effect.void));
57
+ const route = Effect.suspend(() => Ref.get(outstanding).pipe(Effect.flatMap((n) => n <= 0 ? Effect.void : Queue.take(verdicts).pipe(Effect.flatMap((v) => {
58
+ const call = callById.get(v.call_id);
59
+ return (call === void 0 ? Effect.void : resolve(call, v.decision === "approve" ? approve(call) : reject(denied(call, v.reason)))).pipe(Effect.andThen(route));
60
+ })))));
61
+ if (gated.length > 0) {
62
+ yield* Effect.forkScoped(route);
63
+ if (options?.timeout !== void 0) yield* Effect.forkScoped(Effect.sleep(options.timeout).pipe(Effect.andThen(Effect.forEach(gated, (call) => resolve(call, reject(cancelled(call, "approval timed out")))))));
64
+ }
55
65
  return {
56
66
  approved,
57
- decisions: Stream.fromIterable(gated).pipe(Stream.flatMap((call) => {
58
- const d = deferreds.get(call.call_id);
59
- return Stream.fromEffect(Deferred.await(d).pipe(Effect.map((v) => v.decision === "approve" ? approve(call) : reject(denied(call, v.reason)))));
60
- }, { concurrency: "unbounded" })),
67
+ decisions: Stream.fromIterable(gated).pipe(Stream.flatMap((call) => Stream.fromEffect(Deferred.await(deferreds.get(call.call_id))), { concurrency: "unbounded" })),
61
68
  approvalRequests: Stream.fromIterable(gated.map(approvalRequested))
62
69
  };
63
70
  });
@@ -1 +1 @@
1
- {"version":3,"file":"Approval.mjs","names":[],"sources":["../../src/tool/Approval.ts"],"sourcesContent":["/**\n * Approval helpers for the two transport flavors.\n *\n * These helpers only decide which calls are approved and which synthetic\n * results must be returned to the model. Tool execution stays explicit at\n * the recipe boundary via `Toolkit.run`.\n */\nimport { Data, Deferred, Effect, Queue, Scope, Stream } from \"effect\"\nimport type { ToolCall } from \"../domain/Items.js\"\nimport { type ToolResult, cancelled, denied } from \"./ToolResult.js\"\nimport { ToolEvent } from \"./ToolEvent.js\"\n\nexport type ToolCallPlan = {\n readonly approved: ReadonlyArray<ToolCall>\n readonly rejected: ReadonlyArray<ToolResult>\n}\n\nexport type ApprovalDecision = Data.TaggedEnum<{\n Approved: { readonly call: ToolCall }\n Rejected: { readonly result: ToolResult }\n}>\n\nexport const ApprovalDecision = Data.taggedEnum<ApprovalDecision>()\n\nexport const approve = (call: ToolCall): ApprovalDecision => ApprovalDecision.Approved({ call })\n\nexport const reject = (result: ToolResult): ApprovalDecision =>\n ApprovalDecision.Rejected({ result })\n\nexport const splitApprovalDecisions = (decisions: ReadonlyArray<ApprovalDecision>): ToolCallPlan =>\n decisions.reduce<ToolCallPlan>(\n (acc, decision) =>\n decision._tag === \"Approved\"\n ? { ...acc, approved: [...acc.approved, decision.call] }\n : { ...acc, rejected: [...acc.rejected, decision.result] },\n { approved: [], rejected: [] },\n )\n\nexport const approvalRequested = (call: ToolCall): ToolEvent =>\n ToolEvent.ApprovalRequested({\n call_id: call.call_id,\n tool: call.name,\n arguments: call.arguments,\n })\n\n// ---------------------------------------------------------------------------\n// Verdict queue (WebSocket-style transport).\n// ---------------------------------------------------------------------------\n\nexport type Verdict = {\n readonly call_id: string\n readonly decision: \"approve\" | \"deny\"\n readonly reason?: string\n}\n\n/**\n * Queue-backed approval planner. Safe calls are returned immediately in\n * `approved`; gated calls emit `ApprovalRequested` events and later produce\n * one `ApprovalDecision` when their matching verdict arrives.\n */\nexport const fromQueue =\n (predicate: (call: ToolCall) => boolean, verdicts: Queue.Dequeue<Verdict>) =>\n (\n calls: ReadonlyArray<ToolCall>,\n ): Effect.Effect<\n {\n readonly approved: ReadonlyArray<ToolCall>\n readonly decisions: Stream.Stream<ApprovalDecision>\n readonly approvalRequests: Stream.Stream<ToolEvent>\n },\n never,\n Scope.Scope\n > =>\n Effect.gen(function* () {\n const gated = calls.filter(predicate)\n const approved = calls.filter((call) => !predicate(call))\n\n const entries = yield* Effect.forEach(gated, (call) =>\n Deferred.make<Verdict>().pipe(Effect.map((d) => [call.call_id, d] as const)),\n )\n const deferreds: ReadonlyMap<string, Deferred.Deferred<Verdict>> = new Map(entries)\n\n // Router is forked into the surrounding Scope so it lives as long\n // as the consumer is pulling events. Recipes typically supply the\n // scope by wrapping the events construction in `Stream.unwrap`.\n yield* Effect.forkScoped(\n Effect.forever(\n Effect.gen(function* () {\n const v = yield* Queue.take(verdicts)\n const d = deferreds.get(v.call_id)\n if (d !== undefined) yield* Deferred.succeed(d, v)\n }),\n ),\n )\n\n const decisions = Stream.fromIterable(gated).pipe(\n Stream.flatMap(\n (call) => {\n const d = deferreds.get(call.call_id)!\n return Stream.fromEffect(\n Deferred.await(d).pipe(\n Effect.map((v) =>\n v.decision === \"approve\" ? approve(call) : reject(denied(call, v.reason)),\n ),\n ),\n )\n },\n { concurrency: \"unbounded\" },\n ),\n )\n\n const approvalRequests = Stream.fromIterable<ToolEvent>(gated.map(approvalRequested))\n\n return { approved, decisions, approvalRequests }\n })\n\n// ---------------------------------------------------------------------------\n// Approval map (HTTP-style transport). Verdicts arrive synchronously\n// bundled in the request payload. Missing entries → cancelled.\n// ---------------------------------------------------------------------------\n\nexport type ApprovalMapEntry =\n | { readonly decision: \"approve\" }\n | { readonly decision: \"deny\"; readonly reason?: string }\n\nexport const fromMap =\n (predicate: (call: ToolCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) =>\n (calls: ReadonlyArray<ToolCall>): ToolCallPlan =>\n splitApprovalDecisions(\n calls.map((call) => {\n if (!predicate(call)) return approve(call)\n const v = approvals.get(call.call_id)\n if (v === undefined) return reject(cancelled(call))\n return v.decision === \"approve\" ? approve(call) : reject(denied(call, v.reason))\n }),\n )\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,MAAa,mBAAmB,KAAK,YAA8B;AAEnE,MAAa,WAAW,SAAqC,iBAAiB,SAAS,EAAE,MAAM,CAAC;AAEhG,MAAa,UAAU,WACrB,iBAAiB,SAAS,EAAE,QAAQ,CAAC;AAEvC,MAAa,0BAA0B,cACrC,UAAU,QACP,KAAK,aACJ,SAAS,SAAS,aACd;CAAE,GAAG;CAAK,UAAU,CAAC,GAAG,IAAI,UAAU,SAAS,KAAK;CAAE,GACtD;CAAE,GAAG;CAAK,UAAU,CAAC,GAAG,IAAI,UAAU,SAAS,OAAO;CAAE,EAC9D;CAAE,UAAU,EAAE;CAAE,UAAU,EAAE;CAAE,CAC/B;AAEH,MAAa,qBAAqB,SAChC,UAAU,kBAAkB;CAC1B,SAAS,KAAK;CACd,MAAM,KAAK;CACX,WAAW,KAAK;CACjB,CAAC;;;;;;AAiBJ,MAAa,aACV,WAAwC,cAEvC,UAUA,OAAO,IAAI,aAAa;CACtB,MAAM,QAAQ,MAAM,OAAO,UAAU;CACrC,MAAM,WAAW,MAAM,QAAQ,SAAS,CAAC,UAAU,KAAK,CAAC;CAEzD,MAAM,UAAU,OAAO,OAAO,QAAQ,QAAQ,SAC5C,SAAS,MAAe,CAAC,KAAK,OAAO,KAAK,MAAM,CAAC,KAAK,SAAS,EAAE,CAAU,CAAC,CAC7E;CACD,MAAM,YAA6D,IAAI,IAAI,QAAQ;AAKnF,QAAO,OAAO,WACZ,OAAO,QACL,OAAO,IAAI,aAAa;EACtB,MAAM,IAAI,OAAO,MAAM,KAAK,SAAS;EACrC,MAAM,IAAI,UAAU,IAAI,EAAE,QAAQ;AAClC,MAAI,MAAM,KAAA,EAAW,QAAO,SAAS,QAAQ,GAAG,EAAE;GAClD,CACH,CACF;AAoBD,QAAO;EAAE;EAAU,WAlBD,OAAO,aAAa,MAAM,CAAC,KAC3C,OAAO,SACJ,SAAS;GACR,MAAM,IAAI,UAAU,IAAI,KAAK,QAAQ;AACrC,UAAO,OAAO,WACZ,SAAS,MAAM,EAAE,CAAC,KAChB,OAAO,KAAK,MACV,EAAE,aAAa,YAAY,QAAQ,KAAK,GAAG,OAAO,OAAO,MAAM,EAAE,OAAO,CAAC,CAC1E,CACF,CACF;KAEH,EAAE,aAAa,aAAa,CAC7B,CAKyB;EAAE,kBAFL,OAAO,aAAwB,MAAM,IAAI,kBAAkB,CAEtC;EAAE;EAChD;AAWN,MAAa,WACV,WAAwC,eACxC,UACC,uBACE,MAAM,KAAK,SAAS;AAClB,KAAI,CAAC,UAAU,KAAK,CAAE,QAAO,QAAQ,KAAK;CAC1C,MAAM,IAAI,UAAU,IAAI,KAAK,QAAQ;AACrC,KAAI,MAAM,KAAA,EAAW,QAAO,OAAO,UAAU,KAAK,CAAC;AACnD,QAAO,EAAE,aAAa,YAAY,QAAQ,KAAK,GAAG,OAAO,OAAO,MAAM,EAAE,OAAO,CAAC;EAChF,CACH"}
1
+ {"version":3,"file":"Approval.mjs","names":[],"sources":["../../src/tool/Approval.ts"],"sourcesContent":["/**\n * Approval helpers for the two transport flavors.\n *\n * These helpers only decide which calls are approved and which synthetic\n * results must be returned to the model. Tool execution stays explicit at\n * the recipe boundary via `Toolkit.run`.\n */\nimport { Data, Deferred, type Duration, Effect, Queue, Ref, Scope, Stream } from \"effect\"\nimport type { ToolCall } from \"../domain/Items.js\"\nimport { type ToolResult, cancelled, denied } from \"./ToolResult.js\"\nimport { ToolEvent } from \"./ToolEvent.js\"\n\nexport type ToolCallPlan = {\n readonly approved: ReadonlyArray<ToolCall>\n readonly rejected: ReadonlyArray<ToolResult>\n}\n\nexport type ApprovalDecision = Data.TaggedEnum<{\n Approved: { readonly call: ToolCall }\n Rejected: { readonly result: ToolResult }\n}>\n\nexport const ApprovalDecision = Data.taggedEnum<ApprovalDecision>()\n\nexport const approve = (call: ToolCall): ApprovalDecision => ApprovalDecision.Approved({ call })\n\nexport const reject = (result: ToolResult): ApprovalDecision =>\n ApprovalDecision.Rejected({ result })\n\nexport const splitApprovalDecisions = (decisions: ReadonlyArray<ApprovalDecision>): ToolCallPlan =>\n decisions.reduce<ToolCallPlan>(\n (acc, decision) =>\n decision._tag === \"Approved\"\n ? { ...acc, approved: [...acc.approved, decision.call] }\n : { ...acc, rejected: [...acc.rejected, decision.result] },\n { approved: [], rejected: [] },\n )\n\nexport const approvalRequested = (call: ToolCall): ToolEvent =>\n ToolEvent.ApprovalRequested({\n call_id: call.call_id,\n tool: call.name,\n arguments: call.arguments,\n })\n\n// ---------------------------------------------------------------------------\n// Verdict queue (WebSocket-style transport).\n// ---------------------------------------------------------------------------\n\nexport type Verdict = {\n readonly call_id: string\n readonly decision: \"approve\" | \"deny\"\n readonly reason?: string\n}\n\nexport type FromQueueOptions = {\n /**\n * Resolve any still-pending gated call as `cancelled` once this elapses,\n * so a lost or never-sent verdict cannot hang the decisions stream\n * forever. Omit to wait indefinitely.\n */\n readonly timeout?: Duration.Input\n}\n\n/**\n * Queue-backed approval planner. Safe calls are returned immediately in\n * `approved`; gated calls emit `ApprovalRequested` events and later produce\n * one `ApprovalDecision` when their matching verdict arrives.\n *\n * The verdict queue is a single-consumer transport: run one approval round\n * at a time (the router retires once every gated call of the round is\n * resolved). For overlapping rounds, use one queue per round.\n */\nexport const fromQueue =\n (\n predicate: (call: ToolCall) => boolean,\n verdicts: Queue.Dequeue<Verdict>,\n options?: FromQueueOptions,\n ) =>\n (\n calls: ReadonlyArray<ToolCall>,\n ): Effect.Effect<\n {\n readonly approved: ReadonlyArray<ToolCall>\n readonly decisions: Stream.Stream<ApprovalDecision>\n readonly approvalRequests: Stream.Stream<ToolEvent>\n },\n never,\n Scope.Scope\n > =>\n Effect.gen(function* () {\n const gated = calls.filter(predicate)\n const approved = calls.filter((call) => !predicate(call))\n\n const entries = yield* Effect.forEach(gated, (call) =>\n Deferred.make<ApprovalDecision>().pipe(Effect.map((d) => [call.call_id, d] as const)),\n )\n const deferreds: ReadonlyMap<string, Deferred.Deferred<ApprovalDecision>> = new Map(entries)\n const callById: ReadonlyMap<string, ToolCall> = new Map(gated.map((c) => [c.call_id, c]))\n const outstanding = yield* Ref.make(gated.length)\n\n // Complete a deferred once; decrement only on the winning transition so\n // a late verdict for an already-resolved call is a no-op.\n const resolve = (call: ToolCall, decision: ApprovalDecision) =>\n Deferred.succeed(deferreds.get(call.call_id)!, decision).pipe(\n Effect.flatMap((won) => (won ? Ref.update(outstanding, (n) => n - 1) : Effect.void)),\n )\n\n // Router retires once every gated call is resolved (by verdict or\n // timeout), so sequential rounds over one queue do not accumulate\n // racing routers. Recursive + suspended: stack-safe, no imperative loop.\n const route: Effect.Effect<void> = Effect.suspend(() =>\n Ref.get(outstanding).pipe(\n Effect.flatMap((n) =>\n n <= 0\n ? Effect.void\n : Queue.take(verdicts).pipe(\n Effect.flatMap((v) => {\n const call = callById.get(v.call_id)\n return (\n call === undefined\n ? Effect.void\n : resolve(\n call,\n v.decision === \"approve\"\n ? approve(call)\n : reject(denied(call, v.reason)),\n )\n ).pipe(Effect.andThen(route))\n }),\n ),\n ),\n ),\n )\n\n if (gated.length > 0) {\n yield* Effect.forkScoped(route)\n if (options?.timeout !== undefined) {\n yield* Effect.forkScoped(\n Effect.sleep(options.timeout).pipe(\n Effect.andThen(\n Effect.forEach(gated, (call) =>\n resolve(call, reject(cancelled(call, \"approval timed out\"))),\n ),\n ),\n ),\n )\n }\n }\n\n const decisions = Stream.fromIterable(gated).pipe(\n Stream.flatMap((call) => Stream.fromEffect(Deferred.await(deferreds.get(call.call_id)!)), {\n concurrency: \"unbounded\",\n }),\n )\n\n const approvalRequests = Stream.fromIterable<ToolEvent>(gated.map(approvalRequested))\n\n return { approved, decisions, approvalRequests }\n })\n\n// ---------------------------------------------------------------------------\n// Approval map (HTTP-style transport). Verdicts arrive synchronously\n// bundled in the request payload. Missing entries → cancelled.\n// ---------------------------------------------------------------------------\n\nexport type ApprovalMapEntry =\n | { readonly decision: \"approve\" }\n | { readonly decision: \"deny\"; readonly reason?: string }\n\nexport const fromMap =\n (predicate: (call: ToolCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) =>\n (calls: ReadonlyArray<ToolCall>): ToolCallPlan =>\n splitApprovalDecisions(\n calls.map((call) => {\n if (!predicate(call)) return approve(call)\n const v = approvals.get(call.call_id)\n if (v === undefined) return reject(cancelled(call))\n return v.decision === \"approve\" ? approve(call) : reject(denied(call, v.reason))\n }),\n )\n"],"mappings":"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"}