@effect-uai/core 0.2.0 → 0.4.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 (209) hide show
  1. package/README.md +1 -1
  2. package/dist/{AiError-CqmYjXyx.d.mts → AiError-csR8Bhxx.d.mts} +26 -4
  3. package/dist/{AiError-CqmYjXyx.d.mts.map → AiError-csR8Bhxx.d.mts.map} +1 -1
  4. package/dist/Audio-BfCTGnH3.d.mts +61 -0
  5. package/dist/Audio-BfCTGnH3.d.mts.map +1 -0
  6. package/dist/Image-DxyXqzAM.d.mts +61 -0
  7. package/dist/Image-DxyXqzAM.d.mts.map +1 -0
  8. package/dist/{Items-D1C2686t.d.mts → Items-Hg5AsYxl.d.mts} +132 -80
  9. package/dist/Items-Hg5AsYxl.d.mts.map +1 -0
  10. package/dist/Media-D_CpcM1Z.d.mts +57 -0
  11. package/dist/Media-D_CpcM1Z.d.mts.map +1 -0
  12. package/dist/{StructuredFormat-B5ueioNr.d.mts → StructuredFormat-Cl41C56K.d.mts} +5 -5
  13. package/dist/StructuredFormat-Cl41C56K.d.mts.map +1 -0
  14. package/dist/{Tool-5wxOCuOh.d.mts → Tool-B8B5qVEy.d.mts} +13 -13
  15. package/dist/Tool-B8B5qVEy.d.mts.map +1 -0
  16. package/dist/{Turn-Bi83du4I.d.mts → Turn-7geUcKsf.d.mts} +5 -11
  17. package/dist/Turn-7geUcKsf.d.mts.map +1 -0
  18. package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
  19. package/dist/dist-DV5ISja1.mjs +13782 -0
  20. package/dist/dist-DV5ISja1.mjs.map +1 -0
  21. package/dist/domain/AiError.d.mts +2 -2
  22. package/dist/domain/AiError.mjs +19 -3
  23. package/dist/domain/AiError.mjs.map +1 -1
  24. package/dist/domain/Audio.d.mts +2 -0
  25. package/dist/domain/Audio.mjs +14 -0
  26. package/dist/domain/Audio.mjs.map +1 -0
  27. package/dist/domain/Image.d.mts +2 -0
  28. package/dist/domain/Image.mjs +58 -0
  29. package/dist/domain/Image.mjs.map +1 -0
  30. package/dist/domain/Items.d.mts +2 -2
  31. package/dist/domain/Items.mjs +19 -42
  32. package/dist/domain/Items.mjs.map +1 -1
  33. package/dist/domain/Media.d.mts +2 -0
  34. package/dist/domain/Media.mjs +14 -0
  35. package/dist/domain/Media.mjs.map +1 -0
  36. package/dist/domain/Music.d.mts +116 -0
  37. package/dist/domain/Music.d.mts.map +1 -0
  38. package/dist/domain/Music.mjs +29 -0
  39. package/dist/domain/Music.mjs.map +1 -0
  40. package/dist/domain/Transcript.d.mts +95 -0
  41. package/dist/domain/Transcript.d.mts.map +1 -0
  42. package/dist/domain/Transcript.mjs +22 -0
  43. package/dist/domain/Transcript.mjs.map +1 -0
  44. package/dist/domain/Turn.d.mts +1 -1
  45. package/dist/domain/Turn.mjs +1 -1
  46. package/dist/embedding-model/Embedding.d.mts +107 -0
  47. package/dist/embedding-model/Embedding.d.mts.map +1 -0
  48. package/dist/embedding-model/Embedding.mjs +18 -0
  49. package/dist/embedding-model/Embedding.mjs.map +1 -0
  50. package/dist/embedding-model/EmbeddingModel.d.mts +97 -0
  51. package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -0
  52. package/dist/embedding-model/EmbeddingModel.mjs +17 -0
  53. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -0
  54. package/dist/index.d.mts +21 -7
  55. package/dist/index.mjs +16 -2
  56. package/dist/language-model/LanguageModel.d.mts +12 -20
  57. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  58. package/dist/language-model/LanguageModel.mjs +3 -20
  59. package/dist/language-model/LanguageModel.mjs.map +1 -1
  60. package/dist/loop/Loop.d.mts +31 -7
  61. package/dist/loop/Loop.d.mts.map +1 -1
  62. package/dist/loop/Loop.mjs +39 -6
  63. package/dist/loop/Loop.mjs.map +1 -1
  64. package/dist/loop/Loop.test.d.mts +1 -0
  65. package/dist/loop/Loop.test.mjs +411 -0
  66. package/dist/loop/Loop.test.mjs.map +1 -0
  67. package/dist/magic-string.es-BgIV5Mu3.mjs +1013 -0
  68. package/dist/magic-string.es-BgIV5Mu3.mjs.map +1 -0
  69. package/dist/math/Vector.d.mts +47 -0
  70. package/dist/math/Vector.d.mts.map +1 -0
  71. package/dist/math/Vector.mjs +117 -0
  72. package/dist/math/Vector.mjs.map +1 -0
  73. package/dist/music-generator/MusicGenerator.d.mts +77 -0
  74. package/dist/music-generator/MusicGenerator.d.mts.map +1 -0
  75. package/dist/music-generator/MusicGenerator.mjs +51 -0
  76. package/dist/music-generator/MusicGenerator.mjs.map +1 -0
  77. package/dist/music-generator/MusicGenerator.test.d.mts +1 -0
  78. package/dist/music-generator/MusicGenerator.test.mjs +154 -0
  79. package/dist/music-generator/MusicGenerator.test.mjs.map +1 -0
  80. package/dist/observability/Metrics.d.mts +2 -2
  81. package/dist/observability/Metrics.d.mts.map +1 -1
  82. package/dist/observability/Metrics.mjs +1 -1
  83. package/dist/observability/Metrics.mjs.map +1 -1
  84. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +96 -0
  85. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -0
  86. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +48 -0
  87. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -0
  88. package/dist/speech-synthesizer/SpeechSynthesizer.test.d.mts +1 -0
  89. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +112 -0
  90. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -0
  91. package/dist/streaming/JSONL.d.mts +10 -3
  92. package/dist/streaming/JSONL.d.mts.map +1 -1
  93. package/dist/streaming/JSONL.mjs +13 -2
  94. package/dist/streaming/JSONL.mjs.map +1 -1
  95. package/dist/streaming/JSONL.test.d.mts +1 -0
  96. package/dist/streaming/JSONL.test.mjs +70 -0
  97. package/dist/streaming/JSONL.test.mjs.map +1 -0
  98. package/dist/streaming/Lines.mjs +1 -1
  99. package/dist/streaming/SSE.d.mts +2 -2
  100. package/dist/streaming/SSE.d.mts.map +1 -1
  101. package/dist/streaming/SSE.mjs +1 -1
  102. package/dist/streaming/SSE.mjs.map +1 -1
  103. package/dist/streaming/SSE.test.d.mts +1 -0
  104. package/dist/streaming/SSE.test.mjs +72 -0
  105. package/dist/streaming/SSE.test.mjs.map +1 -0
  106. package/dist/structured-format/StructuredFormat.d.mts +1 -1
  107. package/dist/structured-format/StructuredFormat.mjs +1 -1
  108. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  109. package/dist/testing/MockMusicGenerator.d.mts +39 -0
  110. package/dist/testing/MockMusicGenerator.d.mts.map +1 -0
  111. package/dist/testing/MockMusicGenerator.mjs +96 -0
  112. package/dist/testing/MockMusicGenerator.mjs.map +1 -0
  113. package/dist/testing/MockProvider.d.mts +6 -6
  114. package/dist/testing/MockProvider.d.mts.map +1 -1
  115. package/dist/testing/MockProvider.mjs.map +1 -1
  116. package/dist/testing/MockSpeechSynthesizer.d.mts +37 -0
  117. package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -0
  118. package/dist/testing/MockSpeechSynthesizer.mjs +95 -0
  119. package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -0
  120. package/dist/testing/MockTranscriber.d.mts +37 -0
  121. package/dist/testing/MockTranscriber.d.mts.map +1 -0
  122. package/dist/testing/MockTranscriber.mjs +77 -0
  123. package/dist/testing/MockTranscriber.mjs.map +1 -0
  124. package/dist/tool/HistoryCheck.d.mts +6 -3
  125. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  126. package/dist/tool/HistoryCheck.mjs +7 -1
  127. package/dist/tool/HistoryCheck.mjs.map +1 -1
  128. package/dist/tool/Outcome.d.mts +138 -2
  129. package/dist/tool/Outcome.d.mts.map +1 -0
  130. package/dist/tool/Outcome.mjs +32 -10
  131. package/dist/tool/Outcome.mjs.map +1 -1
  132. package/dist/tool/Resolvers.d.mts +11 -8
  133. package/dist/tool/Resolvers.d.mts.map +1 -1
  134. package/dist/tool/Resolvers.mjs +10 -1
  135. package/dist/tool/Resolvers.mjs.map +1 -1
  136. package/dist/tool/Resolvers.test.d.mts +1 -0
  137. package/dist/tool/Resolvers.test.mjs +317 -0
  138. package/dist/tool/Resolvers.test.mjs.map +1 -0
  139. package/dist/tool/Tool.d.mts +1 -1
  140. package/dist/tool/Tool.mjs +1 -1
  141. package/dist/tool/Tool.mjs.map +1 -1
  142. package/dist/tool/ToolEvent.d.mts +151 -2
  143. package/dist/tool/ToolEvent.d.mts.map +1 -0
  144. package/dist/tool/ToolEvent.mjs +30 -4
  145. package/dist/tool/ToolEvent.mjs.map +1 -1
  146. package/dist/tool/Toolkit.d.mts +19 -10
  147. package/dist/tool/Toolkit.d.mts.map +1 -1
  148. package/dist/tool/Toolkit.mjs +5 -5
  149. package/dist/tool/Toolkit.mjs.map +1 -1
  150. package/dist/tool/Toolkit.test.d.mts +1 -0
  151. package/dist/tool/Toolkit.test.mjs +113 -0
  152. package/dist/tool/Toolkit.test.mjs.map +1 -0
  153. package/dist/transcriber/Transcriber.d.mts +101 -0
  154. package/dist/transcriber/Transcriber.d.mts.map +1 -0
  155. package/dist/transcriber/Transcriber.mjs +49 -0
  156. package/dist/transcriber/Transcriber.mjs.map +1 -0
  157. package/dist/transcriber/Transcriber.test.d.mts +1 -0
  158. package/dist/transcriber/Transcriber.test.mjs +130 -0
  159. package/dist/transcriber/Transcriber.test.mjs.map +1 -0
  160. package/package.json +65 -13
  161. package/src/domain/AiError.ts +21 -0
  162. package/src/domain/Audio.ts +88 -0
  163. package/src/domain/Image.ts +75 -0
  164. package/src/domain/Items.ts +18 -47
  165. package/src/domain/Media.ts +61 -0
  166. package/src/domain/Music.ts +121 -0
  167. package/src/domain/Transcript.ts +83 -0
  168. package/src/embedding-model/Embedding.ts +117 -0
  169. package/src/embedding-model/EmbeddingModel.ts +107 -0
  170. package/src/index.ts +15 -1
  171. package/src/language-model/LanguageModel.ts +2 -22
  172. package/src/loop/Loop.test.ts +114 -2
  173. package/src/loop/Loop.ts +69 -5
  174. package/src/math/Vector.ts +138 -0
  175. package/src/music-generator/MusicGenerator.test.ts +170 -0
  176. package/src/music-generator/MusicGenerator.ts +123 -0
  177. package/src/observability/Metrics.ts +1 -1
  178. package/src/speech-synthesizer/SpeechSynthesizer.test.ts +141 -0
  179. package/src/speech-synthesizer/SpeechSynthesizer.ts +131 -0
  180. package/src/streaming/JSONL.ts +12 -0
  181. package/src/streaming/SSE.ts +1 -1
  182. package/src/structured-format/StructuredFormat.ts +2 -2
  183. package/src/testing/MockMusicGenerator.ts +170 -0
  184. package/src/testing/MockProvider.ts +2 -2
  185. package/src/testing/MockSpeechSynthesizer.ts +165 -0
  186. package/src/testing/MockTranscriber.ts +139 -0
  187. package/src/tool/HistoryCheck.ts +2 -5
  188. package/src/tool/Outcome.ts +36 -36
  189. package/src/tool/Resolvers.test.ts +11 -35
  190. package/src/tool/Resolvers.ts +5 -14
  191. package/src/tool/Tool.ts +9 -9
  192. package/src/tool/ToolEvent.ts +28 -24
  193. package/src/tool/Toolkit.test.ts +97 -2
  194. package/src/tool/Toolkit.ts +57 -33
  195. package/src/transcriber/Transcriber.test.ts +125 -0
  196. package/src/transcriber/Transcriber.ts +127 -0
  197. package/dist/Items-D1C2686t.d.mts.map +0 -1
  198. package/dist/Outcome-GiaNvt7i.d.mts +0 -32
  199. package/dist/Outcome-GiaNvt7i.d.mts.map +0 -1
  200. package/dist/StructuredFormat-B5ueioNr.d.mts.map +0 -1
  201. package/dist/Tool-5wxOCuOh.d.mts.map +0 -1
  202. package/dist/ToolEvent-wTMgb2GO.d.mts +0 -29
  203. package/dist/ToolEvent-wTMgb2GO.d.mts.map +0 -1
  204. package/dist/Turn-Bi83du4I.d.mts.map +0 -1
  205. package/dist/match/Match.d.mts +0 -16
  206. package/dist/match/Match.d.mts.map +0 -1
  207. package/dist/match/Match.mjs +0 -15
  208. package/dist/match/Match.mjs.map +0 -1
  209. package/src/match/Match.ts +0 -9
