@effect-uai/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +2 -2
  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-rlTfuHaQ.d.mts → Turn-OPaILVIB.d.mts} +12 -29
  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 +2 -2
  31. package/dist/domain/Turn.mjs +12 -8
  32. package/dist/domain/Turn.mjs.map +1 -1
  33. package/dist/embedding-model/Embedding.d.mts +107 -0
  34. package/dist/embedding-model/Embedding.d.mts.map +1 -0
  35. package/dist/embedding-model/Embedding.mjs +18 -0
  36. package/dist/embedding-model/Embedding.mjs.map +1 -0
  37. package/dist/embedding-model/EmbeddingModel.d.mts +97 -0
  38. package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -0
  39. package/dist/embedding-model/EmbeddingModel.mjs +17 -0
  40. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -0
  41. package/dist/index.d.mts +16 -8
  42. package/dist/index.mjs +10 -2
  43. package/dist/language-model/LanguageModel.d.mts +12 -20
  44. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  45. package/dist/language-model/LanguageModel.mjs +3 -20
  46. package/dist/language-model/LanguageModel.mjs.map +1 -1
  47. package/dist/loop/Loop.d.mts +111 -2
  48. package/dist/loop/Loop.d.mts.map +1 -0
  49. package/dist/loop/Loop.mjs +39 -6
  50. package/dist/loop/Loop.mjs.map +1 -1
  51. package/dist/loop/Loop.test.d.mts +1 -0
  52. package/dist/loop/Loop.test.mjs +411 -0
  53. package/dist/loop/Loop.test.mjs.map +1 -0
  54. package/dist/magic-string.es-BgIV5Mu3.mjs +1013 -0
  55. package/dist/magic-string.es-BgIV5Mu3.mjs.map +1 -0
  56. package/dist/math/Vector.d.mts +47 -0
  57. package/dist/math/Vector.d.mts.map +1 -0
  58. package/dist/math/Vector.mjs +117 -0
  59. package/dist/math/Vector.mjs.map +1 -0
  60. package/dist/observability/Metrics.d.mts +2 -2
  61. package/dist/observability/Metrics.d.mts.map +1 -1
  62. package/dist/observability/Metrics.mjs +1 -1
  63. package/dist/observability/Metrics.mjs.map +1 -1
  64. package/dist/streaming/JSONL.mjs +1 -1
  65. package/dist/streaming/JSONL.test.d.mts +1 -0
  66. package/dist/streaming/JSONL.test.mjs +70 -0
  67. package/dist/streaming/JSONL.test.mjs.map +1 -0
  68. package/dist/streaming/Lines.mjs +1 -1
  69. package/dist/streaming/SSE.d.mts +2 -2
  70. package/dist/streaming/SSE.d.mts.map +1 -1
  71. package/dist/streaming/SSE.mjs +1 -1
  72. package/dist/streaming/SSE.mjs.map +1 -1
  73. package/dist/streaming/SSE.test.d.mts +1 -0
  74. package/dist/streaming/SSE.test.mjs +72 -0
  75. package/dist/streaming/SSE.test.mjs.map +1 -0
  76. package/dist/structured-format/StructuredFormat.d.mts +1 -1
  77. package/dist/structured-format/StructuredFormat.mjs +1 -1
  78. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  79. package/dist/testing/MockProvider.d.mts +6 -6
  80. package/dist/testing/MockProvider.d.mts.map +1 -1
  81. package/dist/testing/MockProvider.mjs.map +1 -1
  82. package/dist/tool/HistoryCheck.d.mts +6 -3
  83. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  84. package/dist/tool/HistoryCheck.mjs +7 -1
  85. package/dist/tool/HistoryCheck.mjs.map +1 -1
  86. package/dist/tool/Outcome.d.mts +138 -2
  87. package/dist/tool/Outcome.d.mts.map +1 -0
  88. package/dist/tool/Outcome.mjs +34 -18
  89. package/dist/tool/Outcome.mjs.map +1 -1
  90. package/dist/tool/Resolvers.d.mts +30 -25
  91. package/dist/tool/Resolvers.d.mts.map +1 -1
  92. package/dist/tool/Resolvers.mjs +54 -44
  93. package/dist/tool/Resolvers.mjs.map +1 -1
  94. package/dist/tool/Resolvers.test.d.mts +1 -0
  95. package/dist/tool/Resolvers.test.mjs +317 -0
  96. package/dist/tool/Resolvers.test.mjs.map +1 -0
  97. package/dist/tool/Tool.d.mts +1 -1
  98. package/dist/tool/Tool.mjs +1 -1
  99. package/dist/tool/Tool.mjs.map +1 -1
  100. package/dist/tool/ToolEvent.d.mts +151 -2
  101. package/dist/tool/ToolEvent.d.mts.map +1 -0
  102. package/dist/tool/ToolEvent.mjs +30 -4
  103. package/dist/tool/ToolEvent.mjs.map +1 -1
  104. package/dist/tool/Toolkit.d.mts +24 -15
  105. package/dist/tool/Toolkit.d.mts.map +1 -1
  106. package/dist/tool/Toolkit.mjs +14 -13
  107. package/dist/tool/Toolkit.mjs.map +1 -1
  108. package/dist/tool/Toolkit.test.d.mts +1 -0
  109. package/dist/tool/Toolkit.test.mjs +113 -0
  110. package/dist/tool/Toolkit.test.mjs.map +1 -0
  111. package/package.json +29 -13
  112. package/src/domain/Image.ts +75 -0
  113. package/src/domain/Items.ts +18 -47
  114. package/src/domain/Media.ts +61 -0
  115. package/src/domain/Turn.ts +7 -17
  116. package/src/embedding-model/Embedding.ts +117 -0
  117. package/src/embedding-model/EmbeddingModel.ts +107 -0
  118. package/src/index.ts +9 -1
  119. package/src/language-model/LanguageModel.ts +2 -22
  120. package/src/loop/Loop.test.ts +114 -2
  121. package/src/loop/Loop.ts +69 -5
  122. package/src/math/Vector.ts +138 -0
  123. package/src/observability/Metrics.ts +1 -1
  124. package/src/streaming/SSE.ts +1 -1
  125. package/src/structured-format/StructuredFormat.ts +2 -2
  126. package/src/testing/MockProvider.ts +2 -2
  127. package/src/tool/HistoryCheck.ts +2 -5
  128. package/src/tool/Outcome.ts +39 -53
  129. package/src/tool/Resolvers.test.ts +46 -117
  130. package/src/tool/Resolvers.ts +74 -102
  131. package/src/tool/Tool.ts +9 -9
  132. package/src/tool/ToolEvent.ts +30 -26
  133. package/src/tool/Toolkit.test.ts +97 -2
  134. package/src/tool/Toolkit.ts +65 -67
  135. package/dist/Items-D1C2686t.d.mts.map +0 -1
  136. package/dist/Loop-CzSJo1h8.d.mts +0 -87
  137. package/dist/Loop-CzSJo1h8.d.mts.map +0 -1
  138. package/dist/Outcome-C2JYknCu.d.mts +0 -40
  139. package/dist/Outcome-C2JYknCu.d.mts.map +0 -1
  140. package/dist/StructuredFormat-B5ueioNr.d.mts.map +0 -1
  141. package/dist/Tool-5wxOCuOh.d.mts.map +0 -1
  142. package/dist/ToolEvent-B2N10hr3.d.mts +0 -29
  143. package/dist/ToolEvent-B2N10hr3.d.mts.map +0 -1
  144. package/dist/Turn-rlTfuHaQ.d.mts.map +0 -1
  145. package/dist/match/Match.d.mts +0 -16
  146. package/dist/match/Match.d.mts.map +0 -1
  147. package/dist/match/Match.mjs +0 -15
  148. package/dist/match/Match.mjs.map +0 -1
  149. package/src/match/Match.ts +0 -9
