@effect-uai/core 0.5.2 → 0.6.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.
Files changed (123) hide show
  1. package/README.md +1 -1
  2. package/dist/{Items-BH8xUkoR.d.mts → Items-pXR5BAgW.d.mts} +12 -12
  3. package/dist/Items-pXR5BAgW.d.mts.map +1 -0
  4. package/dist/SandboxError-BvKypkJm.d.mts +158 -0
  5. package/dist/SandboxError-BvKypkJm.d.mts.map +1 -0
  6. package/dist/{Tool-87ViKCCO.d.mts → Tool-DP-AwKOf.d.mts} +11 -11
  7. package/dist/{Tool-87ViKCCO.d.mts.map → Tool-DP-AwKOf.d.mts.map} +1 -1
  8. package/dist/domain/Items.d.mts +2 -2
  9. package/dist/domain/Items.mjs +15 -15
  10. package/dist/domain/Items.mjs.map +1 -1
  11. package/dist/domain/Turn.d.mts +388 -2
  12. package/dist/domain/Turn.d.mts.map +1 -0
  13. package/dist/domain/Turn.mjs +13 -13
  14. package/dist/domain/Turn.mjs.map +1 -1
  15. package/dist/index.d.mts +11 -6
  16. package/dist/index.mjs +8 -3
  17. package/dist/language-model/LanguageModel.d.mts +5 -5
  18. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  19. package/dist/language-model/LanguageModel.mjs.map +1 -1
  20. package/dist/loop/Loop.d.mts +37 -90
  21. package/dist/loop/Loop.d.mts.map +1 -1
  22. package/dist/loop/Loop.mjs +39 -93
  23. package/dist/loop/Loop.mjs.map +1 -1
  24. package/dist/loop/Loop.test.mjs +46 -54
  25. package/dist/loop/Loop.test.mjs.map +1 -1
  26. package/dist/sandbox/Memory.d.mts +53 -0
  27. package/dist/sandbox/Memory.d.mts.map +1 -0
  28. package/dist/sandbox/Memory.mjs +68 -0
  29. package/dist/sandbox/Memory.mjs.map +1 -0
  30. package/dist/sandbox/Sandbox.d.mts +717 -0
  31. package/dist/sandbox/Sandbox.d.mts.map +1 -0
  32. package/dist/sandbox/Sandbox.mjs +192 -0
  33. package/dist/sandbox/Sandbox.mjs.map +1 -0
  34. package/dist/sandbox/SandboxError.d.mts +2 -0
  35. package/dist/sandbox/SandboxError.mjs +89 -0
  36. package/dist/sandbox/SandboxError.mjs.map +1 -0
  37. package/dist/sandbox/SandboxImage.d.mts +28 -0
  38. package/dist/sandbox/SandboxImage.d.mts.map +1 -0
  39. package/dist/sandbox/SandboxImage.mjs +32 -0
  40. package/dist/sandbox/SandboxImage.mjs.map +1 -0
  41. package/dist/sandbox/SandboxNetwork.d.mts +34 -0
  42. package/dist/sandbox/SandboxNetwork.d.mts.map +1 -0
  43. package/dist/sandbox/SandboxNetwork.mjs +36 -0
  44. package/dist/sandbox/SandboxNetwork.mjs.map +1 -0
  45. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +98 -2
  46. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
  47. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +32 -2
  48. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
  49. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +56 -2
  50. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
  51. package/dist/testing/MockProvider.d.mts +3 -3
  52. package/dist/testing/MockProvider.d.mts.map +1 -1
  53. package/dist/testing/MockProvider.mjs.map +1 -1
  54. package/dist/testing/MockSandbox.d.mts +604 -0
  55. package/dist/testing/MockSandbox.d.mts.map +1 -0
  56. package/dist/testing/MockSandbox.mjs +160 -0
  57. package/dist/testing/MockSandbox.mjs.map +1 -0
  58. package/dist/testing/MockSpeechSynthesizer.d.mts +26 -11
  59. package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
  60. package/dist/testing/MockSpeechSynthesizer.mjs +75 -51
  61. package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
  62. package/dist/tool/{Resolvers.d.mts → Approval.d.mts} +27 -27
  63. package/dist/tool/Approval.d.mts.map +1 -0
  64. package/dist/tool/{Resolvers.mjs → Approval.mjs} +18 -18
  65. package/dist/tool/Approval.mjs.map +1 -0
  66. package/dist/tool/{Resolvers.test.mjs → Approval.test.mjs} +40 -40
  67. package/dist/tool/Approval.test.mjs.map +1 -0
  68. package/dist/tool/HistoryCheck.d.mts +6 -6
  69. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  70. package/dist/tool/HistoryCheck.mjs +5 -5
  71. package/dist/tool/HistoryCheck.mjs.map +1 -1
  72. package/dist/tool/Tool.d.mts +2 -2
  73. package/dist/tool/Tool.mjs +4 -4
  74. package/dist/tool/Tool.mjs.map +1 -1
  75. package/dist/tool/ToolEvent.d.mts +19 -19
  76. package/dist/tool/ToolEvent.d.mts.map +1 -1
  77. package/dist/tool/ToolEvent.mjs +8 -8
  78. package/dist/tool/ToolEvent.mjs.map +1 -1
  79. package/dist/tool/{Outcome.d.mts → ToolResult.d.mts} +29 -29
  80. package/dist/tool/ToolResult.d.mts.map +1 -0
  81. package/dist/tool/ToolResult.mjs +61 -0
  82. package/dist/tool/ToolResult.mjs.map +1 -0
  83. package/dist/tool/Toolkit.d.mts +58 -26
  84. package/dist/tool/Toolkit.d.mts.map +1 -1
  85. package/dist/tool/Toolkit.mjs +62 -34
  86. package/dist/tool/Toolkit.mjs.map +1 -1
  87. package/dist/tool/Toolkit.test.mjs +13 -13
  88. package/dist/tool/Toolkit.test.mjs.map +1 -1
  89. package/package.json +31 -7
  90. package/src/domain/Items.ts +9 -9
  91. package/src/domain/Turn.ts +16 -16
  92. package/src/index.ts +7 -2
  93. package/src/language-model/LanguageModel.ts +3 -3
  94. package/src/loop/Loop.test.ts +93 -91
  95. package/src/loop/Loop.ts +72 -170
  96. package/src/sandbox/Memory.ts +128 -0
  97. package/src/sandbox/Sandbox.ts +639 -0
  98. package/src/sandbox/SandboxError.ts +140 -0
  99. package/src/sandbox/SandboxImage.ts +26 -0
  100. package/src/sandbox/SandboxNetwork.ts +34 -0
  101. package/src/speech-synthesizer/SpeechSynthesizer.test.ts +89 -0
  102. package/src/speech-synthesizer/SpeechSynthesizer.ts +124 -0
  103. package/src/testing/MockProvider.ts +21 -20
  104. package/src/testing/MockSandbox.ts +342 -0
  105. package/src/testing/MockSpeechSynthesizer.ts +137 -77
  106. package/src/tool/{Resolvers.test.ts → Approval.test.ts} +45 -54
  107. package/src/tool/{Resolvers.ts → Approval.ts} +25 -25
  108. package/src/tool/HistoryCheck.ts +10 -13
  109. package/src/tool/Tool.ts +13 -15
  110. package/src/tool/ToolEvent.ts +7 -7
  111. package/src/tool/{Outcome.ts → ToolResult.ts} +20 -20
  112. package/src/tool/Toolkit.test.ts +13 -13
  113. package/src/tool/Toolkit.ts +97 -73
  114. package/dist/Items-BH8xUkoR.d.mts.map +0 -1
  115. package/dist/Turn-0CwCAyVe.d.mts +0 -388
  116. package/dist/Turn-0CwCAyVe.d.mts.map +0 -1
  117. package/dist/tool/Outcome.d.mts.map +0 -1
  118. package/dist/tool/Outcome.mjs +0 -61
  119. package/dist/tool/Outcome.mjs.map +0 -1
  120. package/dist/tool/Resolvers.d.mts.map +0 -1
  121. package/dist/tool/Resolvers.mjs.map +0 -1
  122. package/dist/tool/Resolvers.test.mjs.map +0 -1
  123. /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 `executeAll`, with the four wire-shaped scenarios:
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, isValue, toFunctionCallOutput } from "./Outcome.js"
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 { executeAll } from "./Toolkit.js"
26
- import { ToolEvent, isApprovalRequested, isIntermediate, isOutput } from "./ToolEvent.js"
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.FunctionCall) => SENSITIVE.has(call.name)
69
+ const isSensitive = (call: Items.ToolCall) => SENSITIVE.has(call.name)
75
70
 