@@ -23,9 +23,7 @@ import { type ToolResult, cancelled } from "./Outcome.js"
23
23
  * `function_call_output` later in `history` (correlated by `call_id`).
24
24
  * Empty result = history is provider-submittable from this invariant.
25
25
  */
26
- export const findUnansweredCalls = (
27
- history: ReadonlyArray<Item>,
28
- ): ReadonlyArray<FunctionCall> => {
26
+ export const findUnansweredCalls = (history: ReadonlyArray<Item>): ReadonlyArray<FunctionCall> => {
29
27
  const answered = new Set(history.filter(isFunctionCallOutput).map((o) => o.call_id))
30
28
  return history.filter(isFunctionCall).filter((c) => !answered.has(c.call_id))
31
29
  }
@@ -45,5 +43,4 @@ export const isReconciled = (history: ReadonlyArray<Item>): boolean =>
45
43
  export const cancelAllPending = (
46
44
  history: ReadonlyArray<Item>,
47
45
  reason?: string,
48
- ): ReadonlyArray<ToolResult> =>
49
- findUnansweredCalls(history).map((call) => cancelled(call, reason))
46
+ ): ReadonlyArray<ToolResult> => findUnansweredCalls(history).map((call) => cancelled(call, reason))
@@ -11,7 +11,7 @@
11
11
  * and `unknown` would invite non-serializable values (Date, Map, BigInt,
12
12
  * fn). Recipes that want structured detail JSON.stringify themselves.