package/src/loop/Loop.ts CHANGED
@@ -17,7 +17,19 @@
17
17
  * (their producing side effects may already have run). Prefer the
18
18
  * `Loop.nextAfter` / `Loop.stopAfter` helpers to terminate cleanly.
19
19
  */
20
- import { Cause, Channel, Data, Effect, Exit, Function, Option, Ref, Scope, Stream } from "effect"
20
+ import {
21
+ Cause,
22
+ Channel,
23
+ Data,
24
+ Effect,
25
+ Exit,
26
+ Function,
27
+ Option,
28
+ Ref,
29
+ Scope,
30
+ Stream,
31
+ SubscriptionRef,
32
+ } from "effect"
21
33
  import { IncompleteTurn } from "../domain/AiError.js"
22
34
  import { isTurnComplete, type Turn, type TurnEvent } from "../domain/Turn.js"
23
35
 
@@ -83,7 +95,7 @@ export const stopAfter = <A, E, R>(
83
95
  * into an accumulator, and at end-of-stream emit one `next(build(finalAcc))`.
84
96
  *
85
97
  * Subsumes `nextAfter` when state is constant (`reduce: (s, _) => s`,
86
- * `build: (s) => s`). Used by `Toolkit.nextStateFrom` to collect tool
98
+ * `build: (s) => s`). Used by `Toolkit.continueWith` to collect tool
87
99
  * results and build next state without exposing a Ref to recipes.