76
- const fc = (call_id: string, name: string, args: unknown): Items.FunctionCall => ({
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 = fromApprovalMap(isSensitive, approvals)(calls)
99
- return Stream.merge(executeAll(allTools, plan.approved), rejectedStream(plan.rejected))
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: ToolCallDecision): Stream.Stream<ToolEvent> =>
97
+ const eventsFromDecision = (decision: ApprovalDecision): Stream.Stream<ToolEvent> =>
103
98
  decision._tag === "Approved"
104
- ? executeAll(allTools, [decision.call])
99
+ ? run(allTools, [decision.call])
105
100
  : Stream.succeed(ToolEvent.Output({ result: decision.result }))
106
101
 
107
102
  // ---------------------------------------------------------------------------
108
- // fromApprovalMap: HTTP-style scenarios
103
+ // fromMap: HTTP-style scenarios
109
104
  // ---------------------------------------------------------------------------
110
105
 
111
- describe("fromApprovalMap + executeAll", () => {
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: "Value", value: { count: 3 } })
114
+ expect(by.get("c1")).toMatchObject({ _tag: "Ok", value: { count: 3 } })
120
115
  expect(by.get("c2")).toMatchObject({
121
- _tag: "Value",
116
+ _tag: "Ok",
122
117
  value: { status: "sent", delivered: 2 },
123
118
  })
124
119
  expect(by.get("c3")).toMatchObject({
125
- _tag: "Value",
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(isIntermediate).filter((e) => e.tool === "bulk_email")).toHaveLength(0)
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: "Value", value: { count: 3 } })
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: "Value", value: { count: 3 } })
172
- expect(by.get("c2")).toMatchObject({ _tag: "Value", value: { status: "sent" } })
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("executeAll: graceful degradation", () => {
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 = fromApprovalMap(
186
+ const plan = fromMap(
192
187
  isSensitive,
193
188
  new Map([["c3", { decision: "approve" }]]),
194
189
  )(callsWithBogus)
195
- return Stream.merge(executeAll(allTools, plan.approved), rejectedStream(plan.rejected))
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: "Value" })
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: "Value", value: { status: "dropped" } })
197
+ expect(by.get("c3")).toMatchObject({ _tag: "Ok", value: { status: "dropped" } })
203
198
  })
