@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.
- package/README.md +2 -2
- package/dist/{AiError-CqmYjXyx.d.mts → AiError-CBuPHVKA.d.mts} +1 -1
- package/dist/{AiError-CqmYjXyx.d.mts.map → AiError-CBuPHVKA.d.mts.map} +1 -1
- package/dist/Image-BZmKfIdq.d.mts +61 -0
- package/dist/Image-BZmKfIdq.d.mts.map +1 -0
- package/dist/{Items-D1C2686t.d.mts → Items-CB8Bo3FI.d.mts} +132 -80
- package/dist/Items-CB8Bo3FI.d.mts.map +1 -0
- package/dist/Media-D_CpcM1Z.d.mts +57 -0
- package/dist/Media-D_CpcM1Z.d.mts.map +1 -0
- package/dist/{StructuredFormat-B5ueioNr.d.mts → StructuredFormat-BWq5Hd1O.d.mts} +5 -5
- package/dist/StructuredFormat-BWq5Hd1O.d.mts.map +1 -0
- package/dist/{Tool-5wxOCuOh.d.mts → Tool-DjVufH7i.d.mts} +13 -13
- package/dist/Tool-DjVufH7i.d.mts.map +1 -0
- package/dist/{Turn-rlTfuHaQ.d.mts → Turn-OPaILVIB.d.mts} +12 -29
- package/dist/Turn-OPaILVIB.d.mts.map +1 -0
- package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
- package/dist/dist-DV5ISja1.mjs +13782 -0
- package/dist/dist-DV5ISja1.mjs.map +1 -0
- package/dist/domain/AiError.d.mts +1 -1
- package/dist/domain/AiError.mjs +1 -1
- package/dist/domain/Image.d.mts +2 -0
- package/dist/domain/Image.mjs +58 -0
- package/dist/domain/Image.mjs.map +1 -0
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +19 -42
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Media.d.mts +2 -0
- package/dist/domain/Media.mjs +14 -0
- package/dist/domain/Media.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +2 -2
- package/dist/domain/Turn.mjs +12 -8
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/embedding-model/Embedding.d.mts +107 -0
- package/dist/embedding-model/Embedding.d.mts.map +1 -0
- package/dist/embedding-model/Embedding.mjs +18 -0
- package/dist/embedding-model/Embedding.mjs.map +1 -0
- package/dist/embedding-model/EmbeddingModel.d.mts +97 -0
- package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -0
- package/dist/embedding-model/EmbeddingModel.mjs +17 -0
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -0
- package/dist/index.d.mts +16 -8
- package/dist/index.mjs +10 -2
- package/dist/language-model/LanguageModel.d.mts +12 -20
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +3 -20
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +111 -2
- package/dist/loop/Loop.d.mts.map +1 -0
- package/dist/loop/Loop.mjs +39 -6
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.d.mts +1 -0
- package/dist/loop/Loop.test.mjs +411 -0
- package/dist/loop/Loop.test.mjs.map +1 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs +1013 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs.map +1 -0
- package/dist/math/Vector.d.mts +47 -0
- package/dist/math/Vector.d.mts.map +1 -0
- package/dist/math/Vector.mjs +117 -0
- package/dist/math/Vector.mjs.map +1 -0
- package/dist/observability/Metrics.d.mts +2 -2
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +1 -1
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/streaming/JSONL.mjs +1 -1
- package/dist/streaming/JSONL.test.d.mts +1 -0
- package/dist/streaming/JSONL.test.mjs +70 -0
- package/dist/streaming/JSONL.test.mjs.map +1 -0
- package/dist/streaming/Lines.mjs +1 -1
- package/dist/streaming/SSE.d.mts +2 -2
- package/dist/streaming/SSE.d.mts.map +1 -1
- package/dist/streaming/SSE.mjs +1 -1
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.d.mts +1 -0
- package/dist/streaming/SSE.test.mjs +72 -0
- package/dist/streaming/SSE.test.mjs.map +1 -0
- package/dist/structured-format/StructuredFormat.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.mjs +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/testing/MockProvider.d.mts +6 -6
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/tool/HistoryCheck.d.mts +6 -3
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +7 -1
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Outcome.d.mts +138 -2
- package/dist/tool/Outcome.d.mts.map +1 -0
- package/dist/tool/Outcome.mjs +34 -18
- package/dist/tool/Outcome.mjs.map +1 -1
- package/dist/tool/Resolvers.d.mts +30 -25
- package/dist/tool/Resolvers.d.mts.map +1 -1
- package/dist/tool/Resolvers.mjs +54 -44
- package/dist/tool/Resolvers.mjs.map +1 -1
- package/dist/tool/Resolvers.test.d.mts +1 -0
- package/dist/tool/Resolvers.test.mjs +317 -0
- package/dist/tool/Resolvers.test.mjs.map +1 -0
- package/dist/tool/Tool.d.mts +1 -1
- package/dist/tool/Tool.mjs +1 -1
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +151 -2
- package/dist/tool/ToolEvent.d.mts.map +1 -0
- package/dist/tool/ToolEvent.mjs +30 -4
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +24 -15
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +14 -13
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.d.mts +1 -0
- package/dist/tool/Toolkit.test.mjs +113 -0
- package/dist/tool/Toolkit.test.mjs.map +1 -0
- package/package.json +29 -13
- package/src/domain/Image.ts +75 -0
- package/src/domain/Items.ts +18 -47
- package/src/domain/Media.ts +61 -0
- package/src/domain/Turn.ts +7 -17
- package/src/embedding-model/Embedding.ts +117 -0
- package/src/embedding-model/EmbeddingModel.ts +107 -0
- package/src/index.ts +9 -1
- package/src/language-model/LanguageModel.ts +2 -22
- package/src/loop/Loop.test.ts +114 -2
- package/src/loop/Loop.ts +69 -5
- package/src/math/Vector.ts +138 -0
- package/src/observability/Metrics.ts +1 -1
- package/src/streaming/SSE.ts +1 -1
- package/src/structured-format/StructuredFormat.ts +2 -2
- package/src/testing/MockProvider.ts +2 -2
- package/src/tool/HistoryCheck.ts +2 -5
- package/src/tool/Outcome.ts +39 -53
- package/src/tool/Resolvers.test.ts +46 -117
- package/src/tool/Resolvers.ts +74 -102
- package/src/tool/Tool.ts +9 -9
- package/src/tool/ToolEvent.ts +30 -26
- package/src/tool/Toolkit.test.ts +97 -2
- package/src/tool/Toolkit.ts +65 -67
- package/dist/Items-D1C2686t.d.mts.map +0 -1
- package/dist/Loop-CzSJo1h8.d.mts +0 -87
- package/dist/Loop-CzSJo1h8.d.mts.map +0 -1
- package/dist/Outcome-C2JYknCu.d.mts +0 -40
- package/dist/Outcome-C2JYknCu.d.mts.map +0 -1
- package/dist/StructuredFormat-B5ueioNr.d.mts.map +0 -1
- package/dist/Tool-5wxOCuOh.d.mts.map +0 -1
- package/dist/ToolEvent-B2N10hr3.d.mts +0 -29
- package/dist/ToolEvent-B2N10hr3.d.mts.map +0 -1
- package/dist/Turn-rlTfuHaQ.d.mts.map +0 -1
- package/dist/match/Match.d.mts +0 -16
- package/dist/match/Match.d.mts.map +0 -1
- package/dist/match/Match.mjs +0 -15
- package/dist/match/Match.mjs.map +0 -1
- 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 {
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/src/streaming/SSE.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
22
|
+
export type MockRecorder = {
|
|
23
23
|
readonly calls: ReadonlyArray<{
|
|
24
24
|
readonly history: ReadonlyArray<Item>
|
|
25
25
|
readonly turn: Turn
|
package/src/tool/HistoryCheck.ts
CHANGED
|
@@ -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))
|
package/src/tool/Outcome.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (`ToolResult`) for the resolver-based executor.
|
|
2
|
+
* Post-execution and synthetic tool results.
|
|
4
3
|
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
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 {
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
})
|