@effect-uai/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +1 -1
  2. package/dist/{AiError-CqmYjXyx.d.mts → AiError-CBuPHVKA.d.mts} +1 -1
  3. package/dist/{AiError-CqmYjXyx.d.mts.map → AiError-CBuPHVKA.d.mts.map} +1 -1
  4. package/dist/Image-BZmKfIdq.d.mts +61 -0
  5. package/dist/Image-BZmKfIdq.d.mts.map +1 -0
  6. package/dist/{Items-D1C2686t.d.mts → Items-CB8Bo3FI.d.mts} +132 -80
  7. package/dist/Items-CB8Bo3FI.d.mts.map +1 -0
  8. package/dist/Media-D_CpcM1Z.d.mts +57 -0
  9. package/dist/Media-D_CpcM1Z.d.mts.map +1 -0
  10. package/dist/{StructuredFormat-B5ueioNr.d.mts → StructuredFormat-BWq5Hd1O.d.mts} +5 -5
  11. package/dist/StructuredFormat-BWq5Hd1O.d.mts.map +1 -0
  12. package/dist/{Tool-5wxOCuOh.d.mts → Tool-DjVufH7i.d.mts} +13 -13
  13. package/dist/Tool-DjVufH7i.d.mts.map +1 -0
  14. package/dist/{Turn-Bi83du4I.d.mts → Turn-OPaILVIB.d.mts} +5 -11
  15. package/dist/Turn-OPaILVIB.d.mts.map +1 -0
  16. package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
  17. package/dist/dist-DV5ISja1.mjs +13782 -0
  18. package/dist/dist-DV5ISja1.mjs.map +1 -0
  19. package/dist/domain/AiError.d.mts +1 -1
  20. package/dist/domain/AiError.mjs +1 -1
  21. package/dist/domain/Image.d.mts +2 -0
  22. package/dist/domain/Image.mjs +58 -0
  23. package/dist/domain/Image.mjs.map +1 -0
  24. package/dist/domain/Items.d.mts +2 -2
  25. package/dist/domain/Items.mjs +19 -42
  26. package/dist/domain/Items.mjs.map +1 -1
  27. package/dist/domain/Media.d.mts +2 -0
  28. package/dist/domain/Media.mjs +14 -0
  29. package/dist/domain/Media.mjs.map +1 -0
  30. package/dist/domain/Turn.d.mts +1 -1
  31. package/dist/domain/Turn.mjs +1 -1
  32. package/dist/embedding-model/Embedding.d.mts +107 -0
  33. package/dist/embedding-model/Embedding.d.mts.map +1 -0
  34. package/dist/embedding-model/Embedding.mjs +18 -0
  35. package/dist/embedding-model/Embedding.mjs.map +1 -0
  36. package/dist/embedding-model/EmbeddingModel.d.mts +97 -0
  37. package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -0
  38. package/dist/embedding-model/EmbeddingModel.mjs +17 -0
  39. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -0
  40. package/dist/index.d.mts +15 -7
  41. package/dist/index.mjs +10 -2
  42. package/dist/language-model/LanguageModel.d.mts +12 -20
  43. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  44. package/dist/language-model/LanguageModel.mjs +3 -20
  45. package/dist/language-model/LanguageModel.mjs.map +1 -1
  46. package/dist/loop/Loop.d.mts +31 -7
  47. package/dist/loop/Loop.d.mts.map +1 -1
  48. package/dist/loop/Loop.mjs +39 -6
  49. package/dist/loop/Loop.mjs.map +1 -1
  50. package/dist/loop/Loop.test.d.mts +1 -0
  51. package/dist/loop/Loop.test.mjs +411 -0
  52. package/dist/loop/Loop.test.mjs.map +1 -0
  53. package/dist/magic-string.es-BgIV5Mu3.mjs +1013 -0
  54. package/dist/magic-string.es-BgIV5Mu3.mjs.map +1 -0
  55. package/dist/math/Vector.d.mts +47 -0
  56. package/dist/math/Vector.d.mts.map +1 -0
  57. package/dist/math/Vector.mjs +117 -0
  58. package/dist/math/Vector.mjs.map +1 -0
  59. package/dist/observability/Metrics.d.mts +2 -2
  60. package/dist/observability/Metrics.d.mts.map +1 -1
  61. package/dist/observability/Metrics.mjs +1 -1
  62. package/dist/observability/Metrics.mjs.map +1 -1
  63. package/dist/streaming/JSONL.mjs +1 -1
  64. package/dist/streaming/JSONL.test.d.mts +1 -0
  65. package/dist/streaming/JSONL.test.mjs +70 -0
  66. package/dist/streaming/JSONL.test.mjs.map +1 -0
  67. package/dist/streaming/Lines.mjs +1 -1
  68. package/dist/streaming/SSE.d.mts +2 -2
  69. package/dist/streaming/SSE.d.mts.map +1 -1
  70. package/dist/streaming/SSE.mjs +1 -1
  71. package/dist/streaming/SSE.mjs.map +1 -1
  72. package/dist/streaming/SSE.test.d.mts +1 -0
  73. package/dist/streaming/SSE.test.mjs +72 -0
  74. package/dist/streaming/SSE.test.mjs.map +1 -0
  75. package/dist/structured-format/StructuredFormat.d.mts +1 -1
  76. package/dist/structured-format/StructuredFormat.mjs +1 -1
  77. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  78. package/dist/testing/MockProvider.d.mts +6 -6
  79. package/dist/testing/MockProvider.d.mts.map +1 -1
  80. package/dist/testing/MockProvider.mjs.map +1 -1
  81. package/dist/tool/HistoryCheck.d.mts +6 -3
  82. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  83. package/dist/tool/HistoryCheck.mjs +7 -1
  84. package/dist/tool/HistoryCheck.mjs.map +1 -1
  85. package/dist/tool/Outcome.d.mts +138 -2
  86. package/dist/tool/Outcome.d.mts.map +1 -0
  87. package/dist/tool/Outcome.mjs +32 -10
  88. package/dist/tool/Outcome.mjs.map +1 -1
  89. package/dist/tool/Resolvers.d.mts +11 -8
  90. package/dist/tool/Resolvers.d.mts.map +1 -1
  91. package/dist/tool/Resolvers.mjs +10 -1
  92. package/dist/tool/Resolvers.mjs.map +1 -1
  93. package/dist/tool/Resolvers.test.d.mts +1 -0
  94. package/dist/tool/Resolvers.test.mjs +317 -0
  95. package/dist/tool/Resolvers.test.mjs.map +1 -0
  96. package/dist/tool/Tool.d.mts +1 -1
  97. package/dist/tool/Tool.mjs +1 -1
  98. package/dist/tool/Tool.mjs.map +1 -1
  99. package/dist/tool/ToolEvent.d.mts +151 -2
  100. package/dist/tool/ToolEvent.d.mts.map +1 -0
  101. package/dist/tool/ToolEvent.mjs +30 -4
  102. package/dist/tool/ToolEvent.mjs.map +1 -1
  103. package/dist/tool/Toolkit.d.mts +19 -10
  104. package/dist/tool/Toolkit.d.mts.map +1 -1
  105. package/dist/tool/Toolkit.mjs +5 -5
  106. package/dist/tool/Toolkit.mjs.map +1 -1
  107. package/dist/tool/Toolkit.test.d.mts +1 -0
  108. package/dist/tool/Toolkit.test.mjs +113 -0
  109. package/dist/tool/Toolkit.test.mjs.map +1 -0
  110. package/package.json +29 -13
  111. package/src/domain/Image.ts +75 -0
  112. package/src/domain/Items.ts +18 -47
  113. package/src/domain/Media.ts +61 -0
  114. package/src/embedding-model/Embedding.ts +117 -0
  115. package/src/embedding-model/EmbeddingModel.ts +107 -0
  116. package/src/index.ts +9 -1
  117. package/src/language-model/LanguageModel.ts +2 -22
  118. package/src/loop/Loop.test.ts +114 -2
  119. package/src/loop/Loop.ts +69 -5
  120. package/src/math/Vector.ts +138 -0
  121. package/src/observability/Metrics.ts +1 -1
  122. package/src/streaming/SSE.ts +1 -1
  123. package/src/structured-format/StructuredFormat.ts +2 -2
  124. package/src/testing/MockProvider.ts +2 -2
  125. package/src/tool/HistoryCheck.ts +2 -5
  126. package/src/tool/Outcome.ts +36 -36
  127. package/src/tool/Resolvers.test.ts +11 -35
  128. package/src/tool/Resolvers.ts +5 -14
  129. package/src/tool/Tool.ts +9 -9
  130. package/src/tool/ToolEvent.ts +28 -24
  131. package/src/tool/Toolkit.test.ts +97 -2
  132. package/src/tool/Toolkit.ts +57 -33
  133. package/dist/Items-D1C2686t.d.mts.map +0 -1
  134. package/dist/Outcome-GiaNvt7i.d.mts +0 -32
  135. package/dist/Outcome-GiaNvt7i.d.mts.map +0 -1
  136. package/dist/StructuredFormat-B5ueioNr.d.mts.map +0 -1
  137. package/dist/Tool-5wxOCuOh.d.mts.map +0 -1
  138. package/dist/ToolEvent-wTMgb2GO.d.mts +0 -29
  139. package/dist/ToolEvent-wTMgb2GO.d.mts.map +0 -1
  140. package/dist/Turn-Bi83du4I.d.mts.map +0 -1
  141. package/dist/match/Match.d.mts +0 -16
  142. package/dist/match/Match.d.mts.map +0 -1
  143. package/dist/match/Match.mjs +0 -15
  144. package/dist/match/Match.mjs.map +0 -1
  145. package/src/match/Match.ts +0 -9