204
199
  })
205
200
 
206
201
  // ---------------------------------------------------------------------------
207
- // fromVerdictQueue: WebSocket-style scenarios
202
+ // fromQueue: WebSocket-style scenarios
208
203
  // ---------------------------------------------------------------------------
209
204
 
210
- describe("fromVerdictQueue + executeAll", () => {
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 fromVerdictQueue's router.
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, announce } = yield* fromVerdictQueue(
224
+ const { approved, decisions, approvalRequests } = yield* fromQueue(
230
225
  isSensitive,
231
226
  verdicts,
232
227
  )(calls)
233
228
  return Stream.merge(
234
- announce,
229
+ approvalRequests,
235
230
  Stream.merge(
236
- executeAll(allTools, approved),
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: "Value", value: { status: "sent" } })
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.functionCallOutput("c98", JSON.stringify({ count: 0 }))
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(toFunctionCallOutput)]
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 FunctionCallOutput before appending new user message", () => {
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("toFunctionCallOutput", () => {
302
+ describe("toToolCallOutput", () => {
312
303
  it("round-trips a Value result", () => {
313
304
  const r: ToolResult = {
314
- _tag: "Value",
305
+ _tag: "Ok",
315
306
  call_id: "c1",
316
307
  tool: "web_search",
317
308
  value: { count: 3 },
318
309
  }
319
- const out = toFunctionCallOutput(r)
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 = toFunctionCallOutput(r)
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 = toFunctionCallOutput(r)
334
+ const out = toToolCallOutput(r)
344
335
  expect(JSON.parse(out.output)).toEqual({ kind: "cancelled" })
345
336
  })
346
337
  })
347
338
 
348
339
  // ---------------------------------------------------------------------------
349
- // executeAll
340
+ // run
350
341
  // ---------------------------------------------------------------------------
351
342
 
352
- describe("executeAll", () => {
343
+ describe("run", () => {
353
344
  it("runs all calls passed to it", async () => {
354
- const collected = await Effect.runPromise(Stream.runCollect(executeAll(allTools, calls)))
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) => isValue(e.result))).toBe(true)
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.executeAll`.
6
+ * the recipe boundary via `Toolkit.run`.
7
7
  */
8
8
  import { Data, Deferred, Effect, Queue, Scope, Stream } from "effect"
9
- import type { FunctionCall } from "../domain/Items.js"
10
- import { type ToolResult, cancelled, denied } from "./Outcome.js"
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<FunctionCall>
14
+ readonly approved: ReadonlyArray<ToolCall>
15
15
  readonly rejected: ReadonlyArray<ToolResult>
16
16
  }
17
17
 
18
- export type ToolCallDecision = Data.TaggedEnum<{
19
- Approved: { readonly call: FunctionCall }
18
+ export type ApprovalDecision = Data.TaggedEnum<{
19
+ Approved: { readonly call: ToolCall }
20
20
  Rejected: { readonly result: ToolResult }
21
21
  }>
22
22
 
23
- export const ToolCallDecision = Data.taggedEnum<ToolCallDecision>()
23
+ export const ApprovalDecision = Data.taggedEnum<ApprovalDecision>()
24
24
 
25
- export const approve = (call: FunctionCall): ToolCallDecision => ToolCallDecision.Approved({ call })
25
+ export const approve = (call: ToolCall): ApprovalDecision => ApprovalDecision.Approved({ call })
26
26
 
27
- export const reject = (result: ToolResult): ToolCallDecision =>
28
- ToolCallDecision.Rejected({ result })
27
+ export const reject = (result: ToolResult): ApprovalDecision =>
28
+ ApprovalDecision.Rejected({ result })
29
29
 
30
- export const splitToolCallDecisions = (decisions: ReadonlyArray<ToolCallDecision>): ToolCallPlan =>
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: FunctionCall): ToolEvent =>
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 `ToolCallDecision` when their matching verdict arrives.
59
+ * one `ApprovalDecision` when their matching verdict arrives.
60
60
  */
61
- export const fromVerdictQueue =
62
- (predicate: (call: FunctionCall) => boolean, verdicts: Queue.Dequeue<Verdict>) =>
61
+ export const fromQueue =
62
+ (predicate: (call: ToolCall) => boolean, verdicts: Queue.Dequeue<Verdict>) =>
63
63
  (
64
- calls: ReadonlyArray<FunctionCall>,
64
+ calls: ReadonlyArray<ToolCall>,
65
65
  ): Effect.Effect<
66
66
  {
67
- readonly approved: ReadonlyArray<FunctionCall>
68
- readonly decisions: Stream.Stream<ToolCallDecision>
69
- readonly announce: Stream.Stream<ToolEvent>
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 announce = Stream.fromIterable<ToolEvent>(gated.map(approvalRequested))
112
+ const approvalRequests = Stream.fromIterable<ToolEvent>(gated.map(approvalRequested))
113
113
 
114
- return { approved, decisions, announce }
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 fromApprovalMap =
127
- (predicate: (call: FunctionCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) =>
128
- (calls: ReadonlyArray<FunctionCall>): ToolCallPlan =>
129
- splitToolCallDecisions(
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)
@@ -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
- type FunctionCall,
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 = (history: ReadonlyArray<Item>): ReadonlyArray<FunctionCall> => {
27
- const answered = new Set(history.filter(isFunctionCallOutput).map((o) => o.call_id))
28
- return history.filter(isFunctionCall).filter((c) => !answered.has(c.call_id))
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<Item>): boolean =>
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 `toFunctionCallOutput` and appends to history before submitting.
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<Item>,
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 { FunctionCall, FunctionCallOutput } from "../domain/Items.js"
4
- import { functionCallOutput } from "../domain/Items.js"
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 `Toolkit.toDescriptors`.
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.Intermediate`s in real time;
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 AnyKindTool<R = any> = AnyStreamingTool<R> | AnyPlainTool<R>
114
+ export type AnyTool<R = any> = AnyStreamingTool<R> | AnyPlainTool<R>
115
115
 
116
- export const isStreamingTool = <R>(t: AnyKindTool<R>): t is AnyStreamingTool<R> =>
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. Mirrors `Toolkit.toDescriptors` but accepts the union type
122
- * so a single list can carry both kinds.
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: FunctionCall, toolName: string, message: string) => (cause: unknown) =>
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: FunctionCall,
147
- ): Effect.Effect<FunctionCallOutput, ToolError, R> =>
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 functionCallOutput(call.call_id, JSON.stringify(output))
167
+ return toolCallOutput(call.call_id, JSON.stringify(output))
170
168
  })
@@ -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
- * - Intermediate : per-element passthrough from a streaming tool's run
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 `continueWith` and apply
9
- * `toFunctionCallOutput` when appending to history.
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 "./Outcome.js"
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
- Intermediate: {
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, Intermediate, Output })` for
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 isIntermediate = ToolEvent.$is("Intermediate")
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.Value.
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 `toFunctionCallOutput`
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 { FunctionCall, FunctionCallOutput } from "../domain/Items.js"
16
- import { functionCallOutput } from "../domain/Items.js"
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
- Value: {
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("Value")` for type
39
- * narrowing and `ToolResult.$match({ Value, Failure })` for exhaustive
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`, `rejected`) below are kinder constructors than the
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 isValue = ToolResult.$is("Value")
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 `rejected`.
50
+ // anything else is just a recipe-chosen `kind` via `failed`.
51
51
  // ---------------------------------------------------------------------------
52
52
 
53
- export const rejected = (call: FunctionCall, kind: string, reason?: string): ToolResult =>
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: FunctionCall, reason?: string): ToolResult =>
63
- rejected(call, "denied", reason)
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: FunctionCall, reason?: string): ToolResult =>
67
- rejected(call, "cancelled", reason)
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: FunctionCall, reason: string): ToolResult =>
71
- rejected(call, "execution_error", reason)
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 toFunctionCallOutput = (r: ToolResult): FunctionCallOutput =>
77
+ export const toToolCallOutput = (r: ToolResult): ToolCallOutput =>
78
78
  ToolResult.$match(r, {
79
- Value: (v) => functionCallOutput(v.call_id, JSON.stringify(v.value)),
79
+ Ok: (v) => toolCallOutput(v.call_id, JSON.stringify(v.value)),
80
80
  Failure: (f) =>
81
- functionCallOutput(
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 },