13
13
  */
14
- import { Match } from "effect"
14
+ import { Data } from "effect"
15
15
  import type { FunctionCall, FunctionCallOutput } from "../domain/Items.js"
16
16
  import { functionCallOutput } from "../domain/Items.js"
17
17
 
@@ -19,42 +19,44 @@ import { functionCallOutput } from "../domain/Items.js"
19
19
  // ToolResult
20
20
  // ---------------------------------------------------------------------------
21
21
 
22
- export type ToolResult =
23
- | {
24
- readonly _tag: "Value"
25
- readonly call_id: string
26
- readonly tool: string
27
- readonly value: unknown
28
- }
29
- | {
30
- readonly _tag: "Failure"
31
- readonly call_id: string
32
- readonly tool: string
33
- readonly kind: string
34
- readonly reason?: string
35
- }
22
+ export type ToolResult = Data.TaggedEnum<{
23
+ Value: {
24
+ readonly call_id: string
25
+ readonly tool: string
26
+ readonly value: unknown
27
+ }
28
+ Failure: {
29
+ readonly call_id: string
30
+ readonly tool: string
31
+ readonly kind: string
32
+ readonly reason?: string
33
+ }
34
+ }>
36
35
 
37
- export const isValue = (r: ToolResult): r is Extract<ToolResult, { _tag: "Value" }> =>
38
- r._tag === "Value"
36
+ /**
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
40
+ * pattern matching. Synthetic-result helpers (`denied`, `cancelled`,
41
+ * `executionError`, `rejected`) below are kinder constructors than the
42
+ * raw `ToolResult.Failure(...)`.
43
+ */
44
+ export const ToolResult = Data.taggedEnum<ToolResult>()
39
45
 
