@effect-uai/core 0.7.0 → 0.9.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 (137) hide show
  1. package/dist/{AiError-CAX_48RU.d.mts → AiError-CrdR1EQi.d.mts} +2 -2
  2. package/dist/{AiError-CAX_48RU.d.mts.map → AiError-CrdR1EQi.d.mts.map} +1 -1
  3. package/dist/{Audio-DPlTsNdz.d.mts → Audio-DDSKCkSe.d.mts} +2 -2
  4. package/dist/{Audio-DPlTsNdz.d.mts.map → Audio-DDSKCkSe.d.mts.map} +1 -1
  5. package/dist/{Image-HNmMpMTh.d.mts → Image-C_Zjpwhc.d.mts} +2 -2
  6. package/dist/{Image-HNmMpMTh.d.mts.map → Image-C_Zjpwhc.d.mts.map} +1 -1
  7. package/dist/{Items-D8UaXQx_.d.mts → Items-CgKD6Npw.d.mts} +5 -5
  8. package/dist/{Items-D8UaXQx_.d.mts.map → Items-CgKD6Npw.d.mts.map} +1 -1
  9. package/dist/Loop-B5_cqumi.d.mts +173 -0
  10. package/dist/Loop-B5_cqumi.d.mts.map +1 -0
  11. package/dist/{Media-D_CpcM1Z.d.mts → Media-DOLyaygI.d.mts} +1 -1
  12. package/dist/{Media-D_CpcM1Z.d.mts.map → Media-DOLyaygI.d.mts.map} +1 -1
  13. package/dist/{SandboxError-BvKypkJm.d.mts → SandboxError-CIrVDOMV.d.mts} +2 -2
  14. package/dist/{SandboxError-BvKypkJm.d.mts.map → SandboxError-CIrVDOMV.d.mts.map} +1 -1
  15. package/dist/{StructuredFormat-BbN4dosH.d.mts → StructuredFormat-CMJFD3sn.d.mts} +2 -2
  16. package/dist/{StructuredFormat-BbN4dosH.d.mts.map → StructuredFormat-CMJFD3sn.d.mts.map} +1 -1
  17. package/dist/Tool-0XUIT2A2.d.mts +232 -0
  18. package/dist/Tool-0XUIT2A2.d.mts.map +1 -0
  19. package/dist/ToolEvent-DYrAUJGs.d.mts +151 -0
  20. package/dist/ToolEvent-DYrAUJGs.d.mts.map +1 -0
  21. package/dist/ToolResult-BZd-cEWW.d.mts +151 -0
  22. package/dist/ToolResult-BZd-cEWW.d.mts.map +1 -0
  23. package/dist/Toolkit-BKC8b8Xo.d.mts +155 -0
  24. package/dist/Toolkit-BKC8b8Xo.d.mts.map +1 -0
  25. package/dist/{dist-DV5ISja1.mjs → dist-B04dlFHg.mjs} +3 -3
  26. package/dist/{dist-DV5ISja1.mjs.map → dist-B04dlFHg.mjs.map} +1 -1
  27. package/dist/dist-DWgRbDN3.mjs +140 -0
  28. package/dist/dist-DWgRbDN3.mjs.map +1 -0
  29. package/dist/domain/AiError.d.mts +1 -1
  30. package/dist/domain/Audio.d.mts +1 -1
  31. package/dist/domain/Image.d.mts +1 -1
  32. package/dist/domain/Items.d.mts +1 -1
  33. package/dist/domain/Media.d.mts +1 -1
  34. package/dist/domain/Music.d.mts +1 -1
  35. package/dist/domain/Turn.d.mts +4 -4
  36. package/dist/domain/Turn.test.mjs +1 -1
  37. package/dist/embedding-model/Embedding.d.mts +1 -1
  38. package/dist/embedding-model/EmbeddingModel.d.mts +1 -1
  39. package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
  40. package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
  41. package/dist/index.d.mts +16 -13
  42. package/dist/index.mjs +5 -2
  43. package/dist/language-model/LanguageModel.d.mts +10 -5
  44. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  45. package/dist/language-model/LanguageModel.mjs.map +1 -1
  46. package/dist/language-model/LanguageModel.test.mjs +1 -1
  47. package/dist/loop/Loop.d.mts +2 -141
  48. package/dist/loop/Loop.mjs +16 -20
  49. package/dist/loop/Loop.mjs.map +1 -1
  50. package/dist/loop/Loop.test.mjs +18 -1
  51. package/dist/loop/Loop.test.mjs.map +1 -1
  52. package/dist/{magic-string.es-BgIV5Mu3.mjs → magic-string.es-Dk-MMA0F.mjs} +1 -1
  53. package/dist/{magic-string.es-BgIV5Mu3.mjs.map → magic-string.es-Dk-MMA0F.mjs.map} +1 -1
  54. package/dist/music-generator/MusicGenerator.d.mts +2 -2
  55. package/dist/music-generator/MusicGenerator.test.mjs +1 -1
  56. package/dist/observability/Metrics.d.mts +116 -28
  57. package/dist/observability/Metrics.d.mts.map +1 -1
  58. package/dist/observability/Metrics.mjs +250 -38
  59. package/dist/observability/Metrics.mjs.map +1 -1
  60. package/dist/observability/Metrics.test.d.mts +1 -0
  61. package/dist/observability/Metrics.test.mjs +187 -0
  62. package/dist/observability/Metrics.test.mjs.map +1 -0
  63. package/dist/observability/Telemetry.d.mts +37 -0
  64. package/dist/observability/Telemetry.d.mts.map +1 -0
  65. package/dist/observability/Telemetry.mjs +57 -0
  66. package/dist/observability/Telemetry.mjs.map +1 -0
  67. package/dist/observability/Telemetry.test.d.mts +1 -0
  68. package/dist/observability/Telemetry.test.mjs +92 -0
  69. package/dist/observability/Telemetry.test.mjs.map +1 -0
  70. package/dist/retry/Retry.d.mts +2 -2
  71. package/dist/retry/Retry.test.mjs +1 -1
  72. package/dist/sandbox/Sandbox.d.mts +1 -1
  73. package/dist/sandbox/SandboxError.d.mts +1 -1
  74. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +2 -2
  75. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
  76. package/dist/streaming/JSONL.d.mts +1 -1
  77. package/dist/streaming/JSONL.test.mjs +1 -1
  78. package/dist/streaming/SSE.test.mjs +1 -1
  79. package/dist/structured-format/StructuredFormat.d.mts +1 -1
  80. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  81. package/dist/structured-format/StructuredFormat.test.mjs +1 -1
  82. package/dist/testing/MockMusicGenerator.d.mts +1 -1
  83. package/dist/testing/MockProvider.d.mts +1 -1
  84. package/dist/testing/MockSpeechSynthesizer.d.mts +1 -1
  85. package/dist/tool/Approval.d.mts +3 -3
  86. package/dist/tool/Approval.test.mjs +35 -34
  87. package/dist/tool/Approval.test.mjs.map +1 -1
  88. package/dist/tool/HistoryCheck.d.mts +2 -2
  89. package/dist/tool/Tool.d.mts +2 -2
  90. package/dist/tool/Tool.mjs +112 -22
  91. package/dist/tool/Tool.mjs.map +1 -1
  92. package/dist/tool/Tool.test.mjs +41 -3
  93. package/dist/tool/Tool.test.mjs.map +1 -1
  94. package/dist/tool/ToolEvent.d.mts +2 -151
  95. package/dist/tool/ToolResult.d.mts +2 -138
  96. package/dist/tool/ToolResult.mjs +18 -3
  97. package/dist/tool/ToolResult.mjs.map +1 -1
  98. package/dist/tool/Toolkit.d.mts +2 -73
  99. package/dist/tool/Toolkit.mjs +106 -28
  100. package/dist/tool/Toolkit.mjs.map +1 -1
  101. package/dist/tool/Toolkit.test.mjs +113 -7
  102. package/dist/tool/Toolkit.test.mjs.map +1 -1
  103. package/dist/transcriber/Transcriber.d.mts +2 -2
  104. package/dist/transcriber/Transcriber.test.mjs +1 -1
  105. package/dist/web-search/WebSearch.d.mts +119 -0
  106. package/dist/web-search/WebSearch.d.mts.map +1 -0
  107. package/dist/web-search/WebSearch.mjs +42 -0
  108. package/dist/web-search/WebSearch.mjs.map +1 -0
  109. package/dist/web-search/WebSearchTool.d.mts +58 -0
  110. package/dist/web-search/WebSearchTool.d.mts.map +1 -0
  111. package/dist/web-search/WebSearchTool.mjs +69 -0
  112. package/dist/web-search/WebSearchTool.mjs.map +1 -0
  113. package/package.json +9 -1
  114. package/src/embedding-model/EmbeddingModel.test.ts +4 -0
  115. package/src/index.ts +3 -0
  116. package/src/language-model/LanguageModel.ts +7 -2
  117. package/src/loop/Loop.test.ts +22 -0
  118. package/src/loop/Loop.ts +27 -4
  119. package/src/observability/Metrics.test.ts +266 -0
  120. package/src/observability/Metrics.ts +505 -65
  121. package/src/observability/Telemetry.test.ts +123 -0
  122. package/src/observability/Telemetry.ts +96 -0
  123. package/src/structured-format/StructuredFormat.ts +4 -0
  124. package/src/tool/Approval.test.ts +22 -21
  125. package/src/tool/Tool.test.ts +55 -2
  126. package/src/tool/Tool.ts +221 -55
  127. package/src/tool/ToolResult.ts +22 -1
  128. package/src/tool/Toolkit.test.ts +168 -7
  129. package/src/tool/Toolkit.ts +336 -99
  130. package/src/web-search/WebSearch.ts +150 -0
  131. package/src/web-search/WebSearchTool.ts +106 -0
  132. package/dist/Tool-CiFXYjmR.d.mts +0 -102
  133. package/dist/Tool-CiFXYjmR.d.mts.map +0 -1
  134. package/dist/loop/Loop.d.mts.map +0 -1
  135. package/dist/tool/ToolEvent.d.mts.map +0 -1
  136. package/dist/tool/ToolResult.d.mts.map +0 -1
  137. package/dist/tool/Toolkit.d.mts.map +0 -1
