@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.
- package/dist/{AiError-CrdR1EQi.d.mts → AiError-Bf1pq4BB.d.mts} +8 -3
- package/dist/{AiError-CrdR1EQi.d.mts.map → AiError-Bf1pq4BB.d.mts.map} +1 -1
- package/dist/{Audio-DDSKCkSe.d.mts → Audio-CR7CSljJ.d.mts} +2 -2
- package/dist/{Audio-DDSKCkSe.d.mts.map → Audio-CR7CSljJ.d.mts.map} +1 -1
- package/dist/{Image-C_Zjpwhc.d.mts → Image-C4vuBmu0.d.mts} +12 -12
- package/dist/{Image-C_Zjpwhc.d.mts.map → Image-C4vuBmu0.d.mts.map} +1 -1
- package/dist/{Items-CgKD6Npw.d.mts → Items-BWNu6VGp.d.mts} +118 -92
- package/dist/Items-BWNu6VGp.d.mts.map +1 -0
- package/dist/{Loop-B5_cqumi.d.mts → Loop-D8nWpowc.d.mts} +2 -2
- package/dist/{Loop-B5_cqumi.d.mts.map → Loop-D8nWpowc.d.mts.map} +1 -1
- package/dist/{Media-DOLyaygI.d.mts → Media-D_CpcM1Z.d.mts} +1 -1
- package/dist/{Media-DOLyaygI.d.mts.map → Media-D_CpcM1Z.d.mts.map} +1 -1
- package/dist/{StructuredFormat-CMJFD3sn.d.mts → StructuredFormat-DHaUJBdd.d.mts} +1 -1
- package/dist/{StructuredFormat-CMJFD3sn.d.mts.map → StructuredFormat-DHaUJBdd.d.mts.map} +1 -1
- package/dist/{Tool-0XUIT2A2.d.mts → Tool-CXFHCQrT.d.mts} +101 -16
- package/dist/Tool-CXFHCQrT.d.mts.map +1 -0
- package/dist/{ToolEvent-DYrAUJGs.d.mts → ToolEvent-nVijCrvm.d.mts} +2 -2
- package/dist/ToolEvent-nVijCrvm.d.mts.map +1 -0
- package/dist/{ToolResult-BZd-cEWW.d.mts → ToolResult-CQqKkcDa.d.mts} +10 -4
- package/dist/ToolResult-CQqKkcDa.d.mts.map +1 -0
- package/dist/{Toolkit-BKC8b8Xo.d.mts → Toolkit-BYPwwu-j.d.mts} +65 -24
- package/dist/Toolkit-BYPwwu-j.d.mts.map +1 -0
- package/dist/browser/Browser.d.mts +190 -0
- package/dist/browser/Browser.d.mts.map +1 -0
- package/dist/browser/Browser.mjs +34 -0
- package/dist/browser/Browser.mjs.map +1 -0
- package/dist/browser/BrowserError.d.mts +141 -0
- package/dist/browser/BrowserError.d.mts.map +1 -0
- package/dist/browser/BrowserError.mjs +89 -0
- package/dist/browser/BrowserError.mjs.map +1 -0
- package/dist/browser/BrowserTool.d.mts +59 -0
- package/dist/browser/BrowserTool.d.mts.map +1 -0
- package/dist/browser/BrowserTool.mjs +86 -0
- package/dist/browser/BrowserTool.mjs.map +1 -0
- package/dist/{dist-DWgRbDN3.mjs → dist-C0m0QnUP.mjs} +71 -30
- package/dist/dist-C0m0QnUP.mjs.map +1 -0
- package/dist/{dist-B04dlFHg.mjs → dist-CcRV3-A5.mjs} +8 -8
- package/dist/{dist-B04dlFHg.mjs.map → dist-CcRV3-A5.mjs.map} +1 -1
- package/dist/domain/AiError.d.mts +2 -2
- package/dist/domain/AiError.mjs +22 -3
- package/dist/domain/AiError.mjs.map +1 -1
- package/dist/domain/Audio.d.mts +1 -1
- package/dist/domain/Citation.d.mts +60 -0
- package/dist/domain/Citation.d.mts.map +1 -0
- package/dist/domain/Citation.mjs +7 -0
- package/dist/domain/Citation.mjs.map +1 -0
- package/dist/domain/Image.d.mts +1 -1
- package/dist/domain/Items.d.mts +1 -1
- package/dist/domain/Items.mjs +13 -3
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Media.d.mts +1 -1
- package/dist/domain/Music.d.mts +1 -1
- package/dist/domain/Research.d.mts +37 -0
- package/dist/domain/Research.d.mts.map +1 -0
- package/dist/domain/Research.mjs +7 -0
- package/dist/domain/Research.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +92 -10
- package/dist/domain/Turn.d.mts.map +1 -1
- package/dist/domain/Turn.mjs +8 -1
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/domain/Turn.test.mjs +1 -1
- package/dist/embedding-model/Embedding.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.mjs +2 -2
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
- package/dist/index.d.mts +22 -13
- package/dist/index.mjs +18 -9
- package/dist/job/Job.d.mts +80 -0
- package/dist/job/Job.d.mts.map +1 -0
- package/dist/job/Job.mjs +49 -0
- package/dist/job/Job.mjs.map +1 -0
- package/dist/language-model/LanguageModel.d.mts +4 -4
- package/dist/language-model/LanguageModel.mjs +2 -2
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.mjs +1 -1
- package/dist/loop/Loop.d.mts +1 -1
- package/dist/loop/Loop.test.mjs +1 -1
- package/dist/music-generator/MusicGenerator.d.mts +2 -2
- package/dist/music-generator/MusicGenerator.mjs +4 -4
- package/dist/music-generator/MusicGenerator.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.test.mjs +1 -1
- package/dist/observability/Metrics.d.mts +1 -1
- package/dist/observability/Metrics.test.mjs +2 -2
- package/dist/observability/Telemetry.test.mjs +2 -2
- package/dist/research/DeepResearch.d.mts +71 -0
- package/dist/research/DeepResearch.d.mts.map +1 -0
- package/dist/research/DeepResearch.mjs +61 -0
- package/dist/research/DeepResearch.mjs.map +1 -0
- package/dist/retry/Retry.d.mts +1 -1
- package/dist/retry/Retry.test.mjs +1 -1
- package/dist/sandbox/Sandbox.mjs +18 -18
- package/dist/sandbox/Sandbox.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +2 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +8 -8
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
- package/dist/streaming/JSONL.d.mts +3 -1
- package/dist/streaming/JSONL.d.mts.map +1 -1
- package/dist/streaming/JSONL.mjs +5 -11
- package/dist/streaming/JSONL.mjs.map +1 -1
- package/dist/streaming/JSONL.test.mjs +10 -1
- package/dist/streaming/JSONL.test.mjs.map +1 -1
- package/dist/streaming/SSE.d.mts +5 -3
- package/dist/streaming/SSE.d.mts.map +1 -1
- package/dist/streaming/SSE.mjs +28 -38
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.mjs +54 -4
- package/dist/streaming/SSE.test.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.test.mjs +1 -1
- package/dist/testing/MockMusicGenerator.d.mts +1 -1
- package/dist/testing/MockProvider.d.mts +1 -1
- package/dist/testing/MockSpeechSynthesizer.d.mts +1 -1
- package/dist/tool/Approval.d.mts +19 -7
- package/dist/tool/Approval.d.mts.map +1 -1
- package/dist/tool/Approval.mjs +18 -11
- package/dist/tool/Approval.mjs.map +1 -1
- package/dist/tool/Approval.test.mjs +47 -8
- package/dist/tool/Approval.test.mjs.map +1 -1
- package/dist/tool/HistoryCheck.d.mts +2 -2
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +112 -28
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.mjs +127 -3
- package/dist/tool/Tool.test.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +1 -1
- package/dist/tool/ToolEvent.mjs +5 -0
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/ToolResult.d.mts +2 -2
- package/dist/tool/ToolResult.mjs +15 -6
- package/dist/tool/ToolResult.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +2 -2
- package/dist/tool/Toolkit.mjs +62 -22
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +207 -9
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.d.mts +2 -2
- package/dist/transcriber/Transcriber.mjs +3 -3
- package/dist/transcriber/Transcriber.mjs.map +1 -1
- package/dist/transcriber/Transcriber.test.mjs +1 -1
- package/dist/web-read/WebRead.d.mts +44 -0
- package/dist/web-read/WebRead.d.mts.map +1 -0
- package/dist/web-read/WebRead.mjs +18 -0
- package/dist/web-read/WebRead.mjs.map +1 -0
- package/dist/web-read/WebReadTool.d.mts +57 -0
- package/dist/web-read/WebReadTool.d.mts.map +1 -0
- package/dist/web-read/WebReadTool.mjs +59 -0
- package/dist/web-read/WebReadTool.mjs.map +1 -0
- package/dist/web-search/WebSearch.d.mts +1 -1
- package/dist/web-search/WebSearch.mjs +1 -1
- package/dist/web-search/WebSearch.mjs.map +1 -1
- package/dist/web-search/WebSearchTool.d.mts +12 -6
- package/dist/web-search/WebSearchTool.d.mts.map +1 -1
- package/dist/web-search/WebSearchTool.mjs +10 -5
- package/dist/web-search/WebSearchTool.mjs.map +1 -1
- package/package.json +39 -3
- package/src/browser/Browser.ts +268 -0
- package/src/browser/BrowserError.ts +145 -0
- package/src/browser/BrowserTool.ts +161 -0
- package/src/domain/AiError.ts +27 -1
- package/src/domain/Citation.ts +64 -0
- package/src/domain/Items.ts +14 -4
- package/src/domain/Research.ts +42 -0
- package/src/domain/Turn.ts +26 -0
- package/src/embedding-model/EmbeddingModel.ts +2 -2
- package/src/index.ts +9 -0
- package/src/job/Job.ts +125 -0
- package/src/language-model/LanguageModel.ts +2 -2
- package/src/music-generator/MusicGenerator.ts +4 -4
- package/src/research/DeepResearch.ts +137 -0
- package/src/sandbox/Sandbox.ts +18 -18
- package/src/speech-synthesizer/SpeechSynthesizer.ts +8 -8
- package/src/streaming/JSONL.test.ts +19 -0
- package/src/streaming/JSONL.ts +6 -38
- package/src/streaming/SSE.test.ts +86 -3
- package/src/streaming/SSE.ts +36 -80
- package/src/tool/Approval.test.ts +55 -5
- package/src/tool/Approval.ts +72 -27
- package/src/tool/Tool.test.ts +151 -1
- package/src/tool/Tool.ts +263 -66
- package/src/tool/ToolEvent.ts +5 -0
- package/src/tool/ToolResult.ts +14 -8
- package/src/tool/Toolkit.test.ts +231 -2
- package/src/tool/Toolkit.ts +175 -62
- package/src/transcriber/Transcriber.ts +3 -3
- package/src/web-read/WebRead.ts +70 -0
- package/src/web-read/WebReadTool.ts +96 -0
- package/src/web-search/WebSearch.ts +1 -1
- package/src/web-search/WebSearchTool.ts +11 -5
- package/dist/Items-CgKD6Npw.d.mts.map +0 -1
- package/dist/Tool-0XUIT2A2.d.mts.map +0 -1
- package/dist/ToolEvent-DYrAUJGs.d.mts.map +0 -1
- package/dist/ToolResult-BZd-cEWW.d.mts.map +0 -1
- package/dist/Toolkit-BKC8b8Xo.d.mts.map +0 -1
- package/dist/dist-DWgRbDN3.mjs.map +0 -1
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { fromBytes, parse, toBytes } from "./JSONL.mjs";
|
|
2
|
-
import {
|
|
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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"}
|
package/dist/streaming/SSE.d.mts
CHANGED
|
@@ -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"
|
|
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
|
|
21
|
-
*
|
|
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":";;;;;;;;;;;;
|
|
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"}
|
package/dist/streaming/SSE.mjs
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
39
|
-
*
|
|
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.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
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\"
|
|
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 {
|
|
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("
|
|
40
|
-
globalExpect(await collect(fromBytes(bytesOf("data:
|
|
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-
|
|
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 {
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
package/dist/tool/Approval.d.mts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { v as ToolCall } from "../Items-
|
|
2
|
-
import { t as ToolResult } from "../ToolResult-
|
|
3
|
-
import { t as ToolEvent } from "../ToolEvent-
|
|
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
|
|
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;;;;;;
|
|
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"}
|
package/dist/tool/Approval.mjs
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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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"}
|