88
100
  */
89
101
  export const nextAfterFold = <A, B, S, E, R>(
@@ -107,7 +119,7 @@ export const nextAfterFold = <A, B, S, E, R>(
107
119
  )
108
120
 
109
121
  // ---------------------------------------------------------------------------
110
- // streamUntilComplete - turn-aware stream operator for loop bodies
122
+ // onTurnComplete - turn-aware stream operator for loop bodies
111
123
  // ---------------------------------------------------------------------------
112
124
 
113
125
  /**
@@ -125,7 +137,7 @@ export const nextAfterFold = <A, B, S, E, R>(
125
137
  * fails with `AiError.IncompleteTurn`. Catch it via `Stream.catchTag` if
126
138
  * you want to recover.
127
139
  */
128
- export const streamUntilComplete =
140
+ export const onTurnComplete =
129
141
  <S, A, E2 = never, R2 = never>(
130
142
  then: (turn: Turn) => Effect.Effect<Stream.Stream<Event<A, S>, E2, R2>, E2, R2>,
131
143
  ) =>
@@ -162,7 +174,7 @@ export const streamUntilComplete =
162
174
  const isNonEmpty = <A>(array: ReadonlyArray<A>): array is readonly [A, ...Array<A>] =>
163
175
  array.length > 0
164
176
 
165
- interface CurrentBody<S, A, E, R> {
177
+ type CurrentBody<S, A, E, R> = {
166
178
  readonly scope: Scope.Closeable
167
179
  readonly pull: Effect.Effect<ReadonlyArray<Event<A, S>>, E | Cause.Done<void>, R>
168
180
  }
@@ -293,3 +305,55 @@ export const loop: {
293
305
  ),
294
306
  ),
295
307
  )
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // loopWithState - same body protocol, plus a live state observable.
311
+ // ---------------------------------------------------------------------------
312
+
313
+ /**
314
+ * Like `loop`, but exposes the current loop state as a `SubscriptionRef`
315
+ * alongside the value stream.
316
+ *
317
+ * Allocates one `SubscriptionRef<S>` seeded with `initial`, then runs the
318
+ * loop with a wrapped body that taps every `Next(s)` event into the ref
319
+ * before forwarding it. The caller decides how to consume both channels:
320
+ *
321
+ * - **Final state**: drain the stream, then `SubscriptionRef.get(state)`
322
+ * - the ref holds the state from the last `Next` (or `initial` if the
323
+ * loop ended without advancing).
324
+ * - **Live transitions**: `SubscriptionRef.changes(state)` is a
325
+ * `Stream<S>` of every state observed; subscribe alongside the value
326
+ * stream.
327
+ * - **Mid-iteration peek**: `SubscriptionRef.get(state)` at any time.
328
+ *
329
+ * The returned stream and ref are independent of each other - the ref
330
+ * lives outside the stream's scope, so reading it after the stream
331
+ * completes is safe.
332
+ */
333
+ export const loopWithState = <S, A, E, R>(
334
+ initial: S,
335
+ body: LoopBody<S, A, E, R>,
336
+ ): Effect.Effect<{
337
+ readonly stream: Stream.Stream<A, E, R>
338
+ readonly state: SubscriptionRef.SubscriptionRef<S>
339
+ }> =>
340
+ Effect.gen(function* () {
341
+ const stateRef = yield* SubscriptionRef.make(initial)
342
+
343
+ const tap = (stream: Stream.Stream<Event<A, S>, E, R>): Stream.Stream<Event<A, S>, E, R> =>
344
+ stream.pipe(
345
+ Stream.tap((event) =>
346
+ event._tag === "Next" ? SubscriptionRef.set(stateRef, event.state) : Effect.void,
347
+ ),
348
+ )
349
+
350
+ const wrappedBody: LoopBody<S, A, E, R> = (s) => {
351
+ const result = body(s)
352
+ return Effect.isEffect(result) ? Effect.map(result, tap) : tap(result)
353
+ }
354
+
355
+ return {
356
+ stream: loop(initial, wrappedBody),
357
+ state: stateRef,
358
+ }
359
+ })
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Linear-algebra primitives for embedding vectors:
3
+ *
4
+ * - **Dense float32**: `dot`, `l2Norm`, `normalize`, `cosine`,
5
+ * `euclidean`. Used for retrieval over single-vector embeddings.
6
+ * - **Sparse**: `sparseDot`, `sparseL2Norm`, `sparseCosine`. Used with
7
+ * `SparseEmbedding`, e.g. Jina ELSER outputs.
8
+ * - **Multivector** (late-interaction): `maxSim`. Used with
9
+ * `MultivectorEmbedding`, e.g. Jina v4 multivector / ColBERT.
10
+ *
11
+ * Hot loops are allocation-free; consumers can call these inside
12
+ * `.map()` over thousands of vectors without GC pressure. For
13
+ * GPU / SIMD / WASM-accelerated math at vector-DB scale, reach for a
14
+ * dedicated library - this module deliberately stays at the
15
+ * recipe-volume tier.
16
+ */
17
+ import type { MultivectorEmbedding, SparseEmbedding } from "../embedding-model/Embedding.js"
18
+
19
+ /** Inner / dot product. */
20
+ export const dot = (a: Float32Array, b: Float32Array): number => {
21
+ let s = 0
22
+ const n = Math.min(a.length, b.length)
23
+ for (let i = 0; i < n; i++) s += a[i]! * b[i]!
24
+ return s
25
+ }
26
+
27
+ /** L2 norm (Euclidean magnitude). */
28
+ export const l2Norm = (v: Float32Array): number => {
29
+ let s = 0
30
+ for (let i = 0; i < v.length; i++) s += v[i]! * v[i]!
31
+ return Math.sqrt(s)
32
+ }
33
+
34
+ /**
35
+ * L2-normalize to a unit vector. Allocates a new `Float32Array`. A zero
36
+ * vector returns zeros (no division-by-zero).
37
+ */
38
+ export const normalize = (v: Float32Array): Float32Array => {
39
+ const n = l2Norm(v)
40
+ if (n === 0) return new Float32Array(v.length)
41
+ const out = new Float32Array(v.length)
42
+ for (let i = 0; i < v.length; i++) out[i] = v[i]! / n
43
+ return out
44
+ }
45
+
46
+ /**
47
+ * Cosine similarity. Range `[-1, 1]`; higher = more similar. Returns
48
+ * `NaN` if either vector has zero magnitude.
49
+ */
50
+ export const cosine = (a: Float32Array, b: Float32Array): number => {
51
+ let d = 0
52
+ let na = 0
53
+ let nb = 0
54
+ const n = Math.min(a.length, b.length)
55
+ for (let i = 0; i < n; i++) {
56
+ const ai = a[i]!
57
+ const bi = b[i]!
58
+ d += ai * bi
59
+ na += ai * ai
60
+ nb += bi * bi
61
+ }
62
+ return d / (Math.sqrt(na) * Math.sqrt(nb))
63
+ }
64
+
65
+ /** Euclidean (L2) distance. */
66
+ export const euclidean = (a: Float32Array, b: Float32Array): number => {
67
+ let s = 0
68
+ const n = Math.min(a.length, b.length)
69
+ for (let i = 0; i < n; i++) {
70
+ const d = a[i]! - b[i]!
71
+ s += d * d
72
+ }
73
+ return Math.sqrt(s)
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Sparse vectors (Record<string, number>)
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /** Inner product over the intersection of token keys. */
81
+ export const sparseDot = (a: SparseEmbedding, b: SparseEmbedding): number => {
82
+ // Iterate the smaller map; lookup against the larger one. O(min(|a|, |b|)).
83
+ const aSize = Object.keys(a.weights).length
84
+ const bSize = Object.keys(b.weights).length
85
+ const [smaller, larger] = aSize <= bSize ? [a.weights, b.weights] : [b.weights, a.weights]
86
+ let s = 0
87
+ for (const token in smaller) {
88
+ const other = larger[token]
89
+ if (other !== undefined) s += smaller[token]! * other
90
+ }
91
+ return s
92
+ }
93
+
94
+ /** L2 norm of a sparse vector. */
95
+ export const sparseL2Norm = (v: SparseEmbedding): number => {
96
+ let s = 0
97
+ for (const token in v.weights) {
98
+ const w = v.weights[token]!
99
+ s += w * w
100
+ }
101
+ return Math.sqrt(s)
102
+ }
103
+
104
+ /**
105
+ * Sparse cosine similarity. Range `[-1, 1]` (typically `[0, 1]` for
106
+ * learned-sparse encoders since weights are non-negative). Returns
107
+ * `NaN` if either vector has zero magnitude.
108
+ */
109
+ export const sparseCosine = (a: SparseEmbedding, b: SparseEmbedding): number =>
110
+ sparseDot(a, b) / (sparseL2Norm(a) * sparseL2Norm(b))
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Multivector / late-interaction (ColBERT-style)
114
+ // ---------------------------------------------------------------------------
115
+
116
+ /**
117
+ * MaxSim score for late-interaction retrieval. For each *query* vector,
118
+ * find the maximum dot product with any *document* vector, then sum.
119
+ *
120
+ * Captures fine-grained relevance that single-vector cosine smears out:
121
+ * each query token finds its own best-matching document token.
122
+ *
123
+ * Cost: O(|q| × |d| × dim). Fine at recipe volume; for production-scale
124
+ * retrieval use a vector store with native multivector indexing
125
+ * (Vespa, Qdrant, PLAID).
126
+ */
127
+ export const maxSim = (q: MultivectorEmbedding, d: MultivectorEmbedding): number => {
128
+ let total = 0
129
+ for (const qv of q.vectors) {
130
+ let best = -Infinity
131
+ for (const dv of d.vectors) {
132
+ const s = dot(qv, dv)
133
+ if (s > best) best = s
134
+ }
135
+ total += best
136
+ }
137
+ return total
138
+ }
@@ -38,7 +38,7 @@ export const timeToFirst =
38
38
  Effect.map(Option.map(({ elapsed }) => elapsed)),
39
39
  )