@@ -19,7 +19,7 @@ export type StructuredSchema<Output = unknown> = StandardSchemaV1<unknown, Outpu
19
19
  * cross-validator schema with metadata providers need (name, description,
20
20
  * strict-mode flag).
21
21
  */
22
- export interface StructuredFormat<A> {
22
+ export type StructuredFormat<A> = {
23
23
  readonly name: string
24
24
  readonly description?: string
25
25
  readonly schema: StructuredSchema<A>
@@ -31,7 +31,7 @@ export interface StructuredFormat<A> {
31
31
  }
32
32
 
33
33
  /** A single path-scoped validation problem. Library-agnostic shape. */
34
- export interface DecodeIssue {
34
+ export type DecodeIssue = {
35
35
  readonly path: ReadonlyArray<string | number>
36
36
  readonly message: string
37
37
  }
@@ -4,7 +4,7 @@ import type { Item } from "../domain/Items.js"
4
4
  import { LanguageModel, type LanguageModelService } from "../language-model/LanguageModel.js"
5
5
  import type { Turn, TurnEvent } from "../domain/Turn.js"
6
6
 
7
- export interface MockOptions {
7
+ export type MockOptions = {
8
8
  /**
9
9
  * If set, deltas of each scripted turn are spaced by this duration via
10
10
  * `Schedule.spaced`. Combine with `TestClock.adjust` for deterministic
@@ -19,7 +19,7 @@ export interface MockOptions {
19
19
  * deltas (text → tool_call_start → tool_call_args_delta → ... → turn_complete)
20
20
  * so streaming consumers can see realistic delta shapes.
21
21
  */
22
- export interface MockRecorder {
22
+ export type MockRecorder = {
23
23
  readonly calls: ReadonlyArray<{
24
24
  readonly history: ReadonlyArray<Item>
25
25
  readonly turn: Turn
@@ -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
+ })