@effect-uai/core 0.5.2 → 0.7.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 +1 -1
- package/dist/{Audio-BfCTGnH3.d.mts → Audio-DPlTsNdz.d.mts} +3 -2
- package/dist/Audio-DPlTsNdz.d.mts.map +1 -0
- package/dist/{Items-BH8xUkoR.d.mts → Items-D8UaXQx_.d.mts} +15 -15
- package/dist/Items-D8UaXQx_.d.mts.map +1 -0
- package/dist/SandboxError-BvKypkJm.d.mts +158 -0
- package/dist/SandboxError-BvKypkJm.d.mts.map +1 -0
- package/dist/{Tool-87ViKCCO.d.mts → Tool-CiFXYjmR.d.mts} +11 -11
- package/dist/{Tool-87ViKCCO.d.mts.map → Tool-CiFXYjmR.d.mts.map} +1 -1
- package/dist/capabilities/Capabilities.d.mts +78 -0
- package/dist/capabilities/Capabilities.d.mts.map +1 -0
- package/dist/capabilities/Capabilities.mjs +65 -0
- package/dist/capabilities/Capabilities.mjs.map +1 -0
- package/dist/domain/Audio.d.mts +1 -1
- package/dist/domain/Audio.mjs.map +1 -1
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +15 -15
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Music.d.mts +131 -72
- package/dist/domain/Music.d.mts.map +1 -1
- package/dist/domain/Music.mjs +36 -9
- package/dist/domain/Music.mjs.map +1 -1
- package/dist/domain/Transcript.d.mts +8 -2
- package/dist/domain/Transcript.d.mts.map +1 -1
- package/dist/domain/Transcript.mjs.map +1 -1
- package/dist/domain/Turn.d.mts +388 -2
- package/dist/domain/Turn.d.mts.map +1 -0
- package/dist/domain/Turn.mjs +13 -13
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/embedding-model/Embedding.d.mts +12 -2
- package/dist/embedding-model/Embedding.d.mts.map +1 -1
- package/dist/embedding-model/Embedding.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.d.mts +36 -23
- package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -1
- package/dist/embedding-model/EmbeddingModel.mjs +20 -1
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +15 -3
- package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
- package/dist/index.d.mts +13 -7
- package/dist/index.mjs +9 -3
- package/dist/language-model/LanguageModel.d.mts +5 -5
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +37 -90
- package/dist/loop/Loop.d.mts.map +1 -1
- package/dist/loop/Loop.mjs +39 -93
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +46 -54
- package/dist/loop/Loop.test.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.d.mts +48 -25
- package/dist/music-generator/MusicGenerator.d.mts.map +1 -1
- package/dist/music-generator/MusicGenerator.mjs +17 -9
- package/dist/music-generator/MusicGenerator.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.test.mjs +81 -56
- package/dist/music-generator/MusicGenerator.test.mjs.map +1 -1
- package/dist/sandbox/Memory.d.mts +53 -0
- package/dist/sandbox/Memory.d.mts.map +1 -0
- package/dist/sandbox/Memory.mjs +68 -0
- package/dist/sandbox/Memory.mjs.map +1 -0
- package/dist/sandbox/Sandbox.d.mts +717 -0
- package/dist/sandbox/Sandbox.d.mts.map +1 -0
- package/dist/sandbox/Sandbox.mjs +192 -0
- package/dist/sandbox/Sandbox.mjs.map +1 -0
- package/dist/sandbox/SandboxError.d.mts +2 -0
- package/dist/sandbox/SandboxError.mjs +89 -0
- package/dist/sandbox/SandboxError.mjs.map +1 -0
- package/dist/sandbox/SandboxImage.d.mts +28 -0
- package/dist/sandbox/SandboxImage.d.mts.map +1 -0
- package/dist/sandbox/SandboxImage.mjs +32 -0
- package/dist/sandbox/SandboxImage.mjs.map +1 -0
- package/dist/sandbox/SandboxNetwork.d.mts +34 -0
- package/dist/sandbox/SandboxNetwork.d.mts.map +1 -0
- package/dist/sandbox/SandboxNetwork.mjs +36 -0
- package/dist/sandbox/SandboxNetwork.mjs.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +84 -3
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +32 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +59 -5
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
- package/dist/testing/MockMusicGenerator.d.mts +12 -7
- package/dist/testing/MockMusicGenerator.d.mts.map +1 -1
- package/dist/testing/MockMusicGenerator.mjs +4 -3
- package/dist/testing/MockMusicGenerator.mjs.map +1 -1
- package/dist/testing/MockProvider.d.mts +3 -3
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/testing/MockSandbox.d.mts +604 -0
- package/dist/testing/MockSandbox.d.mts.map +1 -0
- package/dist/testing/MockSandbox.mjs +160 -0
- package/dist/testing/MockSandbox.mjs.map +1 -0
- package/dist/testing/MockSpeechSynthesizer.d.mts +27 -12
- package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.mjs +75 -51
- package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
- package/dist/tool/{Resolvers.d.mts → Approval.d.mts} +27 -27
- package/dist/tool/Approval.d.mts.map +1 -0
- package/dist/tool/{Resolvers.mjs → Approval.mjs} +18 -18
- package/dist/tool/Approval.mjs.map +1 -0
- package/dist/tool/{Resolvers.test.mjs → Approval.test.mjs} +40 -40
- package/dist/tool/Approval.test.mjs.map +1 -0
- package/dist/tool/HistoryCheck.d.mts +6 -6
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +5 -5
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +4 -4
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +19 -19
- package/dist/tool/ToolEvent.d.mts.map +1 -1
- package/dist/tool/ToolEvent.mjs +8 -8
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/{Outcome.d.mts → ToolResult.d.mts} +29 -29
- package/dist/tool/ToolResult.d.mts.map +1 -0
- package/dist/tool/ToolResult.mjs +61 -0
- package/dist/tool/ToolResult.mjs.map +1 -0
- package/dist/tool/Toolkit.d.mts +58 -26
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +62 -34
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +13 -13
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.d.mts +11 -10
- package/dist/transcriber/Transcriber.d.mts.map +1 -1
- package/dist/transcriber/Transcriber.mjs.map +1 -1
- package/dist/transcriber/Transcriber.test.mjs +3 -3
- package/dist/transcriber/Transcriber.test.mjs.map +1 -1
- package/package.json +35 -7
- package/src/capabilities/Capabilities.ts +81 -0
- package/src/domain/Audio.ts +2 -1
- package/src/domain/Items.ts +9 -9
- package/src/domain/Music.ts +142 -68
- package/src/domain/Transcript.ts +7 -1
- package/src/domain/Turn.ts +16 -16
- package/src/embedding-model/Embedding.ts +11 -0
- package/src/embedding-model/EmbeddingModel.test.ts +15 -4
- package/src/embedding-model/EmbeddingModel.ts +48 -21
- package/src/index.ts +8 -2
- package/src/language-model/LanguageModel.ts +3 -3
- package/src/loop/Loop.test.ts +93 -91
- package/src/loop/Loop.ts +72 -170
- package/src/music-generator/MusicGenerator.test.ts +78 -53
- package/src/music-generator/MusicGenerator.ts +65 -24
- package/src/sandbox/Memory.ts +128 -0
- package/src/sandbox/Sandbox.ts +639 -0
- package/src/sandbox/SandboxError.ts +140 -0
- package/src/sandbox/SandboxImage.ts +26 -0
- package/src/sandbox/SandboxNetwork.ts +34 -0
- package/src/speech-synthesizer/SpeechSynthesizer.test.ts +92 -3
- package/src/speech-synthesizer/SpeechSynthesizer.ts +109 -0
- package/src/testing/MockMusicGenerator.ts +28 -18
- package/src/testing/MockProvider.ts +21 -20
- package/src/testing/MockSandbox.ts +342 -0
- package/src/testing/MockSpeechSynthesizer.ts +137 -77
- package/src/tool/{Resolvers.test.ts → Approval.test.ts} +45 -54
- package/src/tool/{Resolvers.ts → Approval.ts} +25 -25
- package/src/tool/HistoryCheck.ts +10 -13
- package/src/tool/Tool.ts +13 -15
- package/src/tool/ToolEvent.ts +7 -7
- package/src/tool/{Outcome.ts → ToolResult.ts} +20 -20
- package/src/tool/Toolkit.test.ts +13 -13
- package/src/tool/Toolkit.ts +97 -73
- package/src/transcriber/Transcriber.test.ts +3 -3
- package/src/transcriber/Transcriber.ts +10 -7
- package/dist/Audio-BfCTGnH3.d.mts.map +0 -1
- package/dist/Items-BH8xUkoR.d.mts.map +0 -1
- package/dist/Turn-0CwCAyVe.d.mts +0 -388
- package/dist/Turn-0CwCAyVe.d.mts.map +0 -1
- package/dist/tool/Outcome.d.mts.map +0 -1
- package/dist/tool/Outcome.mjs +0 -61
- package/dist/tool/Outcome.mjs.map +0 -1
- package/dist/tool/Resolvers.d.mts.map +0 -1
- package/dist/tool/Resolvers.mjs.map +0 -1
- package/dist/tool/Resolvers.test.mjs.map +0 -1
- /package/dist/tool/{Resolvers.test.d.mts → Approval.test.d.mts} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for approval planners + history-reconciliation primitives. Exercises
|
|
3
3
|
* the full HITL + streaming-tool stack end-to-end by composing approval plans
|
|
4
|
-
* with `
|
|
4
|
+
* with `run`, 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,16 +14,11 @@ 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 { type ToolResult, isFailure,
|
|
18
|
-
import {
|
|
19
|
-
type ApprovalMapEntry,
|
|
20
|
-
type ToolCallDecision,
|
|
21
|
-
fromApprovalMap,
|
|
22
|
-
fromVerdictQueue,
|
|
23
|
-
} from "./Resolvers.js"
|
|
17
|
+
import { type ToolResult, isFailure, isOk, toToolCallOutput } from "./ToolResult.js"
|
|
18
|
+
import { type ApprovalMapEntry, type ApprovalDecision, fromMap, fromQueue } from "./Approval.js"
|
|
24
19
|
import { fromEffectSchema, make as makeTool, streaming } from "./Tool.js"
|
|
25
|
-
import {
|
|
26
|
-
import { ToolEvent, isApprovalRequested,
|
|
20
|
+
import { run } from "./Toolkit.js"
|
|
21
|
+
import { ToolEvent, isApprovalRequested, isProgress, isOutput } from "./ToolEvent.js"
|
|
27
22
|
|
|
28
23
|
// ---------------------------------------------------------------------------
|
|
29
24
|
// Three demo tools covering the matrix:
|
|
@@ -71,9 +66,9 @@ const deleteDatabase = makeTool({
|
|
|
71
66
|
|
|
72
67
|
const allTools = [webSearch, bulkEmail, deleteDatabase]
|
|
73
68
|
const SENSITIVE = new Set(["bulk_email", "delete_database"])
|
|
74
|
-
const isSensitive = (call: Items.
|
|
69
|
+
const isSensitive = (call: Items.ToolCall) => SENSITIVE.has(call.name)
|
|
75
70
|
|
|
76
|
-
const fc = (call_id: string, name: string, args: unknown): Items.
|
|
71
|
+
const fc = (call_id: string, name: string, args: unknown): Items.ToolCall => ({
|
|
77
72
|
type: "function_call",
|
|
78
73
|
call_id,
|
|
79
74
|
name,
|
|
@@ -95,20 +90,20 @@ const rejectedStream = (rejected: ReadonlyArray<ToolResult>) =>
|
|
|
95
90
|
Stream.fromIterable(rejected.map((result) => ToolEvent.Output({ result })))
|
|
96
91
|
|
|
97
92
|
const eventsFromApprovalMap = (approvals: ReadonlyMap<string, ApprovalMapEntry>) => {
|
|
98
|
-
const plan =
|
|
99
|
-
return Stream.merge(
|
|
93
|
+
const plan = fromMap(isSensitive, approvals)(calls)
|
|
94
|
+
return Stream.merge(run(allTools, plan.approved), rejectedStream(plan.rejected))
|
|
100
95
|
}
|
|
101
96
|
|
|
102
|
-
const eventsFromDecision = (decision:
|
|
97
|
+
const eventsFromDecision = (decision: ApprovalDecision): Stream.Stream<ToolEvent> =>
|
|
103
98
|
decision._tag === "Approved"
|
|
104
|
-
?
|
|
99
|
+
? run(allTools, [decision.call])
|
|
105
100
|
: Stream.succeed(ToolEvent.Output({ result: decision.result }))
|
|
106
101
|
|
|
107
102
|
// ---------------------------------------------------------------------------
|
|
108
|
-
//
|
|
103
|
+
// fromMap: HTTP-style scenarios
|
|
109
104
|
// ---------------------------------------------------------------------------
|
|
110
105
|
|
|
111
|
-
describe("
|
|
106
|
+
describe("fromMap + run", () => {
|
|
112
107
|
it("approval: all gated approved → tools execute, structured Values", async () => {
|
|
113
108
|
const approvals = new Map<string, ApprovalMapEntry>([
|
|
114
109
|
["c2", { decision: "approve" }],
|
|
@@ -116,13 +111,13 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
116
111
|
])
|
|
117
112
|
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
|
|
118
113
|
const by = byCallId(resultsFrom(collected))
|
|
119
|
-
expect(by.get("c1")).toMatchObject({ _tag: "
|
|
114
|
+
expect(by.get("c1")).toMatchObject({ _tag: "Ok", value: { count: 3 } })
|
|
120
115
|
expect(by.get("c2")).toMatchObject({
|
|
121
|
-
_tag: "
|
|
116
|
+
_tag: "Ok",
|
|
122
117
|
value: { status: "sent", delivered: 2 },
|
|
123
118
|
})
|
|
124
119
|
expect(by.get("c3")).toMatchObject({
|
|
125
|
-
_tag: "
|
|
120
|
+
_tag: "Ok",
|
|
126
121
|
value: { status: "dropped", name: "prod" },
|
|
127
122
|
})
|
|
128
123
|
|
|
@@ -138,7 +133,7 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
138
133
|
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
|
|
139
134
|
|
|
140
135
|
// bulk_email never ran.
|
|
141
|
-
expect(collected.filter(
|
|
136
|
+
expect(collected.filter(isProgress).filter((e) => e.tool === "bulk_email")).toHaveLength(0)
|
|
142
137
|
|
|
143
138
|
const by = byCallId(resultsFrom(collected))
|
|
144
139
|
expect(by.get("c2")).toMatchObject({
|
|
@@ -156,7 +151,7 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
156
151
|
it("cancellation: missing verdicts → Failure(cancelled)", async () => {
|
|
157
152
|
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(new Map())))
|
|
158
153
|
const by = byCallId(resultsFrom(collected))
|
|
159
|
-
expect(by.get("c1")).toMatchObject({ _tag: "
|
|
154
|
+
expect(by.get("c1")).toMatchObject({ _tag: "Ok", value: { count: 3 } })
|
|
160
155
|
expect(by.get("c2")).toMatchObject({ _tag: "Failure", kind: "cancelled" })
|
|
161
156
|
expect(by.get("c3")).toMatchObject({ _tag: "Failure", kind: "cancelled" })
|
|
162
157
|
})
|
|
@@ -168,8 +163,8 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
168
163
|
])
|
|
169
164
|
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
|
|
170
165
|
const by = byCallId(resultsFrom(collected))
|
|
171
|
-
expect(by.get("c1")).toMatchObject({ _tag: "
|
|
172
|
-
expect(by.get("c2")).toMatchObject({ _tag: "
|
|
166
|
+
expect(by.get("c1")).toMatchObject({ _tag: "Ok", value: { count: 3 } })
|
|
167
|
+
expect(by.get("c2")).toMatchObject({ _tag: "Ok", value: { status: "sent" } })
|
|
173
168
|
expect(by.get("c3")).toMatchObject({ _tag: "Failure", kind: "cancelled" })
|
|
174
169
|
})
|
|
175
170
|
})
|
|
@@ -178,7 +173,7 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
178
173
|
// Graceful degradation: hallucinated tool name doesn't kill the turn.
|
|
179
174
|
// ---------------------------------------------------------------------------
|
|
180
175
|
|
|
181
|
-
describe("
|
|
176
|
+
describe("run: graceful degradation", () => {
|
|
182
177
|
it("unknown tool name → Failure(unknown_tool); other calls still execute", async () => {
|
|
183
178
|
const callsWithBogus = [
|
|
184
179
|
fc("c1", "web_search", { query: "x" }),
|
|
@@ -188,26 +183,26 @@ describe("executeAll: graceful degradation", () => {
|
|
|
188
183
|
const collected = await Effect.runPromise(
|
|
189
184
|
Stream.runCollect(
|
|
190
185
|
(() => {
|
|
191
|
-
const plan =
|
|
186
|
+
const plan = fromMap(
|
|
192
187
|
isSensitive,
|
|
193
188
|
new Map([["c3", { decision: "approve" }]]),
|
|
194
189
|
)(callsWithBogus)
|
|
195
|
-
return Stream.merge(
|
|
190
|
+
return Stream.merge(run(allTools, plan.approved), rejectedStream(plan.rejected))
|
|
196
191
|
})(),
|
|
197
192
|
),
|
|
198
193
|
)
|
|
199
194
|
const by = byCallId(resultsFrom(collected))
|
|
200
|
-
expect(by.get("c1")).toMatchObject({ _tag: "
|
|
195
|
+
expect(by.get("c1")).toMatchObject({ _tag: "Ok" })
|
|
201
196
|
expect(by.get("c2")).toMatchObject({ _tag: "Failure", kind: "unknown_tool" })
|
|
202
|
-
expect(by.get("c3")).toMatchObject({ _tag: "
|
|
197
|
+
expect(by.get("c3")).toMatchObject({ _tag: "Ok", value: { status: "dropped" } })
|
|
203
198
|
})
|
|
204
199
|
})
|
|
205
200
|
|
|
206
201
|
// ---------------------------------------------------------------------------
|
|
207
|
-
//
|
|
202
|
+
// fromQueue: WebSocket-style scenarios
|
|
208
203
|
// ---------------------------------------------------------------------------
|
|
209
204
|
|
|
210
|
-
describe("
|
|
205
|
+
describe("fromQueue + run", () => {
|
|
211
206
|
it("queue-driven: approve + deny resolve correctly with ApprovalRequested events", async () => {
|
|
212
207
|
const collected = await Effect.runPromise(
|
|
213
208
|
Effect.gen(function* () {
|
|
@@ -223,17 +218,17 @@ describe("fromVerdictQueue + executeAll", () => {
|
|
|
223
218
|
reason: "too risky",
|
|
224
219
|
})
|
|
225
220
|
|
|
226
|
-
// Stream.unwrap supplies the Scope for
|
|
221
|
+
// Stream.unwrap supplies the Scope for fromQueue's router.
|
|
227
222
|
const events = Stream.unwrap(
|
|
228
223
|
Effect.gen(function* () {
|
|
229
|
-
const { approved, decisions,
|
|
224
|
+
const { approved, decisions, approvalRequests } = yield* fromQueue(
|
|
230
225
|
isSensitive,
|
|
231
226
|
verdicts,
|
|
232
227
|
)(calls)
|
|
233
228
|
return Stream.merge(
|
|
234
|
-
|
|
229
|
+
approvalRequests,
|
|
235
230
|
Stream.merge(
|
|
236
|
-
|
|
231
|
+
run(allTools, approved),
|
|
237
232
|
decisions.pipe(Stream.flatMap(eventsFromDecision)),
|
|
238
233
|
),
|
|
239
234
|
)
|
|
@@ -246,7 +241,7 @@ describe("fromVerdictQueue + executeAll", () => {
|
|
|
246
241
|
expect(collected.filter(isApprovalRequested)).toHaveLength(2)
|
|
247
242
|
|
|
248
243
|
const by = byCallId(resultsFrom(collected))
|
|
249
|
-
expect(by.get("c2")).toMatchObject({ _tag: "
|
|
244
|
+
expect(by.get("c2")).toMatchObject({ _tag: "Ok", value: { status: "sent" } })
|
|
250
245
|
expect(by.get("c3")).toMatchObject({
|
|
251
246
|
_tag: "Failure",
|
|
252
247
|
kind: "denied",
|
|
@@ -262,7 +257,7 @@ describe("fromVerdictQueue + executeAll", () => {
|
|
|
262
257
|
describe("findUnansweredCalls / cancelAllPending / isReconciled", () => {
|
|
263
258
|
const orphan = fc("c99", "delete_database", { name: "prod" })
|
|
264
259
|
const answered = fc("c98", "web_search", { query: "x" })
|
|
265
|
-
const answeredOutput = Items.
|
|
260
|
+
const answeredOutput = Items.toolCallOutput("c98", JSON.stringify({ count: 0 }))
|
|
266
261
|
|
|
267
262
|
it("findUnansweredCalls returns only orphans", () => {
|
|
268
263
|
const history = [Items.userText("hi"), answered, orphan, answeredOutput]
|
|
@@ -274,7 +269,7 @@ describe("findUnansweredCalls / cancelAllPending / isReconciled", () => {
|
|
|
274
269
|
it("isReconciled is false when orphans exist, true otherwise", () => {
|
|
275
270
|
const stale = [Items.userText("hi"), orphan]
|
|
276
271
|
expect(isReconciled(stale)).toBe(false)
|
|
277
|
-
const reconciled = [...stale, ...cancelAllPending(stale).map(
|
|
272
|
+
const reconciled = [...stale, ...cancelAllPending(stale).map(toToolCallOutput)]
|
|
278
273
|
expect(isReconciled(reconciled)).toBe(true)
|
|
279
274
|
})
|
|
280
275
|
|
|
@@ -292,14 +287,10 @@ describe("findUnansweredCalls / cancelAllPending / isReconciled", () => {
|
|
|
292
287
|
})
|
|
293
288
|
})
|
|
294
289
|
|
|
295
|
-
it("follow-up: map closures to
|
|
290
|
+
it("follow-up: map closures to ToolCallOutput before appending new user message", () => {
|
|
296
291
|
const stale = [Items.userText("first request"), orphan]
|
|
297
292
|
const closures = cancelAllPending(stale, "user redirected")
|
|
298
|
-
const reconciled = [
|
|
299
|
-
...stale,
|
|
300
|
-
...closures.map(toFunctionCallOutput),
|
|
301
|
-
Items.userText("never mind"),
|
|
302
|
-
]
|
|
293
|
+
const reconciled = [...stale, ...closures.map(toToolCallOutput), Items.userText("never mind")]
|
|
303
294
|
expect(findUnansweredCalls(reconciled)).toHaveLength(0)
|
|
304
295
|
})
|
|
305
296
|
})
|
|
@@ -308,15 +299,15 @@ describe("findUnansweredCalls / cancelAllPending / isReconciled", () => {
|
|
|
308
299
|
// Wire conversion
|
|
309
300
|
// ---------------------------------------------------------------------------
|
|
310
301
|
|
|
311
|
-
describe("
|
|
302
|
+
describe("toToolCallOutput", () => {
|
|
312
303
|
it("round-trips a Value result", () => {
|
|
313
304
|
const r: ToolResult = {
|
|
314
|
-
_tag: "
|
|
305
|
+
_tag: "Ok",
|
|
315
306
|
call_id: "c1",
|
|
316
307
|
tool: "web_search",
|
|
317
308
|
value: { count: 3 },
|
|
318
309
|
}
|
|
319
|
-
const out =
|
|
310
|
+
const out = toToolCallOutput(r)
|
|
320
311
|
expect(out.call_id).toBe("c1")
|
|
321
312
|
expect(JSON.parse(out.output)).toEqual({ count: 3 })
|
|
322
313
|
})
|
|
@@ -329,7 +320,7 @@ describe("toFunctionCallOutput", () => {
|
|
|
329
320
|
kind: "denied",
|
|
330
321
|
reason: "spam concern",
|
|
331
322
|
}
|
|
332
|
-
const out =
|
|
323
|
+
const out = toToolCallOutput(r)
|
|
333
324
|
expect(JSON.parse(out.output)).toEqual({ kind: "denied", reason: "spam concern" })
|
|
334
325
|
})
|
|
335
326
|
|
|
@@ -340,19 +331,19 @@ describe("toFunctionCallOutput", () => {
|
|
|
340
331
|
tool: "delete_database",
|
|
341
332
|
kind: "cancelled",
|
|
342
333
|
}
|
|
343
|
-
const out =
|
|
334
|
+
const out = toToolCallOutput(r)
|
|
344
335
|
expect(JSON.parse(out.output)).toEqual({ kind: "cancelled" })
|
|
345
336
|
})
|
|
346
337
|
})
|
|
347
338
|
|
|
348
339
|
// ---------------------------------------------------------------------------
|
|
349
|
-
//
|
|
340
|
+
// run
|
|
350
341
|
// ---------------------------------------------------------------------------
|
|
351
342
|
|
|
352
|
-
describe("
|
|
343
|
+
describe("run", () => {
|
|
353
344
|
it("runs all calls passed to it", async () => {
|
|
354
|
-
const collected = await Effect.runPromise(Stream.runCollect(
|
|
345
|
+
const collected = await Effect.runPromise(Stream.runCollect(run(allTools, calls)))
|
|
355
346
|
expect(collected.filter(isOutput)).toHaveLength(3)
|
|
356
|
-
expect(collected.filter(isOutput).every((e) =>
|
|
347
|
+
expect(collected.filter(isOutput).every((e) => isOk(e.result))).toBe(true)
|
|
357
348
|
})
|
|
358
349
|
})
|
|
@@ -3,31 +3,31 @@
|
|
|
3
3
|
*
|
|
4
4
|
* These helpers only decide which calls are approved and which synthetic
|
|
5
5
|
* results must be returned to the model. Tool execution stays explicit at
|
|
6
|
-
* the recipe boundary via `Toolkit.
|
|
6
|
+
* the recipe boundary via `Toolkit.run`.
|
|
7
7
|
*/
|
|
8
8
|
import { Data, Deferred, Effect, Queue, Scope, Stream } from "effect"
|
|
9
|
-
import type {
|
|
10
|
-
import { type ToolResult, cancelled, denied } from "./
|
|
9
|
+
import type { ToolCall } from "../domain/Items.js"
|
|
10
|
+
import { type ToolResult, cancelled, denied } from "./ToolResult.js"
|
|
11
11
|
import { ToolEvent } from "./ToolEvent.js"
|
|
12
12
|
|
|
13
13
|
export type ToolCallPlan = {
|
|
14
|
-
readonly approved: ReadonlyArray<
|
|
14
|
+
readonly approved: ReadonlyArray<ToolCall>
|
|
15
15
|
readonly rejected: ReadonlyArray<ToolResult>
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export type
|
|
19
|
-
Approved: { readonly call:
|
|
18
|
+
export type ApprovalDecision = Data.TaggedEnum<{
|
|
19
|
+
Approved: { readonly call: ToolCall }
|
|
20
20
|
Rejected: { readonly result: ToolResult }
|
|
21
21
|
}>
|
|
22
22
|
|
|
23
|
-
export const
|
|
23
|
+
export const ApprovalDecision = Data.taggedEnum<ApprovalDecision>()
|
|
24
24
|
|
|
25
|
-
export const approve = (call:
|
|
25
|
+
export const approve = (call: ToolCall): ApprovalDecision => ApprovalDecision.Approved({ call })
|
|
26
26
|
|
|
27
|
-
export const reject = (result: ToolResult):
|
|
28
|
-
|
|
27
|
+
export const reject = (result: ToolResult): ApprovalDecision =>
|
|
28
|
+
ApprovalDecision.Rejected({ result })
|
|
29
29
|
|
|
30
|
-
export const
|
|
30
|
+
export const splitApprovalDecisions = (decisions: ReadonlyArray<ApprovalDecision>): ToolCallPlan =>
|
|
31
31
|
decisions.reduce<ToolCallPlan>(
|
|
32
32
|
(acc, decision) =>
|
|
33
33
|
decision._tag === "Approved"
|
|
@@ -36,7 +36,7 @@ export const splitToolCallDecisions = (decisions: ReadonlyArray<ToolCallDecision
|
|
|
36
36
|
{ approved: [], rejected: [] },
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
export const approvalRequested = (call:
|
|
39
|
+
export const approvalRequested = (call: ToolCall): ToolEvent =>
|
|
40
40
|
ToolEvent.ApprovalRequested({
|
|
41
41
|
call_id: call.call_id,
|
|
42
42
|
tool: call.name,
|
|
@@ -56,17 +56,17 @@ export type Verdict = {
|
|
|
56
56
|
/**
|
|
57
57
|
* Queue-backed approval planner. Safe calls are returned immediately in
|
|
58
58
|
* `approved`; gated calls emit `ApprovalRequested` events and later produce
|
|
59
|
-
* one `
|
|
59
|
+
* one `ApprovalDecision` when their matching verdict arrives.
|
|
60
60
|
*/
|
|
61
|
-
export const
|
|
62
|
-
(predicate: (call:
|
|
61
|
+
export const fromQueue =
|
|
62
|
+
(predicate: (call: ToolCall) => boolean, verdicts: Queue.Dequeue<Verdict>) =>
|
|
63
63
|
(
|
|
64
|
-
calls: ReadonlyArray<
|
|
64
|
+
calls: ReadonlyArray<ToolCall>,
|
|
65
65
|
): Effect.Effect<
|
|
66
66
|
{
|
|
67
|
-
readonly approved: ReadonlyArray<
|
|
68
|
-
readonly decisions: Stream.Stream<
|
|
69
|
-
readonly
|
|
67
|
+
readonly approved: ReadonlyArray<ToolCall>
|
|
68
|
+
readonly decisions: Stream.Stream<ApprovalDecision>
|
|
69
|
+
readonly approvalRequests: Stream.Stream<ToolEvent>
|
|
70
70
|
},
|
|
71
71
|
never,
|
|
72
72
|
Scope.Scope
|
|
@@ -109,9 +109,9 @@ export const fromVerdictQueue =
|
|
|
109
109
|
),
|
|
110
110
|
)
|
|
111
111
|
|
|
112
|
-
const
|
|
112
|
+
const approvalRequests = Stream.fromIterable<ToolEvent>(gated.map(approvalRequested))
|
|
113
113
|
|
|
114
|
-
return { approved, decisions,
|
|
114
|
+
return { approved, decisions, approvalRequests }
|
|
115
115
|
})
|
|
116
116
|
|
|
117
117
|
// ---------------------------------------------------------------------------
|
|
@@ -123,10 +123,10 @@ export type ApprovalMapEntry =
|
|
|
123
123
|
| { readonly decision: "approve" }
|
|
124
124
|
| { readonly decision: "deny"; readonly reason?: string }
|
|
125
125
|
|
|
126
|
-
export const
|
|
127
|
-
(predicate: (call:
|
|
128
|
-
(calls: ReadonlyArray<
|
|
129
|
-
|
|
126
|
+
export const fromMap =
|
|
127
|
+
(predicate: (call: ToolCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) =>
|
|
128
|
+
(calls: ReadonlyArray<ToolCall>): ToolCallPlan =>
|
|
129
|
+
splitApprovalDecisions(
|
|
130
130
|
calls.map((call) => {
|
|
131
131
|
if (!predicate(call)) return approve(call)
|
|
132
132
|
const v = approvals.get(call.call_id)
|
package/src/tool/HistoryCheck.ts
CHANGED
|
@@ -10,37 +10,34 @@
|
|
|
10
10
|
* Recipe author calls these at known transition points (right before the
|
|
11
11
|
* next provider request). Not invoked from inside the loop.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
type Item,
|
|
16
|
-
isFunctionCall,
|
|
17
|
-
isFunctionCallOutput,
|
|
18
|
-
} from "../domain/Items.js"
|
|
19
|
-
import { type ToolResult, cancelled } from "./Outcome.js"
|
|
13
|
+
import { type ToolCall, type HistoryItem, isToolCall, isToolCallOutput } from "../domain/Items.js"
|
|
14
|
+
import { type ToolResult, cancelled } from "./ToolResult.js"
|
|
20
15
|
|
|
21
16
|
/**
|
|
22
17
|
* Return every `function_call` in `history` that does not have a matching
|
|
23
18
|
* `function_call_output` later in `history` (correlated by `call_id`).
|
|
24
19
|
* Empty result = history is provider-submittable from this invariant.
|
|
25
20
|
*/
|
|
26
|
-
export const findUnansweredCalls = (
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
export const findUnansweredCalls = (
|
|
22
|
+
history: ReadonlyArray<HistoryItem>,
|
|
23
|
+
): ReadonlyArray<ToolCall> => {
|
|
24
|
+
const answered = new Set(history.filter(isToolCallOutput).map((o) => o.call_id))
|
|
25
|
+
return history.filter(isToolCall).filter((c) => !answered.has(c.call_id))
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
/** Cheap predicate: is this history submittable to a provider? */
|
|
32
|
-
export const isReconciled = (history: ReadonlyArray<
|
|
29
|
+
export const isReconciled = (history: ReadonlyArray<HistoryItem>): boolean =>
|
|
33
30
|
findUnansweredCalls(history).length === 0
|
|
34
31
|
|
|
35
32
|
/**
|
|
36
33
|
* Synthesize cancellation results for every unanswered call. Caller maps
|
|
37
|
-
* via `
|
|
34
|
+
* via `toToolCallOutput` and appends to history before submitting.
|
|
38
35
|
*
|
|
39
36
|
* Use when: a new user message arrives mid-approval; an approval timer
|
|
40
37
|
* fires; a persisted checkpoint contains orphans (crash recovery); a
|
|
41
38
|
* stateless HTTP server reconstructed history from a stale checkpoint.
|
|
42
39
|
*/
|
|
43
40
|
export const cancelAllPending = (
|
|
44
|
-
history: ReadonlyArray<
|
|
41
|
+
history: ReadonlyArray<HistoryItem>,
|
|
45
42
|
reason?: string,
|
|
46
43
|
): ReadonlyArray<ToolResult> => findUnansweredCalls(history).map((call) => cancelled(call, reason))
|
package/src/tool/Tool.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec"
|
|
2
2
|
import { Effect, Schema, Stream } from "effect"
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
3
|
+
import type { ToolCall, ToolCallOutput } from "../domain/Items.js"
|
|
4
|
+
import { toolCallOutput } from "../domain/Items.js"
|
|
5
5
|
|
|
6
6
|
export class ToolError extends Schema.TaggedErrorClass<ToolError>("@betalyra/effect-uai/ToolError")(
|
|
7
7
|
"ToolError",
|
|
@@ -73,7 +73,7 @@ export type Tool<Name extends string, Input, Output, R = never> = {
|
|
|
73
73
|
/**
|
|
74
74
|
* Provider-agnostic tool descriptor. Each provider maps `inputSchema`
|
|
75
75
|
* to its own wire field (OpenAI → `parameters`, Anthropic →
|
|
76
|
-
* `input_schema`). Built from a `Tool` by `
|
|
76
|
+
* `input_schema`). Built from a `Tool` by `Tool.toDescriptors`.
|
|
77
77
|
*/
|
|
78
78
|
export type ToolDescriptor = {
|
|
79
79
|
readonly name: string
|
|
@@ -90,7 +90,7 @@ export const make = <Name extends string, Input, Output, R = never>(
|
|
|
90
90
|
// Streaming tools
|
|
91
91
|
//
|
|
92
92
|
// `run` returns a `Stream<Event>` instead of an `Effect<Output>`. Events
|
|
93
|
-
// flow through to the consumer as `ToolEvent.
|
|
93
|
+
// flow through to the consumer as `ToolEvent.Progress`s in real time;
|
|
94
94
|
// at end-of-stream `finalize(events)` reduces them to the model-facing
|
|
95
95
|
// `Output`. Sub-agents, slow downloads with progress, recipe streamers.
|
|
96
96
|
// ---------------------------------------------------------------------------
|
|
@@ -111,19 +111,17 @@ export const streaming = <Name extends string, Input, Event, Output, R = never>(
|
|
|
111
111
|
|
|
112
112
|
export type AnyStreamingTool<R = any> = StreamingTool<string, any, any, any, R>
|
|
113
113
|
export type AnyPlainTool<R = any> = Tool<string, any, any, R>
|
|
114
|
-
export type
|
|
114
|
+
export type AnyTool<R = any> = AnyStreamingTool<R> | AnyPlainTool<R>
|
|
115
115
|
|
|
116
|
-
export const isStreamingTool = <R>(t:
|
|
116
|
+
export const isStreamingTool = <R>(t: AnyTool<R>): t is AnyStreamingTool<R> =>
|
|
117
117
|
"_kind" in t && t._kind === "streaming"
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
120
|
* Render any-kind tools (mixed plain and streaming) to provider-agnostic
|
|
121
|
-
* descriptors.
|
|
122
|
-
*
|
|
121
|
+
* descriptors. Accepts the union type so a single list can carry both
|
|
122
|
+
* plain and streaming tools.
|
|
123
123
|
*/
|
|
124
|
-
export const toDescriptors = <R>(
|
|
125
|
-
tools: ReadonlyArray<AnyKindTool<R>>,
|
|
126
|
-
): ReadonlyArray<ToolDescriptor> =>
|
|
124
|
+
export const toDescriptors = <R>(tools: ReadonlyArray<AnyTool<R>>): ReadonlyArray<ToolDescriptor> =>
|
|
127
125
|
tools.map((tool) => {
|
|
128
126
|
const inputSchema = tool.inputSchema["~standard"].jsonSchema.input({
|
|
129
127
|
target: "draft-2020-12",
|
|
@@ -133,7 +131,7 @@ export const toDescriptors = <R>(
|
|
|
133
131
|
: { name: tool.name, description: tool.description, inputSchema }
|
|
134
132
|
})
|
|
135
133
|
|
|
136
|
-
const toToolError = (call:
|
|
134
|
+
const toToolError = (call: ToolCall, toolName: string, message: string) => (cause: unknown) =>
|
|
137
135
|
new ToolError({ call_id: call.call_id, tool: toolName, message, cause })
|
|
138
136
|
|
|
139
137
|
/**
|
|
@@ -143,8 +141,8 @@ const toToolError = (call: FunctionCall, toolName: string, message: string) => (
|
|
|
143
141
|
*/
|
|
144
142
|
export const execute = <Name extends string, Input, Output, R>(
|
|
145
143
|
tool: Tool<Name, Input, Output, R>,
|
|
146
|
-
call:
|
|
147
|
-
): Effect.Effect<
|
|
144
|
+
call: ToolCall,
|
|
145
|
+
): Effect.Effect<ToolCallOutput, ToolError, R> =>
|
|
148
146
|
Effect.gen(function* () {
|
|
149
147
|
const parsed = yield* Effect.try({
|
|
150
148
|
try: () => JSON.parse(call.arguments) as unknown,
|
|
@@ -166,5 +164,5 @@ export const execute = <Name extends string, Input, Output, R>(
|
|
|
166
164
|
const output = yield* tool
|
|
167
165
|
.run(result.value)
|
|
168
166
|
.pipe(Effect.mapError(toToolError(call, tool.name, "Tool execution failed")))
|
|
169
|
-
return
|
|
167
|
+
return toolCallOutput(call.call_id, JSON.stringify(output))
|
|
170
168
|
})
|
package/src/tool/ToolEvent.ts
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* The event type emitted while handling tool calls.
|
|
3
3
|
*
|
|
4
4
|
* - ApprovalRequested : gated calls waiting for approval
|
|
5
|
-
* -
|
|
5
|
+
* - Progress : 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 `
|
|
9
|
-
* `
|
|
8
|
+
* Recipes thread `ToolEvent.Output.result` through `continueWithResults` and
|
|
9
|
+
* apply `toToolCallOutput` when appending to history.
|
|
10
10
|
*/
|
|
11
11
|
import { Data } from "effect"
|
|
12
|
-
import type { ToolResult } from "./
|
|
12
|
+
import type { ToolResult } from "./ToolResult.js"
|
|
13
13
|
|
|
14
14
|
export type ToolEvent = Data.TaggedEnum<{
|
|
15
15
|
ApprovalRequested: {
|
|
@@ -17,7 +17,7 @@ export type ToolEvent = Data.TaggedEnum<{
|
|
|
17
17
|
readonly tool: string
|
|
18
18
|
readonly arguments: string
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
Progress: {
|
|
21
21
|
readonly call_id: string
|
|
22
22
|
readonly tool: string
|
|
23
23
|
readonly data: unknown
|
|
@@ -31,11 +31,11 @@ export type ToolEvent = Data.TaggedEnum<{
|
|
|
31
31
|
* Namespace of constructors, type guards, and matchers for `ToolEvent`,
|
|
32
32
|
* provided by `Data.taggedEnum`. Use `ToolEvent.Output({ result })` to build
|
|
33
33
|
* an event, `ToolEvent.$is("Output")` for type narrowing,
|
|
34
|
-
* `ToolEvent.$match({ ApprovalRequested,
|
|
34
|
+
* `ToolEvent.$match({ ApprovalRequested, Progress, Output })` for
|
|
35
35
|
* exhaustive pattern matching.
|
|
36
36
|
*/
|
|
37
37
|
export const ToolEvent = Data.taggedEnum<ToolEvent>()
|
|
38
38
|
|
|
39
39
|
export const isApprovalRequested = ToolEvent.$is("ApprovalRequested")
|
|
40
|
-
export const
|
|
40
|
+
export const isProgress = ToolEvent.$is("Progress")
|
|
41
41
|
export const isOutput = ToolEvent.$is("Output")
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Post-execution and synthetic tool results.
|
|
3
3
|
*
|
|
4
|
-
* - Executed tools emit ToolResult.
|
|
4
|
+
* - Executed tools emit ToolResult.Ok.
|
|
5
5
|
* - Approval/cancellation policy emits synthetic ToolResult.Failure.
|
|
6
6
|
*
|
|
7
|
-
* Wire conversion stays at the recipe boundary via `
|
|
7
|
+
* Wire conversion stays at the recipe boundary via `toToolCallOutput`
|
|
8
8
|
* so recipes can inspect, redact, or audit values before serialization.
|
|
9
9
|
*
|
|
10
10
|
* `output` and `reason` are `string`, not `unknown`: the wire wants strings,
|
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
* fn). Recipes that want structured detail JSON.stringify themselves.
|
|
13
13
|
*/
|
|
14
14
|
import { Data } from "effect"
|
|
15
|
-
import type {
|
|
16
|
-
import {
|
|
15
|
+
import type { ToolCall, ToolCallOutput } from "../domain/Items.js"
|
|
16
|
+
import { toolCallOutput } from "../domain/Items.js"
|
|
17
17
|
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
// ToolResult
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
|
|
22
22
|
export type ToolResult = Data.TaggedEnum<{
|
|
23
|
-
|
|
23
|
+
Ok: {
|
|
24
24
|
readonly call_id: string
|
|
25
25
|
readonly tool: string
|
|
26
26
|
readonly value: unknown
|
|
@@ -35,22 +35,22 @@ export type ToolResult = Data.TaggedEnum<{
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Namespace of constructors, type guards, and matchers for `ToolResult`,
|
|
38
|
-
* provided by `Data.taggedEnum`. Use `ToolResult.$is("
|
|
39
|
-
* narrowing and `ToolResult.$match({
|
|
38
|
+
* provided by `Data.taggedEnum`. Use `ToolResult.$is("Ok")` for type
|
|
39
|
+
* narrowing and `ToolResult.$match({ Ok, Failure })` for exhaustive
|
|
40
40
|
* pattern matching. Synthetic-result helpers (`denied`, `cancelled`,
|
|
41
|
-
* `executionError`, `
|
|
41
|
+
* `executionError`, `failed`) below are kinder constructors than the
|
|
42
42
|
* raw `ToolResult.Failure(...)`.
|
|
43
43
|
*/
|
|
44
44
|
export const ToolResult = Data.taggedEnum<ToolResult>()
|
|
45
45
|
|
|
46
|
-
export const
|
|
46
|
+
export const isOk = ToolResult.$is("Ok")
|
|
47
47
|
export const isFailure = ToolResult.$is("Failure")
|
|
48
48
|
|
|
49
49
|
// Synthesizers. `denied` and `cancelled` are operationally distinct;
|
|
50
|
-
// anything else is just a recipe-chosen `kind` via `
|
|
50
|
+
// anything else is just a recipe-chosen `kind` via `failed`.
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
52
52
|
|
|
53
|
-
export const
|
|
53
|
+
export const failed = (call: ToolCall, kind: string, reason?: string): ToolResult =>
|
|
54
54
|
ToolResult.Failure({
|
|
55
55
|
call_id: call.call_id,
|
|
56
56
|
tool: call.name,
|
|
@@ -59,26 +59,26 @@ export const rejected = (call: FunctionCall, kind: string, reason?: string): Too
|
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
/** Explicit user/policy rejection. */
|
|
62
|
-
export const denied = (call:
|
|
63
|
-
|
|
62
|
+
export const denied = (call: ToolCall, reason?: string): ToolResult =>
|
|
63
|
+
failed(call, "denied", reason)
|
|
64
64
|
|
|
65
65
|
/** Implicit non-answer (follow-up, inactivity, abort). */
|
|
66
|
-
export const cancelled = (call:
|
|
67
|
-
|
|
66
|
+
export const cancelled = (call: ToolCall, reason?: string): ToolResult =>
|
|
67
|
+
failed(call, "cancelled", reason)
|
|
68
68
|
|
|
69
69
|
/** Tool's own execution failed (parse error, schema, runtime crash). */
|
|
70
|
-
export const executionError = (call:
|
|
71
|
-
|
|
70
|
+
export const executionError = (call: ToolCall, reason: string): ToolResult =>
|
|
71
|
+
failed(call, "execution_error", reason)
|
|
72
72
|
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
74
|
// Wire conversion - the one place structured → string happens.
|
|
75
75
|
// ---------------------------------------------------------------------------
|
|
76
76
|
|
|
77
|
-
export const
|
|
77
|
+
export const toToolCallOutput = (r: ToolResult): ToolCallOutput =>
|
|
78
78
|
ToolResult.$match(r, {
|
|
79
|
-
|
|
79
|
+
Ok: (v) => toolCallOutput(v.call_id, JSON.stringify(v.value)),
|
|
80
80
|
Failure: (f) =>
|
|
81
|
-
|
|
81
|
+
toolCallOutput(
|
|
82
82
|
f.call_id,
|
|
83
83
|
JSON.stringify(
|
|
84
84
|
f.reason !== undefined ? { kind: f.kind, reason: f.reason } : { kind: f.kind },
|