@@ -0,0 +1,266 @@
1
+ import { describe, it } from "@effect/vitest"
2
+ import { Array as Arr, Duration, Effect, Fiber, Option, Stream } from "effect"
3
+ import * as TestClock from "effect/testing/TestClock"
4
+ import { expect } from "vitest"
5
+ import type { Usage } from "../domain/Items.js"
6
+ import { TurnEvent } from "../domain/Turn.js"
7
+ import {
8
+ type MetricEvent,
9
+ type Throughput,
10
+ type TimeToCompletion,
11
+ type TimeToFirstToken,
12
+ type TokenTotals,
13
+ computeThroughputTick,
14
+ isMetricEvent,
15
+ makeEvent,
16
+ metricEvents,
17
+ throughput,
18
+ timeToCompletion,
19
+ timeToFirstToken,
20
+ tokenTotals,
21
+ } from "./Metrics.js"
22
+
23
+ const turnComplete = (usage: Usage): TurnEvent =>
24
+ TurnEvent.TurnComplete({ turn: { items: [], usage, stop_reason: "stop" } })
25
+
26
+ const text = (s: string): TurnEvent => TurnEvent.TextDelta({ text: s })
27
+
28
+ const samples = (events: ReadonlyArray<unknown>): ReadonlyArray<MetricEvent> =>
29
+ events.filter(isMetricEvent)
30
+
31
+ const head = <A>(xs: ReadonlyArray<A>): A => Option.getOrThrow(Arr.head(xs))
32
+
33
+ const tagged = <T extends MetricEvent["_tag"]>(
34
+ events: ReadonlyArray<unknown>,
35
+ tag: T,
36
+ ): ReadonlyArray<Extract<MetricEvent, { readonly _tag: T }>> =>
37
+ samples(events).filter((e): e is Extract<MetricEvent, { readonly _tag: T }> => e._tag === tag)
38
+
39
+ // A stream that emits each event after the given delay since the previous one.
40
+ const timed = <A>(steps: ReadonlyArray<readonly [Duration.Input, A]>): Stream.Stream<A> =>
41
+ Stream.fromIterable(steps).pipe(
42
+ Stream.mapEffect(([delay, ev]) => Effect.as(Effect.sleep(delay), ev)),
43
+ )
44
+
45
+ // Collect a metered stream while advancing the test clock by each step, so
46
+ // every per-event clock read lands at a known virtual time.
47
+ const runTimed = <A>(
48
+ metered: Stream.Stream<A>,
49
+ steps: ReadonlyArray<Duration.Input>,
50
+ ): Effect.Effect<ReadonlyArray<A>> =>
51
+ Effect.gen(function* () {
52
+ const fiber = yield* Effect.forkChild(Stream.runCollect(metered))
53
+ yield* Effect.forEach(steps, (step) => TestClock.adjust(step), { discard: true })
54
+ return yield* Fiber.join(fiber)
55
+ })
56
+
57
+ describe("tokenTotals", () => {
58
+ it.effect("turn level: a single TurnComplete reports that generation's usage", () =>
59
+ Effect.gen(function* () {
60
+ const stream = Stream.make(
61
+ text("hi"),
62
+ turnComplete({ input_tokens: 100, output_tokens: 20, total_tokens: 120 }),
63
+ )
64
+ const out = yield* Stream.runCollect(stream.pipe(tokenTotals))
65
+ const totals = tagged(out, "TokenTotals") as ReadonlyArray<TokenTotals>
66
+ expect(totals).toHaveLength(1)
67
+ expect(head(totals).turnIndex).toBe(0)
68
+ expect(head(totals).usage.output_tokens).toBe(20)
69
+ expect(head(totals).cumulative.output_tokens).toBe(20)
70
+ }),
71
+ )
72
+
73
+ it.effect("loop level: cumulative sums across every turn, usage stays per-turn", () =>
74
+ Effect.gen(function* () {
75
+ const stream = Stream.make(
76
+ text("a"),
77
+ turnComplete({ input_tokens: 1000, output_tokens: 120 }),
78
+ turnComplete({ input_tokens: 1300, output_tokens: 410 }),
79
+ )
80
+ const out = yield* Stream.runCollect(stream.pipe(tokenTotals))
81
+ const totals = tagged(out, "TokenTotals") as ReadonlyArray<TokenTotals>
82
+ expect(totals.map((t) => t.turnIndex)).toEqual([0, 1])
83
+ expect(totals.map((t) => t.usage.output_tokens)).toEqual([120, 410])
84
+ expect(totals.map((t) => t.cumulative.input_tokens)).toEqual([1000, 2300])
85
+ expect(totals.map((t) => t.cumulative.output_tokens)).toEqual([120, 530])
86
+ }),
87
+ )
88
+
89
+ it.effect("emits an incremental output_tokens counter measurement per turn", () =>
90
+ Effect.gen(function* () {
91
+ const stream = Stream.make(turnComplete({ output_tokens: 7 }))
92
+ const out = yield* Stream.runCollect(stream.pipe(tokenTotals))
93
+ const m = head(samples(out)).measurements.find((m) => m.name === "effect_uai_output_tokens")
94
+ expect(m).toEqual({ name: "effect_uai_output_tokens", kind: "counter", value: 7 })
95
+ }),
96
+ )
97
+ })
98
+
99
+ describe("timeToFirstToken", () => {
100
+ it.effect("fires on the first content delta, measured from request (stream init)", () =>
101
+ Effect.gen(function* () {
102
+ const stream = timed<TurnEvent>([
103
+ ["300 millis", text("hello")],
104
+ ["700 millis", turnComplete({})],
105
+ ]).pipe(timeToFirstToken())
106
+ const out = yield* runTimed(stream, ["300 millis", "700 millis"])
107
+ const ttft = tagged(out, "TimeToFirstToken") as ReadonlyArray<TimeToFirstToken>
108
+ expect(ttft).toHaveLength(1)
109
+ expect(Duration.toMillis(head(ttft).elapsed)).toBe(300)
110
+ expect(head(ttft).kind).toBe("text")
111
+ }),
112
+ )
113
+
114
+ it.effect("reports kind reasoning when reasoning arrives first", () =>
115
+ Effect.gen(function* () {
116
+ const stream = timed<TurnEvent>([
117
+ ["120 millis", TurnEvent.ReasoningDelta({ text: "...", kind: "trace" })],
118
+ ["80 millis", text("answer")],
119
+ ["50 millis", turnComplete({})],
120
+ ]).pipe(timeToFirstToken())
121
+ const out = yield* runTimed(stream, ["120 millis", "80 millis", "50 millis"])
122
+ const ttft = tagged(out, "TimeToFirstToken") as ReadonlyArray<TimeToFirstToken>
123
+ expect(ttft).toHaveLength(1)
124
+ expect(head(ttft).kind).toBe("reasoning")
125
+ expect(Duration.toMillis(head(ttft).elapsed)).toBe(120)
126
+ }),
127
+ )
128
+
129
+ it.effect("loop level: one TTFT per turn, turnIndex increments", () =>
130
+ Effect.gen(function* () {
131
+ const stream = timed<TurnEvent>([
132
+ ["200 millis", text("a")],
133
+ ["100 millis", turnComplete({})],
134
+ ["300 millis", text("b")],
135
+ ["100 millis", turnComplete({})],
136
+ ]).pipe(timeToFirstToken())
137
+ const out = yield* runTimed(stream, ["200 millis", "100 millis", "300 millis", "100 millis"])
138
+ const ttft = tagged(out, "TimeToFirstToken") as ReadonlyArray<TimeToFirstToken>
139
+ expect(ttft.map((t) => t.turnIndex)).toEqual([0, 1])
140
+ // turn 0 from init; turn 1 from the previous TurnComplete (loop-scope approximation)
141
+ expect(ttft.map((t) => Duration.toMillis(t.elapsed))).toEqual([200, 300])
142
+ }),
143
+ )
144
+ })
145
+
146
+ describe("timeToCompletion", () => {
147
+ it.effect("reports duration (request->complete) and generation (first token->complete)", () =>
148
+ Effect.gen(function* () {
149
+ const stream = timed<TurnEvent>([
150
+ ["300 millis", text("hi")],
151
+ ["700 millis", turnComplete({})],
152
+ ]).pipe(timeToCompletion)
153
+ const out = yield* runTimed(stream, ["300 millis", "700 millis"])
154
+ const done = tagged(out, "TimeToCompletion") as ReadonlyArray<TimeToCompletion>
155
+ expect(done).toHaveLength(1)
156
+ expect(Duration.toMillis(head(done).duration)).toBe(1000)
157
+ expect(Duration.toMillis(head(done).generation)).toBe(700)
158
+ }),
159
+ )
160
+ })
161
+
162
+ describe("computeThroughputTick", () => {
163
+ const base = {
164
+ units: 0,
165
+ lastUnits: 0,
166
+ lastMillis: 0,
167
+ firstMillis: Option.none<number>(),
168
+ smoothed: Option.none<number>(),
169
+ turnIndex: 0,
170
+ }
171
+
172
+ it("returns None before the first token", () => {
173
+ expect(Option.isNone(computeThroughputTick(base, 500, "windowed", undefined))).toBe(true)
174
+ })
175
+
176
+ it("windowed: rate is units in the last interval over that interval", () => {
177
+ const state = {
178
+ ...base,
179
+ units: 30,
180
+ lastUnits: 10,
181
+ lastMillis: 500,
182
+ firstMillis: Option.some(100),
183
+ }
184
+ const r = computeThroughputTick(state, 1000, "windowed", undefined)
185
+ // (30 - 10) units over (1000 - 500) ms = 40/s
186
+ expect(Option.getOrThrow(r).rate).toBe(40)
187
+ })
188
+
189
+ it("cumulative: rate is total units over time since first token", () => {
190
+ const state = {
191
+ ...base,
192
+ units: 30,
193
+ lastUnits: 10,
194
+ lastMillis: 500,
195
+ firstMillis: Option.some(100),
196
+ }
197
+ const r = computeThroughputTick(state, 1000, "cumulative", undefined)
198
+ // 30 units over (1000 - 100) ms ~= 33.33/s
199
+ expect(Option.getOrThrow(r).rate).toBeCloseTo(33.333, 2)
200
+ })
201
+
202
+ it("smooth: blends the instantaneous rate with the carried average", () => {
203
+ const state = {
204
+ ...base,
205
+ units: 20,
206
+ lastUnits: 0,
207
+ lastMillis: 0,
208
+ firstMillis: Option.some(0),
209
+ smoothed: Option.some(100),
210
+ }
211
+ // windowed instant = 20 units / 1000 ms = 20/s; smoothed = 0.5*20 + 0.5*100 = 60
212
+ const r = computeThroughputTick(state, 1000, "windowed", 0.5)
213
+ expect(Option.getOrThrow(r).rate).toBe(60)
214
+ })
215
+ })
216
+
217
+ describe("throughput (metronome)", () => {
218
+ it.effect("emits a live char rate on the tick, over the elapsed window", () =>
219
+ Effect.gen(function* () {
220
+ // 10 chars land at t=0; the source then stays open so the metronome can
221
+ // tick. take(1) ends the stream on the first sample, so termination does
222
+ // not depend on the source closing.
223
+ const source = Stream.make(text("aaaaa"), text("aaaaa")).pipe(
224
+ Stream.concat(timed<TurnEvent>([["10 seconds", turnComplete({})]])),
225
+ )
226
+ const stream = source.pipe(
227
+ throughput({ every: "1 second", unit: "char" }),
228
+ metricEvents,
229
+ Stream.take(1),
230
+ )
231
+ const out = yield* runTimed(stream, ["1 second"])
232
+ const rates = out.filter((e): e is Throughput => e._tag === "Throughput")
233
+ expect(rates).toHaveLength(1)
234
+ expect(head(rates).unit).toBe("char")
235
+ expect(head(rates).turnIndex).toBe(0)
236
+ // exact rate math is pinned by the computeThroughputTick tests; here we
237
+ // only assert the metronome wired a positive char rate onto the tick.
238
+ expect(head(rates).ratePerSecond).toBeGreaterThan(0)
239
+ }),
240
+ )
241
+ })
242
+
243
+ describe("custom events and helpers", () => {
244
+ it("makeEvent stamps the brand so isMetricEvent and the recorder see it", () => {
245
+ const ev = makeEvent({
246
+ _tag: "ToolLatency",
247
+ turnIndex: 0,
248
+ measurements: [{ name: "myapp_tool_latency", kind: "timer", value: Duration.millis(42) }],
249
+ })
250
+ expect(isMetricEvent(ev)).toBe(true)
251
+ expect(isMetricEvent(text("x"))).toBe(false)
252
+ expect(isMetricEvent(turnComplete({}))).toBe(false)
253
+ })
254
+
255
+ it.effect("metricEvents projects a mixed stream onto just the samples", () =>
256
+ Effect.gen(function* () {
257
+ const stream = Stream.make(text("hi"), turnComplete({ output_tokens: 3 })).pipe(
258
+ tokenTotals,
259
+ metricEvents,
260
+ )
261
+ const out = yield* Stream.runCollect(stream)
262
+ expect(out.every(isMetricEvent)).toBe(true)
263
+ expect(out.map((e) => e._tag)).toEqual(["TokenTotals"])
264
+ }),
265
+ )
266
+ })