@effect-uai/core 0.1.0 → 0.3.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/README.md +2 -2
- package/dist/{AiError-CqmYjXyx.d.mts → AiError-CBuPHVKA.d.mts} +1 -1
- package/dist/{AiError-CqmYjXyx.d.mts.map → AiError-CBuPHVKA.d.mts.map} +1 -1
- package/dist/Image-BZmKfIdq.d.mts +61 -0
- package/dist/Image-BZmKfIdq.d.mts.map +1 -0
- package/dist/{Items-D1C2686t.d.mts → Items-CB8Bo3FI.d.mts} +132 -80
- package/dist/Items-CB8Bo3FI.d.mts.map +1 -0
- package/dist/Media-D_CpcM1Z.d.mts +57 -0
- package/dist/Media-D_CpcM1Z.d.mts.map +1 -0
- package/dist/{StructuredFormat-B5ueioNr.d.mts → StructuredFormat-BWq5Hd1O.d.mts} +5 -5
- package/dist/StructuredFormat-BWq5Hd1O.d.mts.map +1 -0
- package/dist/{Tool-5wxOCuOh.d.mts → Tool-DjVufH7i.d.mts} +13 -13
- package/dist/Tool-DjVufH7i.d.mts.map +1 -0
- package/dist/{Turn-rlTfuHaQ.d.mts → Turn-OPaILVIB.d.mts} +12 -29
- package/dist/Turn-OPaILVIB.d.mts.map +1 -0
- package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
- package/dist/dist-DV5ISja1.mjs +13782 -0
- package/dist/dist-DV5ISja1.mjs.map +1 -0
- package/dist/domain/AiError.d.mts +1 -1
- package/dist/domain/AiError.mjs +1 -1
- package/dist/domain/Image.d.mts +2 -0
- package/dist/domain/Image.mjs +58 -0
- package/dist/domain/Image.mjs.map +1 -0
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +19 -42
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Media.d.mts +2 -0
- package/dist/domain/Media.mjs +14 -0
- package/dist/domain/Media.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +2 -2
- package/dist/domain/Turn.mjs +12 -8
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/embedding-model/Embedding.d.mts +107 -0
- package/dist/embedding-model/Embedding.d.mts.map +1 -0
- package/dist/embedding-model/Embedding.mjs +18 -0
- package/dist/embedding-model/Embedding.mjs.map +1 -0
- package/dist/embedding-model/EmbeddingModel.d.mts +97 -0
- package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -0
- package/dist/embedding-model/EmbeddingModel.mjs +17 -0
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -0
- package/dist/index.d.mts +16 -8
- package/dist/index.mjs +10 -2
- package/dist/language-model/LanguageModel.d.mts +12 -20
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +3 -20
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +111 -2
- package/dist/loop/Loop.d.mts.map +1 -0
- package/dist/loop/Loop.mjs +39 -6
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.d.mts +1 -0
- package/dist/loop/Loop.test.mjs +411 -0
- package/dist/loop/Loop.test.mjs.map +1 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs +1013 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs.map +1 -0
- package/dist/math/Vector.d.mts +47 -0
- package/dist/math/Vector.d.mts.map +1 -0
- package/dist/math/Vector.mjs +117 -0
- package/dist/math/Vector.mjs.map +1 -0
- package/dist/observability/Metrics.d.mts +2 -2
- 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/streaming/JSONL.mjs +1 -1
- package/dist/streaming/JSONL.test.d.mts +1 -0
- package/dist/streaming/JSONL.test.mjs +70 -0
- package/dist/streaming/JSONL.test.mjs.map +1 -0
- package/dist/streaming/Lines.mjs +1 -1
- package/dist/streaming/SSE.d.mts +2 -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 -0
- package/dist/streaming/SSE.test.mjs +72 -0
- package/dist/streaming/SSE.test.mjs.map +1 -0
- 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/testing/MockProvider.d.mts +6 -6
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/tool/HistoryCheck.d.mts +6 -3
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +7 -1
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Outcome.d.mts +138 -2
- package/dist/tool/Outcome.d.mts.map +1 -0
- package/dist/tool/Outcome.mjs +34 -18
- package/dist/tool/Outcome.mjs.map +1 -1
- package/dist/tool/Resolvers.d.mts +30 -25
- package/dist/tool/Resolvers.d.mts.map +1 -1
- package/dist/tool/Resolvers.mjs +54 -44
- package/dist/tool/Resolvers.mjs.map +1 -1
- package/dist/tool/Resolvers.test.d.mts +1 -0
- package/dist/tool/Resolvers.test.mjs +317 -0
- package/dist/tool/Resolvers.test.mjs.map +1 -0
- package/dist/tool/Tool.d.mts +1 -1
- package/dist/tool/Tool.mjs +1 -1
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +151 -2
- package/dist/tool/ToolEvent.d.mts.map +1 -0
- package/dist/tool/ToolEvent.mjs +30 -4
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +24 -15
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +14 -13
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.d.mts +1 -0
- package/dist/tool/Toolkit.test.mjs +113 -0
- package/dist/tool/Toolkit.test.mjs.map +1 -0
- package/package.json +29 -13
- package/src/domain/Image.ts +75 -0
- package/src/domain/Items.ts +18 -47
- package/src/domain/Media.ts +61 -0
- package/src/domain/Turn.ts +7 -17
- package/src/embedding-model/Embedding.ts +117 -0
- package/src/embedding-model/EmbeddingModel.ts +107 -0
- package/src/index.ts +9 -1
- package/src/language-model/LanguageModel.ts +2 -22
- package/src/loop/Loop.test.ts +114 -2
- package/src/loop/Loop.ts +69 -5
- package/src/math/Vector.ts +138 -0
- package/src/observability/Metrics.ts +1 -1
- package/src/streaming/SSE.ts +1 -1
- package/src/structured-format/StructuredFormat.ts +2 -2
- package/src/testing/MockProvider.ts +2 -2
- package/src/tool/HistoryCheck.ts +2 -5
- package/src/tool/Outcome.ts +39 -53
- package/src/tool/Resolvers.test.ts +46 -117
- package/src/tool/Resolvers.ts +74 -102
- package/src/tool/Tool.ts +9 -9
- package/src/tool/ToolEvent.ts +30 -26
- package/src/tool/Toolkit.test.ts +97 -2
- package/src/tool/Toolkit.ts +65 -67
- package/dist/Items-D1C2686t.d.mts.map +0 -1
- package/dist/Loop-CzSJo1h8.d.mts +0 -87
- package/dist/Loop-CzSJo1h8.d.mts.map +0 -1
- package/dist/Outcome-C2JYknCu.d.mts +0 -40
- package/dist/Outcome-C2JYknCu.d.mts.map +0 -1
- package/dist/StructuredFormat-B5ueioNr.d.mts.map +0 -1
- package/dist/Tool-5wxOCuOh.d.mts.map +0 -1
- package/dist/ToolEvent-B2N10hr3.d.mts +0 -29
- package/dist/ToolEvent-B2N10hr3.d.mts.map +0 -1
- package/dist/Turn-rlTfuHaQ.d.mts.map +0 -1
- package/dist/match/Match.d.mts +0 -16
- package/dist/match/Match.d.mts.map +0 -1
- package/dist/match/Match.mjs +0 -15
- package/dist/match/Match.mjs.map +0 -1
- package/src/match/Match.ts +0 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for
|
|
3
|
-
*
|
|
4
|
-
* `
|
|
2
|
+
* Tests for approval planners + history-reconciliation primitives. Exercises
|
|
3
|
+
* the full HITL + streaming-tool stack end-to-end by composing approval plans
|
|
4
|
+
* with `executeAll`, with the four wire-shaped scenarios:
|
|
5
5
|
*
|
|
6
6
|
* 1. Approval : gated calls approved → tools execute, structured Values
|
|
7
7
|
* 2. Denial : gated calls denied → Failure(denied) results
|
|
@@ -14,27 +14,16 @@ import { Effect, Queue, Schema, Stream } from "effect"
|
|
|
14
14
|
import { describe, expect, it } from "vitest"
|
|
15
15
|
import * as Items from "../domain/Items.js"
|
|
16
16
|
import { findUnansweredCalls, cancelAllPending, isReconciled } from "./HistoryCheck.js"
|
|
17
|
-
import {
|
|
18
|
-
type ToolResult,
|
|
19
|
-
isFailure,
|
|
20
|
-
isValue,
|
|
21
|
-
toFunctionCallOutput,
|
|
22
|
-
} from "./Outcome.js"
|
|
17
|
+
import { type ToolResult, isFailure, isValue, toFunctionCallOutput } from "./Outcome.js"
|
|
23
18
|
import {
|
|
24
19
|
type ApprovalMapEntry,
|
|
20
|
+
type ToolCallDecision,
|
|
25
21
|
fromApprovalMap,
|
|
26
22
|
fromVerdictQueue,
|
|
27
|
-
withFallback,
|
|
28
|
-
withPermissions,
|
|
29
23
|
} from "./Resolvers.js"
|
|
30
24
|
import { fromEffectSchema, make as makeTool, streaming } from "./Tool.js"
|
|
31
|
-
import { executeAll,
|
|
32
|
-
import {
|
|
33
|
-
type ToolEvent,
|
|
34
|
-
isApprovalRequested,
|
|
35
|
-
isIntermediate,
|
|
36
|
-
isOutput,
|
|
37
|
-
} from "./ToolEvent.js"
|
|
25
|
+
import { executeAll, outputEvent, outputEvents } from "./Toolkit.js"
|
|
26
|
+
import { type ToolEvent, isApprovalRequested, isIntermediate, isOutput } from "./ToolEvent.js"
|
|
38
27
|
|
|
39
28
|
// ---------------------------------------------------------------------------
|
|
40
29
|
// Three demo tools covering the matrix:
|
|
@@ -97,28 +86,32 @@ const calls = [
|
|
|
97
86
|
fc("c3", "delete_database", { name: "prod" }),
|
|
98
87
|
]
|
|
99
88
|
|
|
100
|
-
const resultsFrom = (
|
|
101
|
-
collected
|
|
102
|
-
|
|
89
|
+
const resultsFrom = (collected: ReadonlyArray<ToolEvent>): ReadonlyArray<ToolResult> =>
|
|
90
|
+
collected.filter(isOutput).map((e) => e.result)
|
|
91
|
+
|
|
92
|
+
const byCallId = (results: ReadonlyArray<ToolResult>) => new Map(results.map((r) => [r.call_id, r]))
|
|
103
93
|
|
|
104
|
-
const
|
|
105
|
-
|
|
94
|
+
const eventsFromApprovalMap = (approvals: ReadonlyMap<string, ApprovalMapEntry>) => {
|
|
95
|
+
const plan = fromApprovalMap(isSensitive, approvals)(calls)
|
|
96
|
+
return Stream.merge(executeAll(allTools, plan.approved), outputEvents(plan.rejected))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const eventsFromDecision = (decision: ToolCallDecision): Stream.Stream<ToolEvent> =>
|
|
100
|
+
decision._tag === "Approved"
|
|
101
|
+
? executeAll(allTools, [decision.call])
|
|
102
|
+
: Stream.succeed(outputEvent(decision.result))
|
|
106
103
|
|
|
107
104
|
// ---------------------------------------------------------------------------
|
|
108
105
|
// fromApprovalMap: HTTP-style scenarios
|
|
109
106
|
// ---------------------------------------------------------------------------
|
|
110
107
|
|
|
111
|
-
describe("
|
|
108
|
+
describe("fromApprovalMap + executeAll", () => {
|
|
112
109
|
it("approval: all gated approved → tools execute, structured Values", async () => {
|
|
113
110
|
const approvals = new Map<string, ApprovalMapEntry>([
|
|
114
111
|
["c2", { decision: "approve" }],
|
|
115
112
|
["c3", { decision: "approve" }],
|
|
116
113
|
])
|
|
117
|
-
const collected = await Effect.runPromise(
|
|
118
|
-
Stream.runCollect(
|
|
119
|
-
executeAllWithResolver(allTools, calls, fromApprovalMap(isSensitive, approvals)),
|
|
120
|
-
),
|
|
121
|
-
)
|
|
114
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
|
|
122
115
|
const by = byCallId(resultsFrom(collected))
|
|
123
116
|
expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
|
|
124
117
|
expect(by.get("c2")).toMatchObject({
|
|
@@ -139,16 +132,10 @@ describe("executeAllWithResolver + fromApprovalMap", () => {
|
|
|
139
132
|
["c2", { decision: "deny", reason: "spam concern" }],
|
|
140
133
|
["c3", { decision: "deny", reason: "prod is sacred" }],
|
|
141
134
|
])
|
|
142
|
-
const collected = await Effect.runPromise(
|
|
143
|
-
Stream.runCollect(
|
|
144
|
-
executeAllWithResolver(allTools, calls, fromApprovalMap(isSensitive, approvals)),
|
|
145
|
-
),
|
|
146
|
-
)
|
|
135
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
|
|
147
136
|
|
|
148
137
|
// bulk_email never ran.
|
|
149
|
-
expect(
|
|
150
|
-
collected.filter(isIntermediate).filter((e) => e.tool === "bulk_email"),
|
|
151
|
-
).toHaveLength(0)
|
|
138
|
+
expect(collected.filter(isIntermediate).filter((e) => e.tool === "bulk_email")).toHaveLength(0)
|
|
152
139
|
|
|
153
140
|
const by = byCallId(resultsFrom(collected))
|
|
154
141
|
expect(by.get("c2")).toMatchObject({
|
|
@@ -164,15 +151,7 @@ describe("executeAllWithResolver + fromApprovalMap", () => {
|
|
|
164
151
|
})
|
|
165
152
|
|
|
166
153
|
it("cancellation: missing verdicts → Failure(cancelled)", async () => {
|
|
167
|
-
const collected = await Effect.runPromise(
|
|
168
|
-
Stream.runCollect(
|
|
169
|
-
executeAllWithResolver(
|
|
170
|
-
allTools,
|
|
171
|
-
calls,
|
|
172
|
-
fromApprovalMap(isSensitive, new Map()),
|
|
173
|
-
),
|
|
174
|
-
),
|
|
175
|
-
)
|
|
154
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(new Map())))
|
|
176
155
|
const by = byCallId(resultsFrom(collected))
|
|
177
156
|
expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
|
|
178
157
|
expect(by.get("c2")).toMatchObject({ _tag: "Failure", kind: "cancelled" })
|
|
@@ -184,11 +163,7 @@ describe("executeAllWithResolver + fromApprovalMap", () => {
|
|
|
184
163
|
["c2", { decision: "approve" }],
|
|
185
164
|
// c3 omitted → cancelled
|
|
186
165
|
])
|
|
187
|
-
const collected = await Effect.runPromise(
|
|
188
|
-
Stream.runCollect(
|
|
189
|
-
executeAllWithResolver(allTools, calls, fromApprovalMap(isSensitive, approvals)),
|
|
190
|
-
),
|
|
191
|
-
)
|
|
166
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
|
|
192
167
|
const by = byCallId(resultsFrom(collected))
|
|
193
168
|
expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
|
|
194
169
|
expect(by.get("c2")).toMatchObject({ _tag: "Value", value: { status: "sent" } })
|
|
@@ -200,7 +175,7 @@ describe("executeAllWithResolver + fromApprovalMap", () => {
|
|
|
200
175
|
// Graceful degradation: hallucinated tool name doesn't kill the turn.
|
|
201
176
|
// ---------------------------------------------------------------------------
|
|
202
177
|
|
|
203
|
-
describe("
|
|
178
|
+
describe("executeAll: graceful degradation", () => {
|
|
204
179
|
it("unknown tool name → Failure(unknown_tool); other calls still execute", async () => {
|
|
205
180
|
const callsWithBogus = [
|
|
206
181
|
fc("c1", "web_search", { query: "x" }),
|
|
@@ -209,11 +184,13 @@ describe("executeAllWithResolver: graceful degradation", () => {
|
|
|
209
184
|
]
|
|
210
185
|
const collected = await Effect.runPromise(
|
|
211
186
|
Stream.runCollect(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
187
|
+
(() => {
|
|
188
|
+
const plan = fromApprovalMap(
|
|
189
|
+
isSensitive,
|
|
190
|
+
new Map([["c3", { decision: "approve" }]]),
|
|
191
|
+
)(callsWithBogus)
|
|
192
|
+
return Stream.merge(executeAll(allTools, plan.approved), outputEvents(plan.rejected))
|
|
193
|
+
})(),
|
|
217
194
|
),
|
|
218
195
|
)
|
|
219
196
|
const by = byCallId(resultsFrom(collected))
|
|
@@ -227,7 +204,7 @@ describe("executeAllWithResolver: graceful degradation", () => {
|
|
|
227
204
|
// fromVerdictQueue: WebSocket-style scenarios
|
|
228
205
|
// ---------------------------------------------------------------------------
|
|
229
206
|
|
|
230
|
-
describe("
|
|
207
|
+
describe("fromVerdictQueue + executeAll", () => {
|
|
231
208
|
it("queue-driven: approve + deny resolve correctly with ApprovalRequested events", async () => {
|
|
232
209
|
const collected = await Effect.runPromise(
|
|
233
210
|
Effect.gen(function* () {
|
|
@@ -246,11 +223,17 @@ describe("executeAllWithResolver + fromVerdictQueue", () => {
|
|
|
246
223
|
// Stream.unwrap supplies the Scope for fromVerdictQueue's router.
|
|
247
224
|
const events = Stream.unwrap(
|
|
248
225
|
Effect.gen(function* () {
|
|
249
|
-
const {
|
|
226
|
+
const { approved, decisions, announce } = yield* fromVerdictQueue(
|
|
250
227
|
isSensitive,
|
|
251
228
|
verdicts,
|
|
252
229
|
)(calls)
|
|
253
|
-
return Stream.merge(
|
|
230
|
+
return Stream.merge(
|
|
231
|
+
announce,
|
|
232
|
+
Stream.merge(
|
|
233
|
+
executeAll(allTools, approved),
|
|
234
|
+
decisions.pipe(Stream.flatMap(eventsFromDecision)),
|
|
235
|
+
),
|
|
236
|
+
)
|
|
254
237
|
}),
|
|
255
238
|
)
|
|
256
239
|
return yield* Stream.runCollect(events)
|
|
@@ -269,58 +252,6 @@ describe("executeAllWithResolver + fromVerdictQueue", () => {
|
|
|
269
252
|
})
|
|
270
253
|
})
|
|
271
254
|
|
|
272
|
-
// ---------------------------------------------------------------------------
|
|
273
|
-
// Combinators
|
|
274
|
-
// ---------------------------------------------------------------------------
|
|
275
|
-
|
|
276
|
-
describe("withPermissions / withFallback", () => {
|
|
277
|
-
it("withPermissions short-circuits with permission_denied when canApprove returns false", async () => {
|
|
278
|
-
const canApprove = (call: Items.FunctionCall) =>
|
|
279
|
-
Effect.succeed(call.name !== "delete_database")
|
|
280
|
-
const inner = fromApprovalMap(
|
|
281
|
-
isSensitive,
|
|
282
|
-
new Map<string, ApprovalMapEntry>([
|
|
283
|
-
["c2", { decision: "approve" }],
|
|
284
|
-
["c3", { decision: "approve" }],
|
|
285
|
-
]),
|
|
286
|
-
)
|
|
287
|
-
const collected = await Effect.runPromise(
|
|
288
|
-
Stream.runCollect(
|
|
289
|
-
executeAllWithResolver(allTools, calls, withPermissions(inner, canApprove)),
|
|
290
|
-
),
|
|
291
|
-
)
|
|
292
|
-
const by = byCallId(resultsFrom(collected))
|
|
293
|
-
// c2 allowed → executed; c3 forbidden → permission_denied (no exec)
|
|
294
|
-
expect(by.get("c2")).toMatchObject({ _tag: "Value", value: { status: "sent" } })
|
|
295
|
-
expect(by.get("c3")).toMatchObject({
|
|
296
|
-
_tag: "Failure",
|
|
297
|
-
kind: "permission_denied",
|
|
298
|
-
})
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
it("withFallback recovers a Reject by running an alternate decision", async () => {
|
|
302
|
-
const inner = fromApprovalMap(
|
|
303
|
-
isSensitive,
|
|
304
|
-
new Map<string, ApprovalMapEntry>([
|
|
305
|
-
["c2", { decision: "deny", reason: "no" }],
|
|
306
|
-
["c3", { decision: "approve" }],
|
|
307
|
-
]),
|
|
308
|
-
)
|
|
309
|
-
// Recover only `denied` rejections; turn them into Execute (re-run anyway).
|
|
310
|
-
const recoverable = (r: ToolResult) => isFailure(r) && r.kind === "denied"
|
|
311
|
-
const fallbackResolver = withFallback(inner, recoverable, () =>
|
|
312
|
-
Effect.succeed({ _tag: "Execute" } as const),
|
|
313
|
-
)
|
|
314
|
-
const collected = await Effect.runPromise(
|
|
315
|
-
Stream.runCollect(executeAllWithResolver(allTools, calls, fallbackResolver)),
|
|
316
|
-
)
|
|
317
|
-
const by = byCallId(resultsFrom(collected))
|
|
318
|
-
// c2 was denied but fallback re-ran the tool.
|
|
319
|
-
expect(by.get("c2")).toMatchObject({ _tag: "Value", value: { status: "sent" } })
|
|
320
|
-
expect(by.get("c3")).toMatchObject({ _tag: "Value", value: { status: "dropped" } })
|
|
321
|
-
})
|
|
322
|
-
})
|
|
323
|
-
|
|
324
255
|
// ---------------------------------------------------------------------------
|
|
325
256
|
// History reconciliation
|
|
326
257
|
// ---------------------------------------------------------------------------
|
|
@@ -412,14 +343,12 @@ describe("toFunctionCallOutput", () => {
|
|
|
412
343
|
})
|
|
413
344
|
|
|
414
345
|
// ---------------------------------------------------------------------------
|
|
415
|
-
// executeAll
|
|
346
|
+
// executeAll
|
|
416
347
|
// ---------------------------------------------------------------------------
|
|
417
348
|
|
|
418
349
|
describe("executeAll", () => {
|
|
419
|
-
it("
|
|
420
|
-
const collected = await Effect.runPromise(
|
|
421
|
-
Stream.runCollect(executeAll(allTools, calls)),
|
|
422
|
-
)
|
|
350
|
+
it("runs all calls passed to it", async () => {
|
|
351
|
+
const collected = await Effect.runPromise(Stream.runCollect(executeAll(allTools, calls)))
|
|
423
352
|
expect(collected.filter(isOutput)).toHaveLength(3)
|
|
424
353
|
expect(collected.filter(isOutput).every((e) => isValue(e.result))).toBe(true)
|
|
425
354
|
})
|
package/src/tool/Resolvers.ts
CHANGED
|
@@ -1,55 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* for layering policy on top.
|
|
2
|
+
* Approval helpers for the two transport flavors.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - `withFallback` : recovery wrapper.
|
|
9
|
-
*
|
|
10
|
-
* None of these know about the executor's stream shape; they just produce
|
|
11
|
-
* `Effect<ToolDecision>`s a `Resolver` can return.
|
|
4
|
+
* These helpers only decide which calls are approved and which synthetic
|
|
5
|
+
* results must be returned to the model. Tool execution stays explicit at
|
|
6
|
+
* the recipe boundary via `Toolkit.executeAll`.
|
|
12
7
|
*/
|
|
13
8
|
import { Deferred, Effect, Queue, Scope, Stream } from "effect"
|
|
14
9
|
import type { FunctionCall } from "../domain/Items.js"
|
|
15
|
-
import {
|
|
16
|
-
type ToolDecision,
|
|
17
|
-
type ToolResult,
|
|
18
|
-
cancelled,
|
|
19
|
-
denied,
|
|
20
|
-
execute,
|
|
21
|
-
reject,
|
|
22
|
-
rejected,
|
|
23
|
-
} from "./Outcome.js"
|
|
24
|
-
import type { Resolver } from "./Toolkit.js"
|
|
10
|
+
import { type ToolResult, cancelled, denied } from "./Outcome.js"
|
|
25
11
|
import type { ToolEvent } from "./ToolEvent.js"
|
|
26
12
|
|
|
13
|
+
export type ToolCallPlan = {
|
|
14
|
+
readonly approved: ReadonlyArray<FunctionCall>
|
|
15
|
+
readonly rejected: ReadonlyArray<ToolResult>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ToolCallDecision =
|
|
19
|
+
| { readonly _tag: "Approved"; readonly call: FunctionCall }
|
|
20
|
+
| { readonly _tag: "Rejected"; readonly result: ToolResult }
|
|
21
|
+
|
|
22
|
+
export const approve = (call: FunctionCall): ToolCallDecision => ({
|
|
23
|
+
_tag: "Approved",
|
|
24
|
+
call,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const reject = (result: ToolResult): ToolCallDecision => ({
|
|
28
|
+
_tag: "Rejected",
|
|
29
|
+
result,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export const splitToolCallDecisions = (decisions: ReadonlyArray<ToolCallDecision>): ToolCallPlan =>
|
|
33
|
+
decisions.reduce<ToolCallPlan>(
|
|
34
|
+
(acc, decision) =>
|
|
35
|
+
decision._tag === "Approved"
|
|
36
|
+
? { ...acc, approved: [...acc.approved, decision.call] }
|
|
37
|
+
: { ...acc, rejected: [...acc.rejected, decision.result] },
|
|
38
|
+
{ approved: [], rejected: [] },
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
export const approvalRequested = (call: FunctionCall): ToolEvent => ({
|
|
42
|
+
_tag: "ApprovalRequested",
|
|
43
|
+
call_id: call.call_id,
|
|
44
|
+
tool: call.name,
|
|
45
|
+
arguments: call.arguments,
|
|
46
|
+
})
|
|
47
|
+
|
|
27
48
|
// ---------------------------------------------------------------------------
|
|
28
49
|
// Verdict queue (WebSocket-style transport).
|
|
29
50
|
// ---------------------------------------------------------------------------
|
|
30
51
|
|
|
31
|
-
export
|
|
52
|
+
export type Verdict = {
|
|
32
53
|
readonly call_id: string
|
|
33
54
|
readonly decision: "approve" | "deny"
|
|
34
55
|
readonly reason?: string
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
/**
|
|
38
|
-
* Queue-backed
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* merges the announce stream into its consumer view.
|
|
59
|
+
* Queue-backed approval planner. Safe calls are returned immediately in
|
|
60
|
+
* `approved`; gated calls emit `ApprovalRequested` events and later produce
|
|
61
|
+
* one `ToolCallDecision` when their matching verdict arrives.
|
|
42
62
|
*/
|
|
43
63
|
export const fromVerdictQueue =
|
|
44
|
-
(
|
|
45
|
-
predicate: (call: FunctionCall) => boolean,
|
|
46
|
-
verdicts: Queue.Dequeue<Verdict>,
|
|
47
|
-
) =>
|
|
64
|
+
(predicate: (call: FunctionCall) => boolean, verdicts: Queue.Dequeue<Verdict>) =>
|
|
48
65
|
(
|
|
49
66
|
calls: ReadonlyArray<FunctionCall>,
|
|
50
67
|
): Effect.Effect<
|
|
51
68
|
{
|
|
52
|
-
readonly
|
|
69
|
+
readonly approved: ReadonlyArray<FunctionCall>
|
|
70
|
+
readonly decisions: Stream.Stream<ToolCallDecision>
|
|
53
71
|
readonly announce: Stream.Stream<ToolEvent>
|
|
54
72
|
},
|
|
55
73
|
never,
|
|
@@ -57,6 +75,7 @@ export const fromVerdictQueue =
|
|
|
57
75
|
> =>
|
|
58
76
|
Effect.gen(function* () {
|
|
59
77
|
const gated = calls.filter(predicate)
|
|
78
|
+
const approved = calls.filter((call) => !predicate(call))
|
|
60
79
|
|
|
61
80
|
const entries = yield* Effect.forEach(gated, (call) =>
|
|
62
81
|
Deferred.make<Verdict>().pipe(Effect.map((d) => [call.call_id, d] as const)),
|
|
@@ -76,26 +95,25 @@ export const fromVerdictQueue =
|
|
|
76
95
|
),
|
|
77
96
|
)
|
|
78
97
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
tool: call.name,
|
|
94
|
-
arguments: call.arguments,
|
|
95
|
-
})),
|
|
98
|
+
const decisions = Stream.fromIterable(gated).pipe(
|
|
99
|
+
Stream.flatMap(
|
|
100
|
+
(call) => {
|
|
101
|
+
const d = deferreds.get(call.call_id)!
|
|
102
|
+
return Stream.fromEffect(
|
|
103
|
+
Deferred.await(d).pipe(
|
|
104
|
+
Effect.map((v) =>
|
|
105
|
+
v.decision === "approve" ? approve(call) : reject(denied(call, v.reason)),
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
},
|
|
110
|
+
{ concurrency: "unbounded" },
|
|
111
|
+
),
|
|
96
112
|
)
|
|
97
113
|
|
|
98
|
-
|
|
114
|
+
const announce = Stream.fromIterable<ToolEvent>(gated.map(approvalRequested))
|
|
115
|
+
|
|
116
|
+
return { approved, decisions, announce }
|
|
99
117
|
})
|
|
100
118
|
|
|
101
119
|
// ---------------------------------------------------------------------------
|
|
@@ -108,59 +126,13 @@ export type ApprovalMapEntry =
|
|
|
108
126
|
| { readonly decision: "deny"; readonly reason?: string }
|
|
109
127
|
|
|
110
128
|
export const fromApprovalMap =
|
|
111
|
-
(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
v.decision === "approve" ? execute : reject(denied(call, v.reason)),
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
// Combinators - compose policy onto an inner resolver.
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Authz gate. `canApprove` runs BEFORE the inner resolver; failures
|
|
130
|
-
* short-circuit to a `permission_denied` rejection. Override `onForbidden`
|
|
131
|
-
* if your audit format wants a different kind or reason.
|
|
132
|
-
*/
|
|
133
|
-
export const withPermissions =
|
|
134
|
-
(
|
|
135
|
-
inner: Resolver,
|
|
136
|
-
canApprove: (call: FunctionCall) => Effect.Effect<boolean>,
|
|
137
|
-
onForbidden: (call: FunctionCall) => ToolResult = (call) =>
|
|
138
|
-
rejected(call, "permission_denied", "missing permissions"),
|
|
139
|
-
): Resolver =>
|
|
140
|
-
(call) =>
|
|
141
|
-
canApprove(call).pipe(
|
|
142
|
-
Effect.flatMap((allowed) =>
|
|
143
|
-
allowed ? inner(call) : Effect.succeed(reject(onForbidden(call))),
|
|
144
|
-
),
|
|
129
|
+
(predicate: (call: FunctionCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) =>
|
|
130
|
+
(calls: ReadonlyArray<FunctionCall>): ToolCallPlan =>
|
|
131
|
+
splitToolCallDecisions(
|
|
132
|
+
calls.map((call) => {
|
|
133
|
+
if (!predicate(call)) return approve(call)
|
|
134
|
+
const v = approvals.get(call.call_id)
|
|
135
|
+
if (v === undefined) return reject(cancelled(call))
|
|
136
|
+
return v.decision === "approve" ? approve(call) : reject(denied(call, v.reason))
|
|
137
|
+
}),
|
|
145
138
|
)
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Fallback gate. If `inner` returns a Reject whose result matches the
|
|
149
|
-
* `recoverable` predicate, run `fallback(call)` instead and use that
|
|
150
|
-
* decision. Otherwise pass the original Reject through.
|
|
151
|
-
*/
|
|
152
|
-
export const withFallback =
|
|
153
|
-
(
|
|
154
|
-
inner: Resolver,
|
|
155
|
-
recoverable: (result: ToolResult) => boolean,
|
|
156
|
-
fallback: (call: FunctionCall) => Effect.Effect<ToolDecision>,
|
|
157
|
-
): Resolver =>
|
|
158
|
-
(call) =>
|
|
159
|
-
inner(call).pipe(
|
|
160
|
-
Effect.flatMap((decision) =>
|
|
161
|
-
decision._tag === "Reject" && recoverable(decision.result)
|
|
162
|
-
? fallback(call)
|
|
163
|
-
: Effect.succeed(decision),
|
|
164
|
-
),
|
|
165
|
-
)
|
|
166
|
-
|
package/src/tool/Tool.ts
CHANGED
|
@@ -36,7 +36,7 @@ export const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(
|
|
|
36
36
|
Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema)) as unknown as S &
|
|
37
37
|
ToolInputSchema<S["Type"]>
|
|
38
38
|
|
|
39
|
-
export
|
|
39
|
+
export type Tool<Name extends string, Input, Output, R = never> = {
|
|
40
40
|
readonly name: Name
|
|
41
41
|
readonly description: string
|
|
42
42
|
readonly inputSchema: ToolInputSchema<Input>
|
|
@@ -55,7 +55,7 @@ export interface Tool<Name extends string, Input, Output, R = never> {
|
|
|
55
55
|
* to its own wire field (OpenAI → `parameters`, Anthropic →
|
|
56
56
|
* `input_schema`). Built from a `Tool` by `Toolkit.toDescriptors`.
|
|
57
57
|
*/
|
|
58
|
-
export
|
|
58
|
+
export type ToolDescriptor = {
|
|
59
59
|
readonly name: string
|
|
60
60
|
readonly description: string
|
|
61
61
|
readonly inputSchema: Record<string, unknown>
|
|
@@ -75,7 +75,7 @@ export const make = <Name extends string, Input, Output, R = never>(
|
|
|
75
75
|
// `Output`. Sub-agents, slow downloads with progress, recipe streamers.
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
77
|
|
|
78
|
-
export
|
|
78
|
+
export type StreamingTool<Name extends string, Input, Event, Output, R = never> = {
|
|
79
79
|
readonly _kind: "streaming"
|
|
80
80
|
readonly name: Name
|
|
81
81
|
readonly description: string
|
|
@@ -89,11 +89,11 @@ export const streaming = <Name extends string, Input, Event, Output, R = never>(
|
|
|
89
89
|
spec: Omit<StreamingTool<Name, Input, Event, Output, R>, "_kind">,
|
|
90
90
|
): StreamingTool<Name, Input, Event, Output, R> => ({ _kind: "streaming", ...spec })
|
|
91
91
|
|
|
92
|
-
export type AnyStreamingTool = StreamingTool<string, any, any, any,
|
|
93
|
-
export type AnyPlainTool = Tool<string, any, any,
|
|
94
|
-
export type AnyKindTool = AnyStreamingTool | AnyPlainTool
|
|
92
|
+
export type AnyStreamingTool<R = any> = StreamingTool<string, any, any, any, R>
|
|
93
|
+
export type AnyPlainTool<R = any> = Tool<string, any, any, R>
|
|
94
|
+
export type AnyKindTool<R = any> = AnyStreamingTool<R> | AnyPlainTool<R>
|
|
95
95
|
|
|
96
|
-
export const isStreamingTool = (t: AnyKindTool): t is AnyStreamingTool =>
|
|
96
|
+
export const isStreamingTool = <R>(t: AnyKindTool<R>): t is AnyStreamingTool<R> =>
|
|
97
97
|
"_kind" in t && t._kind === "streaming"
|
|
98
98
|
|
|
99
99
|
/**
|
|
@@ -101,8 +101,8 @@ export const isStreamingTool = (t: AnyKindTool): t is AnyStreamingTool =>
|
|
|
101
101
|
* descriptors. Mirrors `Toolkit.toDescriptors` but accepts the union type
|
|
102
102
|
* so a single list can carry both kinds.
|
|
103
103
|
*/
|
|
104
|
-
export const toDescriptors = (
|
|
105
|
-
tools: ReadonlyArray<AnyKindTool
|
|
104
|
+
export const toDescriptors = <R>(
|
|
105
|
+
tools: ReadonlyArray<AnyKindTool<R>>,
|
|
106
106
|
): ReadonlyArray<ToolDescriptor> =>
|
|
107
107
|
tools.map((tool) => {
|
|
108
108
|
const inputSchema = tool.inputSchema["~standard"].jsonSchema.input({
|
package/src/tool/ToolEvent.ts
CHANGED
|
@@ -1,37 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The event type emitted
|
|
2
|
+
* The event type emitted while handling tool calls.
|
|
3
3
|
*
|
|
4
|
-
* - ApprovalRequested : gated calls
|
|
4
|
+
* - ApprovalRequested : gated calls waiting for approval
|
|
5
5
|
* - Intermediate : per-element passthrough from a streaming tool's run
|
|
6
6
|
* - Output : terminal result (carries a structured ToolResult)
|
|
7
7
|
*
|
|
8
|
-
* Recipes thread `ToolEvent.Output.result` through `
|
|
8
|
+
* Recipes thread `ToolEvent.Output.result` through `continueWith` and apply
|
|
9
9
|
* `toFunctionCallOutput` when appending to history.
|
|
10
10
|
*/
|
|
11
|
+
import { Data } from "effect"
|
|
11
12
|
import type { ToolResult } from "./Outcome.js"
|
|
12
13
|
|
|
13
|
-
export type ToolEvent =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
export type ToolEvent = Data.TaggedEnum<{
|
|
15
|
+
ApprovalRequested: {
|
|
16
|
+
readonly call_id: string
|
|
17
|
+
readonly tool: string
|
|
18
|
+
readonly arguments: string
|
|
19
|
+
}
|
|
20
|
+
Intermediate: {
|
|
21
|
+
readonly call_id: string
|
|
22
|
+
readonly tool: string
|
|
23
|
+
readonly data: unknown
|
|
24
|
+
}
|
|
25
|
+
Output: {
|
|
26
|
+
readonly result: ToolResult
|
|
27
|
+
}
|
|
28
|
+
}>
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Namespace of constructors, type guards, and matchers for `ToolEvent`,
|
|
32
|
+
* provided by `Data.taggedEnum`. Use `ToolEvent.Output({ result })` to build
|
|
33
|
+
* an event, `ToolEvent.$is("Output")` for type narrowing,
|
|
34
|
+
* `ToolEvent.$match({ ApprovalRequested, Intermediate, Output })` for
|
|
35
|
+
* exhaustive pattern matching.
|
|
36
|
+
*/
|
|
37
|
+
export const ToolEvent = Data.taggedEnum<ToolEvent>()
|
|
35
38
|
|
|
36
|
-
export const
|
|
37
|
-
|
|
39
|
+
export const isApprovalRequested = ToolEvent.$is("ApprovalRequested")
|
|
40
|
+
export const isIntermediate = ToolEvent.$is("Intermediate")
|
|
41
|
+
export const isOutput = ToolEvent.$is("Output")
|