40
- export const isFailure = (r: ToolResult): r is Extract<ToolResult, { _tag: "Failure" }> =>
41
- r._tag === "Failure"
46
+ export const isValue = ToolResult.$is("Value")
47
+ export const isFailure = ToolResult.$is("Failure")
42
48
 
43
49
  // Synthesizers. `denied` and `cancelled` are operationally distinct;
44
50
  // anything else is just a recipe-chosen `kind` via `rejected`.
45
51
  // ---------------------------------------------------------------------------
46
52
 
47
- export const rejected = (
48
- call: FunctionCall,
49
- kind: string,
50
- reason?: string,
51
- ): ToolResult => ({
52
- _tag: "Failure",
53
- call_id: call.call_id,
54
- tool: call.name,
55
- kind,
56
- ...(reason !== undefined ? { reason } : {}),
57
- })
53
+ export const rejected = (call: FunctionCall, kind: string, reason?: string): ToolResult =>
54
+ ToolResult.Failure({
55
+ call_id: call.call_id,
56
+ tool: call.name,
57
+ kind,
58
+ ...(reason !== undefined ? { reason } : {}),
59
+ })
58
60
 
59
61
  /** Explicit user/policy rejection. */
60
62
  export const denied = (call: FunctionCall, reason?: string): ToolResult =>
@@ -73,15 +75,13 @@ export const executionError = (call: FunctionCall, reason: string): ToolResult =
73
75
  // ---------------------------------------------------------------------------
74
76
 
75
77
  export const toFunctionCallOutput = (r: ToolResult): FunctionCallOutput =>
76
- Match.value(r).pipe(
77
- Match.tag("Value", (v) => functionCallOutput(v.call_id, JSON.stringify(v.value))),
78
- Match.tag("Failure", (f) =>
78
+ ToolResult.$match(r, {
79
+ Value: (v) => functionCallOutput(v.call_id, JSON.stringify(v.value)),
80
+ Failure: (f) =>
79
81
  functionCallOutput(
80
82
  f.call_id,
81
83
  JSON.stringify(
82
84
  f.reason !== undefined ? { kind: f.kind, reason: f.reason } : { kind: f.kind },
83
85
  ),
84
86
  ),
85
- ),
86
- Match.exhaustive,
87
- )
87
+ })
@@ -14,12 +14,7 @@ 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,
25
20
  type ToolCallDecision,
@@ -28,12 +23,7 @@ import {
28
23
  } from "./Resolvers.js"
29
24
  import { fromEffectSchema, make as makeTool, streaming } from "./Tool.js"
30
25
  import { executeAll, outputEvent, outputEvents } from "./Toolkit.js"
31
- import {
32
- type ToolEvent,
33
- isApprovalRequested,
34
- isIntermediate,
35
- isOutput,
36
- } from "./ToolEvent.js"
26
+ import { type ToolEvent, isApprovalRequested, isIntermediate, isOutput } from "./ToolEvent.js"
37
27
 
38
28
  // ---------------------------------------------------------------------------
39
29
  // Three demo tools covering the matrix:
@@ -96,12 +86,10 @@ const calls = [
96
86
  fc("c3", "delete_database", { name: "prod" }),
97
87
  ]
98
88
 
99
- const resultsFrom = (
100
- collected: ReadonlyArray<ToolEvent>,
101
- ): ReadonlyArray<ToolResult> => collected.filter(isOutput).map((e) => e.result)
89
+ const resultsFrom = (collected: ReadonlyArray<ToolEvent>): ReadonlyArray<ToolResult> =>
90
+ collected.filter(isOutput).map((e) => e.result)
102
91
 