40
40
 
41
- export interface RatePoint<A> {
41
+ export type RatePoint<A> = {
42
42
  readonly value: A
43
43
  readonly total: number
44
44
  readonly ratePerSecond: number
@@ -6,7 +6,7 @@ import { Stream } from "effect"
6
6
  * - `data`: payload, with multiple `data:` lines joined by `\n`
7
7
  * - `id`: optional last-event id
8
8
  */
9
- export interface Event {
9
+ export type Event = {
10
10
  readonly event?: string
11
11
  readonly data: string
12
12
  readonly id?: string
@@ -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))
@@ -1,9 +1,8 @@
1
1
  /**
2
- * Pre-execution decision (`ToolDecision`) and post-execution result
3
- * (`ToolResult`) for the resolver-based executor.
2
+ * Post-execution and synthetic tool results.
4
3
  *
5
- * - Resolver returns ToolDecision (Execute | Reject(result)) per call.
6
- * - Executor emits ToolResult (Value | Failure) per call.
4
+ * - Executed tools emit ToolResult.Value.
5
+ * - Approval/cancellation policy emits synthetic ToolResult.Failure.
7
6
  *
8
7
  * Wire conversion stays at the recipe boundary via `toFunctionCallOutput`
9
8
  * so recipes can inspect, redact, or audit values before serialization.
@@ -12,7 +11,7 @@
12
11
  * and `unknown` would invite non-serializable values (Date, Map, BigInt,
13
12
  * fn). Recipes that want structured detail JSON.stringify themselves.
14
13
  */
15
- import { Match } from "effect"
14
+ import { Data } from "effect"
16
15
  import type { FunctionCall, FunctionCallOutput } from "../domain/Items.js"
17
16
  import { functionCallOutput } from "../domain/Items.js"
18
17
 
@@ -20,55 +19,44 @@ import { functionCallOutput } from "../domain/Items.js"
20
19
  // ToolResult
21
20
  // ---------------------------------------------------------------------------
22
21
 
23
- export type ToolResult =
24
- | {
25
- readonly _tag: "Value"
26
- readonly call_id: string
27
- readonly tool: string
28
- readonly value: unknown
29
- }
30
- | {
31
- readonly _tag: "Failure"
32
- readonly call_id: string
33
- readonly tool: string
34
- readonly kind: string
35
- readonly reason?: string
36
- }
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
+ }>
37
35
 
38
- export const isValue = (r: ToolResult): r is Extract<ToolResult, { _tag: "Value" }> =>
39
- r._tag === "Value"
40
-
41
- export const isFailure = (r: ToolResult): r is Extract<ToolResult, { _tag: "Failure" }> =>
42
- r._tag === "Failure"
43
-
44
- // ---------------------------------------------------------------------------
45
- // ToolDecision
46
- // ---------------------------------------------------------------------------
47
-
48
- export type ToolDecision =
49
- | { readonly _tag: "Execute" }
50
- | { readonly _tag: "Reject"; readonly result: ToolResult }
51
-
52
- export const execute: ToolDecision = { _tag: "Execute" }
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>()
53
45
 
54
- export const reject = (result: ToolResult): ToolDecision => ({ _tag: "Reject", result })
46
+ export const isValue = ToolResult.$is("Value")
47
+ export const isFailure = ToolResult.$is("Failure")
55
48
 
56
- // ---------------------------------------------------------------------------
57
49
  // Synthesizers. `denied` and `cancelled` are operationally distinct;
58
50
  // anything else is just a recipe-chosen `kind` via `rejected`.
59
51
  // ---------------------------------------------------------------------------
60
52
 
61
- export const rejected = (
62
- call: FunctionCall,
63
- kind: string,
64
- reason?: string,
65
- ): ToolResult => ({
66
- _tag: "Failure",
67
- call_id: call.call_id,
68
- tool: call.name,
69
- kind,
70
- ...(reason !== undefined ? { reason } : {}),
71
- })
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
+ })
72
60
 
73
61
  /** Explicit user/policy rejection. */
74
62
  export const denied = (call: FunctionCall, reason?: string): ToolResult =>
@@ -87,15 +75,13 @@ export const executionError = (call: FunctionCall, reason: string): ToolResult =
87
75
  // ---------------------------------------------------------------------------
88
76
 
89
77
  export const toFunctionCallOutput = (r: ToolResult): FunctionCallOutput =>
90
- Match.value(r).pipe(
91
- Match.tag("Value", (v) => functionCallOutput(v.call_id, JSON.stringify(v.value))),
92
- Match.tag("Failure", (f) =>
78
+ ToolResult.$match(r, {
79
+ Value: (v) => functionCallOutput(v.call_id, JSON.stringify(v.value)),
80
+ Failure: (f) =>
93
81
  functionCallOutput(
94
82
  f.call_id,
95
83
  JSON.stringify(
96
84
  f.reason !== undefined ? { kind: f.kind, reason: f.reason } : { kind: f.kind },
97
85
  ),
98
86
  ),
99
- ),
100
- Match.exhaustive,
101
- )
87
+ })