@effect-uai/core 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AiError-Bf1pq4BB.d.mts → AiError-eLV9t1lU.d.mts} +12 -16
- package/dist/AiError-eLV9t1lU.d.mts.map +1 -0
- package/dist/{Audio-CR7CSljJ.d.mts → Audio-60lKKHxS.d.mts} +4 -5
- package/dist/Audio-60lKKHxS.d.mts.map +1 -0
- package/dist/{Image-C4vuBmu0.d.mts → Image-BK8-4H6s.d.mts} +12 -14
- package/dist/Image-BK8-4H6s.d.mts.map +1 -0
- package/dist/{Items-BKQgqJ8A.d.mts → Items-SfUabNuB.d.mts} +74 -76
- package/dist/Items-SfUabNuB.d.mts.map +1 -0
- package/dist/{Loop-D8nWpowc.d.mts → Loop-B54AB1ME.d.mts} +19 -21
- package/dist/Loop-B54AB1ME.d.mts.map +1 -0
- package/dist/{Media-D_CpcM1Z.d.mts → Media-DwgpifPD.d.mts} +1 -1
- package/dist/Media-DwgpifPD.d.mts.map +1 -0
- package/dist/{SandboxError-CIrVDOMV.d.mts → SandboxError-BN8b28Bh.d.mts} +11 -15
- package/dist/SandboxError-BN8b28Bh.d.mts.map +1 -0
- package/dist/{StructuredFormat-DHaUJBdd.d.mts → StructuredFormat-BlZhyOsi.d.mts} +3 -7
- package/dist/StructuredFormat-BlZhyOsi.d.mts.map +1 -0
- package/dist/{Tool-u5bag8SO.d.mts → Tool-DE-rnvkw.d.mts} +5 -8
- package/dist/Tool-DE-rnvkw.d.mts.map +1 -0
- package/dist/{ToolEvent-BBfOdhF_.d.mts → ToolEvent-D99MGQYH.d.mts} +56 -63
- package/dist/ToolEvent-D99MGQYH.d.mts.map +1 -0
- package/dist/{ToolResult-CRpmkBXR.d.mts → ToolResult-Cj8ChodL.d.mts} +49 -54
- package/dist/ToolResult-Cj8ChodL.d.mts.map +1 -0
- package/dist/{Toolkit-D4mQvhjq.d.mts → Toolkit-BEZrNPTI.d.mts} +13 -16
- package/dist/Toolkit-BEZrNPTI.d.mts.map +1 -0
- package/dist/browser/Browser.d.mts +11 -7
- package/dist/browser/Browser.d.mts.map +1 -1
- package/dist/browser/Browser.mjs +1 -1
- package/dist/browser/Browser.mjs.map +1 -1
- package/dist/browser/BrowserError.d.mts +9 -13
- package/dist/browser/BrowserError.d.mts.map +1 -1
- package/dist/browser/BrowserError.mjs +1 -1
- package/dist/browser/BrowserError.mjs.map +1 -1
- package/dist/browser/BrowserTool.d.mts +33 -36
- package/dist/browser/BrowserTool.d.mts.map +1 -1
- package/dist/browser/BrowserTool.mjs +1 -1
- package/dist/browser/BrowserTool.mjs.map +1 -1
- package/dist/capabilities/Capabilities.d.mts +8 -6
- package/dist/capabilities/Capabilities.d.mts.map +1 -1
- package/dist/capabilities/Capabilities.mjs +1 -1
- package/dist/capabilities/Capabilities.mjs.map +1 -1
- package/dist/dist-BaenoU1A.mjs +145 -0
- package/dist/dist-BaenoU1A.mjs.map +1 -0
- package/dist/{dist-C0m0QnUP.mjs → dist-nohcnkD6.mjs} +16 -15
- package/dist/dist-nohcnkD6.mjs.map +1 -0
- package/dist/domain/AiError.d.mts +1 -1
- package/dist/domain/AiError.mjs +1 -1
- package/dist/domain/AiError.mjs.map +1 -1
- package/dist/domain/Audio.d.mts +1 -1
- package/dist/domain/Audio.mjs +1 -1
- package/dist/domain/Citation.d.mts +0 -2
- package/dist/domain/Citation.d.mts.map +1 -1
- package/dist/domain/Citation.mjs +1 -1
- package/dist/domain/Image.d.mts +1 -1
- package/dist/domain/Image.mjs +1 -1
- package/dist/domain/Image.mjs.map +1 -1
- package/dist/domain/Items.d.mts +1 -1
- package/dist/domain/Items.mjs +1 -1
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Media.d.mts +1 -1
- package/dist/domain/Media.mjs +1 -1
- package/dist/domain/Music.d.mts +18 -11
- package/dist/domain/Music.d.mts.map +1 -1
- package/dist/domain/Music.mjs +1 -1
- package/dist/domain/Music.mjs.map +1 -1
- package/dist/domain/Research.d.mts +3 -4
- package/dist/domain/Research.d.mts.map +1 -1
- package/dist/domain/Research.mjs +1 -1
- package/dist/domain/Transcript.d.mts +2 -3
- package/dist/domain/Transcript.d.mts.map +1 -1
- package/dist/domain/Transcript.mjs +1 -1
- package/dist/domain/Turn.d.mts +143 -164
- package/dist/domain/Turn.d.mts.map +1 -1
- package/dist/domain/Turn.mjs +1 -1
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/domain/Turn.test.d.mts +1 -1
- package/dist/domain/Turn.test.mjs +28 -18
- package/dist/domain/Turn.test.mjs.map +1 -1
- package/dist/embedding-model/Embedding.d.mts +1 -3
- package/dist/embedding-model/Embedding.d.mts.map +1 -1
- package/dist/embedding-model/Embedding.mjs +1 -1
- package/dist/embedding-model/EmbeddingModel.d.mts +1 -3
- package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -1
- package/dist/embedding-model/EmbeddingModel.mjs +1 -1
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.test.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +14 -9
- package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
- package/dist/index.d.mts +12 -12
- package/dist/job/Job.d.mts +4 -4
- package/dist/job/Job.d.mts.map +1 -1
- package/dist/job/Job.mjs +1 -1
- package/dist/job/Job.mjs.map +1 -1
- package/dist/language-model/LanguageModel.d.mts +4 -6
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +1 -1
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.d.mts +1 -1
- package/dist/language-model/LanguageModel.test.mjs +6 -3
- package/dist/language-model/LanguageModel.test.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +1 -1
- package/dist/loop/Loop.mjs +1 -1
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.d.mts +1 -1
- package/dist/loop/Loop.test.mjs +60 -34
- package/dist/loop/Loop.test.mjs.map +1 -1
- package/dist/{magic-string.es-Dk-MMA0F.mjs → magic-string.es-CB2hOw9v.mjs} +14 -13
- package/dist/{magic-string.es-Dk-MMA0F.mjs.map → magic-string.es-CB2hOw9v.mjs.map} +1 -1
- package/dist/math/Vector.d.mts +0 -2
- package/dist/math/Vector.d.mts.map +1 -1
- package/dist/math/Vector.mjs +1 -1
- package/dist/math/Vector.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.d.mts +2 -4
- package/dist/music-generator/MusicGenerator.d.mts.map +1 -1
- package/dist/music-generator/MusicGenerator.mjs +1 -1
- package/dist/music-generator/MusicGenerator.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.test.d.mts +1 -1
- package/dist/music-generator/MusicGenerator.test.mjs +19 -10
- package/dist/music-generator/MusicGenerator.test.mjs.map +1 -1
- package/dist/observability/Metrics.d.mts +10 -7
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +1 -1
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/observability/Metrics.test.d.mts +1 -1
- package/dist/observability/Metrics.test.mjs +50 -28
- package/dist/observability/Metrics.test.mjs.map +1 -1
- package/dist/observability/Telemetry.d.mts +2 -3
- package/dist/observability/Telemetry.d.mts.map +1 -1
- package/dist/observability/Telemetry.mjs +1 -1
- package/dist/observability/Telemetry.mjs.map +1 -1
- package/dist/observability/Telemetry.test.d.mts +1 -1
- package/dist/observability/Telemetry.test.mjs +15 -7
- package/dist/observability/Telemetry.test.mjs.map +1 -1
- package/dist/research/DeepResearch.d.mts +1 -3
- package/dist/research/DeepResearch.d.mts.map +1 -1
- package/dist/research/DeepResearch.mjs +1 -1
- package/dist/research/DeepResearch.mjs.map +1 -1
- package/dist/retry/Retry.d.mts +2 -6
- package/dist/retry/Retry.d.mts.map +1 -1
- package/dist/retry/Retry.mjs +1 -1
- package/dist/retry/Retry.mjs.map +1 -1
- package/dist/retry/Retry.test.d.mts +1 -1
- package/dist/retry/Retry.test.mjs +6 -3
- package/dist/retry/Retry.test.mjs.map +1 -1
- package/dist/sandbox/Memory.d.mts +0 -2
- package/dist/sandbox/Memory.d.mts.map +1 -1
- package/dist/sandbox/Memory.mjs +4 -4
- package/dist/sandbox/Memory.mjs.map +1 -1
- package/dist/sandbox/Sandbox.d.mts +144 -156
- package/dist/sandbox/Sandbox.d.mts.map +1 -1
- package/dist/sandbox/Sandbox.mjs +1 -1
- package/dist/sandbox/Sandbox.mjs.map +1 -1
- package/dist/sandbox/SandboxError.d.mts +1 -1
- package/dist/sandbox/SandboxError.mjs +1 -1
- package/dist/sandbox/SandboxError.mjs.map +1 -1
- package/dist/sandbox/SandboxImage.d.mts +0 -2
- package/dist/sandbox/SandboxImage.d.mts.map +1 -1
- package/dist/sandbox/SandboxImage.mjs +1 -1
- package/dist/sandbox/SandboxImage.mjs.map +1 -1
- package/dist/sandbox/SandboxNetwork.d.mts +0 -2
- package/dist/sandbox/SandboxNetwork.d.mts.map +1 -1
- package/dist/sandbox/SandboxNetwork.mjs +1 -1
- package/dist/sandbox/SandboxNetwork.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +8 -7
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.d.mts +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +31 -16
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
- package/dist/streaming/JSONL.d.mts +1 -5
- package/dist/streaming/JSONL.d.mts.map +1 -1
- package/dist/streaming/JSONL.mjs +1 -1
- package/dist/streaming/JSONL.mjs.map +1 -1
- package/dist/streaming/JSONL.test.d.mts +1 -1
- package/dist/streaming/JSONL.test.mjs +16 -8
- package/dist/streaming/JSONL.test.mjs.map +1 -1
- package/dist/streaming/Lines.d.mts +0 -2
- package/dist/streaming/Lines.d.mts.map +1 -1
- package/dist/streaming/Lines.mjs +1 -1
- package/dist/streaming/Lines.mjs.map +1 -1
- package/dist/streaming/SSE.d.mts +0 -2
- package/dist/streaming/SSE.d.mts.map +1 -1
- package/dist/streaming/SSE.mjs +1 -1
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.d.mts +1 -1
- package/dist/streaming/SSE.test.mjs +34 -17
- package/dist/streaming/SSE.test.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.mjs +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.test.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.test.mjs +7 -4
- package/dist/structured-format/StructuredFormat.test.mjs.map +1 -1
- package/dist/{dist-CcRV3-A5.mjs → test.DNmyFkvJ-YPzDR0Co.mjs} +1644 -5172
- package/dist/test.DNmyFkvJ-YPzDR0Co.mjs.map +1 -0
- package/dist/testing/MockMusicGenerator.d.mts +4 -3
- package/dist/testing/MockMusicGenerator.d.mts.map +1 -1
- package/dist/testing/MockMusicGenerator.mjs.map +1 -1
- package/dist/testing/MockProvider.d.mts +1 -2
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/testing/MockSandbox.d.mts +256 -290
- package/dist/testing/MockSandbox.d.mts.map +1 -1
- package/dist/testing/MockSandbox.mjs.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.d.mts +10 -6
- package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
- package/dist/testing/MockTranscriber.d.mts +3 -2
- package/dist/testing/MockTranscriber.d.mts.map +1 -1
- package/dist/testing/MockTranscriber.mjs.map +1 -1
- package/dist/tool/Approval.d.mts +31 -36
- package/dist/tool/Approval.d.mts.map +1 -1
- package/dist/tool/Approval.mjs +1 -1
- package/dist/tool/Approval.mjs.map +1 -1
- package/dist/tool/Approval.test.d.mts +1 -1
- package/dist/tool/Approval.test.mjs +37 -24
- package/dist/tool/Approval.test.mjs.map +1 -1
- package/dist/tool/HistoryCheck.d.mts +2 -4
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +1 -1
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Tool.d.mts +1 -1
- package/dist/tool/Tool.mjs +4 -4
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.d.mts +1 -1
- package/dist/tool/Tool.test.mjs +23 -11
- package/dist/tool/Tool.test.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +1 -1
- package/dist/tool/ToolEvent.mjs +1 -1
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/ToolResult.d.mts +1 -1
- package/dist/tool/ToolResult.mjs +1 -1
- package/dist/tool/ToolResult.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +1 -1
- package/dist/tool/Toolkit.mjs +4 -3
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.d.mts +1 -1
- package/dist/tool/Toolkit.test.mjs +40 -21
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.d.mts +6 -6
- package/dist/transcriber/Transcriber.d.mts.map +1 -1
- package/dist/transcriber/Transcriber.mjs +1 -1
- package/dist/transcriber/Transcriber.mjs.map +1 -1
- package/dist/transcriber/Transcriber.test.d.mts +1 -1
- package/dist/transcriber/Transcriber.test.mjs +20 -10
- package/dist/transcriber/Transcriber.test.mjs.map +1 -1
- package/dist/utils-CiH_DjFW.mjs +3918 -0
- package/dist/utils-CiH_DjFW.mjs.map +1 -0
- package/dist/web-read/WebRead.d.mts +15 -9
- package/dist/web-read/WebRead.d.mts.map +1 -1
- package/dist/web-read/WebRead.mjs +1 -1
- package/dist/web-read/WebRead.mjs.map +1 -1
- package/dist/web-read/WebReadTool.d.mts +4 -5
- package/dist/web-read/WebReadTool.d.mts.map +1 -1
- package/dist/web-read/WebReadTool.mjs +1 -1
- package/dist/web-read/WebReadTool.mjs.map +1 -1
- package/dist/web-search/WebSearch.d.mts +22 -13
- package/dist/web-search/WebSearch.d.mts.map +1 -1
- package/dist/web-search/WebSearch.mjs +1 -1
- package/dist/web-search/WebSearch.mjs.map +1 -1
- package/dist/web-search/WebSearchTool.d.mts +6 -6
- package/dist/web-search/WebSearchTool.d.mts.map +1 -1
- package/dist/web-search/WebSearchTool.mjs +1 -1
- package/dist/web-search/WebSearchTool.mjs.map +1 -1
- package/package.json +4 -4
- package/src/loop/Loop.test.ts +13 -14
- package/src/loop/Loop.ts +74 -76
- package/src/testing/MockProvider.ts +8 -9
- package/src/tool/Tool.ts +13 -13
- package/dist/AiError-Bf1pq4BB.d.mts.map +0 -1
- package/dist/Audio-CR7CSljJ.d.mts.map +0 -1
- package/dist/Image-C4vuBmu0.d.mts.map +0 -1
- package/dist/Items-BKQgqJ8A.d.mts.map +0 -1
- package/dist/Loop-D8nWpowc.d.mts.map +0 -1
- package/dist/Media-D_CpcM1Z.d.mts.map +0 -1
- package/dist/SandboxError-CIrVDOMV.d.mts.map +0 -1
- package/dist/StructuredFormat-DHaUJBdd.d.mts.map +0 -1
- package/dist/Tool-u5bag8SO.d.mts.map +0 -1
- package/dist/ToolEvent-BBfOdhF_.d.mts.map +0 -1
- package/dist/ToolResult-CRpmkBXR.d.mts.map +0 -1
- package/dist/Toolkit-D4mQvhjq.d.mts.map +0 -1
- package/dist/dist-C0m0QnUP.mjs.map +0 -1
- package/dist/dist-CcRV3-A5.mjs.map +0 -1
- /package/dist/{chunk-uyGKjUfl.mjs → rolldown-runtime-EAsCxrDo.mjs} +0 -0
|
@@ -5,7 +5,8 @@ import { ToolEvent, isApprovalRequested, isOutput, isProgress } from "./ToolEven
|
|
|
5
5
|
import { make as make$1, run } from "./Toolkit.mjs";
|
|
6
6
|
import { fromMap, fromQueue } from "./Approval.mjs";
|
|
7
7
|
import { cancelAllPending, findUnansweredCalls, isReconciled } from "./HistoryCheck.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { m as it, o as describe } from "../utils-CiH_DjFW.mjs";
|
|
9
|
+
import { n as globalExpect } from "../test.DNmyFkvJ-YPzDR0Co.mjs";
|
|
9
10
|
import { Effect, Queue, Schema, Stream } from "effect";
|
|
10
11
|
//#region src/tool/Approval.test.ts
|
|
11
12
|
/**
|
|
@@ -20,7 +21,7 @@ import { Effect, Queue, Schema, Stream } from "effect";
|
|
|
20
21
|
*
|
|
21
22
|
* Plus: hallucinated tool name (graceful Failure), unknown_tool kind.
|
|
22
23
|
*/
|
|
23
|
-
const
|
|
24
|
+
const webSearch = make({
|
|
24
25
|
name: "web_search",
|
|
25
26
|
description: "search",
|
|
26
27
|
inputSchema: fromEffectSchema(Schema.Struct({ query: Schema.String })),
|
|
@@ -42,7 +43,8 @@ const allTools = make$1(make({
|
|
|
42
43
|
yield* Stream.runForEach(Stream.fromIterable(hits), emit);
|
|
43
44
|
return { count: hits.length };
|
|
44
45
|
})
|
|
45
|
-
})
|
|
46
|
+
});
|
|
47
|
+
const bulkEmail = make({
|
|
46
48
|
name: "bulk_email",
|
|
47
49
|
description: "send",
|
|
48
50
|
inputSchema: fromEffectSchema(Schema.Struct({
|
|
@@ -60,7 +62,8 @@ const allTools = make$1(make({
|
|
|
60
62
|
delivered: recipients.length
|
|
61
63
|
};
|
|
62
64
|
})
|
|
63
|
-
})
|
|
65
|
+
});
|
|
66
|
+
const deleteDatabase = make({
|
|
64
67
|
name: "delete_database",
|
|
65
68
|
description: "drop",
|
|
66
69
|
inputSchema: fromEffectSchema(Schema.Struct({ name: Schema.String })),
|
|
@@ -68,8 +71,9 @@ const allTools = make$1(make({
|
|
|
68
71
|
status: "dropped",
|
|
69
72
|
name
|
|
70
73
|
})
|
|
71
|
-
})
|
|
72
|
-
const
|
|
74
|
+
});
|
|
75
|
+
const allTools = make$1(webSearch, bulkEmail, deleteDatabase);
|
|
76
|
+
const SENSITIVE = /* @__PURE__ */ new Set(["bulk_email", "delete_database"]);
|
|
73
77
|
const isSensitive = (call) => SENSITIVE.has(call.name);
|
|
74
78
|
const fc = (call_id, name, args) => ({
|
|
75
79
|
type: "function_call",
|
|
@@ -95,7 +99,7 @@ const eventsFromApprovalMap = (approvals) => {
|
|
|
95
99
|
const eventsFromDecision = (decision) => decision._tag === "Approved" ? run(allTools, [decision.call]) : Stream.succeed(ToolEvent.Output({ result: decision.result }));
|
|
96
100
|
describe("fromMap + run", () => {
|
|
97
101
|
it("approval: all gated approved → tools execute, structured Values", async () => {
|
|
98
|
-
const approvals = new Map([["c2", { decision: "approve" }], ["c3", { decision: "approve" }]]);
|
|
102
|
+
const approvals = /* @__PURE__ */ new Map([["c2", { decision: "approve" }], ["c3", { decision: "approve" }]]);
|
|
99
103
|
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)));
|
|
100
104
|
const by = byCallId(resultsFrom(collected));
|
|
101
105
|
globalExpect(by.get("c1")).toMatchObject({
|
|
@@ -119,7 +123,7 @@ describe("fromMap + run", () => {
|
|
|
119
123
|
globalExpect(collected.filter(isApprovalRequested)).toHaveLength(0);
|
|
120
124
|
});
|
|
121
125
|
it("denial: gated denied → Failure(denied), no execution", async () => {
|
|
122
|
-
const approvals = new Map([["c2", {
|
|
126
|
+
const approvals = /* @__PURE__ */ new Map([["c2", {
|
|
123
127
|
decision: "deny",
|
|
124
128
|
reason: "spam concern"
|
|
125
129
|
}], ["c3", {
|
|
@@ -141,7 +145,8 @@ describe("fromMap + run", () => {
|
|
|
141
145
|
});
|
|
142
146
|
});
|
|
143
147
|
it("cancellation: missing verdicts → Failure(cancelled)", async () => {
|
|
144
|
-
const
|
|
148
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(/* @__PURE__ */ new Map())));
|
|
149
|
+
const by = byCallId(resultsFrom(collected));
|
|
145
150
|
globalExpect(by.get("c1")).toMatchObject({
|
|
146
151
|
_tag: "Ok",
|
|
147
152
|
value: { count: 3 }
|
|
@@ -156,8 +161,9 @@ describe("fromMap + run", () => {
|
|
|
156
161
|
});
|
|
157
162
|
});
|
|
158
163
|
it("mixed: approve + deny + omit → all three kinds", async () => {
|
|
159
|
-
const approvals = new Map([["c2", { decision: "approve" }]]);
|
|
160
|
-
const
|
|
164
|
+
const approvals = /* @__PURE__ */ new Map([["c2", { decision: "approve" }]]);
|
|
165
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)));
|
|
166
|
+
const by = byCallId(resultsFrom(collected));
|
|
161
167
|
globalExpect(by.get("c1")).toMatchObject({
|
|
162
168
|
_tag: "Ok",
|
|
163
169
|
value: { count: 3 }
|
|
@@ -179,10 +185,11 @@ describe("run: graceful degradation", () => {
|
|
|
179
185
|
fc("c2", "does_not_exist", {}),
|
|
180
186
|
fc("c3", "delete_database", { name: "prod" })
|
|
181
187
|
];
|
|
182
|
-
const
|
|
183
|
-
const plan = fromMap(isSensitive, new Map([["c3", { decision: "approve" }]]))(callsWithBogus);
|
|
188
|
+
const collected = await Effect.runPromise(Stream.runCollect((() => {
|
|
189
|
+
const plan = fromMap(isSensitive, /* @__PURE__ */ new Map([["c3", { decision: "approve" }]]))(callsWithBogus);
|
|
184
190
|
return Stream.merge(run(allTools, plan.approved), rejectedStream(plan.rejected));
|
|
185
|
-
})()))
|
|
191
|
+
})()));
|
|
192
|
+
const by = byCallId(resultsFrom(collected));
|
|
186
193
|
globalExpect(by.get("c1")).toMatchObject({ _tag: "Ok" });
|
|
187
194
|
globalExpect(by.get("c2")).toMatchObject({
|
|
188
195
|
_tag: "Failure",
|
|
@@ -253,10 +260,12 @@ describe("fromQueue + run", () => {
|
|
|
253
260
|
});
|
|
254
261
|
});
|
|
255
262
|
it("timeout resolves an unanswered gated call as cancelled", async () => {
|
|
256
|
-
|
|
257
|
-
const
|
|
263
|
+
const decisions = await Effect.runPromise(Effect.scoped(Effect.gen(function* () {
|
|
264
|
+
const verdicts = yield* Queue.unbounded();
|
|
265
|
+
const { decisions } = yield* fromQueue(isSensitive, verdicts, { timeout: "10 millis" })([fc("c1", "delete_database", { name: "prod" })]);
|
|
258
266
|
return Array.from(yield* Stream.runCollect(decisions));
|
|
259
|
-
})))
|
|
267
|
+
})));
|
|
268
|
+
globalExpect(decisions[0]).toMatchObject({
|
|
260
269
|
_tag: "Rejected",
|
|
261
270
|
result: {
|
|
262
271
|
_tag: "Failure",
|
|
@@ -270,27 +279,30 @@ describe("findUnansweredCalls / cancelAllPending / isReconciled", () => {
|
|
|
270
279
|
const answered = fc("c98", "web_search", { query: "x" });
|
|
271
280
|
const answeredOutput = toolCallOutput("c98", JSON.stringify({ count: 0 }));
|
|
272
281
|
it("findUnansweredCalls returns only orphans", () => {
|
|
273
|
-
const
|
|
282
|
+
const history = [
|
|
274
283
|
userText("hi"),
|
|
275
284
|
answered,
|
|
276
285
|
orphan,
|
|
277
286
|
answeredOutput
|
|
278
|
-
]
|
|
287
|
+
];
|
|
288
|
+
const unanswered = findUnansweredCalls(history);
|
|
279
289
|
globalExpect(unanswered).toHaveLength(1);
|
|
280
290
|
globalExpect(unanswered[0].call_id).toBe("c99");
|
|
281
291
|
});
|
|
282
292
|
it("isReconciled is false when orphans exist, true otherwise", () => {
|
|
283
293
|
const stale = [userText("hi"), orphan];
|
|
284
294
|
globalExpect(isReconciled(stale)).toBe(false);
|
|
285
|
-
|
|
295
|
+
const reconciled = [...stale, ...cancelAllPending(stale).map(toToolCallOutput)];
|
|
296
|
+
globalExpect(isReconciled(reconciled)).toBe(true);
|
|
286
297
|
});
|
|
287
298
|
it("cancelAllPending synthesizes one Failure(cancelled) per orphan", () => {
|
|
288
|
-
const
|
|
299
|
+
const history = [
|
|
289
300
|
userText("hi"),
|
|
290
301
|
answered,
|
|
291
302
|
orphan,
|
|
292
303
|
answeredOutput
|
|
293
|
-
]
|
|
304
|
+
];
|
|
305
|
+
const closures = cancelAllPending(history, "user moved on");
|
|
294
306
|
globalExpect(closures).toHaveLength(1);
|
|
295
307
|
const c = closures[0];
|
|
296
308
|
globalExpect(isFailure(c)).toBe(true);
|
|
@@ -304,11 +316,12 @@ describe("findUnansweredCalls / cancelAllPending / isReconciled", () => {
|
|
|
304
316
|
it("follow-up: map closures to ToolCallOutput before appending new user message", () => {
|
|
305
317
|
const stale = [userText("first request"), orphan];
|
|
306
318
|
const closures = cancelAllPending(stale, "user redirected");
|
|
307
|
-
|
|
319
|
+
const reconciled = [
|
|
308
320
|
...stale,
|
|
309
321
|
...closures.map(toToolCallOutput),
|
|
310
322
|
userText("never mind")
|
|
311
|
-
]
|
|
323
|
+
];
|
|
324
|
+
globalExpect(findUnansweredCalls(reconciled)).toHaveLength(0);
|
|
312
325
|
});
|
|
313
326
|
});
|
|
314
327
|
describe("toToolCallOutput", () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Approval.test.mjs","names":["makeToolkit","makeTool","Items.toolCallOutput","Items.userText"],"sources":["../../src/tool/Approval.test.ts"],"sourcesContent":["/**\n * Tests for approval planners + history-reconciliation primitives. Exercises\n * the full HITL + streaming-tool stack end-to-end by composing approval plans\n * with `run`, with the four wire-shaped scenarios:\n *\n * 1. Approval : gated calls approved → tools execute, structured Values\n * 2. Denial : gated calls denied → Failure(denied) results\n * 3. Cancellation : missing verdicts → Failure(cancelled) results\n * 4. Mixed + history : reconciliation via cancelAllPending\n *\n * Plus: hallucinated tool name (graceful Failure), unknown_tool kind.\n */\nimport { Effect, Queue, Schema, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport * as Items from \"../domain/Items.js\"\nimport { findUnansweredCalls, cancelAllPending, isReconciled } from \"./HistoryCheck.js\"\nimport { type ToolResult, isFailure, isOk, toToolCallOutput } from \"./ToolResult.js\"\nimport {\n type ApprovalMapEntry,\n type ApprovalDecision,\n type Verdict,\n fromMap,\n fromQueue,\n} from \"./Approval.js\"\nimport { fromEffectSchema, make as makeTool } from \"./Tool.js\"\nimport { make as makeToolkit, run } from \"./Toolkit.js\"\nimport { ToolEvent, isApprovalRequested, isProgress, isOutput } from \"./ToolEvent.js\"\n\n// ---------------------------------------------------------------------------\n// Three demo tools covering the matrix:\n// - web_search : streaming, no approval\n// - bulk_email : streaming, requires approval\n// - delete_database : non-streaming, requires approval\n// ---------------------------------------------------------------------------\n\nconst webSearch = makeTool({\n name: \"web_search\",\n description: \"search\",\n inputSchema: fromEffectSchema(Schema.Struct({ query: Schema.String })),\n run: ({ query }, emit) =>\n Effect.gen(function* () {\n const hits = [\n { url: \"a\", title: `${query} 1` },\n { url: \"b\", title: `${query} 2` },\n { url: \"c\", title: `${query} 3` },\n ]\n yield* Stream.runForEach(Stream.fromIterable(hits), emit)\n return { count: hits.length }\n }),\n})\n\nconst bulkEmail = makeTool({\n name: \"bulk_email\",\n description: \"send\",\n inputSchema: fromEffectSchema(\n Schema.Struct({ recipients: Schema.Array(Schema.String), subject: Schema.String }),\n ),\n run: ({ recipients }, emit) =>\n Effect.gen(function* () {\n yield* Effect.forEach(recipients, (_, i) =>\n emit({ type: \"progress\" as const, sent: i + 1, total: recipients.length }),\n )\n return { status: \"sent\" as const, delivered: recipients.length }\n }),\n})\n\nconst deleteDatabase = makeTool({\n name: \"delete_database\",\n description: \"drop\",\n inputSchema: fromEffectSchema(Schema.Struct({ name: Schema.String })),\n run: ({ name }) => Effect.succeed({ status: \"dropped\", name }),\n})\n\nconst allTools = makeToolkit(webSearch, bulkEmail, deleteDatabase)\nconst SENSITIVE = new Set([\"bulk_email\", \"delete_database\"])\nconst isSensitive = (call: Items.ToolCall) => SENSITIVE.has(call.name)\n\nconst fc = (call_id: string, name: string, args: unknown): Items.ToolCall => ({\n type: \"function_call\",\n call_id,\n name,\n arguments: JSON.stringify(args),\n})\n\nconst calls = [\n fc(\"c1\", \"web_search\", { query: \"effect\" }),\n fc(\"c2\", \"bulk_email\", { recipients: [\"a@x\", \"b@x\"], subject: \"Hi\" }),\n fc(\"c3\", \"delete_database\", { name: \"prod\" }),\n]\n\nconst resultsFrom = (collected: ReadonlyArray<ToolEvent>): ReadonlyArray<ToolResult> =>\n collected.filter(isOutput).map((e) => e.result)\n\nconst byCallId = (results: ReadonlyArray<ToolResult>) => new Map(results.map((r) => [r.call_id, r]))\n\nconst rejectedStream = (rejected: ReadonlyArray<ToolResult>) =>\n Stream.fromIterable(rejected.map((result) => ToolEvent.Output({ result })))\n\nconst eventsFromApprovalMap = (approvals: ReadonlyMap<string, ApprovalMapEntry>) => {\n const plan = fromMap(isSensitive, approvals)(calls)\n return Stream.merge(run(allTools, plan.approved), rejectedStream(plan.rejected))\n}\n\nconst eventsFromDecision = (decision: ApprovalDecision): Stream.Stream<ToolEvent> =>\n decision._tag === \"Approved\"\n ? run(allTools, [decision.call])\n : Stream.succeed(ToolEvent.Output({ result: decision.result }))\n\n// ---------------------------------------------------------------------------\n// fromMap: HTTP-style scenarios\n// ---------------------------------------------------------------------------\n\ndescribe(\"fromMap + run\", () => {\n it(\"approval: all gated approved → tools execute, structured Values\", async () => {\n const approvals = new Map<string, ApprovalMapEntry>([\n [\"c2\", { decision: \"approve\" }],\n [\"c3\", { decision: \"approve\" }],\n ])\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\", value: { count: 3 } })\n expect(by.get(\"c2\")).toMatchObject({\n _tag: \"Ok\",\n value: { status: \"sent\", delivered: 2 },\n })\n expect(by.get(\"c3\")).toMatchObject({\n _tag: \"Ok\",\n value: { status: \"dropped\", name: \"prod\" },\n })\n\n // No ApprovalRequested events from the pure HTTP flow.\n expect(collected.filter(isApprovalRequested)).toHaveLength(0)\n })\n\n it(\"denial: gated denied → Failure(denied), no execution\", async () => {\n const approvals = new Map<string, ApprovalMapEntry>([\n [\"c2\", { decision: \"deny\", reason: \"spam concern\" }],\n [\"c3\", { decision: \"deny\", reason: \"prod is sacred\" }],\n ])\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))\n\n // bulk_email never ran.\n expect(collected.filter(isProgress).filter((e) => e.tool === \"bulk_email\")).toHaveLength(0)\n\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c2\")).toMatchObject({\n _tag: \"Failure\",\n kind: \"denied\",\n reason: \"spam concern\",\n })\n expect(by.get(\"c3\")).toMatchObject({\n _tag: \"Failure\",\n kind: \"denied\",\n reason: \"prod is sacred\",\n })\n })\n\n it(\"cancellation: missing verdicts → Failure(cancelled)\", async () => {\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(new Map())))\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\", value: { count: 3 } })\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Failure\", kind: \"cancelled\" })\n expect(by.get(\"c3\")).toMatchObject({ _tag: \"Failure\", kind: \"cancelled\" })\n })\n\n it(\"mixed: approve + deny + omit → all three kinds\", async () => {\n const approvals = new Map<string, ApprovalMapEntry>([\n [\"c2\", { decision: \"approve\" }],\n // c3 omitted → cancelled\n ])\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\", value: { count: 3 } })\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Ok\", value: { status: \"sent\" } })\n expect(by.get(\"c3\")).toMatchObject({ _tag: \"Failure\", kind: \"cancelled\" })\n })\n})\n\n// ---------------------------------------------------------------------------\n// Graceful degradation: hallucinated tool name doesn't kill the turn.\n// ---------------------------------------------------------------------------\n\ndescribe(\"run: graceful degradation\", () => {\n it(\"unknown tool name → Failure(unknown_tool); other calls still execute\", async () => {\n const callsWithBogus = [\n fc(\"c1\", \"web_search\", { query: \"x\" }),\n fc(\"c2\", \"does_not_exist\", {}),\n fc(\"c3\", \"delete_database\", { name: \"prod\" }),\n ]\n const collected = await Effect.runPromise(\n Stream.runCollect(\n (() => {\n const plan = fromMap(\n isSensitive,\n new Map([[\"c3\", { decision: \"approve\" }]]),\n )(callsWithBogus)\n return Stream.merge(run(allTools, plan.approved), rejectedStream(plan.rejected))\n })(),\n ),\n )\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\" })\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Failure\", kind: \"unknown_tool\" })\n expect(by.get(\"c3\")).toMatchObject({ _tag: \"Ok\", value: { status: \"dropped\" } })\n })\n})\n\n// ---------------------------------------------------------------------------\n// fromQueue: WebSocket-style scenarios\n// ---------------------------------------------------------------------------\n\ndescribe(\"fromQueue + run\", () => {\n it(\"queue-driven: approve + deny resolve correctly with ApprovalRequested events\", async () => {\n const collected = await Effect.runPromise(\n Effect.gen(function* () {\n const verdicts = yield* Queue.unbounded<{\n readonly call_id: string\n readonly decision: \"approve\" | \"deny\"\n readonly reason?: string\n }>()\n yield* Queue.offer(verdicts, { call_id: \"c2\", decision: \"approve\" })\n yield* Queue.offer(verdicts, {\n call_id: \"c3\",\n decision: \"deny\",\n reason: \"too risky\",\n })\n\n // Stream.unwrap supplies the Scope for fromQueue's router.\n const events = Stream.unwrap(\n Effect.gen(function* () {\n const { approved, decisions, approvalRequests } = yield* fromQueue(\n isSensitive,\n verdicts,\n )(calls)\n return Stream.merge(\n approvalRequests,\n Stream.merge(\n run(allTools, approved),\n decisions.pipe(Stream.flatMap(eventsFromDecision)),\n ),\n )\n }),\n )\n return yield* Stream.runCollect(events)\n }),\n )\n\n expect(collected.filter(isApprovalRequested)).toHaveLength(2)\n\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Ok\", value: { status: \"sent\" } })\n expect(by.get(\"c3\")).toMatchObject({\n _tag: \"Failure\",\n kind: \"denied\",\n reason: \"too risky\",\n })\n })\n\n it(\"router retires per round, so sequential rounds over one queue both resolve\", async () => {\n // A verdict-per-round over one shared queue and scope. The old\n // never-terminating router would still be alive for round 1 and steal\n // round 2's verdict (not in its map), hanging round 2.\n const decisions = await Effect.runPromise(\n Effect.scoped(\n Effect.gen(function* () {\n const verdicts = yield* Queue.unbounded<Verdict>()\n const round = (id: string) => [fc(id, \"delete_database\", { name: \"prod\" })]\n\n const r1 = yield* fromQueue(isSensitive, verdicts)(round(\"c1\"))\n yield* Queue.offer(verdicts, { call_id: \"c1\", decision: \"approve\" })\n const d1 = Array.from(yield* Stream.runCollect(r1.decisions))\n\n const r2 = yield* fromQueue(isSensitive, verdicts)(round(\"c2\"))\n yield* Queue.offer(verdicts, { call_id: \"c2\", decision: \"deny\", reason: \"no\" })\n const d2 = Array.from(yield* Stream.runCollect(r2.decisions))\n\n return { d1, d2 }\n }),\n ),\n )\n expect(decisions.d1[0]).toMatchObject({ _tag: \"Approved\" })\n expect(decisions.d2[0]).toMatchObject({ _tag: \"Rejected\", result: { kind: \"denied\" } })\n })\n\n it(\"timeout resolves an unanswered gated call as cancelled\", async () => {\n const decisions = await Effect.runPromise(\n Effect.scoped(\n Effect.gen(function* () {\n const verdicts = yield* Queue.unbounded<Verdict>()\n const { decisions } = yield* fromQueue(isSensitive, verdicts, { timeout: \"10 millis\" })([\n fc(\"c1\", \"delete_database\", { name: \"prod\" }),\n ])\n return Array.from(yield* Stream.runCollect(decisions))\n }),\n ),\n )\n expect(decisions[0]).toMatchObject({\n _tag: \"Rejected\",\n result: { _tag: \"Failure\", kind: \"cancelled\" },\n })\n })\n})\n\n// ---------------------------------------------------------------------------\n// History reconciliation\n// ---------------------------------------------------------------------------\n\ndescribe(\"findUnansweredCalls / cancelAllPending / isReconciled\", () => {\n const orphan = fc(\"c99\", \"delete_database\", { name: \"prod\" })\n const answered = fc(\"c98\", \"web_search\", { query: \"x\" })\n const answeredOutput = Items.toolCallOutput(\"c98\", JSON.stringify({ count: 0 }))\n\n it(\"findUnansweredCalls returns only orphans\", () => {\n const history = [Items.userText(\"hi\"), answered, orphan, answeredOutput]\n const unanswered = findUnansweredCalls(history)\n expect(unanswered).toHaveLength(1)\n expect(unanswered[0]!.call_id).toBe(\"c99\")\n })\n\n it(\"isReconciled is false when orphans exist, true otherwise\", () => {\n const stale = [Items.userText(\"hi\"), orphan]\n expect(isReconciled(stale)).toBe(false)\n const reconciled = [...stale, ...cancelAllPending(stale).map(toToolCallOutput)]\n expect(isReconciled(reconciled)).toBe(true)\n })\n\n it(\"cancelAllPending synthesizes one Failure(cancelled) per orphan\", () => {\n const history = [Items.userText(\"hi\"), answered, orphan, answeredOutput]\n const closures = cancelAllPending(history, \"user moved on\")\n expect(closures).toHaveLength(1)\n const c = closures[0]!\n expect(isFailure(c)).toBe(true)\n expect(c).toMatchObject({\n _tag: \"Failure\",\n call_id: \"c99\",\n kind: \"cancelled\",\n reason: \"user moved on\",\n })\n })\n\n it(\"follow-up: map closures to ToolCallOutput before appending new user message\", () => {\n const stale = [Items.userText(\"first request\"), orphan]\n const closures = cancelAllPending(stale, \"user redirected\")\n const reconciled = [...stale, ...closures.map(toToolCallOutput), Items.userText(\"never mind\")]\n expect(findUnansweredCalls(reconciled)).toHaveLength(0)\n })\n})\n\n// ---------------------------------------------------------------------------\n// Wire conversion\n// ---------------------------------------------------------------------------\n\ndescribe(\"toToolCallOutput\", () => {\n it(\"round-trips a Value result\", () => {\n const r: ToolResult = {\n _tag: \"Ok\",\n call_id: \"c1\",\n tool: \"web_search\",\n value: { count: 3 },\n }\n const out = toToolCallOutput(r)\n expect(out.call_id).toBe(\"c1\")\n expect(JSON.parse(out.output)).toEqual({ count: 3 })\n })\n\n it(\"renders a Failure with reason as a nested error object\", () => {\n const r: ToolResult = {\n _tag: \"Failure\",\n call_id: \"c2\",\n tool: \"bulk_email\",\n kind: \"denied\",\n reason: \"spam concern\",\n }\n const out = toToolCallOutput(r)\n expect(JSON.parse(out.output)).toEqual({ error: { kind: \"denied\", message: \"spam concern\" } })\n })\n\n it(\"renders a Failure without reason (omits message)\", () => {\n const r: ToolResult = {\n _tag: \"Failure\",\n call_id: \"c3\",\n tool: \"delete_database\",\n kind: \"cancelled\",\n }\n const out = toToolCallOutput(r)\n expect(JSON.parse(out.output)).toEqual({ error: { kind: \"cancelled\" } })\n })\n})\n\n// ---------------------------------------------------------------------------\n// run\n// ---------------------------------------------------------------------------\n\ndescribe(\"run\", () => {\n it(\"runs all calls passed to it\", async () => {\n const collected = await Effect.runPromise(Stream.runCollect(run(allTools, calls)))\n expect(collected.filter(isOutput)).toHaveLength(3)\n expect(collected.filter(isOutput).every((e) => isOk(e.result))).toBe(true)\n })\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Approval.test.mjs","names":["makeTool","makeToolkit","Items.toolCallOutput","Items.userText"],"sources":["../../src/tool/Approval.test.ts"],"sourcesContent":["/**\n * Tests for approval planners + history-reconciliation primitives. Exercises\n * the full HITL + streaming-tool stack end-to-end by composing approval plans\n * with `run`, with the four wire-shaped scenarios:\n *\n * 1. Approval : gated calls approved → tools execute, structured Values\n * 2. Denial : gated calls denied → Failure(denied) results\n * 3. Cancellation : missing verdicts → Failure(cancelled) results\n * 4. Mixed + history : reconciliation via cancelAllPending\n *\n * Plus: hallucinated tool name (graceful Failure), unknown_tool kind.\n */\nimport { Effect, Queue, Schema, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport * as Items from \"../domain/Items.js\"\nimport { findUnansweredCalls, cancelAllPending, isReconciled } from \"./HistoryCheck.js\"\nimport { type ToolResult, isFailure, isOk, toToolCallOutput } from \"./ToolResult.js\"\nimport {\n type ApprovalMapEntry,\n type ApprovalDecision,\n type Verdict,\n fromMap,\n fromQueue,\n} from \"./Approval.js\"\nimport { fromEffectSchema, make as makeTool } from \"./Tool.js\"\nimport { make as makeToolkit, run } from \"./Toolkit.js\"\nimport { ToolEvent, isApprovalRequested, isProgress, isOutput } from \"./ToolEvent.js\"\n\n// ---------------------------------------------------------------------------\n// Three demo tools covering the matrix:\n// - web_search : streaming, no approval\n// - bulk_email : streaming, requires approval\n// - delete_database : non-streaming, requires approval\n// ---------------------------------------------------------------------------\n\nconst webSearch = makeTool({\n name: \"web_search\",\n description: \"search\",\n inputSchema: fromEffectSchema(Schema.Struct({ query: Schema.String })),\n run: ({ query }, emit) =>\n Effect.gen(function* () {\n const hits = [\n { url: \"a\", title: `${query} 1` },\n { url: \"b\", title: `${query} 2` },\n { url: \"c\", title: `${query} 3` },\n ]\n yield* Stream.runForEach(Stream.fromIterable(hits), emit)\n return { count: hits.length }\n }),\n})\n\nconst bulkEmail = makeTool({\n name: \"bulk_email\",\n description: \"send\",\n inputSchema: fromEffectSchema(\n Schema.Struct({ recipients: Schema.Array(Schema.String), subject: Schema.String }),\n ),\n run: ({ recipients }, emit) =>\n Effect.gen(function* () {\n yield* Effect.forEach(recipients, (_, i) =>\n emit({ type: \"progress\" as const, sent: i + 1, total: recipients.length }),\n )\n return { status: \"sent\" as const, delivered: recipients.length }\n }),\n})\n\nconst deleteDatabase = makeTool({\n name: \"delete_database\",\n description: \"drop\",\n inputSchema: fromEffectSchema(Schema.Struct({ name: Schema.String })),\n run: ({ name }) => Effect.succeed({ status: \"dropped\", name }),\n})\n\nconst allTools = makeToolkit(webSearch, bulkEmail, deleteDatabase)\nconst SENSITIVE = new Set([\"bulk_email\", \"delete_database\"])\nconst isSensitive = (call: Items.ToolCall) => SENSITIVE.has(call.name)\n\nconst fc = (call_id: string, name: string, args: unknown): Items.ToolCall => ({\n type: \"function_call\",\n call_id,\n name,\n arguments: JSON.stringify(args),\n})\n\nconst calls = [\n fc(\"c1\", \"web_search\", { query: \"effect\" }),\n fc(\"c2\", \"bulk_email\", { recipients: [\"a@x\", \"b@x\"], subject: \"Hi\" }),\n fc(\"c3\", \"delete_database\", { name: \"prod\" }),\n]\n\nconst resultsFrom = (collected: ReadonlyArray<ToolEvent>): ReadonlyArray<ToolResult> =>\n collected.filter(isOutput).map((e) => e.result)\n\nconst byCallId = (results: ReadonlyArray<ToolResult>) => new Map(results.map((r) => [r.call_id, r]))\n\nconst rejectedStream = (rejected: ReadonlyArray<ToolResult>) =>\n Stream.fromIterable(rejected.map((result) => ToolEvent.Output({ result })))\n\nconst eventsFromApprovalMap = (approvals: ReadonlyMap<string, ApprovalMapEntry>) => {\n const plan = fromMap(isSensitive, approvals)(calls)\n return Stream.merge(run(allTools, plan.approved), rejectedStream(plan.rejected))\n}\n\nconst eventsFromDecision = (decision: ApprovalDecision): Stream.Stream<ToolEvent> =>\n decision._tag === \"Approved\"\n ? run(allTools, [decision.call])\n : Stream.succeed(ToolEvent.Output({ result: decision.result }))\n\n// ---------------------------------------------------------------------------\n// fromMap: HTTP-style scenarios\n// ---------------------------------------------------------------------------\n\ndescribe(\"fromMap + run\", () => {\n it(\"approval: all gated approved → tools execute, structured Values\", async () => {\n const approvals = new Map<string, ApprovalMapEntry>([\n [\"c2\", { decision: \"approve\" }],\n [\"c3\", { decision: \"approve\" }],\n ])\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\", value: { count: 3 } })\n expect(by.get(\"c2\")).toMatchObject({\n _tag: \"Ok\",\n value: { status: \"sent\", delivered: 2 },\n })\n expect(by.get(\"c3\")).toMatchObject({\n _tag: \"Ok\",\n value: { status: \"dropped\", name: \"prod\" },\n })\n\n // No ApprovalRequested events from the pure HTTP flow.\n expect(collected.filter(isApprovalRequested)).toHaveLength(0)\n })\n\n it(\"denial: gated denied → Failure(denied), no execution\", async () => {\n const approvals = new Map<string, ApprovalMapEntry>([\n [\"c2\", { decision: \"deny\", reason: \"spam concern\" }],\n [\"c3\", { decision: \"deny\", reason: \"prod is sacred\" }],\n ])\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))\n\n // bulk_email never ran.\n expect(collected.filter(isProgress).filter((e) => e.tool === \"bulk_email\")).toHaveLength(0)\n\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c2\")).toMatchObject({\n _tag: \"Failure\",\n kind: \"denied\",\n reason: \"spam concern\",\n })\n expect(by.get(\"c3\")).toMatchObject({\n _tag: \"Failure\",\n kind: \"denied\",\n reason: \"prod is sacred\",\n })\n })\n\n it(\"cancellation: missing verdicts → Failure(cancelled)\", async () => {\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(new Map())))\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\", value: { count: 3 } })\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Failure\", kind: \"cancelled\" })\n expect(by.get(\"c3\")).toMatchObject({ _tag: \"Failure\", kind: \"cancelled\" })\n })\n\n it(\"mixed: approve + deny + omit → all three kinds\", async () => {\n const approvals = new Map<string, ApprovalMapEntry>([\n [\"c2\", { decision: \"approve\" }],\n // c3 omitted → cancelled\n ])\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\", value: { count: 3 } })\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Ok\", value: { status: \"sent\" } })\n expect(by.get(\"c3\")).toMatchObject({ _tag: \"Failure\", kind: \"cancelled\" })\n })\n})\n\n// ---------------------------------------------------------------------------\n// Graceful degradation: hallucinated tool name doesn't kill the turn.\n// ---------------------------------------------------------------------------\n\ndescribe(\"run: graceful degradation\", () => {\n it(\"unknown tool name → Failure(unknown_tool); other calls still execute\", async () => {\n const callsWithBogus = [\n fc(\"c1\", \"web_search\", { query: \"x\" }),\n fc(\"c2\", \"does_not_exist\", {}),\n fc(\"c3\", \"delete_database\", { name: \"prod\" }),\n ]\n const collected = await Effect.runPromise(\n Stream.runCollect(\n (() => {\n const plan = fromMap(\n isSensitive,\n new Map([[\"c3\", { decision: \"approve\" }]]),\n )(callsWithBogus)\n return Stream.merge(run(allTools, plan.approved), rejectedStream(plan.rejected))\n })(),\n ),\n )\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\" })\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Failure\", kind: \"unknown_tool\" })\n expect(by.get(\"c3\")).toMatchObject({ _tag: \"Ok\", value: { status: \"dropped\" } })\n })\n})\n\n// ---------------------------------------------------------------------------\n// fromQueue: WebSocket-style scenarios\n// ---------------------------------------------------------------------------\n\ndescribe(\"fromQueue + run\", () => {\n it(\"queue-driven: approve + deny resolve correctly with ApprovalRequested events\", async () => {\n const collected = await Effect.runPromise(\n Effect.gen(function* () {\n const verdicts = yield* Queue.unbounded<{\n readonly call_id: string\n readonly decision: \"approve\" | \"deny\"\n readonly reason?: string\n }>()\n yield* Queue.offer(verdicts, { call_id: \"c2\", decision: \"approve\" })\n yield* Queue.offer(verdicts, {\n call_id: \"c3\",\n decision: \"deny\",\n reason: \"too risky\",\n })\n\n // Stream.unwrap supplies the Scope for fromQueue's router.\n const events = Stream.unwrap(\n Effect.gen(function* () {\n const { approved, decisions, approvalRequests } = yield* fromQueue(\n isSensitive,\n verdicts,\n )(calls)\n return Stream.merge(\n approvalRequests,\n Stream.merge(\n run(allTools, approved),\n decisions.pipe(Stream.flatMap(eventsFromDecision)),\n ),\n )\n }),\n )\n return yield* Stream.runCollect(events)\n }),\n )\n\n expect(collected.filter(isApprovalRequested)).toHaveLength(2)\n\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Ok\", value: { status: \"sent\" } })\n expect(by.get(\"c3\")).toMatchObject({\n _tag: \"Failure\",\n kind: \"denied\",\n reason: \"too risky\",\n })\n })\n\n it(\"router retires per round, so sequential rounds over one queue both resolve\", async () => {\n // A verdict-per-round over one shared queue and scope. The old\n // never-terminating router would still be alive for round 1 and steal\n // round 2's verdict (not in its map), hanging round 2.\n const decisions = await Effect.runPromise(\n Effect.scoped(\n Effect.gen(function* () {\n const verdicts = yield* Queue.unbounded<Verdict>()\n const round = (id: string) => [fc(id, \"delete_database\", { name: \"prod\" })]\n\n const r1 = yield* fromQueue(isSensitive, verdicts)(round(\"c1\"))\n yield* Queue.offer(verdicts, { call_id: \"c1\", decision: \"approve\" })\n const d1 = Array.from(yield* Stream.runCollect(r1.decisions))\n\n const r2 = yield* fromQueue(isSensitive, verdicts)(round(\"c2\"))\n yield* Queue.offer(verdicts, { call_id: \"c2\", decision: \"deny\", reason: \"no\" })\n const d2 = Array.from(yield* Stream.runCollect(r2.decisions))\n\n return { d1, d2 }\n }),\n ),\n )\n expect(decisions.d1[0]).toMatchObject({ _tag: \"Approved\" })\n expect(decisions.d2[0]).toMatchObject({ _tag: \"Rejected\", result: { kind: \"denied\" } })\n })\n\n it(\"timeout resolves an unanswered gated call as cancelled\", async () => {\n const decisions = await Effect.runPromise(\n Effect.scoped(\n Effect.gen(function* () {\n const verdicts = yield* Queue.unbounded<Verdict>()\n const { decisions } = yield* fromQueue(isSensitive, verdicts, { timeout: \"10 millis\" })([\n fc(\"c1\", \"delete_database\", { name: \"prod\" }),\n ])\n return Array.from(yield* Stream.runCollect(decisions))\n }),\n ),\n )\n expect(decisions[0]).toMatchObject({\n _tag: \"Rejected\",\n result: { _tag: \"Failure\", kind: \"cancelled\" },\n })\n })\n})\n\n// ---------------------------------------------------------------------------\n// History reconciliation\n// ---------------------------------------------------------------------------\n\ndescribe(\"findUnansweredCalls / cancelAllPending / isReconciled\", () => {\n const orphan = fc(\"c99\", \"delete_database\", { name: \"prod\" })\n const answered = fc(\"c98\", \"web_search\", { query: \"x\" })\n const answeredOutput = Items.toolCallOutput(\"c98\", JSON.stringify({ count: 0 }))\n\n it(\"findUnansweredCalls returns only orphans\", () => {\n const history = [Items.userText(\"hi\"), answered, orphan, answeredOutput]\n const unanswered = findUnansweredCalls(history)\n expect(unanswered).toHaveLength(1)\n expect(unanswered[0]!.call_id).toBe(\"c99\")\n })\n\n it(\"isReconciled is false when orphans exist, true otherwise\", () => {\n const stale = [Items.userText(\"hi\"), orphan]\n expect(isReconciled(stale)).toBe(false)\n const reconciled = [...stale, ...cancelAllPending(stale).map(toToolCallOutput)]\n expect(isReconciled(reconciled)).toBe(true)\n })\n\n it(\"cancelAllPending synthesizes one Failure(cancelled) per orphan\", () => {\n const history = [Items.userText(\"hi\"), answered, orphan, answeredOutput]\n const closures = cancelAllPending(history, \"user moved on\")\n expect(closures).toHaveLength(1)\n const c = closures[0]!\n expect(isFailure(c)).toBe(true)\n expect(c).toMatchObject({\n _tag: \"Failure\",\n call_id: \"c99\",\n kind: \"cancelled\",\n reason: \"user moved on\",\n })\n })\n\n it(\"follow-up: map closures to ToolCallOutput before appending new user message\", () => {\n const stale = [Items.userText(\"first request\"), orphan]\n const closures = cancelAllPending(stale, \"user redirected\")\n const reconciled = [...stale, ...closures.map(toToolCallOutput), Items.userText(\"never mind\")]\n expect(findUnansweredCalls(reconciled)).toHaveLength(0)\n })\n})\n\n// ---------------------------------------------------------------------------\n// Wire conversion\n// ---------------------------------------------------------------------------\n\ndescribe(\"toToolCallOutput\", () => {\n it(\"round-trips a Value result\", () => {\n const r: ToolResult = {\n _tag: \"Ok\",\n call_id: \"c1\",\n tool: \"web_search\",\n value: { count: 3 },\n }\n const out = toToolCallOutput(r)\n expect(out.call_id).toBe(\"c1\")\n expect(JSON.parse(out.output)).toEqual({ count: 3 })\n })\n\n it(\"renders a Failure with reason as a nested error object\", () => {\n const r: ToolResult = {\n _tag: \"Failure\",\n call_id: \"c2\",\n tool: \"bulk_email\",\n kind: \"denied\",\n reason: \"spam concern\",\n }\n const out = toToolCallOutput(r)\n expect(JSON.parse(out.output)).toEqual({ error: { kind: \"denied\", message: \"spam concern\" } })\n })\n\n it(\"renders a Failure without reason (omits message)\", () => {\n const r: ToolResult = {\n _tag: \"Failure\",\n call_id: \"c3\",\n tool: \"delete_database\",\n kind: \"cancelled\",\n }\n const out = toToolCallOutput(r)\n expect(JSON.parse(out.output)).toEqual({ error: { kind: \"cancelled\" } })\n })\n})\n\n// ---------------------------------------------------------------------------\n// run\n// ---------------------------------------------------------------------------\n\ndescribe(\"run\", () => {\n it(\"runs all calls passed to it\", async () => {\n const collected = await Effect.runPromise(Stream.runCollect(run(allTools, calls)))\n expect(collected.filter(isOutput)).toHaveLength(3)\n expect(collected.filter(isOutput).every((e) => isOk(e.result))).toBe(true)\n })\n})\n"],"mappings":"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"}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { o as HistoryItem, v as ToolCall } from "../Items-
|
|
2
|
-
import { t as ToolResult } from "../ToolResult-
|
|
3
|
-
|
|
4
|
-
//#region src/tool/HistoryCheck.d.ts
|
|
1
|
+
import { o as HistoryItem, v as ToolCall } from "../Items-SfUabNuB.mjs";
|
|
2
|
+
import { t as ToolResult } from "../ToolResult-Cj8ChodL.mjs";
|
|
5
3
|
declare namespace HistoryCheck_d_exports {
|
|
6
4
|
export { cancelAllPending, findUnansweredCalls, isReconciled };
|
|
7
5
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HistoryCheck.d.mts","names":[],"sources":["../../src/tool/HistoryCheck.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"HistoryCheck.d.mts","names":[],"sources":["../../src/tool/HistoryCheck.ts"],"mappings":";;;;;;;;;;cAoBa,sBAAmB,SACrB,cAAc,iBACtB,cAAc;;cAMJ,eAAY,SAAa,cAAc;;;;;;;;;cAWvC,mBAAgB,SAClB,cAAc,cAAY,oBAElC,cAAc"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as __exportAll } from "../
|
|
1
|
+
import { n as __exportAll } from "../rolldown-runtime-EAsCxrDo.mjs";
|
|
2
2
|
import { isToolCall, isToolCallOutput } from "../domain/Items.mjs";
|
|
3
3
|
import { cancelled } from "./ToolResult.mjs";
|
|
4
4
|
//#region src/tool/HistoryCheck.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HistoryCheck.mjs","names":[],"sources":["../../src/tool/HistoryCheck.ts"],"sourcesContent":["/**\n * History-consistency primitives. Useful even WITHOUT HITL.\n *\n * Every provider rejects a new request if any prior `function_call` lacks\n * a matching `function_call_output`. Multi-turn flows that can be\n * interrupted, restarted, or branched (HITL, mid-stream abort, persisted\n * checkpoints, stateless HTTP servers) need to detect orphans and\n * synthesize closing outputs before submitting.\n *\n * Recipe author calls these at known transition points (right before the\n * next provider request). Not invoked from inside the loop.\n */\nimport { type ToolCall, type HistoryItem, isToolCall, isToolCallOutput } from \"../domain/Items.js\"\nimport { type ToolResult, cancelled } from \"./ToolResult.js\"\n\n/**\n * Return every `function_call` in `history` that does not have a matching\n * `function_call_output` later in `history` (correlated by `call_id`).\n * Empty result = history is provider-submittable from this invariant.\n */\nexport const findUnansweredCalls = (\n history: ReadonlyArray<HistoryItem>,\n): ReadonlyArray<ToolCall> => {\n const answered = new Set(history.filter(isToolCallOutput).map((o) => o.call_id))\n return history.filter(isToolCall).filter((c) => !answered.has(c.call_id))\n}\n\n/** Cheap predicate: is this history submittable to a provider? */\nexport const isReconciled = (history: ReadonlyArray<HistoryItem>): boolean =>\n findUnansweredCalls(history).length === 0\n\n/**\n * Synthesize cancellation results for every unanswered call. Caller maps\n * via `toToolCallOutput` and appends to history before submitting.\n *\n * Use when: a new user message arrives mid-approval; an approval timer\n * fires; a persisted checkpoint contains orphans (crash recovery); a\n * stateless HTTP server reconstructed history from a stale checkpoint.\n */\nexport const cancelAllPending = (\n history: ReadonlyArray<HistoryItem>,\n reason?: string,\n): ReadonlyArray<ToolResult> => findUnansweredCalls(history).map((call) => cancelled(call, reason))\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,uBACX,YAC4B;CAC5B,MAAM,WAAW,IAAI,IAAI,QAAQ,OAAO,
|
|
1
|
+
{"version":3,"file":"HistoryCheck.mjs","names":[],"sources":["../../src/tool/HistoryCheck.ts"],"sourcesContent":["/**\n * History-consistency primitives. Useful even WITHOUT HITL.\n *\n * Every provider rejects a new request if any prior `function_call` lacks\n * a matching `function_call_output`. Multi-turn flows that can be\n * interrupted, restarted, or branched (HITL, mid-stream abort, persisted\n * checkpoints, stateless HTTP servers) need to detect orphans and\n * synthesize closing outputs before submitting.\n *\n * Recipe author calls these at known transition points (right before the\n * next provider request). Not invoked from inside the loop.\n */\nimport { type ToolCall, type HistoryItem, isToolCall, isToolCallOutput } from \"../domain/Items.js\"\nimport { type ToolResult, cancelled } from \"./ToolResult.js\"\n\n/**\n * Return every `function_call` in `history` that does not have a matching\n * `function_call_output` later in `history` (correlated by `call_id`).\n * Empty result = history is provider-submittable from this invariant.\n */\nexport const findUnansweredCalls = (\n history: ReadonlyArray<HistoryItem>,\n): ReadonlyArray<ToolCall> => {\n const answered = new Set(history.filter(isToolCallOutput).map((o) => o.call_id))\n return history.filter(isToolCall).filter((c) => !answered.has(c.call_id))\n}\n\n/** Cheap predicate: is this history submittable to a provider? */\nexport const isReconciled = (history: ReadonlyArray<HistoryItem>): boolean =>\n findUnansweredCalls(history).length === 0\n\n/**\n * Synthesize cancellation results for every unanswered call. Caller maps\n * via `toToolCallOutput` and appends to history before submitting.\n *\n * Use when: a new user message arrives mid-approval; an approval timer\n * fires; a persisted checkpoint contains orphans (crash recovery); a\n * stateless HTTP server reconstructed history from a stale checkpoint.\n */\nexport const cancelAllPending = (\n history: ReadonlyArray<HistoryItem>,\n reason?: string,\n): ReadonlyArray<ToolResult> => findUnansweredCalls(history).map((call) => cancelled(call, reason))\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,uBACX,YAC4B;CAC5B,MAAM,WAAW,IAAI,IAAI,QAAQ,OAAO,gBAAgB,CAAC,CAAC,KAAK,MAAM,EAAE,OAAO,CAAC;CAC/E,OAAO,QAAQ,OAAO,UAAU,CAAC,CAAC,QAAQ,MAAM,CAAC,SAAS,IAAI,EAAE,OAAO,CAAC;AAC1E;;AAGA,MAAa,gBAAgB,YAC3B,oBAAoB,OAAO,CAAC,CAAC,WAAW;;;;;;;;;AAU1C,MAAa,oBACX,SACA,WAC8B,oBAAoB,OAAO,CAAC,CAAC,KAAK,SAAS,UAAU,MAAM,MAAM,CAAC"}
|
package/dist/tool/Tool.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as providerToolsOf, C as fromEffectSchema, D as make, E as isProviderTool, M as toDescriptors, N as withName, O as noInput, P as withRun, S as fail, T as interaction, a as Emit, b as descriptorsOf, c as SignalTool, d as ToolE, f as ToolError, g as ToolValidationError, h as ToolR, i as DecodeResult, j as signal, k as provider, l as Tool, m as ToolInputSchema, n as AnyTool, o as InteractionTool, p as ToolFailed, r as DecodableTool, s as ProviderTool, t as AnyLocalTool, u as ToolDescriptor, v as decodeArgs, w as fromStandardSchema, x as execute, y as decodeCallInput } from "../Tool-
|
|
1
|
+
import { A as providerToolsOf, C as fromEffectSchema, D as make, E as isProviderTool, M as toDescriptors, N as withName, O as noInput, P as withRun, S as fail, T as interaction, a as Emit, b as descriptorsOf, c as SignalTool, d as ToolE, f as ToolError, g as ToolValidationError, h as ToolR, i as DecodeResult, j as signal, k as provider, l as Tool, m as ToolInputSchema, n as AnyTool, o as InteractionTool, p as ToolFailed, r as DecodableTool, s as ProviderTool, t as AnyLocalTool, u as ToolDescriptor, v as decodeArgs, w as fromStandardSchema, x as execute, y as decodeCallInput } from "../Tool-DE-rnvkw.mjs";
|
|
2
2
|
export { AnyLocalTool, AnyTool, DecodableTool, DecodeResult, Emit, InteractionTool, ProviderTool, SignalTool, Tool, ToolDescriptor, ToolE, ToolError, ToolFailed, ToolInputSchema, ToolR, ToolValidationError, decodeArgs, decodeCallInput, descriptorsOf, execute, fail, fromEffectSchema, fromStandardSchema, interaction, isProviderTool, make, noInput, provider, providerToolsOf, signal, toDescriptors, withName, withRun };
|
package/dist/tool/Tool.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as __exportAll } from "../
|
|
1
|
+
import { n as __exportAll } from "../rolldown-runtime-EAsCxrDo.mjs";
|
|
2
2
|
import { toolCallOutput } from "../domain/Items.mjs";
|
|
3
3
|
import { serializeValue } from "./ToolResult.mjs";
|
|
4
4
|
import { Effect, Match, Schema } from "effect";
|
|
@@ -25,7 +25,7 @@ var Tool_exports = /* @__PURE__ */ __exportAll({
|
|
|
25
25
|
withName: () => withName,
|
|
26
26
|
withRun: () => withRun
|
|
27
27
|
});
|
|
28
|
-
var ToolError = class extends Schema.
|
|
28
|
+
var ToolError = class extends Schema.TaggedError("@betalyra/effect-uai/ToolError")("ToolError", {
|
|
29
29
|
call_id: Schema.String,
|
|
30
30
|
tool: Schema.String,
|
|
31
31
|
message: Schema.String,
|
|
@@ -46,7 +46,7 @@ const ToolValidationIssue = Schema.Struct({
|
|
|
46
46
|
* contract violation apart from a parse or execution failure, and so the
|
|
47
47
|
* structured `issues` survive instead of being flattened into a message string.
|
|
48
48
|
*/
|
|
49
|
-
var ToolValidationError = class extends Schema.
|
|
49
|
+
var ToolValidationError = class extends Schema.TaggedError("@betalyra/effect-uai/ToolValidationError")("ToolValidationError", {
|
|
50
50
|
call_id: Schema.String,
|
|
51
51
|
tool: Schema.String,
|
|
52
52
|
issues: Schema.Array(ToolValidationIssue)
|
|
@@ -60,7 +60,7 @@ var ToolValidationError = class extends Schema.TaggedErrorClass("@betalyra/effec
|
|
|
60
60
|
* (default `"tool_failed"`) so a tool can hand the model a distinguishable
|
|
61
61
|
* category (`"not_found"`, `"rate_limited"`) without a new result variant.
|
|
62
62
|
*/
|
|
63
|
-
var ToolFailed = class extends Schema.
|
|
63
|
+
var ToolFailed = class extends Schema.TaggedError("@betalyra/effect-uai/ToolFailed")("ToolFailed", {
|
|
64
64
|
message: Schema.String,
|
|
65
65
|
kind: Schema.optional(Schema.String)
|
|
66
66
|
}) {};
|
package/dist/tool/Tool.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tool.mjs","names":[],"sources":["../../src/tool/Tool.ts"],"sourcesContent":["import type { StandardJSONSchemaV1, StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { Effect, Match, Schema } from \"effect\"\nimport type { ToolCall, ToolCallOutput } from \"../domain/Items.js\"\nimport { toolCallOutput } from \"../domain/Items.js\"\nimport { serializeValue } from \"./ToolResult.js\"\n\nexport class ToolError extends Schema.TaggedErrorClass<ToolError>(\"@betalyra/effect-uai/ToolError\")(\n \"ToolError\",\n {\n call_id: Schema.String,\n tool: Schema.String,\n message: Schema.String,\n cause: Schema.optional(Schema.Unknown),\n },\n) {}\n\n/**\n * One Standard Schema validation issue, carried on `ToolValidationError`.\n * Mirrors `StandardSchemaV1.Issue`; `path` is left as `unknown` elements\n * because Standard Schema path segments may be symbols or `{ key }` objects.\n */\nconst ToolValidationIssue = Schema.Struct({\n message: Schema.String,\n path: Schema.optional(Schema.Array(Schema.Unknown)),\n})\n\n/**\n * The model's `arguments` for a call failed the tool's `inputSchema`. Distinct\n * from `ToolError` (malformed JSON, runtime crash) so callers can tell a\n * contract violation apart from a parse or execution failure, and so the\n * structured `issues` survive instead of being flattened into a message string.\n */\nexport class ToolValidationError extends Schema.TaggedErrorClass<ToolValidationError>(\n \"@betalyra/effect-uai/ToolValidationError\",\n)(\"ToolValidationError\", {\n call_id: Schema.String,\n tool: Schema.String,\n issues: Schema.Array(ToolValidationIssue),\n}) {}\n\n/**\n * The deliberate \"speak to the model\" failure sentinel. A tool's `run` that\n * fails with a `ToolFailed` (or a bare `string`, its sugar) is absorbed by\n * `Toolkit.run` into a `ToolResult.Failure` the model reads and adapts to,\n * instead of ending the run. Every other error type propagates typed on the\n * executor's stream. `kind` overrides the synthesized `Failure.kind`\n * (default `\"tool_failed\"`) so a tool can hand the model a distinguishable\n * category (`\"not_found\"`, `\"rate_limited\"`) without a new result variant.\n */\nexport class ToolFailed extends Schema.TaggedErrorClass<ToolFailed>(\n \"@betalyra/effect-uai/ToolFailed\",\n)(\"ToolFailed\", {\n message: Schema.String,\n kind: Schema.optional(Schema.String),\n}) {}\n\n/**\n * Fail a tool's `run` with a model-visible message (optionally a `kind`).\n * `yield* Tool.fail(\"page not found\", { kind: \"not_found\" })` inside a\n * `run`'s `Effect.gen`; the executor absorbs it as `ToolResult.Failure`.\n */\nexport const fail = (\n message: string,\n options?: { readonly kind?: string },\n): Effect.Effect<never, ToolFailed> =>\n Effect.fail(\n new ToolFailed({ message, ...(options?.kind !== undefined ? { kind: options.kind } : {}) }),\n )\n\n/**\n * Schemas accepted on `Tool.inputSchema`. Must implement both Standard\n * Schema validation and JSON Schema conversion (for rendering tool\n * descriptors to provider request bodies).\n *\n * Any Standard-Schema-compliant library that exposes both interfaces\n * works directly: Zod 4+, Valibot, ArkType, Effect Schema (after\n * `fromEffectSchema`), etc.\n */\nexport type ToolInputSchema<Input = unknown> = StandardSchemaV1<unknown, Input> &\n StandardJSONSchemaV1<unknown, Input>\n\n/**\n * Convenience wrapper for Effect Schema users - adds both the\n * `validate` and `jsonSchema` extensions to a plain Effect Schema so it\n * can be used as a `Tool.inputSchema`.\n */\nexport const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(\n schema: S,\n): S & ToolInputSchema<S[\"Type\"]> =>\n Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema))\n\n/**\n * Use any schema library that implements both Standard Schema (validation)\n * and Standard JSON Schema (JSON Schema generation) as a `Tool.inputSchema`.\n * Covers Zod 4.2+, Valibot 1.2+, and ArkType 2.1.28+ in one helper.\n *\n * Effect Schema doesn't implement Standard JSON Schema natively — use\n * `fromEffectSchema` for those.\n *\n * The intersection constraint catches missing interfaces at compile time:\n * a Zod v3 schema (no Standard JSON Schema) produces a precise type error\n * pointing at the missing interface rather than a runtime surprise. The\n * helper itself is a thin type-narrowing identity — schemas that satisfy\n * both standards already structurally satisfy `ToolInputSchema`; the\n * helper makes the input type inference explicit at the call site.\n */\nexport const fromStandardSchema = <S extends StandardSchemaV1 & StandardJSONSchemaV1>(\n schema: S,\n): S & ToolInputSchema<StandardSchemaV1.InferOutput<S>> =>\n schema as S & ToolInputSchema<StandardSchemaV1.InferOutput<S>>\n\n/**\n * Input schema for a tool that takes no arguments. Renders a clean\n * `{ type: \"object\", properties: {} }`; validation accepts anything and\n * yields `{}`. Use it instead of `fromEffectSchema(Schema.Struct({}))`,\n * which renders a degenerate `anyOf` (object-or-array) that strict\n * providers like Gemini reject.\n */\nexport const noInput: ToolInputSchema<Record<string, never>> = {\n \"~standard\": {\n version: 1,\n vendor: \"effect-uai\",\n validate: () => ({ value: {} }),\n jsonSchema: {\n input: () => ({ type: \"object\", properties: {} }),\n output: () => ({ type: \"object\", properties: {} }),\n },\n },\n}\n\n/**\n * Emit one intermediate `Event` from inside a tool's `run`. Backed by the\n * executor's per-call queue, so it returns an `Effect` (backpressure) and\n * composes with `Stream.runForEach(emit)`.\n */\nexport type Emit<Event> = (event: Event) => Effect.Effect<void>\n\n/**\n * A model-visible tool with a local `Effect` executor. `run` computes the\n * model-facing `Output`; emitting progress is a side channel via `emit`. A\n * plain tool never calls `emit` (its `Event` is irrelevant); a streaming tool\n * emits intermediate `Event`s that flow to the consumer as\n * `ToolEvent.Progress` in real time.\n *\n * This is one of four tool *kinds*, discriminated by `_tag`. The other three\n * (`ProviderTool`, `SignalTool`, `InteractionTool`) are model-visible but have\n * no local executor — see their constructors below. `Tool` keeps its name (and\n * the `Tool.make` constructor) because the local kind is by far the common one.\n */\nexport type Tool<Name extends string, Input, Event, Output, E = unknown, R = never> = {\n readonly _tag: \"LocalTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n readonly run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output, E, R>\n /**\n * Bound on this tool's emit queue. Unbounded when omitted. Per-tool because\n * backpressure depends on how a given tool emits, unlike `concurrency`\n * (cross-tool, on `Toolkit.run`'s options).\n */\n readonly emitBufferSize?: number\n /**\n * Whether the provider should render this tool with its strict-mode\n * flag (OpenAI's `strict: true`, etc). Default: true. The framework\n * never rewrites the schema; if the rendered JSON Schema isn't\n * compatible, the provider returns an error.\n */\n readonly strict?: boolean\n}\n\n/**\n * A provider-hosted tool: visible to the model and executed by the *provider*\n * (native web search, code execution, RAG grounding), never by this process.\n * It has no local `run`; `provider`/`config` tell the provider adapter how to\n * render it. Passing it to `Toolkit.run` yields a `non_local_tool` result.\n */\nexport type ProviderTool<Name extends string, Input, Provider extends string, Config> = {\n readonly _tag: \"ProviderTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n readonly provider: Provider\n readonly config: Config\n readonly strict?: boolean\n}\n\n/**\n * A typed request for the agent loop to change control flow (escalate, pause,\n * schedule, hand off). Model-visible and decodable (`decodeArgs`) but never\n * locally executed — the loop intercepts the call in `onTurnComplete` and acts\n * on it, so there is no fake `run`. `Toolkit.run` reports it as `non_local_tool`.\n */\nexport type SignalTool<Name extends string, Input> = {\n readonly _tag: \"SignalTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/**\n * A typed request for an external actor (user, channel, frontend) to respond\n * before the loop resumes. Like `SignalTool` it is decode-only, but its\n * lifecycle differs: the loop usually stops/pauses and resumes later by\n * appending a `function_call_output` once the actor replies.\n */\nexport type InteractionTool<Name extends string, Input> = {\n readonly _tag: \"InteractionTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/** A local (executable) tool of any shape. Readability alias. */\nexport type AnyLocalTool<E = any, R = any> = Tool<string, any, any, any, E, R>\n\n/** Any of the four model-visible tool kinds. */\nexport type AnyTool<E = any, R = any> =\n | Tool<string, any, any, any, E, R>\n | ProviderTool<string, any, string, any>\n | SignalTool<string, any>\n | InteractionTool<string, any>\n\n/** A model-visible tool that carries a decodable input schema (every kind). */\nexport type DecodableTool<Input> = {\n readonly name: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/** Extract the `R` of a local tool; non-local kinds contribute `never`. */\nexport type ToolR<T> = T extends Tool<string, any, any, any, any, infer R> ? R : never\n\n/** Extract the `E` of a local tool; non-local kinds contribute `never`. */\nexport type ToolE<T> = T extends Tool<string, any, any, any, infer E, any> ? E : never\n\n/**\n * Provider-agnostic tool descriptor. Each provider maps `inputSchema`\n * to its own wire field (OpenAI → `parameters`, Anthropic →\n * `input_schema`). Built from a `Tool` by `Tool.toDescriptors`.\n */\nexport type ToolDescriptor = {\n readonly name: string\n readonly description: string\n readonly inputSchema: Record<string, unknown>\n readonly strict?: boolean\n}\n\nconst resolveInputSchema = (\n schema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>,\n): ToolInputSchema<any> =>\n Schema.isSchema(schema)\n ? fromEffectSchema(schema as Schema.Codec<any, any, never, any>)\n : (schema as ToolInputSchema<any>)\n\n/** A model-visible tool with a local `Effect` executor. */\nexport const make: {\n <\n Name extends string,\n S extends Schema.Codec<any, any, never, any>,\n Event,\n Output,\n E = never,\n R = never,\n >(\n spec: Omit<Tool<Name, S[\"Type\"], Event, Output, E, R>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): Tool<Name, S[\"Type\"], Event, Output, E, R>\n <Name extends string, Input, Event, Output, E = never, R = never>(\n spec: Omit<Tool<Name, Input, Event, Output, E, R>, \"_tag\">,\n ): Tool<Name, Input, Event, Output, E, R>\n} = (\n spec: Omit<AnyLocalTool, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): AnyLocalTool => ({\n _tag: \"LocalTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A provider-hosted tool (native web search, code execution, RAG grounding).\n * No local executor — `provider`/`config` drive the provider adapter's\n * rendering. The model sees it; `Toolkit.run` reports it as `non_local_tool`.\n */\nexport const provider: {\n <\n Name extends string,\n S extends Schema.Codec<any, any, never, any>,\n Provider extends string,\n Config,\n >(\n spec: Omit<ProviderTool<Name, S[\"Type\"], Provider, Config>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): ProviderTool<Name, S[\"Type\"], Provider, Config>\n <Name extends string, Input, Provider extends string, Config>(\n spec: Omit<ProviderTool<Name, Input, Provider, Config>, \"_tag\">,\n ): ProviderTool<Name, Input, Provider, Config>\n} = (\n spec: Omit<ProviderTool<string, any, string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): ProviderTool<string, any, string, any> => ({\n _tag: \"ProviderTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A typed control-flow signal to the agent loop (escalate, pause, schedule,\n * hand off). Decode-only: the loop interprets the call rather than executing a\n * handler, so there is no `run`.\n */\nexport const signal: {\n <Name extends string, S extends Schema.Codec<any, any, never, any>>(\n spec: Omit<SignalTool<Name, S[\"Type\"]>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): SignalTool<Name, S[\"Type\"]>\n <Name extends string, Input>(spec: Omit<SignalTool<Name, Input>, \"_tag\">): SignalTool<Name, Input>\n} = (\n spec: Omit<SignalTool<string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): SignalTool<string, any> => ({\n _tag: \"SignalTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A typed request for an external actor to respond before the loop resumes\n * (frontend, Telegram, CLI prompt). Decode-only; the loop stops/pauses and\n * resumes by appending the actor's `function_call_output`.\n */\nexport const interaction: {\n <Name extends string, S extends Schema.Codec<any, any, never, any>>(\n spec: Omit<InteractionTool<Name, S[\"Type\"]>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): InteractionTool<Name, S[\"Type\"]>\n <Name extends string, Input>(\n spec: Omit<InteractionTool<Name, Input>, \"_tag\">,\n ): InteractionTool<Name, Input>\n} = (\n spec: Omit<InteractionTool<string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): InteractionTool<string, any> => ({\n _tag: \"InteractionTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * Return a copy of a tool under a new `name`. Useful for resolving a name\n * clash before combining tools into one `Toolkit`. Literal-preserving, so the\n * renamed tool keys the toolkit record under the new name statically.\n */\nexport const withName = <N extends string, T extends AnyTool>(\n tool: T,\n name: N,\n): Omit<T, \"name\"> & { readonly name: N } => ({ ...tool, name })\n\n/**\n * Replace a local tool's `run`, keeping its model-facing definition (name,\n * description, schema, `_tag`) identical. The new `run`'s `input` is typed from\n * the original tool, so override/mock need no annotation:\n *\n * const safe = { ...toolkit, send_email: Tool.withRun(toolkit.send_email,\n * ({ to }) => Effect.succeed({ status: \"dry-run\", to })) }\n *\n * Descriptors stay identical to production, so a mocked toolkit can't drift\n * from the contract the model sees.\n */\nexport const withRun = <\n Name extends string,\n Input,\n Event,\n Output,\n E,\n R,\n Output2,\n E2 = never,\n R2 = never,\n>(\n tool: Tool<Name, Input, Event, Output, E, R>,\n run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output2, E2, R2>,\n): Tool<Name, Input, Event, Output2, E2, R2> => ({ ...tool, run })\n\n/** True for provider-hosted tools — the kind a provider renders natively. */\nexport const isProviderTool = (tool: AnyTool): tool is ProviderTool<string, any, string, any> =>\n tool._tag === \"ProviderTool\"\n\n/**\n * The `ProviderTool`s in a toolkit, for a provider adapter to render as its\n * native hosted-tool entries (web search, code execution, grounding). The other\n * kinds render as function declarations via `descriptorsOf`.\n */\nexport const providerToolsOf = (\n toolkit?: Readonly<Record<string, AnyTool>>,\n): ReadonlyArray<ProviderTool<string, any, string, any>> =>\n toolkit === undefined ? [] : Object.values(toolkit).filter(isProviderTool)\n\n/**\n * Render tools to provider-agnostic descriptors. `ProviderTool`s are excluded:\n * they are provider-hosted and must never leak into the function-declaration\n * list — each adapter renders them natively via `providerToolsOf`. Every other\n * kind is model-visible as a declaration.\n */\nexport const toDescriptors = (tools: ReadonlyArray<AnyTool>): ReadonlyArray<ToolDescriptor> =>\n tools\n .filter((tool) => !isProviderTool(tool))\n .map((tool) => {\n const inputSchema = tool.inputSchema[\"~standard\"].jsonSchema.input({\n target: \"draft-2020-12\",\n })\n // `strict` only exists on local/provider kinds; signals/interactions omit it.\n const strict = \"strict\" in tool ? tool.strict : undefined\n return strict !== undefined\n ? { name: tool.name, description: tool.description, inputSchema, strict }\n : { name: tool.name, description: tool.description, inputSchema }\n })\n\n/**\n * Render a `Toolkit` (a name-indexed record of tools) to descriptors, treating\n * an absent toolkit as no tools. The normalization point the `LanguageModel`\n * boundary uses so a request can carry the toolkit itself instead of a\n * pre-rendered descriptor array. Lives here (not on `Toolkit`) to keep providers\n * free of a dependency on the toolkit module — it only needs the tool values.\n */\nexport const descriptorsOf = (\n toolkit?: Readonly<Record<string, AnyTool>>,\n): ReadonlyArray<ToolDescriptor> =>\n toolkit === undefined ? [] : toDescriptors(Object.values(toolkit))\n\n/**\n * Outcome of decoding a call's `arguments` against a tool's `inputSchema`,\n * as data so both the typed (`decodeArgs`) and result-synthesizing\n * (`Toolkit.run`) callers can translate it their own way.\n */\nexport type DecodeResult<Input> =\n | { readonly _tag: \"ok\"; readonly input: Input }\n | { readonly _tag: \"parseError\" }\n | { readonly _tag: \"invalid\"; readonly issues: ReadonlyArray<StandardSchemaV1.Issue> }\n\nconst VALIDATION_THREW: ReadonlyArray<StandardSchemaV1.Issue> = [\n { message: \"input schema validation threw\" },\n]\n\n/**\n * Parse and validate a call's `arguments`. Empty arguments normalize to\n * `{}` (some models emit `\"\"` for zero-arg tools); a throwing validator is\n * captured, not propagated. Never fails.\n */\nexport const decodeCallInput = <Input>(\n tool: DecodableTool<Input>,\n call: ToolCall,\n): Effect.Effect<DecodeResult<Input>> => {\n const raw = call.arguments.trim() === \"\" ? \"{}\" : call.arguments\n return Effect.try({\n // @effect-diagnostics-next-line effect/preferSchemaOverJson:off\n try: () => JSON.parse(raw) as unknown,\n catch: (): DecodeResult<Input> => ({ _tag: \"parseError\" }),\n }).pipe(\n Effect.flatMap((parsed) =>\n Effect.tryPromise({\n try: () => Promise.resolve(tool.inputSchema[\"~standard\"].validate(parsed)),\n catch: (): DecodeResult<Input> => ({ _tag: \"invalid\", issues: VALIDATION_THREW }),\n }).pipe(\n Effect.map(\n (result): DecodeResult<Input> =>\n result.issues !== undefined\n ? { _tag: \"invalid\", issues: result.issues }\n : { _tag: \"ok\", input: result.value },\n ),\n ),\n ),\n Effect.catch((captured: DecodeResult<Input>) => Effect.succeed(captured)),\n )\n}\n\n/**\n * Decode and validate a function_call's JSON `arguments` against the tool's\n * own `inputSchema`, yielding the typed input. The decode-only half of\n * `execute` - reach for it when you intercept a call to translate it into a\n * control-flow decision rather than running the tool's `run`.\n *\n * Malformed JSON fails with `ToolError`; a parsed body that violates\n * `inputSchema` fails with `ToolValidationError` carrying the issues.\n */\nexport const decodeArgs = <Input>(\n tool: DecodableTool<Input>,\n call: ToolCall,\n): Effect.Effect<Input, ToolError | ToolValidationError> =>\n decodeCallInput(tool, call).pipe(\n Effect.flatMap(\n Match.type<DecodeResult<Input>>().pipe(\n Match.tag(\"ok\", ({ input }) => Effect.succeed(input)),\n Match.tag(\"parseError\", () =>\n Effect.fail(\n new ToolError({\n call_id: call.call_id,\n tool: tool.name,\n message: \"Failed to parse JSON arguments\",\n }),\n ),\n ),\n Match.tag(\"invalid\", ({ issues }) =>\n Effect.fail(new ToolValidationError({ call_id: call.call_id, tool: tool.name, issues })),\n ),\n Match.exhaustive,\n ),\n ),\n )\n\n/**\n * Decode and validate the JSON arguments of a function_call against the\n * tool's input schema, run the tool, and serialize the output into a\n * function_call_output item. Single-shot: intermediate events emitted by\n * `run` are discarded (use `Toolkit.run` to stream them).\n */\nexport const execute = <Name extends string, Input, Event, Output, E, R>(\n tool: Tool<Name, Input, Event, Output, E, R>,\n call: ToolCall,\n): Effect.Effect<ToolCallOutput, E | ToolError | ToolValidationError, R> =>\n decodeArgs(tool, call).pipe(\n Effect.flatMap((input) => tool.run(input, () => Effect.void)),\n Effect.map((output) => toolCallOutput(call.call_id, serializeValue(output))),\n )\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Tool.mjs","names":[],"sources":["../../src/tool/Tool.ts"],"sourcesContent":["import type { StandardJSONSchemaV1, StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { Effect, Match, Schema } from \"effect\"\nimport type { ToolCall, ToolCallOutput } from \"../domain/Items.js\"\nimport { toolCallOutput } from \"../domain/Items.js\"\nimport { serializeValue } from \"./ToolResult.js\"\n\nexport class ToolError extends Schema.TaggedError<ToolError>(\"@betalyra/effect-uai/ToolError\")(\n \"ToolError\",\n {\n call_id: Schema.String,\n tool: Schema.String,\n message: Schema.String,\n cause: Schema.optional(Schema.Unknown),\n },\n) {}\n\n/**\n * One Standard Schema validation issue, carried on `ToolValidationError`.\n * Mirrors `StandardSchemaV1.Issue`; `path` is left as `unknown` elements\n * because Standard Schema path segments may be symbols or `{ key }` objects.\n */\nconst ToolValidationIssue = Schema.Struct({\n message: Schema.String,\n path: Schema.optional(Schema.Array(Schema.Unknown)),\n})\n\n/**\n * The model's `arguments` for a call failed the tool's `inputSchema`. Distinct\n * from `ToolError` (malformed JSON, runtime crash) so callers can tell a\n * contract violation apart from a parse or execution failure, and so the\n * structured `issues` survive instead of being flattened into a message string.\n */\nexport class ToolValidationError extends Schema.TaggedError<ToolValidationError>(\n \"@betalyra/effect-uai/ToolValidationError\",\n)(\"ToolValidationError\", {\n call_id: Schema.String,\n tool: Schema.String,\n issues: Schema.Array(ToolValidationIssue),\n}) {}\n\n/**\n * The deliberate \"speak to the model\" failure sentinel. A tool's `run` that\n * fails with a `ToolFailed` (or a bare `string`, its sugar) is absorbed by\n * `Toolkit.run` into a `ToolResult.Failure` the model reads and adapts to,\n * instead of ending the run. Every other error type propagates typed on the\n * executor's stream. `kind` overrides the synthesized `Failure.kind`\n * (default `\"tool_failed\"`) so a tool can hand the model a distinguishable\n * category (`\"not_found\"`, `\"rate_limited\"`) without a new result variant.\n */\nexport class ToolFailed extends Schema.TaggedError<ToolFailed>(\"@betalyra/effect-uai/ToolFailed\")(\n \"ToolFailed\",\n {\n message: Schema.String,\n kind: Schema.optional(Schema.String),\n },\n) {}\n\n/**\n * Fail a tool's `run` with a model-visible message (optionally a `kind`).\n * `yield* Tool.fail(\"page not found\", { kind: \"not_found\" })` inside a\n * `run`'s `Effect.gen`; the executor absorbs it as `ToolResult.Failure`.\n */\nexport const fail = (\n message: string,\n options?: { readonly kind?: string },\n): Effect.Effect<never, ToolFailed> =>\n Effect.fail(\n new ToolFailed({ message, ...(options?.kind !== undefined ? { kind: options.kind } : {}) }),\n )\n\n/**\n * Schemas accepted on `Tool.inputSchema`. Must implement both Standard\n * Schema validation and JSON Schema conversion (for rendering tool\n * descriptors to provider request bodies).\n *\n * Any Standard-Schema-compliant library that exposes both interfaces\n * works directly: Zod 4+, Valibot, ArkType, Effect Schema (after\n * `fromEffectSchema`), etc.\n */\nexport type ToolInputSchema<Input = unknown> = StandardSchemaV1<unknown, Input> &\n StandardJSONSchemaV1<unknown, Input>\n\n/**\n * Convenience wrapper for Effect Schema users - adds both the\n * `validate` and `jsonSchema` extensions to a plain Effect Schema so it\n * can be used as a `Tool.inputSchema`.\n */\nexport const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(\n schema: S,\n): S & ToolInputSchema<S[\"Type\"]> =>\n Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema))\n\n/**\n * Use any schema library that implements both Standard Schema (validation)\n * and Standard JSON Schema (JSON Schema generation) as a `Tool.inputSchema`.\n * Covers Zod 4.2+, Valibot 1.2+, and ArkType 2.1.28+ in one helper.\n *\n * Effect Schema doesn't implement Standard JSON Schema natively — use\n * `fromEffectSchema` for those.\n *\n * The intersection constraint catches missing interfaces at compile time:\n * a Zod v3 schema (no Standard JSON Schema) produces a precise type error\n * pointing at the missing interface rather than a runtime surprise. The\n * helper itself is a thin type-narrowing identity — schemas that satisfy\n * both standards already structurally satisfy `ToolInputSchema`; the\n * helper makes the input type inference explicit at the call site.\n */\nexport const fromStandardSchema = <S extends StandardSchemaV1 & StandardJSONSchemaV1>(\n schema: S,\n): S & ToolInputSchema<StandardSchemaV1.InferOutput<S>> =>\n schema as S & ToolInputSchema<StandardSchemaV1.InferOutput<S>>\n\n/**\n * Input schema for a tool that takes no arguments. Renders a clean\n * `{ type: \"object\", properties: {} }`; validation accepts anything and\n * yields `{}`. Use it instead of `fromEffectSchema(Schema.Struct({}))`,\n * which renders a degenerate `anyOf` (object-or-array) that strict\n * providers like Gemini reject.\n */\nexport const noInput: ToolInputSchema<Record<string, never>> = {\n \"~standard\": {\n version: 1,\n vendor: \"effect-uai\",\n validate: () => ({ value: {} }),\n jsonSchema: {\n input: () => ({ type: \"object\", properties: {} }),\n output: () => ({ type: \"object\", properties: {} }),\n },\n },\n}\n\n/**\n * Emit one intermediate `Event` from inside a tool's `run`. Backed by the\n * executor's per-call queue, so it returns an `Effect` (backpressure) and\n * composes with `Stream.runForEach(emit)`.\n */\nexport type Emit<Event> = (event: Event) => Effect.Effect<void>\n\n/**\n * A model-visible tool with a local `Effect` executor. `run` computes the\n * model-facing `Output`; emitting progress is a side channel via `emit`. A\n * plain tool never calls `emit` (its `Event` is irrelevant); a streaming tool\n * emits intermediate `Event`s that flow to the consumer as\n * `ToolEvent.Progress` in real time.\n *\n * This is one of four tool *kinds*, discriminated by `_tag`. The other three\n * (`ProviderTool`, `SignalTool`, `InteractionTool`) are model-visible but have\n * no local executor — see their constructors below. `Tool` keeps its name (and\n * the `Tool.make` constructor) because the local kind is by far the common one.\n */\nexport type Tool<Name extends string, Input, Event, Output, E = unknown, R = never> = {\n readonly _tag: \"LocalTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n readonly run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output, E, R>\n /**\n * Bound on this tool's emit queue. Unbounded when omitted. Per-tool because\n * backpressure depends on how a given tool emits, unlike `concurrency`\n * (cross-tool, on `Toolkit.run`'s options).\n */\n readonly emitBufferSize?: number\n /**\n * Whether the provider should render this tool with its strict-mode\n * flag (OpenAI's `strict: true`, etc). Default: true. The framework\n * never rewrites the schema; if the rendered JSON Schema isn't\n * compatible, the provider returns an error.\n */\n readonly strict?: boolean\n}\n\n/**\n * A provider-hosted tool: visible to the model and executed by the *provider*\n * (native web search, code execution, RAG grounding), never by this process.\n * It has no local `run`; `provider`/`config` tell the provider adapter how to\n * render it. Passing it to `Toolkit.run` yields a `non_local_tool` result.\n */\nexport type ProviderTool<Name extends string, Input, Provider extends string, Config> = {\n readonly _tag: \"ProviderTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n readonly provider: Provider\n readonly config: Config\n readonly strict?: boolean\n}\n\n/**\n * A typed request for the agent loop to change control flow (escalate, pause,\n * schedule, hand off). Model-visible and decodable (`decodeArgs`) but never\n * locally executed — the loop intercepts the call in `onTurnComplete` and acts\n * on it, so there is no fake `run`. `Toolkit.run` reports it as `non_local_tool`.\n */\nexport type SignalTool<Name extends string, Input> = {\n readonly _tag: \"SignalTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/**\n * A typed request for an external actor (user, channel, frontend) to respond\n * before the loop resumes. Like `SignalTool` it is decode-only, but its\n * lifecycle differs: the loop usually stops/pauses and resumes later by\n * appending a `function_call_output` once the actor replies.\n */\nexport type InteractionTool<Name extends string, Input> = {\n readonly _tag: \"InteractionTool\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/** A local (executable) tool of any shape. Readability alias. */\nexport type AnyLocalTool<E = any, R = any> = Tool<string, any, any, any, E, R>\n\n/** Any of the four model-visible tool kinds. */\nexport type AnyTool<E = any, R = any> =\n | Tool<string, any, any, any, E, R>\n | ProviderTool<string, any, string, any>\n | SignalTool<string, any>\n | InteractionTool<string, any>\n\n/** A model-visible tool that carries a decodable input schema (every kind). */\nexport type DecodableTool<Input> = {\n readonly name: string\n readonly inputSchema: ToolInputSchema<Input>\n}\n\n/** Extract the `R` of a local tool; non-local kinds contribute `never`. */\nexport type ToolR<T> = T extends Tool<string, any, any, any, any, infer R> ? R : never\n\n/** Extract the `E` of a local tool; non-local kinds contribute `never`. */\nexport type ToolE<T> = T extends Tool<string, any, any, any, infer E, any> ? E : never\n\n/**\n * Provider-agnostic tool descriptor. Each provider maps `inputSchema`\n * to its own wire field (OpenAI → `parameters`, Anthropic →\n * `input_schema`). Built from a `Tool` by `Tool.toDescriptors`.\n */\nexport type ToolDescriptor = {\n readonly name: string\n readonly description: string\n readonly inputSchema: Record<string, unknown>\n readonly strict?: boolean\n}\n\nconst resolveInputSchema = (\n schema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>,\n): ToolInputSchema<any> =>\n Schema.isSchema(schema)\n ? fromEffectSchema(schema as Schema.Codec<any, any, never, any>)\n : (schema as ToolInputSchema<any>)\n\n/** A model-visible tool with a local `Effect` executor. */\nexport const make: {\n <\n Name extends string,\n S extends Schema.Codec<any, any, never, any>,\n Event,\n Output,\n E = never,\n R = never,\n >(\n spec: Omit<Tool<Name, S[\"Type\"], Event, Output, E, R>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): Tool<Name, S[\"Type\"], Event, Output, E, R>\n <Name extends string, Input, Event, Output, E = never, R = never>(\n spec: Omit<Tool<Name, Input, Event, Output, E, R>, \"_tag\">,\n ): Tool<Name, Input, Event, Output, E, R>\n} = (\n spec: Omit<AnyLocalTool, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): AnyLocalTool => ({\n _tag: \"LocalTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A provider-hosted tool (native web search, code execution, RAG grounding).\n * No local executor — `provider`/`config` drive the provider adapter's\n * rendering. The model sees it; `Toolkit.run` reports it as `non_local_tool`.\n */\nexport const provider: {\n <\n Name extends string,\n S extends Schema.Codec<any, any, never, any>,\n Provider extends string,\n Config,\n >(\n spec: Omit<ProviderTool<Name, S[\"Type\"], Provider, Config>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): ProviderTool<Name, S[\"Type\"], Provider, Config>\n <Name extends string, Input, Provider extends string, Config>(\n spec: Omit<ProviderTool<Name, Input, Provider, Config>, \"_tag\">,\n ): ProviderTool<Name, Input, Provider, Config>\n} = (\n spec: Omit<ProviderTool<string, any, string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): ProviderTool<string, any, string, any> => ({\n _tag: \"ProviderTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A typed control-flow signal to the agent loop (escalate, pause, schedule,\n * hand off). Decode-only: the loop interprets the call rather than executing a\n * handler, so there is no `run`.\n */\nexport const signal: {\n <Name extends string, S extends Schema.Codec<any, any, never, any>>(\n spec: Omit<SignalTool<Name, S[\"Type\"]>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): SignalTool<Name, S[\"Type\"]>\n <Name extends string, Input>(spec: Omit<SignalTool<Name, Input>, \"_tag\">): SignalTool<Name, Input>\n} = (\n spec: Omit<SignalTool<string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): SignalTool<string, any> => ({\n _tag: \"SignalTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * A typed request for an external actor to respond before the loop resumes\n * (frontend, Telegram, CLI prompt). Decode-only; the loop stops/pauses and\n * resumes by appending the actor's `function_call_output`.\n */\nexport const interaction: {\n <Name extends string, S extends Schema.Codec<any, any, never, any>>(\n spec: Omit<InteractionTool<Name, S[\"Type\"]>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: S\n },\n ): InteractionTool<Name, S[\"Type\"]>\n <Name extends string, Input>(\n spec: Omit<InteractionTool<Name, Input>, \"_tag\">,\n ): InteractionTool<Name, Input>\n} = (\n spec: Omit<InteractionTool<string, any>, \"_tag\" | \"inputSchema\"> & {\n readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>\n },\n): InteractionTool<string, any> => ({\n _tag: \"InteractionTool\",\n ...spec,\n inputSchema: resolveInputSchema(spec.inputSchema),\n})\n\n/**\n * Return a copy of a tool under a new `name`. Useful for resolving a name\n * clash before combining tools into one `Toolkit`. Literal-preserving, so the\n * renamed tool keys the toolkit record under the new name statically.\n */\nexport const withName = <N extends string, T extends AnyTool>(\n tool: T,\n name: N,\n): Omit<T, \"name\"> & { readonly name: N } => ({ ...tool, name })\n\n/**\n * Replace a local tool's `run`, keeping its model-facing definition (name,\n * description, schema, `_tag`) identical. The new `run`'s `input` is typed from\n * the original tool, so override/mock need no annotation:\n *\n * const safe = { ...toolkit, send_email: Tool.withRun(toolkit.send_email,\n * ({ to }) => Effect.succeed({ status: \"dry-run\", to })) }\n *\n * Descriptors stay identical to production, so a mocked toolkit can't drift\n * from the contract the model sees.\n */\nexport const withRun = <\n Name extends string,\n Input,\n Event,\n Output,\n E,\n R,\n Output2,\n E2 = never,\n R2 = never,\n>(\n tool: Tool<Name, Input, Event, Output, E, R>,\n run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output2, E2, R2>,\n): Tool<Name, Input, Event, Output2, E2, R2> => ({ ...tool, run })\n\n/** True for provider-hosted tools — the kind a provider renders natively. */\nexport const isProviderTool = (tool: AnyTool): tool is ProviderTool<string, any, string, any> =>\n tool._tag === \"ProviderTool\"\n\n/**\n * The `ProviderTool`s in a toolkit, for a provider adapter to render as its\n * native hosted-tool entries (web search, code execution, grounding). The other\n * kinds render as function declarations via `descriptorsOf`.\n */\nexport const providerToolsOf = (\n toolkit?: Readonly<Record<string, AnyTool>>,\n): ReadonlyArray<ProviderTool<string, any, string, any>> =>\n toolkit === undefined ? [] : Object.values(toolkit).filter(isProviderTool)\n\n/**\n * Render tools to provider-agnostic descriptors. `ProviderTool`s are excluded:\n * they are provider-hosted and must never leak into the function-declaration\n * list — each adapter renders them natively via `providerToolsOf`. Every other\n * kind is model-visible as a declaration.\n */\nexport const toDescriptors = (tools: ReadonlyArray<AnyTool>): ReadonlyArray<ToolDescriptor> =>\n tools\n .filter((tool) => !isProviderTool(tool))\n .map((tool) => {\n const inputSchema = tool.inputSchema[\"~standard\"].jsonSchema.input({\n target: \"draft-2020-12\",\n })\n // `strict` only exists on local/provider kinds; signals/interactions omit it.\n const strict = \"strict\" in tool ? tool.strict : undefined\n return strict !== undefined\n ? { name: tool.name, description: tool.description, inputSchema, strict }\n : { name: tool.name, description: tool.description, inputSchema }\n })\n\n/**\n * Render a `Toolkit` (a name-indexed record of tools) to descriptors, treating\n * an absent toolkit as no tools. The normalization point the `LanguageModel`\n * boundary uses so a request can carry the toolkit itself instead of a\n * pre-rendered descriptor array. Lives here (not on `Toolkit`) to keep providers\n * free of a dependency on the toolkit module — it only needs the tool values.\n */\nexport const descriptorsOf = (\n toolkit?: Readonly<Record<string, AnyTool>>,\n): ReadonlyArray<ToolDescriptor> =>\n toolkit === undefined ? [] : toDescriptors(Object.values(toolkit))\n\n/**\n * Outcome of decoding a call's `arguments` against a tool's `inputSchema`,\n * as data so both the typed (`decodeArgs`) and result-synthesizing\n * (`Toolkit.run`) callers can translate it their own way.\n */\nexport type DecodeResult<Input> =\n | { readonly _tag: \"ok\"; readonly input: Input }\n | { readonly _tag: \"parseError\" }\n | { readonly _tag: \"invalid\"; readonly issues: ReadonlyArray<StandardSchemaV1.Issue> }\n\nconst VALIDATION_THREW: ReadonlyArray<StandardSchemaV1.Issue> = [\n { message: \"input schema validation threw\" },\n]\n\n/**\n * Parse and validate a call's `arguments`. Empty arguments normalize to\n * `{}` (some models emit `\"\"` for zero-arg tools); a throwing validator is\n * captured, not propagated. Never fails.\n */\nexport const decodeCallInput = <Input>(\n tool: DecodableTool<Input>,\n call: ToolCall,\n): Effect.Effect<DecodeResult<Input>> => {\n const raw = call.arguments.trim() === \"\" ? \"{}\" : call.arguments\n return Effect.try({\n // @effect-diagnostics-next-line effect/preferSchemaOverJson:off\n try: () => JSON.parse(raw) as unknown,\n catch: (): DecodeResult<Input> => ({ _tag: \"parseError\" }),\n }).pipe(\n Effect.flatMap((parsed) =>\n Effect.tryPromise({\n try: () => Promise.resolve(tool.inputSchema[\"~standard\"].validate(parsed)),\n catch: (): DecodeResult<Input> => ({ _tag: \"invalid\", issues: VALIDATION_THREW }),\n }).pipe(\n Effect.map((result): DecodeResult<Input> =>\n result.issues !== undefined\n ? { _tag: \"invalid\", issues: result.issues }\n : { _tag: \"ok\", input: result.value },\n ),\n ),\n ),\n Effect.catch((captured: DecodeResult<Input>) => Effect.succeed(captured)),\n )\n}\n\n/**\n * Decode and validate a function_call's JSON `arguments` against the tool's\n * own `inputSchema`, yielding the typed input. The decode-only half of\n * `execute` - reach for it when you intercept a call to translate it into a\n * control-flow decision rather than running the tool's `run`.\n *\n * Malformed JSON fails with `ToolError`; a parsed body that violates\n * `inputSchema` fails with `ToolValidationError` carrying the issues.\n */\nexport const decodeArgs = <Input>(\n tool: DecodableTool<Input>,\n call: ToolCall,\n): Effect.Effect<Input, ToolError | ToolValidationError> =>\n decodeCallInput(tool, call).pipe(\n Effect.flatMap(\n Match.type<DecodeResult<Input>>().pipe(\n Match.tag(\"ok\", ({ input }) => Effect.succeed(input)),\n Match.tag(\"parseError\", () =>\n Effect.fail(\n new ToolError({\n call_id: call.call_id,\n tool: tool.name,\n message: \"Failed to parse JSON arguments\",\n }),\n ),\n ),\n Match.tag(\"invalid\", ({ issues }) =>\n Effect.fail(new ToolValidationError({ call_id: call.call_id, tool: tool.name, issues })),\n ),\n Match.exhaustive,\n ),\n ),\n )\n\n/**\n * Decode and validate the JSON arguments of a function_call against the\n * tool's input schema, run the tool, and serialize the output into a\n * function_call_output item. Single-shot: intermediate events emitted by\n * `run` are discarded (use `Toolkit.run` to stream them).\n */\nexport const execute = <Name extends string, Input, Event, Output, E, R>(\n tool: Tool<Name, Input, Event, Output, E, R>,\n call: ToolCall,\n): Effect.Effect<ToolCallOutput, E | ToolError | ToolValidationError, R> =>\n decodeArgs(tool, call).pipe(\n Effect.flatMap((input) => tool.run(input, () => Effect.void)),\n Effect.map((output) => toolCallOutput(call.call_id, serializeValue(output))),\n )\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
package/dist/tool/Tool.test.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { decodeArgs, descriptorsOf, fromEffectSchema, fromStandardSchema, isProviderTool, make, noInput, provider, providerToolsOf, signal, toDescriptors, withRun } from "./Tool.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { m as it, o as describe } from "../utils-CiH_DjFW.mjs";
|
|
3
|
+
import { n as globalExpect } from "../test.DNmyFkvJ-YPzDR0Co.mjs";
|
|
4
|
+
import { t as import_dist } from "../dist-BaenoU1A.mjs";
|
|
3
5
|
import { Context, Effect, Schema } from "effect";
|
|
4
6
|
//#region src/tool/Tool.test.ts
|
|
5
7
|
const emailRecipientSchema = { "~standard": {
|
|
@@ -31,8 +33,10 @@ describe("Tool.fromStandardSchema", () => {
|
|
|
31
33
|
it("returns the schema (structurally) typed as ToolInputSchema<Output>", () => {
|
|
32
34
|
const adapted = fromStandardSchema(emailRecipientSchema);
|
|
33
35
|
globalExpect(adapted).toBe(emailRecipientSchema);
|
|
34
|
-
|
|
35
|
-
globalExpect(
|
|
36
|
+
const valid = adapted["~standard"].validate({ to: "hi@example.com" });
|
|
37
|
+
globalExpect(valid).toEqual({ value: { to: "hi@example.com" } });
|
|
38
|
+
const json = adapted["~standard"].jsonSchema.input({ target: "draft-2020-12" });
|
|
39
|
+
globalExpect(json).toEqual({
|
|
36
40
|
type: "object",
|
|
37
41
|
properties: { to: { type: "string" } },
|
|
38
42
|
required: ["to"]
|
|
@@ -45,7 +49,8 @@ describe("Tool.fromStandardSchema", () => {
|
|
|
45
49
|
inputSchema: fromStandardSchema(emailRecipientSchema),
|
|
46
50
|
run: ({ to }) => Effect.succeed(`queued: ${to}`)
|
|
47
51
|
});
|
|
48
|
-
|
|
52
|
+
const result = await Effect.runPromise(sendEmail.run({ to: "x@y.z" }, () => Effect.void));
|
|
53
|
+
globalExpect(result).toBe("queued: x@y.z");
|
|
49
54
|
});
|
|
50
55
|
it("type: rejects schemas missing the Standard JSON Schema half at compile time", () => {
|
|
51
56
|
fromStandardSchema(standardOnly);
|
|
@@ -70,7 +75,8 @@ describe("Tool.withRun", () => {
|
|
|
70
75
|
it("swaps run while keeping the model-facing descriptor identical", async () => {
|
|
71
76
|
const dryRun = withRun(sendEmail, ({ to }) => Effect.succeed(`dry-run: ${to}`));
|
|
72
77
|
globalExpect(toDescriptors([dryRun])).toEqual(toDescriptors([sendEmail]));
|
|
73
|
-
|
|
78
|
+
const out = await Effect.runPromise(dryRun.run({ to: "x@y.z" }, () => Effect.void));
|
|
79
|
+
globalExpect(out).toBe("dry-run: x@y.z");
|
|
74
80
|
});
|
|
75
81
|
});
|
|
76
82
|
describe("Tool.decodeArgs", () => {
|
|
@@ -92,10 +98,12 @@ describe("Tool.decodeArgs", () => {
|
|
|
92
98
|
(0, import_dist.expectTypeOf)(input).toEqualTypeOf();
|
|
93
99
|
});
|
|
94
100
|
it("fails with ToolError when the arguments aren't valid JSON", async () => {
|
|
95
|
-
|
|
101
|
+
const exit = await Effect.runPromiseExit(decodeArgs(sendEmail, callWith("not json")));
|
|
102
|
+
globalExpect(exit._tag).toBe("Failure");
|
|
96
103
|
});
|
|
97
104
|
it("fails with ToolError when the arguments don't satisfy the schema", async () => {
|
|
98
|
-
|
|
105
|
+
const exit = await Effect.runPromiseExit(decodeArgs(sendEmail, callWith("{\"to\":42}")));
|
|
106
|
+
globalExpect(exit._tag).toBe("Failure");
|
|
99
107
|
});
|
|
100
108
|
});
|
|
101
109
|
describe("Effect Schema inputSchema", () => {
|
|
@@ -113,7 +121,8 @@ describe("Effect Schema inputSchema", () => {
|
|
|
113
121
|
arguments: args
|
|
114
122
|
});
|
|
115
123
|
it("runs with the input inferred from the schema's Type", async () => {
|
|
116
|
-
|
|
124
|
+
const result = await Effect.runPromise(sendEmail.run({ to: "x@y.z" }, () => Effect.void));
|
|
125
|
+
globalExpect(result).toBe("queued: x@y.z");
|
|
117
126
|
});
|
|
118
127
|
it("type: Input is the schema's Type", () => {
|
|
119
128
|
(0, import_dist.expectTypeOf)().toEqualTypeOf();
|
|
@@ -139,7 +148,8 @@ describe("Effect Schema inputSchema", () => {
|
|
|
139
148
|
(0, import_dist.expectTypeOf)(input).toEqualTypeOf();
|
|
140
149
|
});
|
|
141
150
|
it("fails with ToolValidationError when the arguments don't satisfy the schema", async () => {
|
|
142
|
-
|
|
151
|
+
const exit = await Effect.runPromiseExit(decodeArgs(sendEmail, callWith("{\"to\":42}")));
|
|
152
|
+
globalExpect(exit._tag).toBe("Failure");
|
|
143
153
|
});
|
|
144
154
|
it("renders a JSON Schema descriptor from the wrapped schema", () => {
|
|
145
155
|
const [descriptor] = toDescriptors([sendEmail]);
|
|
@@ -158,7 +168,8 @@ describe("Effect Schema inputSchema", () => {
|
|
|
158
168
|
run: ({ to }) => Effect.succeed(`queued: ${to}`)
|
|
159
169
|
});
|
|
160
170
|
globalExpect(toDescriptors([wrapped])).toEqual(toDescriptors([sendEmail]));
|
|
161
|
-
|
|
171
|
+
const input = await Effect.runPromise(decodeArgs(wrapped, callWith("{\"to\":\"x@y.z\"}")));
|
|
172
|
+
(0, import_dist.expectTypeOf)(input).toEqualTypeOf();
|
|
162
173
|
});
|
|
163
174
|
it("signal: decode-only kinds accept an Effect Schema directly", async () => {
|
|
164
175
|
const escalate = signal({
|
|
@@ -201,7 +212,8 @@ describe("provider-hosted tools", () => {
|
|
|
201
212
|
web_search: hostedTool
|
|
202
213
|
};
|
|
203
214
|
it("descriptorsOf excludes provider-hosted tools", () => {
|
|
204
|
-
|
|
215
|
+
const descriptors = descriptorsOf(toolkit);
|
|
216
|
+
globalExpect(descriptors.map((d) => d.name)).toEqual(["send_email", "escalate"]);
|
|
205
217
|
});
|
|
206
218
|
it("toDescriptors drops a ProviderTool from a mixed array", () => {
|
|
207
219
|
globalExpect(toDescriptors([localTool, hostedTool]).map((d) => d.name)).toEqual(["send_email"]);
|