103
- const byCallId = (results: ReadonlyArray<ToolResult>) =>
104
- new Map(results.map((r) => [r.call_id, r]))
92
+ const byCallId = (results: ReadonlyArray<ToolResult>) => new Map(results.map((r) => [r.call_id, r]))
105
93
 
106
94
  const eventsFromApprovalMap = (approvals: ReadonlyMap<string, ApprovalMapEntry>) => {
107
95
  const plan = fromApprovalMap(isSensitive, approvals)(calls)
@@ -123,9 +111,7 @@ describe("fromApprovalMap + executeAll", () => {
123
111
  ["c2", { decision: "approve" }],
124
112
  ["c3", { decision: "approve" }],
125
113
  ])
126
- const collected = await Effect.runPromise(
127
- Stream.runCollect(eventsFromApprovalMap(approvals)),
128
- )
114
+ const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
129
115
  const by = byCallId(resultsFrom(collected))
130
116
  expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
131
117
  expect(by.get("c2")).toMatchObject({
@@ -146,14 +132,10 @@ describe("fromApprovalMap + executeAll", () => {
146
132
  ["c2", { decision: "deny", reason: "spam concern" }],
147
133
  ["c3", { decision: "deny", reason: "prod is sacred" }],
148
134
  ])
149
- const collected = await Effect.runPromise(
150
- Stream.runCollect(eventsFromApprovalMap(approvals)),
151
- )
135
+ const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
152
136
 
153
137
  // bulk_email never ran.
154
- expect(
155
- collected.filter(isIntermediate).filter((e) => e.tool === "bulk_email"),
156
- ).toHaveLength(0)
138
+ expect(collected.filter(isIntermediate).filter((e) => e.tool === "bulk_email")).toHaveLength(0)
157
139
 
158
140
  const by = byCallId(resultsFrom(collected))
159
141
  expect(by.get("c2")).toMatchObject({
@@ -169,9 +151,7 @@ describe("fromApprovalMap + executeAll", () => {
169
151
  })
170
152
 
171
153
  it("cancellation: missing verdicts → Failure(cancelled)", async () => {
172
- const collected = await Effect.runPromise(
173
- Stream.runCollect(eventsFromApprovalMap(new Map())),
174
- )
154
+ const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(new Map())))
175
155
  const by = byCallId(resultsFrom(collected))
176
156
  expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
177
157
  expect(by.get("c2")).toMatchObject({ _tag: "Failure", kind: "cancelled" })
@@ -183,9 +163,7 @@ describe("fromApprovalMap + executeAll", () => {
183
163
  ["c2", { decision: "approve" }],
184
164
  // c3 omitted → cancelled
185
165
  ])
186
- const collected = await Effect.runPromise(
187
- Stream.runCollect(eventsFromApprovalMap(approvals)),
188
- )
166
+ const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
189
167
  const by = byCallId(resultsFrom(collected))
190
168
  expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
191
169
  expect(by.get("c2")).toMatchObject({ _tag: "Value", value: { status: "sent" } })
@@ -370,9 +348,7 @@ describe("toFunctionCallOutput", () => {
370
348
 
371
349
  describe("executeAll", () => {
372
350
  it("runs all calls passed to it", async () => {
373
- const collected = await Effect.runPromise(
374
- Stream.runCollect(executeAll(allTools, calls)),
375
- )
351
+ const collected = await Effect.runPromise(Stream.runCollect(executeAll(allTools, calls)))
376
352
  expect(collected.filter(isOutput)).toHaveLength(3)
377
353
  expect(collected.filter(isOutput).every((e) => isValue(e.result))).toBe(true)
378
354
  })
@@ -10,7 +10,7 @@ import type { FunctionCall } from "../domain/Items.js"
10
10
  import { type ToolResult, cancelled, denied } from "./Outcome.js"
11
11
  import type { ToolEvent } from "./ToolEvent.js"
12
12
 
13
- export interface ToolCallPlan {
13
+ export type ToolCallPlan = {
14
14
  readonly approved: ReadonlyArray<FunctionCall>
15
15
  readonly rejected: ReadonlyArray<ToolResult>
16
16
  }
@@ -29,9 +29,7 @@ export const reject = (result: ToolResult): ToolCallDecision => ({
29
29
  result,
30
30
  })
31
31
 
32
- export const splitToolCallDecisions = (
33
- decisions: ReadonlyArray<ToolCallDecision>,
34
- ): ToolCallPlan =>
32
+ export const splitToolCallDecisions = (decisions: ReadonlyArray<ToolCallDecision>): ToolCallPlan =>
35
33
  decisions.reduce<ToolCallPlan>(
36
34
  (acc, decision) =>
37
35
  decision._tag === "Approved"
@@ -51,7 +49,7 @@ export const approvalRequested = (call: FunctionCall): ToolEvent => ({
51
49
  // Verdict queue (WebSocket-style transport).
52
50
  // ---------------------------------------------------------------------------
53
51
 
54
- export interface Verdict {
52
+ export type Verdict = {
55
53
  readonly call_id: string
56
54
  readonly decision: "approve" | "deny"
57
55
  readonly reason?: string
@@ -63,10 +61,7 @@ export interface Verdict {
63
61
  * one `ToolCallDecision` when their matching verdict arrives.
64
62
  */
65
63
  export const fromVerdictQueue =
66
- (
67
- predicate: (call: FunctionCall) => boolean,
68
- verdicts: Queue.Dequeue<Verdict>,
69
- ) =>
64
+ (predicate: (call: FunctionCall) => boolean, verdicts: Queue.Dequeue<Verdict>) =>
70
65
  (
71
66
  calls: ReadonlyArray<FunctionCall>,
72
67
  ): Effect.Effect<
@@ -131,10 +126,7 @@ export type ApprovalMapEntry =
131
126
  | { readonly decision: "deny"; readonly reason?: string }
132
127
 
133
128
  export const fromApprovalMap =
134
- (
135
- predicate: (call: FunctionCall) => boolean,
136
- approvals: ReadonlyMap<string, ApprovalMapEntry>,
137
- ) =>
129
+ (predicate: (call: FunctionCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) =>
138
130
  (calls: ReadonlyArray<FunctionCall>): ToolCallPlan =>
139
131
  splitToolCallDecisions(
140
132
  calls.map((call) => {
@@ -144,4 +136,3 @@ export const fromApprovalMap =
144
136
  return v.decision === "approve" ? approve(call) : reject(denied(call, v.reason))
145
137
  }),
146
138
  )
147
-
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 interface Tool<Name extends string, Input, Output, R = never> {
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 interface ToolDescriptor {
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 interface StreamingTool<Name extends string, Input, Event, Output, R = never> {
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, never>
93
- export type AnyPlainTool = Tool<string, any, any, never>
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({
@@ -5,33 +5,37 @@
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 `nextStateFrom` and apply
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
- readonly _tag: "ApprovalRequested"
16
- readonly call_id: string
17
- readonly tool: string
18
- readonly arguments: string
19
- }
20
- | {
21
- readonly _tag: "Intermediate"
22
- readonly call_id: string
23
- readonly tool: string
24
- readonly data: unknown
25
- }
26
- | { readonly _tag: "Output"; readonly result: ToolResult }
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
- export const isApprovalRequested = (
29
- e: ToolEvent,
30
- ): e is Extract<ToolEvent, { _tag: "ApprovalRequested" }> => e._tag === "ApprovalRequested"
31
-
32
- export const isIntermediate = (
33
- e: ToolEvent,
34
- ): e is Extract<ToolEvent, { _tag: "Intermediate" }> => e._tag === "Intermediate"
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 isOutput = (e: ToolEvent): e is Extract<ToolEvent, { _tag: "Output" }> =>
37
- e._tag === "Output"
39
+ export const isApprovalRequested = ToolEvent.$is("ApprovalRequested")
40
+ export const isIntermediate = ToolEvent.$is("Intermediate")
41
+ export const isOutput = ToolEvent.$is("Output")
@@ -1,5 +1,8 @@
1
- import { Effect, Schema } from "effect"
2
- import { describe, expect, it } from "vitest"
1
+ import { Context, Effect, Layer, Schema, Stream } from "effect"
2
+ import { describe, expect, expectTypeOf, it } from "vitest"
3
+ import type { FunctionCall } from "../domain/Items.js"
4
+ import { isOutput } from "./ToolEvent.js"
5
+ import { isValue } from "./Outcome.js"
3
6
  import * as Tool from "./Tool.js"
4
7
  import * as Toolkit from "./Toolkit.js"
5
8
 
@@ -43,3 +46,95 @@ describe("Toolkit.toDescriptors", () => {
43
46
  expect(l).not.toHaveProperty("strict")
44
47
  })
45
48
  })
49
+
50
+ describe("Toolkit.executeAll - tools with R requirements", () => {
51
+ // Two distinct services, modelling the "typed per-tool context" use case
52
+ // (cf. AI SDK 7's `toolsContext`). In Effect each tool declares its R, the
53
+ // compiler enforces it, and `executeAll` surfaces the union for the caller
54
+ // to provide via Layer.
55
+ type WeatherApiKeyShape = { readonly key: string }
56
+ class WeatherApiKey extends Context.Service<WeatherApiKey, WeatherApiKeyShape>()(
57
+ "test/WeatherApiKey",
58
+ ) {}
59
+
60
+ type GeoApiKeyShape = { readonly key: string }
61
+ class GeoApiKey extends Context.Service<GeoApiKey, GeoApiKeyShape>()("test/GeoApiKey") {}
62
+
63
+ const Empty = Schema.Struct({})
64
+
65
+ const getWeather = Tool.make({
66
+ name: "get_weather",
67
+ description: "",
68
+ inputSchema: Tool.fromEffectSchema(Empty),
69
+ run: () =>
70
+ Effect.gen(function* () {
71
+ const { key } = yield* WeatherApiKey
72
+ return { source: "weather", key }
73
+ }),
74
+ })
75
+
76
+ const getCoords = Tool.make({
77
+ name: "get_coords",
78
+ description: "",
79
+ inputSchema: Tool.fromEffectSchema(Empty),
80
+ run: () =>
81
+ Effect.gen(function* () {
82
+ const { key } = yield* GeoApiKey
83
+ return { source: "geo", key }
84
+ }),
85
+ })
86
+
87
+ const call = (name: string, id: string): FunctionCall => ({
88
+ type: "function_call",
89
+ call_id: id,
90
+ name,
91
+ arguments: "{}",
92
+ })
93
+
94
+ it("propagates each tool's R into the resulting Stream's requirements", () => {
95
+ const stream = Toolkit.executeAll([getWeather, getCoords], [])
96
+ expectTypeOf(stream).toEqualTypeOf<
97
+ Stream.Stream<import("./ToolEvent.js").ToolEvent, never, WeatherApiKey | GeoApiKey>
98
+ >()
99
+ })
100
+
101
+ it("runs each tool with its own service injected", async () => {
102
+ const layer = Layer.mergeAll(
103
+ Layer.succeed(WeatherApiKey, { key: "weather-123" }),
104
+ Layer.succeed(GeoApiKey, { key: "geo-456" }),
105
+ )
106
+
107
+ const program = Toolkit.executeAll(
108
+ [getWeather, getCoords],
109
+ [call("get_weather", "c1"), call("get_coords", "c2")],
110
+ ).pipe(Stream.runCollect, Effect.provide(layer))
111
+
112
+ const events = await Effect.runPromise(program)
113
+ const outputs = Array.from(events).filter(isOutput)
114
+ const byCall = new Map(outputs.map((e) => [e.result.call_id, e.result]))
115
+
116
+ const w = byCall.get("c1")
117
+ const g = byCall.get("c2")
118
+ expect(w !== undefined && isValue(w) && w.value).toEqual({
119
+ source: "weather",
120
+ key: "weather-123",
121
+ })
122
+ expect(g !== undefined && isValue(g) && g.value).toEqual({
123
+ source: "geo",
124
+ key: "geo-456",
125
+ })
126
+ })
127
+
128
+ it("with no service-needing tools, R is never", () => {
129
+ const plain = Tool.make({
130
+ name: "plain",
131
+ description: "",
132
+ inputSchema: Tool.fromEffectSchema(Empty),
133
+ run: () => Effect.succeed(0),
134
+ })
135
+ const stream = Toolkit.executeAll([plain], [])
136
+ expectTypeOf(stream).toEqualTypeOf<
137
+ Stream.Stream<import("./ToolEvent.js").ToolEvent, never, never>
138
+ >()
139
+ })
140
+ })
@@ -1,4 +1,4 @@
1
- import { Array as Arr, Effect, Ref, Stream } from "effect"
1
+ import { Array as Arr, Effect, Function, Ref, Stream } from "effect"
2
2
  import * as Loop from "../loop/Loop.js"
3
3
  import type { FunctionCall } from "../domain/Items.js"
4
4
  import {
@@ -6,14 +6,11 @@ import {
6
6
  type AnyPlainTool,
7
7
  type AnyStreamingTool,
8
8
  isStreamingTool,
9
+ type StreamingTool,
9
10
  type Tool,
10
11
  type ToolDescriptor,
11
12
  } from "./Tool.js"
12
- import {
13
- type ToolResult,
14
- executionError,
15
- rejected,
16
- } from "./Outcome.js"
13
+ import { type ToolResult, executionError, rejected } from "./Outcome.js"
17
14
  import type { ToolEvent } from "./ToolEvent.js"
18
15
  import { isOutput } from "./ToolEvent.js"
19
16
 
@@ -26,6 +23,18 @@ export type Toolkit<Tools extends ReadonlyArray<AnyTool>> = {
26
23
  export type ToolsR<Tools extends ReadonlyArray<AnyTool>> =
27
24
  Tools[number] extends Tool<any, any, any, infer R> ? R : never
28
25
 
26
+ /**
27
+ * Union of every tool's `R` requirements in a mixed plain + streaming array.
28
+ * Used by `executeAll` to surface the services tools need at the recipe
29
+ * level, so the loop's stream type carries them through to `Effect.provide`.
30
+ */
31
+ export type ToolKindR<Tools extends ReadonlyArray<AnyKindTool<any>>> =
32
+ Tools[number] extends StreamingTool<any, any, any, any, infer R>
33
+ ? R
34
+ : Tools[number] extends Tool<any, any, any, infer R>
35
+ ? R
36
+ : never
37
+
29
38
  export const make = <const Tools extends ReadonlyArray<AnyTool>>(tools: Tools): Toolkit<Tools> => ({
30
39
  tools,
31
40
  })
@@ -53,16 +62,16 @@ export const toDescriptors = <Tools extends ReadonlyArray<AnyTool>>(
53
62
  // only the calls they have already decided should run.
54
63
  // ---------------------------------------------------------------------------
55
64
 
56
- export interface ExecuteOptions {
65
+ export type ExecuteOptions = {
57
66
  readonly concurrency?: number | "unbounded"
58
67
  }
59
68
 
60
69
  /** Execute every provided call. Approval/rejection policy belongs upstream. */
61
- export const executeAll = (
62
- tools: ReadonlyArray<AnyKindTool>,
70
+ export const executeAll = <Tools extends ReadonlyArray<AnyKindTool<any>>>(
71
+ tools: Tools,
63
72
  calls: ReadonlyArray<FunctionCall>,
64
73
  options?: ExecuteOptions,
65
- ): Stream.Stream<ToolEvent> =>
74
+ ): Stream.Stream<ToolEvent, never, ToolKindR<Tools>> =>
66
75
  Stream.fromIterable(calls).pipe(
67
76
  Stream.flatMap((call) => runOne(tools, call), {
68
77
  concurrency: options?.concurrency ?? "unbounded",
@@ -71,9 +80,8 @@ export const executeAll = (
71
80
 
72
81
  export const outputEvent = (result: ToolResult): ToolEvent => ({ _tag: "Output", result })
73
82
 
74
- export const outputEvents = (
75
- results: ReadonlyArray<ToolResult>,
76
- ): Stream.Stream<ToolEvent> => Stream.fromIterable(results.map(outputEvent))
83
+ export const outputEvents = (results: ReadonlyArray<ToolResult>): Stream.Stream<ToolEvent> =>
84
+ Stream.fromIterable(results.map(outputEvent))
77
85
 
78
86
  const valueResult = (call: FunctionCall, tool: string, value: unknown): ToolResult => ({
79
87
  _tag: "Value",
@@ -82,10 +90,10 @@ const valueResult = (call: FunctionCall, tool: string, value: unknown): ToolResu
82
90
  value,
83
91
  })
84
92
 
85
- const runOne = (
86
- tools: ReadonlyArray<AnyKindTool>,
93
+ const runOne = <R>(
94
+ tools: ReadonlyArray<AnyKindTool<R>>,
87
95
  call: FunctionCall,
88
- ): Stream.Stream<ToolEvent> => {
96
+ ): Stream.Stream<ToolEvent, never, R> => {
89
97
  const tool = tools.find((t) => t.name === call.name)
90
98
  if (tool === undefined) {
91
99
  // Graceful: emit a synthetic Failure so OTHER calls in this turn
@@ -99,10 +107,10 @@ const runOne = (
99
107
  return runPlain(tool, call)
100
108
  }
101
109
 
102
- const runPlain = (
103
- tool: AnyPlainTool,
110
+ const runPlain = <R>(
111
+ tool: AnyPlainTool<R>,
104
112
  call: FunctionCall,
105
- ): Stream.Stream<ToolEvent> =>
113
+ ): Stream.Stream<ToolEvent, never, R> =>
106
114
  Stream.fromEffect(
107
115
  Effect.gen(function* () {
108
116
  const parsed = yield* Effect.try({
@@ -124,10 +132,10 @@ const runPlain = (
124
132
  ),
125
133
  )
126
134
 
127
- const runStreaming = (
128
- tool: AnyStreamingTool,
135
+ const runStreaming = <R>(
136
+ tool: AnyStreamingTool<R>,
129
137
  call: FunctionCall,
130
- ): Stream.Stream<ToolEvent> =>
138
+ ): Stream.Stream<ToolEvent, never, R> =>
131
139
  Stream.unwrap(
132
140
  Effect.gen(function* () {
133
141
  const parsed = yield* Effect.try({
@@ -184,19 +192,35 @@ const runStreaming = (
184
192
  )
185
193
 
186
194
  // ---------------------------------------------------------------------------
187
- // `nextStateFrom` - bridge from a `Stream<ToolEvent>` to the loop's emit
195
+ // `continueWith` - bridge from a `Stream<ToolEvent>` to the loop's emit
188
196
  // shape. Drains the stream to the consumer in real-time, taps every
189
197
  // `Output` into an internal Ref, and at end-of-stream emits
190
198
  // `Loop.next(build(results))`. Recipe never sees the Ref.
199
+ //
200
+ // Dual: data-first `continueWith(stream, build)` and data-last
201
+ // `stream.pipe(continueWith(build))` both work.
191
202
  // ---------------------------------------------------------------------------
192
203
 
193
- export const nextStateFrom = <S>(
194
- stream: Stream.Stream<ToolEvent>,
195
- build: (results: ReadonlyArray<ToolResult>) => S,
196
- ): Stream.Stream<Loop.Event<ToolEvent, S>> =>
197
- Loop.nextAfterFold(
198
- stream,
199
- [] as ReadonlyArray<ToolResult>,
200
- (acc, e) => (isOutput(e) ? Arr.append(acc, e.result) : acc),
201
- build,
202
- )
204
+ export const continueWith: {
205
+ <S>(
206
+ build: (results: ReadonlyArray<ToolResult>) => S,
207
+ ): <R>(
208
+ stream: Stream.Stream<ToolEvent, never, R>,
209
+ ) => Stream.Stream<Loop.Event<ToolEvent, S>, never, R>
210
+ <S, R>(
211
+ stream: Stream.Stream<ToolEvent, never, R>,
212
+ build: (results: ReadonlyArray<ToolResult>) => S,
213
+ ): Stream.Stream<Loop.Event<ToolEvent, S>, never, R>
214
+ } = Function.dual(
215
+ 2,
216
+ <S, R>(
217
+ stream: Stream.Stream<ToolEvent, never, R>,
218
+ build: (results: ReadonlyArray<ToolResult>) => S,
219
+ ): Stream.Stream<Loop.Event<ToolEvent, S>, never, R> =>
220
+ Loop.nextAfterFold(
221
+ stream,
222
+ [] as ReadonlyArray<ToolResult>,
223
+ (acc, e) => (isOutput(e) ? Arr.append(acc, e.result) : acc),
224
+ build,
225
+ ),
226
+ )