@effect-uai/core 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AiError-CAX_48RU.d.mts → AiError-Bf1pq4BB.d.mts} +9 -4
- package/dist/{AiError-CAX_48RU.d.mts.map → AiError-Bf1pq4BB.d.mts.map} +1 -1
- package/dist/{Audio-DPlTsNdz.d.mts → Audio-CR7CSljJ.d.mts} +1 -1
- package/dist/{Audio-DPlTsNdz.d.mts.map → Audio-CR7CSljJ.d.mts.map} +1 -1
- package/dist/{Image-HNmMpMTh.d.mts → Image-BZmKfIdq.d.mts} +1 -1
- package/dist/{Image-HNmMpMTh.d.mts.map → Image-BZmKfIdq.d.mts.map} +1 -1
- package/dist/{Items-7ljaVxH_.d.mts → Items-BnIduEV2.d.mts} +7 -7
- package/dist/{Items-7ljaVxH_.d.mts.map → Items-BnIduEV2.d.mts.map} +1 -1
- package/dist/Loop-D8nWpowc.d.mts +173 -0
- package/dist/Loop-D8nWpowc.d.mts.map +1 -0
- package/dist/{SandboxError-BvKypkJm.d.mts → SandboxError-CIrVDOMV.d.mts} +2 -2
- package/dist/{SandboxError-BvKypkJm.d.mts.map → SandboxError-CIrVDOMV.d.mts.map} +1 -1
- package/dist/{StructuredFormat-BbN4dosH.d.mts → StructuredFormat-DHaUJBdd.d.mts} +2 -2
- package/dist/{StructuredFormat-BbN4dosH.d.mts.map → StructuredFormat-DHaUJBdd.d.mts.map} +1 -1
- package/dist/Tool-2lC_XMth.d.mts +284 -0
- package/dist/Tool-2lC_XMth.d.mts.map +1 -0
- package/dist/ToolEvent-cWZ2BnbS.d.mts +151 -0
- package/dist/ToolEvent-cWZ2BnbS.d.mts.map +1 -0
- package/dist/ToolResult-CL4hK_9z.d.mts +157 -0
- package/dist/ToolResult-CL4hK_9z.d.mts.map +1 -0
- package/dist/Toolkit-Dr9l_hNy.d.mts +196 -0
- package/dist/Toolkit-Dr9l_hNy.d.mts.map +1 -0
- package/dist/browser/Browser.d.mts +190 -0
- package/dist/browser/Browser.d.mts.map +1 -0
- package/dist/browser/Browser.mjs +34 -0
- package/dist/browser/Browser.mjs.map +1 -0
- package/dist/browser/BrowserError.d.mts +141 -0
- package/dist/browser/BrowserError.d.mts.map +1 -0
- package/dist/browser/BrowserError.mjs +89 -0
- package/dist/browser/BrowserError.mjs.map +1 -0
- package/dist/browser/BrowserTool.d.mts +58 -0
- package/dist/browser/BrowserTool.d.mts.map +1 -0
- package/dist/browser/BrowserTool.mjs +86 -0
- package/dist/browser/BrowserTool.mjs.map +1 -0
- package/dist/{dist-DV5ISja1.mjs → dist-B04dlFHg.mjs} +3 -3
- package/dist/{dist-DV5ISja1.mjs.map → dist-B04dlFHg.mjs.map} +1 -1
- package/dist/dist-DWgRbDN3.mjs +140 -0
- package/dist/dist-DWgRbDN3.mjs.map +1 -0
- package/dist/domain/AiError.d.mts +2 -2
- package/dist/domain/AiError.mjs +22 -3
- package/dist/domain/AiError.mjs.map +1 -1
- package/dist/domain/Audio.d.mts +1 -1
- package/dist/domain/Image.d.mts +1 -1
- package/dist/domain/Items.d.mts +1 -1
- package/dist/domain/Music.d.mts +1 -1
- package/dist/domain/Turn.d.mts +4 -4
- package/dist/domain/Turn.test.mjs +1 -1
- package/dist/embedding-model/Embedding.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
- package/dist/index.d.mts +19 -13
- package/dist/index.mjs +15 -9
- package/dist/language-model/LanguageModel.d.mts +10 -5
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.mjs +1 -1
- package/dist/loop/Loop.d.mts +2 -141
- package/dist/loop/Loop.mjs +16 -20
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +18 -1
- package/dist/loop/Loop.test.mjs.map +1 -1
- package/dist/{magic-string.es-BgIV5Mu3.mjs → magic-string.es-Dk-MMA0F.mjs} +1 -1
- package/dist/{magic-string.es-BgIV5Mu3.mjs.map → magic-string.es-Dk-MMA0F.mjs.map} +1 -1
- package/dist/music-generator/MusicGenerator.d.mts +2 -2
- package/dist/music-generator/MusicGenerator.test.mjs +1 -1
- package/dist/observability/Metrics.d.mts +116 -28
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +250 -38
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/observability/Metrics.test.d.mts +1 -0
- package/dist/observability/Metrics.test.mjs +187 -0
- package/dist/observability/Metrics.test.mjs.map +1 -0
- package/dist/observability/Telemetry.d.mts +37 -0
- package/dist/observability/Telemetry.d.mts.map +1 -0
- package/dist/observability/Telemetry.mjs +57 -0
- package/dist/observability/Telemetry.mjs.map +1 -0
- package/dist/observability/Telemetry.test.d.mts +1 -0
- package/dist/observability/Telemetry.test.mjs +92 -0
- package/dist/observability/Telemetry.test.mjs.map +1 -0
- package/dist/retry/Retry.d.mts +2 -2
- package/dist/retry/Retry.test.mjs +1 -1
- package/dist/sandbox/Sandbox.d.mts +1 -1
- package/dist/sandbox/SandboxError.d.mts +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +2 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
- package/dist/streaming/JSONL.d.mts +1 -1
- package/dist/streaming/JSONL.test.mjs +1 -1
- package/dist/streaming/SSE.test.mjs +1 -1
- package/dist/structured-format/StructuredFormat.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.test.mjs +1 -1
- package/dist/testing/MockMusicGenerator.d.mts +1 -1
- package/dist/testing/MockProvider.d.mts +1 -1
- package/dist/testing/MockSpeechSynthesizer.d.mts +1 -1
- package/dist/tool/Approval.d.mts +19 -7
- package/dist/tool/Approval.d.mts.map +1 -1
- package/dist/tool/Approval.mjs +18 -11
- package/dist/tool/Approval.mjs.map +1 -1
- package/dist/tool/Approval.test.mjs +80 -40
- package/dist/tool/Approval.test.mjs.map +1 -1
- package/dist/tool/HistoryCheck.d.mts +2 -2
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +190 -36
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.mjs +41 -3
- package/dist/tool/Tool.test.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +2 -151
- package/dist/tool/ToolEvent.mjs +5 -0
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/ToolResult.d.mts +2 -138
- package/dist/tool/ToolResult.mjs +32 -8
- package/dist/tool/ToolResult.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +2 -73
- package/dist/tool/Toolkit.mjs +153 -35
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +271 -11
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.d.mts +2 -2
- package/dist/transcriber/Transcriber.test.mjs +1 -1
- package/dist/web-read/WebRead.d.mts +44 -0
- package/dist/web-read/WebRead.d.mts.map +1 -0
- package/dist/web-read/WebRead.mjs +18 -0
- package/dist/web-read/WebRead.mjs.map +1 -0
- package/dist/web-read/WebReadTool.d.mts +57 -0
- package/dist/web-read/WebReadTool.d.mts.map +1 -0
- package/dist/web-read/WebReadTool.mjs +59 -0
- package/dist/web-read/WebReadTool.mjs.map +1 -0
- package/dist/web-search/WebSearch.d.mts +1 -1
- package/dist/web-search/WebSearchTool.d.mts +12 -6
- package/dist/web-search/WebSearchTool.d.mts.map +1 -1
- package/dist/web-search/WebSearchTool.mjs +10 -5
- package/dist/web-search/WebSearchTool.mjs.map +1 -1
- package/package.json +21 -1
- package/src/browser/Browser.ts +268 -0
- package/src/browser/BrowserError.ts +145 -0
- package/src/browser/BrowserTool.ts +161 -0
- package/src/domain/AiError.ts +27 -1
- package/src/embedding-model/EmbeddingModel.test.ts +4 -0
- package/src/index.ts +6 -0
- package/src/language-model/LanguageModel.ts +7 -2
- package/src/loop/Loop.test.ts +22 -0
- package/src/loop/Loop.ts +27 -4
- package/src/observability/Metrics.test.ts +266 -0
- package/src/observability/Metrics.ts +505 -65
- package/src/observability/Telemetry.test.ts +123 -0
- package/src/observability/Telemetry.ts +96 -0
- package/src/structured-format/StructuredFormat.ts +4 -0
- package/src/tool/Approval.test.ts +77 -26
- package/src/tool/Approval.ts +72 -27
- package/src/tool/Tool.test.ts +55 -2
- package/src/tool/Tool.ts +335 -70
- package/src/tool/ToolEvent.ts +5 -0
- package/src/tool/ToolResult.ts +36 -9
- package/src/tool/Toolkit.test.ts +339 -9
- package/src/tool/Toolkit.ts +470 -120
- package/src/web-read/WebRead.ts +70 -0
- package/src/web-read/WebReadTool.ts +96 -0
- package/src/web-search/WebSearchTool.ts +11 -5
- package/dist/Tool-BKiieFXk.d.mts +0 -102
- package/dist/Tool-BKiieFXk.d.mts.map +0 -1
- package/dist/loop/Loop.d.mts.map +0 -1
- package/dist/tool/ToolEvent.d.mts.map +0 -1
- package/dist/tool/ToolResult.d.mts.map +0 -1
- package/dist/tool/Toolkit.d.mts.map +0 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { toolCallOutput, userText } from "../domain/Items.mjs";
|
|
2
|
-
import { fromEffectSchema, make, streaming } from "./Tool.mjs";
|
|
3
2
|
import { isFailure, isOk, toToolCallOutput } from "./ToolResult.mjs";
|
|
3
|
+
import { fromEffectSchema, make } from "./Tool.mjs";
|
|
4
4
|
import { ToolEvent, isApprovalRequested, isOutput, isProgress } from "./ToolEvent.mjs";
|
|
5
|
-
import { run } from "./Toolkit.mjs";
|
|
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 { a as describe, n as globalExpect, o as it } from "../dist-B04dlFHg.mjs";
|
|
9
9
|
import { Effect, Queue, Schema, Stream } from "effect";
|
|
10
10
|
//#region src/tool/Approval.test.ts
|
|
11
11
|
/**
|
|
@@ -20,12 +20,12 @@ import { Effect, Queue, Schema, Stream } from "effect";
|
|
|
20
20
|
*
|
|
21
21
|
* Plus: hallucinated tool name (graceful Failure), unknown_tool kind.
|
|
22
22
|
*/
|
|
23
|
-
const allTools =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
const allTools = make$1(make({
|
|
24
|
+
name: "web_search",
|
|
25
|
+
description: "search",
|
|
26
|
+
inputSchema: fromEffectSchema(Schema.Struct({ query: Schema.String })),
|
|
27
|
+
run: ({ query }, emit) => Effect.gen(function* () {
|
|
28
|
+
const hits = [
|
|
29
29
|
{
|
|
30
30
|
url: "a",
|
|
31
31
|
title: `${query} 1`
|
|
@@ -38,36 +38,37 @@ const allTools = [
|
|
|
38
38
|
url: "c",
|
|
39
39
|
title: `${query} 3`
|
|
40
40
|
}
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
];
|
|
42
|
+
yield* Stream.runForEach(Stream.fromIterable(hits), emit);
|
|
43
|
+
return { count: hits.length };
|
|
44
|
+
})
|
|
45
|
+
}), make({
|
|
46
|
+
name: "bulk_email",
|
|
47
|
+
description: "send",
|
|
48
|
+
inputSchema: fromEffectSchema(Schema.Struct({
|
|
49
|
+
recipients: Schema.Array(Schema.String),
|
|
50
|
+
subject: Schema.String
|
|
51
|
+
})),
|
|
52
|
+
run: ({ recipients }, emit) => Effect.gen(function* () {
|
|
53
|
+
yield* Effect.forEach(recipients, (_, i) => emit({
|
|
52
54
|
type: "progress",
|
|
53
55
|
sent: i + 1,
|
|
54
56
|
total: recipients.length
|
|
55
|
-
}))
|
|
56
|
-
|
|
57
|
+
}));
|
|
58
|
+
return {
|
|
57
59
|
status: "sent",
|
|
58
|
-
delivered:
|
|
59
|
-
}
|
|
60
|
-
}),
|
|
61
|
-
make({
|
|
62
|
-
name: "delete_database",
|
|
63
|
-
description: "drop",
|
|
64
|
-
inputSchema: fromEffectSchema(Schema.Struct({ name: Schema.String })),
|
|
65
|
-
run: ({ name }) => Effect.succeed({
|
|
66
|
-
status: "dropped",
|
|
67
|
-
name
|
|
68
|
-
})
|
|
60
|
+
delivered: recipients.length
|
|
61
|
+
};
|
|
69
62
|
})
|
|
70
|
-
|
|
63
|
+
}), make({
|
|
64
|
+
name: "delete_database",
|
|
65
|
+
description: "drop",
|
|
66
|
+
inputSchema: fromEffectSchema(Schema.Struct({ name: Schema.String })),
|
|
67
|
+
run: ({ name }) => Effect.succeed({
|
|
68
|
+
status: "dropped",
|
|
69
|
+
name
|
|
70
|
+
})
|
|
71
|
+
}));
|
|
71
72
|
const SENSITIVE = new Set(["bulk_email", "delete_database"]);
|
|
72
73
|
const isSensitive = (call) => SENSITIVE.has(call.name);
|
|
73
74
|
const fc = (call_id, name, args) => ({
|
|
@@ -224,6 +225,45 @@ describe("fromQueue + run", () => {
|
|
|
224
225
|
reason: "too risky"
|
|
225
226
|
});
|
|
226
227
|
});
|
|
228
|
+
it("router retires per round, so sequential rounds over one queue both resolve", async () => {
|
|
229
|
+
const decisions = await Effect.runPromise(Effect.scoped(Effect.gen(function* () {
|
|
230
|
+
const verdicts = yield* Queue.unbounded();
|
|
231
|
+
const round = (id) => [fc(id, "delete_database", { name: "prod" })];
|
|
232
|
+
const r1 = yield* fromQueue(isSensitive, verdicts)(round("c1"));
|
|
233
|
+
yield* Queue.offer(verdicts, {
|
|
234
|
+
call_id: "c1",
|
|
235
|
+
decision: "approve"
|
|
236
|
+
});
|
|
237
|
+
const d1 = Array.from(yield* Stream.runCollect(r1.decisions));
|
|
238
|
+
const r2 = yield* fromQueue(isSensitive, verdicts)(round("c2"));
|
|
239
|
+
yield* Queue.offer(verdicts, {
|
|
240
|
+
call_id: "c2",
|
|
241
|
+
decision: "deny",
|
|
242
|
+
reason: "no"
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
d1,
|
|
246
|
+
d2: Array.from(yield* Stream.runCollect(r2.decisions))
|
|
247
|
+
};
|
|
248
|
+
})));
|
|
249
|
+
globalExpect(decisions.d1[0]).toMatchObject({ _tag: "Approved" });
|
|
250
|
+
globalExpect(decisions.d2[0]).toMatchObject({
|
|
251
|
+
_tag: "Rejected",
|
|
252
|
+
result: { kind: "denied" }
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
it("timeout resolves an unanswered gated call as cancelled", async () => {
|
|
256
|
+
globalExpect((await Effect.runPromise(Effect.scoped(Effect.gen(function* () {
|
|
257
|
+
const { decisions } = yield* fromQueue(isSensitive, yield* Queue.unbounded(), { timeout: "10 millis" })([fc("c1", "delete_database", { name: "prod" })]);
|
|
258
|
+
return Array.from(yield* Stream.runCollect(decisions));
|
|
259
|
+
}))))[0]).toMatchObject({
|
|
260
|
+
_tag: "Rejected",
|
|
261
|
+
result: {
|
|
262
|
+
_tag: "Failure",
|
|
263
|
+
kind: "cancelled"
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
227
267
|
});
|
|
228
268
|
describe("findUnansweredCalls / cancelAllPending / isReconciled", () => {
|
|
229
269
|
const orphan = fc("c99", "delete_database", { name: "prod" });
|
|
@@ -282,7 +322,7 @@ describe("toToolCallOutput", () => {
|
|
|
282
322
|
globalExpect(out.call_id).toBe("c1");
|
|
283
323
|
globalExpect(JSON.parse(out.output)).toEqual({ count: 3 });
|
|
284
324
|
});
|
|
285
|
-
it("
|
|
325
|
+
it("renders a Failure with reason as a nested error object", () => {
|
|
286
326
|
const out = toToolCallOutput({
|
|
287
327
|
_tag: "Failure",
|
|
288
328
|
call_id: "c2",
|
|
@@ -290,19 +330,19 @@ describe("toToolCallOutput", () => {
|
|
|
290
330
|
kind: "denied",
|
|
291
331
|
reason: "spam concern"
|
|
292
332
|
});
|
|
293
|
-
globalExpect(JSON.parse(out.output)).toEqual({
|
|
333
|
+
globalExpect(JSON.parse(out.output)).toEqual({ error: {
|
|
294
334
|
kind: "denied",
|
|
295
|
-
|
|
296
|
-
});
|
|
335
|
+
message: "spam concern"
|
|
336
|
+
} });
|
|
297
337
|
});
|
|
298
|
-
it("
|
|
338
|
+
it("renders a Failure without reason (omits message)", () => {
|
|
299
339
|
const out = toToolCallOutput({
|
|
300
340
|
_tag: "Failure",
|
|
301
341
|
call_id: "c3",
|
|
302
342
|
tool: "delete_database",
|
|
303
343
|
kind: "cancelled"
|
|
304
344
|
});
|
|
305
|
-
globalExpect(JSON.parse(out.output)).toEqual({ kind: "cancelled" });
|
|
345
|
+
globalExpect(JSON.parse(out.output)).toEqual({ error: { kind: "cancelled" } });
|
|
306
346
|
});
|
|
307
347
|
});
|
|
308
348
|
describe("run", () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Approval.test.mjs","names":["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 { type ApprovalMapEntry, type ApprovalDecision, fromMap, fromQueue } from \"./Approval.js\"\nimport { fromEffectSchema, make as makeTool, streaming } from \"./Tool.js\"\nimport { 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 = streaming({\n name: \"web_search\",\n description: \"search\",\n inputSchema: fromEffectSchema(Schema.Struct({ query: Schema.String })),\n run: ({ query }) =>\n Stream.fromIterable([\n { url: \"a\", title: `${query} 1` },\n { url: \"b\", title: `${query} 2` },\n { url: \"c\", title: `${query} 3` },\n ]),\n finalize: (hits) => ({ count: hits.length }),\n})\n\nconst bulkEmail = streaming({\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 }) =>\n Stream.fromIterable(\n recipients.map((_, i) => ({\n type: \"progress\" as const,\n sent: i + 1,\n total: recipients.length,\n })),\n ),\n finalize: (events) => ({ status: \"sent\" as const, delivered: events.length }),\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 = [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\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(\"round-trips a Failure result with reason\", () => {\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({ kind: \"denied\", reason: \"spam concern\" })\n })\n\n it(\"round-trips a Failure result without reason (omits the field)\", () => {\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({ 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":";;;;;;;;;;;;;;;;;;;;;;AAkEA,MAAM,WAAW;CArCC,UAAU;EAC1B,MAAM;EACN,aAAa;EACb,aAAa,iBAAiB,OAAO,OAAO,EAAE,OAAO,OAAO,QAAQ,CAAC,CAAC;EACtE,MAAM,EAAE,YACN,OAAO,aAAa;GAClB;IAAE,KAAK;IAAK,OAAO,GAAG,MAAM;IAAK;GACjC;IAAE,KAAK;IAAK,OAAO,GAAG,MAAM;IAAK;GACjC;IAAE,KAAK;IAAK,OAAO,GAAG,MAAM;IAAK;GAClC,CAAC;EACJ,WAAW,UAAU,EAAE,OAAO,KAAK,QAAQ;EAC5C,CA0B0B;CAxBT,UAAU;EAC1B,MAAM;EACN,aAAa;EACb,aAAa,iBACX,OAAO,OAAO;GAAE,YAAY,OAAO,MAAM,OAAO,OAAO;GAAE,SAAS,OAAO;GAAQ,CAAC,CACnF;EACD,MAAM,EAAE,iBACN,OAAO,aACL,WAAW,KAAK,GAAG,OAAO;GACxB,MAAM;GACN,MAAM,IAAI;GACV,OAAO,WAAW;GACnB,EAAE,CACJ;EACH,WAAW,YAAY;GAAE,QAAQ;GAAiB,WAAW,OAAO;GAAQ;EAC7E,CASqC;CAPfA,KAAS;EAC9B,MAAM;EACN,aAAa;EACb,aAAa,iBAAiB,OAAO,OAAO,EAAE,MAAM,OAAO,QAAQ,CAAC,CAAC;EACrE,MAAM,EAAE,WAAW,OAAO,QAAQ;GAAE,QAAQ;GAAW;GAAM,CAAC;EAC/D,CAEqD;CAAC;AACvD,MAAM,YAAY,IAAI,IAAI,CAAC,cAAc,kBAAkB,CAAC;AAC5D,MAAM,eAAe,SAAyB,UAAU,IAAI,KAAK,KAAK;AAEtE,MAAM,MAAM,SAAiB,MAAc,UAAmC;CAC5E,MAAM;CACN;CACA;CACA,WAAW,KAAK,UAAU,KAAK;CAChC;AAED,MAAM,QAAQ;CACZ,GAAG,MAAM,cAAc,EAAE,OAAO,UAAU,CAAC;CAC3C,GAAG,MAAM,cAAc;EAAE,YAAY,CAAC,OAAO,MAAM;EAAE,SAAS;EAAM,CAAC;CACrE,GAAG,MAAM,mBAAmB,EAAE,MAAM,QAAQ,CAAC;CAC9C;AAED,MAAM,eAAe,cACnB,UAAU,OAAO,SAAS,CAAC,KAAK,MAAM,EAAE,OAAO;AAEjD,MAAM,YAAY,YAAuC,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;AAEpG,MAAM,kBAAkB,aACtB,OAAO,aAAa,SAAS,KAAK,WAAW,UAAU,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;AAE7E,MAAM,yBAAyB,cAAqD;CAClF,MAAM,OAAO,QAAQ,aAAa,UAAU,CAAC,MAAM;AACnD,QAAO,OAAO,MAAM,IAAI,UAAU,KAAK,SAAS,EAAE,eAAe,KAAK,SAAS,CAAC;;AAGlF,MAAM,sBAAsB,aAC1B,SAAS,SAAS,aACd,IAAI,UAAU,CAAC,SAAS,KAAK,CAAC,GAC9B,OAAO,QAAQ,UAAU,OAAO,EAAE,QAAQ,SAAS,QAAQ,CAAC,CAAC;AAMnE,SAAS,uBAAuB;AAC9B,IAAG,mEAAmE,YAAY;EAChF,MAAM,YAAY,IAAI,IAA8B,CAClD,CAAC,MAAM,EAAE,UAAU,WAAW,CAAC,EAC/B,CAAC,MAAM,EAAE,UAAU,WAAW,CAAC,CAChC,CAAC;EACF,MAAM,YAAY,MAAM,OAAO,WAAW,OAAO,WAAW,sBAAsB,UAAU,CAAC,CAAC;EAC9F,MAAM,KAAK,SAAS,YAAY,UAAU,CAAC;AAC3C,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GAAE,MAAM;GAAM,OAAO,EAAE,OAAO,GAAG;GAAE,CAAC;AACvE,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GACjC,MAAM;GACN,OAAO;IAAE,QAAQ;IAAQ,WAAW;IAAG;GACxC,CAAC;AACF,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GACjC,MAAM;GACN,OAAO;IAAE,QAAQ;IAAW,MAAM;IAAQ;GAC3C,CAAC;AAGF,eAAO,UAAU,OAAO,oBAAoB,CAAC,CAAC,aAAa,EAAE;GAC7D;AAEF,IAAG,wDAAwD,YAAY;EACrE,MAAM,YAAY,IAAI,IAA8B,CAClD,CAAC,MAAM;GAAE,UAAU;GAAQ,QAAQ;GAAgB,CAAC,EACpD,CAAC,MAAM;GAAE,UAAU;GAAQ,QAAQ;GAAkB,CAAC,CACvD,CAAC;EACF,MAAM,YAAY,MAAM,OAAO,WAAW,OAAO,WAAW,sBAAsB,UAAU,CAAC,CAAC;AAG9F,eAAO,UAAU,OAAO,WAAW,CAAC,QAAQ,MAAM,EAAE,SAAS,aAAa,CAAC,CAAC,aAAa,EAAE;EAE3F,MAAM,KAAK,SAAS,YAAY,UAAU,CAAC;AAC3C,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GACjC,MAAM;GACN,MAAM;GACN,QAAQ;GACT,CAAC;AACF,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GACjC,MAAM;GACN,MAAM;GACN,QAAQ;GACT,CAAC;GACF;AAEF,IAAG,uDAAuD,YAAY;EAEpE,MAAM,KAAK,SAAS,YAAY,MADR,OAAO,WAAW,OAAO,WAAW,sCAAsB,IAAI,KAAK,CAAC,CAAC,CAAC,CACpD,CAAC;AAC3C,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GAAE,MAAM;GAAM,OAAO,EAAE,OAAO,GAAG;GAAE,CAAC;AACvE,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GAAE,MAAM;GAAW,MAAM;GAAa,CAAC;AAC1E,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GAAE,MAAM;GAAW,MAAM;GAAa,CAAC;GAC1E;AAEF,IAAG,kDAAkD,YAAY;EAC/D,MAAM,YAAY,IAAI,IAA8B,CAClD,CAAC,MAAM,EAAE,UAAU,WAAW,CAAC,CAEhC,CAAC;EAEF,MAAM,KAAK,SAAS,YAAY,MADR,OAAO,WAAW,OAAO,WAAW,sBAAsB,UAAU,CAAC,CAAC,CACpD,CAAC;AAC3C,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GAAE,MAAM;GAAM,OAAO,EAAE,OAAO,GAAG;GAAE,CAAC;AACvE,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GAAE,MAAM;GAAM,OAAO,EAAE,QAAQ,QAAQ;GAAE,CAAC;AAC7E,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GAAE,MAAM;GAAW,MAAM;GAAa,CAAC;GAC1E;EACF;AAMF,SAAS,mCAAmC;AAC1C,IAAG,wEAAwE,YAAY;EACrF,MAAM,iBAAiB;GACrB,GAAG,MAAM,cAAc,EAAE,OAAO,KAAK,CAAC;GACtC,GAAG,MAAM,kBAAkB,EAAE,CAAC;GAC9B,GAAG,MAAM,mBAAmB,EAAE,MAAM,QAAQ,CAAC;GAC9C;EAYD,MAAM,KAAK,SAAS,YAAY,MAXR,OAAO,WAC7B,OAAO,kBACE;GACL,MAAM,OAAO,QACX,aACA,IAAI,IAAI,CAAC,CAAC,MAAM,EAAE,UAAU,WAAW,CAAC,CAAC,CAAC,CAC3C,CAAC,eAAe;AACjB,UAAO,OAAO,MAAM,IAAI,UAAU,KAAK,SAAS,EAAE,eAAe,KAAK,SAAS,CAAC;MAC9E,CACL,CACF,CACyC,CAAC;AAC3C,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc,EAAE,MAAM,MAAM,CAAC;AAClD,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GAAE,MAAM;GAAW,MAAM;GAAgB,CAAC;AAC7E,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GAAE,MAAM;GAAM,OAAO,EAAE,QAAQ,WAAW;GAAE,CAAC;GAChF;EACF;AAMF,SAAS,yBAAyB;AAChC,IAAG,gFAAgF,YAAY;EAC7F,MAAM,YAAY,MAAM,OAAO,WAC7B,OAAO,IAAI,aAAa;GACtB,MAAM,WAAW,OAAO,MAAM,WAI1B;AACJ,UAAO,MAAM,MAAM,UAAU;IAAE,SAAS;IAAM,UAAU;IAAW,CAAC;AACpE,UAAO,MAAM,MAAM,UAAU;IAC3B,SAAS;IACT,UAAU;IACV,QAAQ;IACT,CAAC;GAGF,MAAM,SAAS,OAAO,OACpB,OAAO,IAAI,aAAa;IACtB,MAAM,EAAE,UAAU,WAAW,qBAAqB,OAAO,UACvD,aACA,SACD,CAAC,MAAM;AACR,WAAO,OAAO,MACZ,kBACA,OAAO,MACL,IAAI,UAAU,SAAS,EACvB,UAAU,KAAK,OAAO,QAAQ,mBAAmB,CAAC,CACnD,CACF;KACD,CACH;AACD,UAAO,OAAO,OAAO,WAAW,OAAO;IACvC,CACH;AAED,eAAO,UAAU,OAAO,oBAAoB,CAAC,CAAC,aAAa,EAAE;EAE7D,MAAM,KAAK,SAAS,YAAY,UAAU,CAAC;AAC3C,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GAAE,MAAM;GAAM,OAAO,EAAE,QAAQ,QAAQ;GAAE,CAAC;AAC7E,eAAO,GAAG,IAAI,KAAK,CAAC,CAAC,cAAc;GACjC,MAAM;GACN,MAAM;GACN,QAAQ;GACT,CAAC;GACF;EACF;AAMF,SAAS,+DAA+D;CACtE,MAAM,SAAS,GAAG,OAAO,mBAAmB,EAAE,MAAM,QAAQ,CAAC;CAC7D,MAAM,WAAW,GAAG,OAAO,cAAc,EAAE,OAAO,KAAK,CAAC;CACxD,MAAM,iBAAiBC,eAAqB,OAAO,KAAK,UAAU,EAAE,OAAO,GAAG,CAAC,CAAC;AAEhF,IAAG,kDAAkD;EAEnD,MAAM,aAAa,oBAAoB;GADtBC,SAAe,KAAK;GAAE;GAAU;GAAQ;GACX,CAAC;AAC/C,eAAO,WAAW,CAAC,aAAa,EAAE;AAClC,eAAO,WAAW,GAAI,QAAQ,CAAC,KAAK,MAAM;GAC1C;AAEF,IAAG,kEAAkE;EACnE,MAAM,QAAQ,CAACA,SAAe,KAAK,EAAE,OAAO;AAC5C,eAAO,aAAa,MAAM,CAAC,CAAC,KAAK,MAAM;AAEvC,eAAO,aAAa,CADA,GAAG,OAAO,GAAG,iBAAiB,MAAM,CAAC,IAAI,iBAAiB,CAChD,CAAC,CAAC,CAAC,KAAK,KAAK;GAC3C;AAEF,IAAG,wEAAwE;EAEzE,MAAM,WAAW,iBAAiB;GADjBA,SAAe,KAAK;GAAE;GAAU;GAAQ;GAChB,EAAE,gBAAgB;AAC3D,eAAO,SAAS,CAAC,aAAa,EAAE;EAChC,MAAM,IAAI,SAAS;AACnB,eAAO,UAAU,EAAE,CAAC,CAAC,KAAK,KAAK;AAC/B,eAAO,EAAE,CAAC,cAAc;GACtB,MAAM;GACN,SAAS;GACT,MAAM;GACN,QAAQ;GACT,CAAC;GACF;AAEF,IAAG,qFAAqF;EACtF,MAAM,QAAQ,CAACA,SAAe,gBAAgB,EAAE,OAAO;EACvD,MAAM,WAAW,iBAAiB,OAAO,kBAAkB;AAE3D,eAAO,oBAAoB;GADP,GAAG;GAAO,GAAG,SAAS,IAAI,iBAAiB;GAAEA,SAAe,aAAa;GACxD,CAAC,CAAC,CAAC,aAAa,EAAE;GACvD;EACF;AAMF,SAAS,0BAA0B;AACjC,IAAG,oCAAoC;EAOrC,MAAM,MAAM,iBAAiB;GAL3B,MAAM;GACN,SAAS;GACT,MAAM;GACN,OAAO,EAAE,OAAO,GAAG;GAES,CAAC;AAC/B,eAAO,IAAI,QAAQ,CAAC,KAAK,KAAK;AAC9B,eAAO,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,QAAQ,EAAE,OAAO,GAAG,CAAC;GACpD;AAEF,IAAG,kDAAkD;EAQnD,MAAM,MAAM,iBAAiB;GAN3B,MAAM;GACN,SAAS;GACT,MAAM;GACN,MAAM;GACN,QAAQ;GAEoB,CAAC;AAC/B,eAAO,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,QAAQ;GAAE,MAAM;GAAU,QAAQ;GAAgB,CAAC;GAClF;AAEF,IAAG,uEAAuE;EAOxE,MAAM,MAAM,iBAAiB;GAL3B,MAAM;GACN,SAAS;GACT,MAAM;GACN,MAAM;GAEsB,CAAC;AAC/B,eAAO,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,QAAQ,EAAE,MAAM,aAAa,CAAC;GAC7D;EACF;AAMF,SAAS,aAAa;AACpB,IAAG,+BAA+B,YAAY;EAC5C,MAAM,YAAY,MAAM,OAAO,WAAW,OAAO,WAAW,IAAI,UAAU,MAAM,CAAC,CAAC;AAClF,eAAO,UAAU,OAAO,SAAS,CAAC,CAAC,aAAa,EAAE;AAClD,eAAO,UAAU,OAAO,SAAS,CAAC,OAAO,MAAM,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,KAAK;GAC1E;EACF"}
|
|
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,5 +1,5 @@
|
|
|
1
|
-
import { o as HistoryItem, v as ToolCall } from "../Items-
|
|
2
|
-
import { ToolResult } from "
|
|
1
|
+
import { o as HistoryItem, v as ToolCall } from "../Items-BnIduEV2.mjs";
|
|
2
|
+
import { t as ToolResult } from "../ToolResult-CL4hK_9z.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/tool/HistoryCheck.d.ts
|
|
5
5
|
declare namespace HistoryCheck_d_exports {
|
package/dist/tool/Tool.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export {
|
|
1
|
+
import { A as toDescriptors, C as fromEffectSchema, D as noInput, E as make, M as withRun, O as provider, 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 withName, k as signal, 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-2lC_XMth.mjs";
|
|
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, make, noInput, provider, signal, toDescriptors, withName, withRun };
|
package/dist/tool/Tool.mjs
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
|
|
2
2
|
import { toolCallOutput } from "../domain/Items.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { serializeValue } from "./ToolResult.mjs";
|
|
4
|
+
import { Effect, Match, Schema } from "effect";
|
|
4
5
|
//#region src/tool/Tool.ts
|
|
5
6
|
var Tool_exports = /* @__PURE__ */ __exportAll({
|
|
6
7
|
ToolError: () => ToolError,
|
|
8
|
+
ToolFailed: () => ToolFailed,
|
|
9
|
+
ToolValidationError: () => ToolValidationError,
|
|
10
|
+
decodeArgs: () => decodeArgs,
|
|
11
|
+
decodeCallInput: () => decodeCallInput,
|
|
12
|
+
descriptorsOf: () => descriptorsOf,
|
|
7
13
|
execute: () => execute,
|
|
14
|
+
fail: () => fail,
|
|
8
15
|
fromEffectSchema: () => fromEffectSchema,
|
|
9
16
|
fromStandardSchema: () => fromStandardSchema,
|
|
10
|
-
|
|
17
|
+
interaction: () => interaction,
|
|
11
18
|
make: () => make,
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
noInput: () => noInput,
|
|
20
|
+
provider: () => provider,
|
|
21
|
+
signal: () => signal,
|
|
22
|
+
toDescriptors: () => toDescriptors,
|
|
23
|
+
withName: () => withName,
|
|
24
|
+
withRun: () => withRun
|
|
14
25
|
});
|
|
15
26
|
var ToolError = class extends Schema.TaggedErrorClass("@betalyra/effect-uai/ToolError")("ToolError", {
|
|
16
27
|
call_id: Schema.String,
|
|
@@ -19,6 +30,48 @@ var ToolError = class extends Schema.TaggedErrorClass("@betalyra/effect-uai/Tool
|
|
|
19
30
|
cause: Schema.optional(Schema.Unknown)
|
|
20
31
|
}) {};
|
|
21
32
|
/**
|
|
33
|
+
* One Standard Schema validation issue, carried on `ToolValidationError`.
|
|
34
|
+
* Mirrors `StandardSchemaV1.Issue`; `path` is left as `unknown` elements
|
|
35
|
+
* because Standard Schema path segments may be symbols or `{ key }` objects.
|
|
36
|
+
*/
|
|
37
|
+
const ToolValidationIssue = Schema.Struct({
|
|
38
|
+
message: Schema.String,
|
|
39
|
+
path: Schema.optional(Schema.Array(Schema.Unknown))
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* The model's `arguments` for a call failed the tool's `inputSchema`. Distinct
|
|
43
|
+
* from `ToolError` (malformed JSON, runtime crash) so callers can tell a
|
|
44
|
+
* contract violation apart from a parse or execution failure, and so the
|
|
45
|
+
* structured `issues` survive instead of being flattened into a message string.
|
|
46
|
+
*/
|
|
47
|
+
var ToolValidationError = class extends Schema.TaggedErrorClass("@betalyra/effect-uai/ToolValidationError")("ToolValidationError", {
|
|
48
|
+
call_id: Schema.String,
|
|
49
|
+
tool: Schema.String,
|
|
50
|
+
issues: Schema.Array(ToolValidationIssue)
|
|
51
|
+
}) {};
|
|
52
|
+
/**
|
|
53
|
+
* The deliberate "speak to the model" failure sentinel. A tool's `run` that
|
|
54
|
+
* fails with a `ToolFailed` (or a bare `string`, its sugar) is absorbed by
|
|
55
|
+
* `Toolkit.run` into a `ToolResult.Failure` the model reads and adapts to,
|
|
56
|
+
* instead of ending the run. Every other error type propagates typed on the
|
|
57
|
+
* executor's stream. `kind` overrides the synthesized `Failure.kind`
|
|
58
|
+
* (default `"tool_failed"`) so a tool can hand the model a distinguishable
|
|
59
|
+
* category (`"not_found"`, `"rate_limited"`) without a new result variant.
|
|
60
|
+
*/
|
|
61
|
+
var ToolFailed = class extends Schema.TaggedErrorClass("@betalyra/effect-uai/ToolFailed")("ToolFailed", {
|
|
62
|
+
message: Schema.String,
|
|
63
|
+
kind: Schema.optional(Schema.String)
|
|
64
|
+
}) {};
|
|
65
|
+
/**
|
|
66
|
+
* Fail a tool's `run` with a model-visible message (optionally a `kind`).
|
|
67
|
+
* `yield* Tool.fail("page not found", { kind: "not_found" })` inside a
|
|
68
|
+
* `run`'s `Effect.gen`; the executor absorbs it as `ToolResult.Failure`.
|
|
69
|
+
*/
|
|
70
|
+
const fail = (message, options) => Effect.fail(new ToolFailed({
|
|
71
|
+
message,
|
|
72
|
+
...options?.kind !== void 0 ? { kind: options.kind } : {}
|
|
73
|
+
}));
|
|
74
|
+
/**
|
|
22
75
|
* Convenience wrapper for Effect Schema users - adds both the
|
|
23
76
|
* `validate` and `jsonSchema` extensions to a plain Effect Schema so it
|
|
24
77
|
* can be used as a `Tool.inputSchema`.
|
|
@@ -40,57 +93,158 @@ const fromEffectSchema = (schema) => Schema.toStandardJSONSchemaV1(Schema.toStan
|
|
|
40
93
|
* helper makes the input type inference explicit at the call site.
|
|
41
94
|
*/
|
|
42
95
|
const fromStandardSchema = (schema) => schema;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Input schema for a tool that takes no arguments. Renders a clean
|
|
98
|
+
* `{ type: "object", properties: {} }`; validation accepts anything and
|
|
99
|
+
* yields `{}`. Use it instead of `fromEffectSchema(Schema.Struct({}))`,
|
|
100
|
+
* which renders a degenerate `anyOf` (object-or-array) that strict
|
|
101
|
+
* providers like Gemini reject.
|
|
102
|
+
*/
|
|
103
|
+
const noInput = { "~standard": {
|
|
104
|
+
version: 1,
|
|
105
|
+
vendor: "effect-uai",
|
|
106
|
+
validate: () => ({ value: {} }),
|
|
107
|
+
jsonSchema: {
|
|
108
|
+
input: () => ({
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {}
|
|
111
|
+
}),
|
|
112
|
+
output: () => ({
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {}
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
} };
|
|
118
|
+
/** A model-visible tool with a local `Effect` executor. */
|
|
119
|
+
const make = (spec) => ({
|
|
120
|
+
_tag: "LocalTool",
|
|
121
|
+
...spec
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* A provider-hosted tool (native web search, code execution, RAG grounding).
|
|
125
|
+
* No local executor — `provider`/`config` drive the provider adapter's
|
|
126
|
+
* rendering. The model sees it; `Toolkit.run` reports it as `non_local_tool`.
|
|
127
|
+
*/
|
|
128
|
+
const provider = (spec) => ({
|
|
129
|
+
_tag: "ProviderTool",
|
|
46
130
|
...spec
|
|
47
131
|
});
|
|
48
|
-
const isStreamingTool = (t) => "_kind" in t && t._kind === "streaming";
|
|
49
132
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
133
|
+
* A typed control-flow signal to the agent loop (escalate, pause, schedule,
|
|
134
|
+
* hand off). Decode-only: the loop interprets the call rather than executing a
|
|
135
|
+
* handler, so there is no `run`.
|
|
53
136
|
*/
|
|
137
|
+
const signal = (spec) => ({
|
|
138
|
+
_tag: "SignalTool",
|
|
139
|
+
...spec
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* A typed request for an external actor to respond before the loop resumes
|
|
143
|
+
* (frontend, Telegram, CLI prompt). Decode-only; the loop stops/pauses and
|
|
144
|
+
* resumes by appending the actor's `function_call_output`.
|
|
145
|
+
*/
|
|
146
|
+
const interaction = (spec) => ({
|
|
147
|
+
_tag: "InteractionTool",
|
|
148
|
+
...spec
|
|
149
|
+
});
|
|
150
|
+
/**
|
|
151
|
+
* Return a copy of a tool under a new `name`. Useful for resolving a name
|
|
152
|
+
* clash before combining tools into one `Toolkit`. Literal-preserving, so the
|
|
153
|
+
* renamed tool keys the toolkit record under the new name statically.
|
|
154
|
+
*/
|
|
155
|
+
const withName = (tool, name) => ({
|
|
156
|
+
...tool,
|
|
157
|
+
name
|
|
158
|
+
});
|
|
159
|
+
/**
|
|
160
|
+
* Replace a local tool's `run`, keeping its model-facing definition (name,
|
|
161
|
+
* description, schema, `_tag`) identical. The new `run`'s `input` is typed from
|
|
162
|
+
* the original tool, so override/mock need no annotation:
|
|
163
|
+
*
|
|
164
|
+
* const safe = { ...toolkit, send_email: Tool.withRun(toolkit.send_email,
|
|
165
|
+
* ({ to }) => Effect.succeed({ status: "dry-run", to })) }
|
|
166
|
+
*
|
|
167
|
+
* Descriptors stay identical to production, so a mocked toolkit can't drift
|
|
168
|
+
* from the contract the model sees.
|
|
169
|
+
*/
|
|
170
|
+
const withRun = (tool, run) => ({
|
|
171
|
+
...tool,
|
|
172
|
+
run
|
|
173
|
+
});
|
|
174
|
+
/** Render tools to provider-agnostic descriptors. Every kind is model-visible. */
|
|
54
175
|
const toDescriptors = (tools) => tools.map((tool) => {
|
|
55
176
|
const inputSchema = tool.inputSchema["~standard"].jsonSchema.input({ target: "draft-2020-12" });
|
|
56
|
-
|
|
177
|
+
const strict = "strict" in tool ? tool.strict : void 0;
|
|
178
|
+
return strict !== void 0 ? {
|
|
57
179
|
name: tool.name,
|
|
58
180
|
description: tool.description,
|
|
59
181
|
inputSchema,
|
|
60
|
-
strict
|
|
182
|
+
strict
|
|
61
183
|
} : {
|
|
62
184
|
name: tool.name,
|
|
63
185
|
description: tool.description,
|
|
64
186
|
inputSchema
|
|
65
187
|
};
|
|
66
188
|
});
|
|
67
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Render a `Toolkit` (a name-indexed record of tools) to descriptors, treating
|
|
191
|
+
* an absent toolkit as no tools. The normalization point the `LanguageModel`
|
|
192
|
+
* boundary uses so a request can carry the toolkit itself instead of a
|
|
193
|
+
* pre-rendered descriptor array. Lives here (not on `Toolkit`) to keep providers
|
|
194
|
+
* free of a dependency on the toolkit module — it only needs the tool values.
|
|
195
|
+
*/
|
|
196
|
+
const descriptorsOf = (toolkit) => toolkit === void 0 ? [] : toDescriptors(Object.values(toolkit));
|
|
197
|
+
const VALIDATION_THREW = [{ message: "input schema validation threw" }];
|
|
198
|
+
/**
|
|
199
|
+
* Parse and validate a call's `arguments`. Empty arguments normalize to
|
|
200
|
+
* `{}` (some models emit `""` for zero-arg tools); a throwing validator is
|
|
201
|
+
* captured, not propagated. Never fails.
|
|
202
|
+
*/
|
|
203
|
+
const decodeCallInput = (tool, call) => {
|
|
204
|
+
const raw = call.arguments.trim() === "" ? "{}" : call.arguments;
|
|
205
|
+
return Effect.try({
|
|
206
|
+
try: () => JSON.parse(raw),
|
|
207
|
+
catch: () => ({ _tag: "parseError" })
|
|
208
|
+
}).pipe(Effect.flatMap((parsed) => Effect.tryPromise({
|
|
209
|
+
try: () => Promise.resolve(tool.inputSchema["~standard"].validate(parsed)),
|
|
210
|
+
catch: () => ({
|
|
211
|
+
_tag: "invalid",
|
|
212
|
+
issues: VALIDATION_THREW
|
|
213
|
+
})
|
|
214
|
+
}).pipe(Effect.map((result) => result.issues !== void 0 ? {
|
|
215
|
+
_tag: "invalid",
|
|
216
|
+
issues: result.issues
|
|
217
|
+
} : {
|
|
218
|
+
_tag: "ok",
|
|
219
|
+
input: result.value
|
|
220
|
+
}))), Effect.catch((captured) => Effect.succeed(captured)));
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Decode and validate a function_call's JSON `arguments` against the tool's
|
|
224
|
+
* own `inputSchema`, yielding the typed input. The decode-only half of
|
|
225
|
+
* `execute` - reach for it when you intercept a call to translate it into a
|
|
226
|
+
* control-flow decision rather than running the tool's `run`.
|
|
227
|
+
*
|
|
228
|
+
* Malformed JSON fails with `ToolError`; a parsed body that violates
|
|
229
|
+
* `inputSchema` fails with `ToolValidationError` carrying the issues.
|
|
230
|
+
*/
|
|
231
|
+
const decodeArgs = (tool, call) => decodeCallInput(tool, call).pipe(Effect.flatMap(Match.type().pipe(Match.tag("ok", ({ input }) => Effect.succeed(input)), Match.tag("parseError", () => Effect.fail(new ToolError({
|
|
68
232
|
call_id: call.call_id,
|
|
69
|
-
tool:
|
|
70
|
-
message
|
|
71
|
-
|
|
72
|
-
|
|
233
|
+
tool: tool.name,
|
|
234
|
+
message: "Failed to parse JSON arguments"
|
|
235
|
+
}))), Match.tag("invalid", ({ issues }) => Effect.fail(new ToolValidationError({
|
|
236
|
+
call_id: call.call_id,
|
|
237
|
+
tool: tool.name,
|
|
238
|
+
issues
|
|
239
|
+
}))), Match.exhaustive)));
|
|
73
240
|
/**
|
|
74
241
|
* Decode and validate the JSON arguments of a function_call against the
|
|
75
242
|
* tool's input schema, run the tool, and serialize the output into a
|
|
76
|
-
* function_call_output item.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
try: () => JSON.parse(call.arguments),
|
|
81
|
-
catch: toToolError(call, tool.name, "Failed to parse JSON arguments")
|
|
82
|
-
});
|
|
83
|
-
const result = yield* Effect.promise(() => Promise.resolve(tool.inputSchema["~standard"].validate(parsed)));
|
|
84
|
-
if (result.issues !== void 0) return yield* new ToolError({
|
|
85
|
-
call_id: call.call_id,
|
|
86
|
-
tool: tool.name,
|
|
87
|
-
message: "Tool input failed schema validation",
|
|
88
|
-
cause: result.issues
|
|
89
|
-
});
|
|
90
|
-
const output = yield* tool.run(result.value).pipe(Effect.mapError(toToolError(call, tool.name, "Tool execution failed")));
|
|
91
|
-
return toolCallOutput(call.call_id, JSON.stringify(output));
|
|
92
|
-
});
|
|
243
|
+
* function_call_output item. Single-shot: intermediate events emitted by
|
|
244
|
+
* `run` are discarded (use `Toolkit.run` to stream them).
|
|
245
|
+
*/
|
|
246
|
+
const execute = (tool, call) => decodeArgs(tool, call).pipe(Effect.flatMap((input) => tool.run(input, () => Effect.void)), Effect.map((output) => toolCallOutput(call.call_id, serializeValue(output))));
|
|
93
247
|
//#endregion
|
|
94
|
-
export { ToolError, execute, fromEffectSchema, fromStandardSchema,
|
|
248
|
+
export { ToolError, ToolFailed, ToolValidationError, decodeArgs, decodeCallInput, descriptorsOf, execute, fail, fromEffectSchema, fromStandardSchema, interaction, make, noInput, provider, signal, Tool_exports as t, toDescriptors, withName, withRun };
|
|
95
249
|
|
|
96
250
|
//# sourceMappingURL=Tool.mjs.map
|