@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
@@ -1,52 +1,264 @@
1
1
  import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
2
- import { Clock, Duration, Effect, Option, Stream } from "effect";
2
+ import { Array, Clock, Duration, Effect, Match, Option, Predicate, Ref, Result, Schedule, Stream } from "effect";
3
3
  //#region src/observability/Metrics.ts
4
4
  var Metrics_exports = /* @__PURE__ */ __exportAll({
5
- timeToFirst: () => timeToFirst,
6
- withElapsed: () => withElapsed,
7
- withRate: () => withRate
5
+ MetricEventTypeId: () => MetricEventTypeId,
6
+ allMetrics: () => allMetrics,
7
+ computeThroughputTick: () => computeThroughputTick,
8
+ isMetricEvent: () => isMetricEvent,
9
+ makeEvent: () => makeEvent,
10
+ metricEvents: () => metricEvents,
11
+ throughput: () => throughput,
12
+ timeToCompletion: () => timeToCompletion,
13
+ timeToFirstToken: () => timeToFirstToken,
14
+ tokenTotals: () => tokenTotals
8
15
  });
9
16
  /**
10
- * Annotate every event in a stream with the elapsed `Duration` since the
11
- * stream started consuming. The first event reports its time-from-start,
12
- * which is also the conventional "time to first ____" metric.
17
+ * Brand stamped on every metric event. `isMetricEvent` checks this symbol,
18
+ * not an enumerated `_tag`, so events are distinguished from `TurnEvent`s,
19
+ * tool outputs, and arbitrary custom loop values regardless of their tag.
20
+ * This is the same TypeId-symbol pattern Effect uses for `isStream` etc.
13
21
  */
14
- const withElapsed = (self) => Stream.unwrap(Effect.map(Clock.currentTimeMillis, (start) => self.pipe(Stream.mapEffect((value) => Effect.map(Clock.currentTimeMillis, (now) => ({
15
- value,
16
- elapsed: Duration.millis(now - start)
17
- }))))));
22
+ const MetricEventTypeId = Symbol.for("@effect-uai/core/observability/MetricEvent");
18
23
  /**
19
- * Compute the elapsed time until the first event matching the predicate.
20
- * Returns `Option.none()` if the stream completes without one.
21
- *
22
- * Consumes the stream. To track this *alongside* live consumption, use
23
- * `Stream.broadcast` to fan the source out and run `timeToFirst` on one
24
- * branch.
24
+ * Mint a custom metric event. Stamp it with the brand and provide a
25
+ * well-formed `measurements` array; `Telemetry.record` will export it with no
26
+ * changes to the recorder. The only constraint is that each measurement's
27
+ * `kind` is one the recorder maps.
25
28
  */
26
- const timeToFirst = (predicate) => (self) => withElapsed(self).pipe(Stream.filter(({ value }) => predicate(value)), Stream.runHead, Effect.map(Option.map(({ elapsed }) => elapsed)));
29
+ const makeEvent = (fields) => ({
30
+ [MetricEventTypeId]: MetricEventTypeId,
31
+ ...fields
32
+ });
33
+ /** Structural (brand-based) guard for any metric event, built-in or custom. */
34
+ const isMetricEvent = (u) => Predicate.hasProperty(u, MetricEventTypeId);
27
35
  /**
28
- * Annotate every event with a running total and a rolling rate per second,
29
- * computed from a user-supplied weight function.
30
- *
31
- * The weight is the unit you care about - bytes, tokens, error count, etc.
32
- * For tokens-per-second on `TurnEvent`, pass:
33
- *
34
- * `(d) => d._tag === "TextDelta" ? countTokens(d.text) : 0`
35
- *
36
- * Use any tokenizer you like; the library does not ship one.
36
+ * Project a stream of mixed events onto just its metric samples, for a side
37
+ * channel or a frontend feed.
37
38
  */
38
- const withRate = (weight) => (self) => Stream.unwrap(Effect.map(Clock.currentTimeMillis, (start) => self.pipe(Stream.mapAccumEffect(() => ({ total: 0 }), (acc, value) => Effect.map(Clock.currentTimeMillis, (now) => {
39
- const total = acc.total + weight(value);
40
- const elapsedMs = now - start;
41
- const ratePerSecond = elapsedMs > 0 ? total / elapsedMs * 1e3 : 0;
42
- return [{ total }, [{
43
- value,
44
- total,
45
- ratePerSecond,
46
- elapsed: Duration.millis(elapsedMs)
47
- }]];
39
+ const metricEvents = (self) => self.pipe(Stream.filterMap((ev) => isMetricEvent(ev) ? Result.succeed(ev) : Result.failVoid));
40
+ const MetricName = {
41
+ ttft: "effect_uai_turn_ttft",
42
+ duration: "effect_uai_turn_duration",
43
+ generation: "effect_uai_turn_generation",
44
+ inputTokens: "effect_uai_input_tokens",
45
+ outputTokens: "effect_uai_output_tokens",
46
+ totalTokens: "effect_uai_total_tokens",
47
+ reasoningTokens: "effect_uai_reasoning_tokens",
48
+ cachedInputTokens: "effect_uai_cached_input_tokens"
49
+ };
50
+ /** Read a structural `_tag` off any value without narrowing to a closed union. */
51
+ const tagOf = (u) => Predicate.hasProperty(u, "_tag") && Predicate.isString(u._tag) ? u._tag : void 0;
52
+ const contentDeltaKind = (ev) => {
53
+ const t = tagOf(ev);
54
+ return t === "TextDelta" ? Option.some("text") : t === "ReasoningDelta" ? Option.some("reasoning") : Option.none();
55
+ };
56
+ const isTurnCompleteEvent = (ev) => tagOf(ev) === "TurnComplete";
57
+ const turnOf = (ev) => ev.turn;
58
+ const sumOptional = (a, b) => a === void 0 && b === void 0 ? void 0 : (a ?? 0) + (b ?? 0);
59
+ const addUsage = (a, b) => {
60
+ const cached = sumOptional(a.input_tokens_details?.cached_tokens, b.input_tokens_details?.cached_tokens);
61
+ const reasoning = sumOptional(a.output_tokens_details?.reasoning_tokens, b.output_tokens_details?.reasoning_tokens);
62
+ return {
63
+ input_tokens: sumOptional(a.input_tokens, b.input_tokens),
64
+ output_tokens: sumOptional(a.output_tokens, b.output_tokens),
65
+ total_tokens: sumOptional(a.total_tokens, b.total_tokens),
66
+ input_tokens_details: cached === void 0 ? void 0 : { cached_tokens: cached },
67
+ output_tokens_details: reasoning === void 0 ? void 0 : { reasoning_tokens: reasoning }
68
+ };
69
+ };
70
+ /** Per-turn token counts as incremental counter measurements (present fields only). */
71
+ const usageMeasurements = (usage) => {
72
+ const pairs = [
73
+ [MetricName.inputTokens, usage.input_tokens],
74
+ [MetricName.outputTokens, usage.output_tokens],
75
+ [MetricName.totalTokens, usage.total_tokens],
76
+ [MetricName.reasoningTokens, usage.output_tokens_details?.reasoning_tokens],
77
+ [MetricName.cachedInputTokens, usage.input_tokens_details?.cached_tokens]
78
+ ];
79
+ return Array.filterMap(pairs, ([name, value]) => value === void 0 ? Result.failVoid : Result.succeed({
80
+ name,
81
+ kind: "counter",
82
+ value
83
+ }));
84
+ };
85
+ const timeToFirstToken = (options) => (self) => {
86
+ const fromRequest = (options?.from ?? "request") === "request";
87
+ return Stream.unwrap(Effect.map(Clock.currentTimeMillis, (init) => self.pipe(Stream.mapAccumEffect(() => ({
88
+ turnIndex: 0,
89
+ turnStart: init,
90
+ started: fromRequest,
91
+ seen: false
92
+ }), (state, ev) => Effect.map(Clock.currentTimeMillis, (now) => {
93
+ const started = state.started ? state : {
94
+ ...state,
95
+ turnStart: now,
96
+ started: true
97
+ };
98
+ const kind = contentDeltaKind(ev);
99
+ const emitTtft = !started.seen && Option.isSome(kind);
100
+ const out = emitTtft ? [ev, makeEvent({
101
+ _tag: "TimeToFirstToken",
102
+ turnIndex: started.turnIndex,
103
+ elapsed: Duration.millis(now - started.turnStart),
104
+ kind: kind.value,
105
+ measurements: [{
106
+ name: MetricName.ttft,
107
+ kind: "timer",
108
+ value: Duration.millis(now - started.turnStart)
109
+ }]
110
+ })] : [ev];
111
+ return [isTurnCompleteEvent(ev) ? {
112
+ turnIndex: started.turnIndex + 1,
113
+ turnStart: now,
114
+ started: fromRequest,
115
+ seen: false
116
+ } : {
117
+ ...started,
118
+ seen: started.seen || emitTtft
119
+ }, out];
120
+ })))));
121
+ };
122
+ const timeToCompletion = (self) => Stream.unwrap(Effect.map(Clock.currentTimeMillis, (init) => self.pipe(Stream.mapAccumEffect(() => ({
123
+ turnIndex: 0,
124
+ turnStart: init,
125
+ firstTokenAt: Option.none()
126
+ }), (state, ev) => Effect.map(Clock.currentTimeMillis, (now) => {
127
+ const firstTokenAt = Option.isNone(state.firstTokenAt) && Option.isSome(contentDeltaKind(ev)) ? Option.some(now) : state.firstTokenAt;
128
+ if (!isTurnCompleteEvent(ev)) return [{
129
+ ...state,
130
+ firstTokenAt
131
+ }, [ev]];
132
+ const duration = Duration.millis(now - state.turnStart);
133
+ const generation = Option.match(firstTokenAt, {
134
+ onNone: () => Duration.zero,
135
+ onSome: (ft) => Duration.millis(now - ft)
136
+ });
137
+ const event = makeEvent({
138
+ _tag: "TimeToCompletion",
139
+ turnIndex: state.turnIndex,
140
+ duration,
141
+ generation,
142
+ measurements: [{
143
+ name: MetricName.duration,
144
+ kind: "timer",
145
+ value: duration
146
+ }, {
147
+ name: MetricName.generation,
148
+ kind: "timer",
149
+ value: generation
150
+ }]
151
+ });
152
+ return [{
153
+ turnIndex: state.turnIndex + 1,
154
+ turnStart: now,
155
+ firstTokenAt: Option.none()
156
+ }, [ev, event]];
48
157
  })))));
158
+ const emptyUsage = {};
159
+ const tokenTotals = (self) => self.pipe(Stream.mapAccumEffect(() => ({
160
+ turnIndex: 0,
161
+ cumulative: emptyUsage
162
+ }), (state, ev) => Effect.sync(() => {
163
+ if (!isTurnCompleteEvent(ev)) return [state, [ev]];
164
+ const usage = turnOf(ev).usage;
165
+ const cumulative = addUsage(state.cumulative, usage);
166
+ const event = makeEvent({
167
+ _tag: "TokenTotals",
168
+ turnIndex: state.turnIndex,
169
+ usage,
170
+ cumulative,
171
+ measurements: usageMeasurements(usage)
172
+ });
173
+ return [{
174
+ turnIndex: state.turnIndex + 1,
175
+ cumulative
176
+ }, [ev, event]];
177
+ })));
178
+ const initRateState = {
179
+ units: 0,
180
+ lastUnits: 0,
181
+ lastMillis: 0,
182
+ firstMillis: Option.none(),
183
+ smoothed: Option.none(),
184
+ turnIndex: 0
185
+ };
186
+ const resolveSmooth = (smooth) => smooth === void 0 ? void 0 : smooth === "default" ? .3 : smooth;
187
+ /**
188
+ * Pure per-tick rate computation. Returns `None` before the first token, else
189
+ * the rate, the window it was measured over, and the next accumulator state.
190
+ * Separated from the metronome so the rate math is unit-testable without time.
191
+ *
192
+ * @internal
193
+ */
194
+ const computeThroughputTick = (state, now, mode, smooth) => Option.map(state.firstMillis, (first) => {
195
+ const windowMs = mode === "windowed" ? now - state.lastMillis : now - first;
196
+ const deltaUnits = mode === "windowed" ? state.units - state.lastUnits : state.units;
197
+ const instant = windowMs > 0 ? deltaUnits / windowMs * 1e3 : 0;
198
+ const rate = smooth === void 0 ? instant : Option.match(state.smoothed, {
199
+ onNone: () => instant,
200
+ onSome: (prev) => smooth * instant + (1 - smooth) * prev
201
+ });
202
+ return {
203
+ rate,
204
+ window: Duration.millis(windowMs),
205
+ next: {
206
+ ...state,
207
+ lastUnits: state.units,
208
+ lastMillis: now,
209
+ smoothed: Option.some(rate)
210
+ }
211
+ };
212
+ });
213
+ const addUnits = (state, n, now) => Option.isNone(state.firstMillis) ? {
214
+ ...state,
215
+ units: n,
216
+ lastUnits: 0,
217
+ lastMillis: now,
218
+ firstMillis: Option.some(now)
219
+ } : {
220
+ ...state,
221
+ units: state.units + n
222
+ };
223
+ const resetTurn = (state) => ({
224
+ ...initRateState,
225
+ turnIndex: state.turnIndex + 1
226
+ });
227
+ const unitCount = (ev, unit, tokenizer) => {
228
+ if (tagOf(ev) !== "TextDelta") return Effect.succeed(0);
229
+ const text = ev.text;
230
+ return Match.value(unit).pipe(Match.when("char", () => Effect.succeed([...text].length)), Match.when("event", () => Effect.succeed(1)), Match.when("token", () => tokenizer === void 0 ? Effect.succeed(1) : tokenizer(ev)), Match.exhaustive);
231
+ };
232
+ const throughputName = (unit) => `effect_uai_output_${unit}s_per_second`;
233
+ const throughput = (options) => (self) => {
234
+ const unit = options?.unit ?? "char";
235
+ const mode = options?.mode ?? "windowed";
236
+ const every = options?.every ?? Duration.seconds(1);
237
+ const smooth = resolveSmooth(options?.smooth);
238
+ const tokenizer = options?.tokenizer;
239
+ const name = throughputName(unit);
240
+ return Stream.unwrap(Effect.map(Ref.make(initRateState), (ref) => {
241
+ const source = self.pipe(Stream.tap((ev) => isTurnCompleteEvent(ev) ? Ref.update(ref, resetTurn) : tagOf(ev) === "TextDelta" ? Effect.flatMap(Clock.currentTimeMillis, (now) => Effect.flatMap(unitCount(ev, unit, tokenizer), (n) => Ref.update(ref, (state) => addUnits(state, n, now)))) : Effect.void));
242
+ const metronome = Stream.fromSchedule(Schedule.spaced(every)).pipe(Stream.mapEffect(() => Effect.flatMap(Clock.currentTimeMillis, (now) => Ref.modify(ref, (state) => Option.match(computeThroughputTick(state, now, mode, smooth), {
243
+ onNone: () => [Option.none(), state],
244
+ onSome: ({ next, rate, window }) => [Option.some(makeEvent({
245
+ _tag: "Throughput",
246
+ turnIndex: state.turnIndex,
247
+ ratePerSecond: rate,
248
+ unit,
249
+ window,
250
+ measurements: [{
251
+ name,
252
+ kind: "histogram",
253
+ value: rate
254
+ }]
255
+ })), next]
256
+ })))), Stream.filterMap((o) => Option.isSome(o) ? Result.succeed(o.value) : Result.failVoid));
257
+ return source.pipe(Stream.merge(metronome, { haltStrategy: "left" }));
258
+ }));
259
+ };
260
+ const allMetrics = (options) => (self) => self.pipe(timeToFirstToken(options?.timeToFirstToken), throughput(options?.throughput), tokenTotals, timeToCompletion);
49
261
  //#endregion
50
- export { Metrics_exports as t, timeToFirst, withElapsed, withRate };
262
+ export { MetricEventTypeId, allMetrics, computeThroughputTick, isMetricEvent, makeEvent, metricEvents, Metrics_exports as t, throughput, timeToCompletion, timeToFirstToken, tokenTotals };
51
263
 
52
264
  //# sourceMappingURL=Metrics.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Metrics.mjs","names":[],"sources":["../../src/observability/Metrics.ts"],"sourcesContent":["import { Clock, Duration, Effect, Option, Stream } from \"effect\"\n\n/**\n * Annotate every event in a stream with the elapsed `Duration` since the\n * stream started consuming. The first event reports its time-from-start,\n * which is also the conventional \"time to first ____\" metric.\n */\nexport const withElapsed = <A, E, R>(\n self: Stream.Stream<A, E, R>,\n): Stream.Stream<{ readonly value: A; readonly elapsed: Duration.Duration }, E, R> =>\n Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (start) =>\n self.pipe(\n Stream.mapEffect((value) =>\n Effect.map(Clock.currentTimeMillis, (now) => ({\n value,\n elapsed: Duration.millis(now - start),\n })),\n ),\n ),\n ),\n )\n\n/**\n * Compute the elapsed time until the first event matching the predicate.\n * Returns `Option.none()` if the stream completes without one.\n *\n * Consumes the stream. To track this *alongside* live consumption, use\n * `Stream.broadcast` to fan the source out and run `timeToFirst` on one\n * branch.\n */\nexport const timeToFirst =\n <A>(predicate: (a: A) => boolean) =>\n <E, R>(self: Stream.Stream<A, E, R>): Effect.Effect<Option.Option<Duration.Duration>, E, R> =>\n withElapsed(self).pipe(\n Stream.filter(({ value }) => predicate(value)),\n Stream.runHead,\n Effect.map(Option.map(({ elapsed }) => elapsed)),\n )\n\nexport type RatePoint<A> = {\n readonly value: A\n readonly total: number\n readonly ratePerSecond: number\n readonly elapsed: Duration.Duration\n}\n\n/**\n * Annotate every event with a running total and a rolling rate per second,\n * computed from a user-supplied weight function.\n *\n * The weight is the unit you care about - bytes, tokens, error count, etc.\n * For tokens-per-second on `TurnEvent`, pass:\n *\n * `(d) => d._tag === \"TextDelta\" ? countTokens(d.text) : 0`\n *\n * Use any tokenizer you like; the library does not ship one.\n */\nexport const withRate =\n <A>(weight: (a: A) => number) =>\n <E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<RatePoint<A>, E, R> =>\n Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (start) =>\n self.pipe(\n Stream.mapAccumEffect(\n () => ({ total: 0 }),\n (acc, value) =>\n Effect.map(Clock.currentTimeMillis, (now) => {\n const total = acc.total + weight(value)\n const elapsedMs = now - start\n const ratePerSecond = elapsedMs > 0 ? (total / elapsedMs) * 1000 : 0\n return [\n { total },\n [\n {\n value,\n total,\n ratePerSecond,\n elapsed: Duration.millis(elapsedMs),\n } satisfies RatePoint<A>,\n ],\n ] as const\n }),\n ),\n ),\n ),\n )\n"],"mappings":";;;;;;;;;;;;;AAOA,MAAa,eACX,SAEA,OAAO,OACL,OAAO,IAAI,MAAM,oBAAoB,UACnC,KAAK,KACH,OAAO,WAAW,UAChB,OAAO,IAAI,MAAM,oBAAoB,SAAS;CAC5C;CACA,SAAS,SAAS,OAAO,MAAM,MAAM;CACtC,EAAE,CACJ,CACF,CACF,CACF;;;;;;;;;AAUH,MAAa,eACP,eACG,SACL,YAAY,KAAK,CAAC,KAChB,OAAO,QAAQ,EAAE,YAAY,UAAU,MAAM,CAAC,EAC9C,OAAO,SACP,OAAO,IAAI,OAAO,KAAK,EAAE,cAAc,QAAQ,CAAC,CACjD;;;;;;;;;;;;AAoBL,MAAa,YACP,YACG,SACL,OAAO,OACL,OAAO,IAAI,MAAM,oBAAoB,UACnC,KAAK,KACH,OAAO,sBACE,EAAE,OAAO,GAAG,IAClB,KAAK,UACJ,OAAO,IAAI,MAAM,oBAAoB,QAAQ;CAC3C,MAAM,QAAQ,IAAI,QAAQ,OAAO,MAAM;CACvC,MAAM,YAAY,MAAM;CACxB,MAAM,gBAAgB,YAAY,IAAK,QAAQ,YAAa,MAAO;AACnE,QAAO,CACL,EAAE,OAAO,EACT,CACE;EACE;EACA;EACA;EACA,SAAS,SAAS,OAAO,UAAU;EACpC,CACF,CACF;EACD,CACL,CACF,CACF,CACF"}
1
+ {"version":3,"file":"Metrics.mjs","names":["Arr"],"sources":["../../src/observability/Metrics.ts"],"sourcesContent":["import {\n Array as Arr,\n Clock,\n Duration,\n Effect,\n Match,\n Option,\n Predicate,\n Ref,\n Result,\n Schedule,\n Stream,\n} from \"effect\"\nimport type { Usage } from \"../domain/Items.js\"\nimport type { Turn, TurnEvent } from \"../domain/Turn.js\"\n\n// ---------------------------------------------------------------------------\n// Self-describing measurements (the exporter-facing payload)\n// ---------------------------------------------------------------------------\n\n/**\n * The instrument-agnostic description of a single recorded value. The\n * `Telemetry` recorder reads only this: it builds (or reuses) a `Metric`\n * instrument from `kind` + `name` and updates it with `value`. A new metric,\n * built-in or user-defined, never requires a change to the recorder.\n */\nexport type Measurement = {\n readonly name: string\n readonly kind: \"counter\" | \"histogram\" | \"timer\" | \"gauge\"\n readonly value: number | Duration.Duration\n readonly attributes?: Readonly<Record<string, string>> | undefined\n}\n\n// ---------------------------------------------------------------------------\n// MetricEvent: an open, brand-marked union (built-ins + user metrics)\n// ---------------------------------------------------------------------------\n\n/**\n * Brand stamped on every metric event. `isMetricEvent` checks this symbol,\n * not an enumerated `_tag`, so events are distinguished from `TurnEvent`s,\n * tool outputs, and arbitrary custom loop values regardless of their tag.\n * This is the same TypeId-symbol pattern Effect uses for `isStream` etc.\n */\nexport const MetricEventTypeId: unique symbol = Symbol.for(\n \"@effect-uai/core/observability/MetricEvent\",\n)\n\n/**\n * The structural contract every metric event satisfies, ours and custom.\n * The `_tag` + extra typed fields serve a frontend; the brand + `measurements`\n * serve the exporter.\n */\nexport type MetricEvent = {\n readonly [MetricEventTypeId]: typeof MetricEventTypeId\n readonly _tag: string\n /** Which turn within the piped stream this sample belongs to (0-based). */\n readonly turnIndex: number\n readonly measurements: ReadonlyArray<Measurement>\n}\n\n/** Request/turn start to the first content delta. */\nexport type TimeToFirstToken = MetricEvent & {\n readonly _tag: \"TimeToFirstToken\"\n readonly elapsed: Duration.Duration\n readonly kind: \"text\" | \"reasoning\"\n}\n\n/** A live output rate, emitted on the metronome cadence. */\nexport type Throughput = MetricEvent & {\n readonly _tag: \"Throughput\"\n readonly ratePerSecond: number\n readonly unit: \"char\" | \"token\" | \"event\"\n readonly window: Duration.Duration\n}\n\n/** Provider-reported usage at `TurnComplete`, this turn and cumulative. */\nexport type TokenTotals = MetricEvent & {\n readonly _tag: \"TokenTotals\"\n readonly usage: Usage\n readonly cumulative: Usage\n}\n\n/** Per-turn wall times at `TurnComplete`. */\nexport type TimeToCompletion = MetricEvent & {\n readonly _tag: \"TimeToCompletion\"\n readonly duration: Duration.Duration\n readonly generation: Duration.Duration\n}\n\n/**\n * Mint a custom metric event. Stamp it with the brand and provide a\n * well-formed `measurements` array; `Telemetry.record` will export it with no\n * changes to the recorder. The only constraint is that each measurement's\n * `kind` is one the recorder maps.\n */\nexport const makeEvent = <Fields extends Omit<MetricEvent, typeof MetricEventTypeId>>(\n fields: Fields,\n): Fields & { readonly [MetricEventTypeId]: typeof MetricEventTypeId } => ({\n [MetricEventTypeId]: MetricEventTypeId,\n ...fields,\n})\n\n/** Structural (brand-based) guard for any metric event, built-in or custom. */\nexport const isMetricEvent = (u: unknown): u is MetricEvent =>\n Predicate.hasProperty(u, MetricEventTypeId)\n\n/**\n * Project a stream of mixed events onto just its metric samples, for a side\n * channel or a frontend feed.\n */\nexport const metricEvents = <A, E, R>(\n self: Stream.Stream<A, E, R>,\n): Stream.Stream<MetricEvent, E, R> =>\n self.pipe(Stream.filterMap((ev) => (isMetricEvent(ev) ? Result.succeed(ev) : Result.failVoid)))\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst MetricName = {\n ttft: \"effect_uai_turn_ttft\",\n duration: \"effect_uai_turn_duration\",\n generation: \"effect_uai_turn_generation\",\n inputTokens: \"effect_uai_input_tokens\",\n outputTokens: \"effect_uai_output_tokens\",\n totalTokens: \"effect_uai_total_tokens\",\n reasoningTokens: \"effect_uai_reasoning_tokens\",\n cachedInputTokens: \"effect_uai_cached_input_tokens\",\n} as const\n\n/** Read a structural `_tag` off any value without narrowing to a closed union. */\nconst tagOf = (u: unknown): string | undefined =>\n Predicate.hasProperty(u, \"_tag\") && Predicate.isString(u._tag) ? u._tag : undefined\n\nconst contentDeltaKind = (ev: unknown): Option.Option<\"text\" | \"reasoning\"> => {\n const t = tagOf(ev)\n return t === \"TextDelta\"\n ? Option.some(\"text\")\n : t === \"ReasoningDelta\"\n ? Option.some(\"reasoning\")\n : Option.none()\n}\n\nconst isTurnCompleteEvent = (ev: unknown): boolean => tagOf(ev) === \"TurnComplete\"\n\nconst turnOf = (ev: unknown): Turn =>\n (ev as Extract<TurnEvent, { readonly _tag: \"TurnComplete\" }>).turn\n\nconst sumOptional = (a: number | undefined, b: number | undefined): number | undefined =>\n a === undefined && b === undefined ? undefined : (a ?? 0) + (b ?? 0)\n\nconst addUsage = (a: Usage, b: Usage): Usage => {\n const cached = sumOptional(\n a.input_tokens_details?.cached_tokens,\n b.input_tokens_details?.cached_tokens,\n )\n const reasoning = sumOptional(\n a.output_tokens_details?.reasoning_tokens,\n b.output_tokens_details?.reasoning_tokens,\n )\n return {\n input_tokens: sumOptional(a.input_tokens, b.input_tokens),\n output_tokens: sumOptional(a.output_tokens, b.output_tokens),\n total_tokens: sumOptional(a.total_tokens, b.total_tokens),\n input_tokens_details: cached === undefined ? undefined : { cached_tokens: cached },\n output_tokens_details: reasoning === undefined ? undefined : { reasoning_tokens: reasoning },\n }\n}\n\n/** Per-turn token counts as incremental counter measurements (present fields only). */\nconst usageMeasurements = (usage: Usage): ReadonlyArray<Measurement> => {\n const pairs: ReadonlyArray<readonly [string, number | undefined]> = [\n [MetricName.inputTokens, usage.input_tokens],\n [MetricName.outputTokens, usage.output_tokens],\n [MetricName.totalTokens, usage.total_tokens],\n [MetricName.reasoningTokens, usage.output_tokens_details?.reasoning_tokens],\n [MetricName.cachedInputTokens, usage.input_tokens_details?.cached_tokens],\n ]\n return Arr.filterMap(pairs, ([name, value]) =>\n value === undefined\n ? Result.failVoid\n : Result.succeed<Measurement>({ name, kind: \"counter\", value }),\n )\n}\n\n// ---------------------------------------------------------------------------\n// timeToFirstToken\n// ---------------------------------------------------------------------------\n\nexport type TimeToFirstTokenOptions = {\n /**\n * Where the clock starts. `\"request\"` (default) anchors at stream\n * initialization, i.e. when the provider request fires, so the measurement\n * includes connection + prefill. `\"first-event\"` anchors at the first\n * emitted event, isolating decode latency.\n */\n readonly from?: \"request\" | \"first-event\"\n}\n\ntype TtftState = {\n readonly turnIndex: number\n readonly turnStart: number\n readonly started: boolean\n readonly seen: boolean\n}\n\nexport const timeToFirstToken =\n (options?: TimeToFirstTokenOptions) =>\n <A, E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<A | MetricEvent, E, R> => {\n const fromRequest = (options?.from ?? \"request\") === \"request\"\n return Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (init) =>\n self.pipe(\n Stream.mapAccumEffect(\n (): TtftState => ({ turnIndex: 0, turnStart: init, started: fromRequest, seen: false }),\n (state, ev) =>\n Effect.map(Clock.currentTimeMillis, (now) => {\n const started = state.started ? state : { ...state, turnStart: now, started: true }\n const kind = contentDeltaKind(ev)\n const emitTtft = !started.seen && Option.isSome(kind)\n const out: ReadonlyArray<A | MetricEvent> = emitTtft\n ? [\n ev,\n makeEvent<Omit<TimeToFirstToken, typeof MetricEventTypeId>>({\n _tag: \"TimeToFirstToken\",\n turnIndex: started.turnIndex,\n elapsed: Duration.millis(now - started.turnStart),\n kind: (kind as Option.Some<\"text\" | \"reasoning\">).value,\n measurements: [\n {\n name: MetricName.ttft,\n kind: \"timer\",\n value: Duration.millis(now - started.turnStart),\n },\n ],\n }),\n ]\n : [ev]\n const next: TtftState = isTurnCompleteEvent(ev)\n ? {\n turnIndex: started.turnIndex + 1,\n turnStart: now,\n started: fromRequest,\n seen: false,\n }\n : { ...started, seen: started.seen || emitTtft }\n return [next, out] as const\n }),\n ),\n ),\n ),\n )\n }\n\n// ---------------------------------------------------------------------------\n// timeToCompletion\n// ---------------------------------------------------------------------------\n\ntype CompletionState = {\n readonly turnIndex: number\n readonly turnStart: number\n readonly firstTokenAt: Option.Option<number>\n}\n\nexport const timeToCompletion = <A, E, R>(\n self: Stream.Stream<A, E, R>,\n): Stream.Stream<A | MetricEvent, E, R> =>\n Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (init) =>\n self.pipe(\n Stream.mapAccumEffect(\n (): CompletionState => ({ turnIndex: 0, turnStart: init, firstTokenAt: Option.none() }),\n (state, ev) =>\n Effect.map(Clock.currentTimeMillis, (now) => {\n const firstTokenAt =\n Option.isNone(state.firstTokenAt) && Option.isSome(contentDeltaKind(ev))\n ? Option.some(now)\n : state.firstTokenAt\n if (!isTurnCompleteEvent(ev)) {\n return [{ ...state, firstTokenAt }, [ev]] as const\n }\n const duration = Duration.millis(now - state.turnStart)\n const generation = Option.match(firstTokenAt, {\n onNone: () => Duration.zero,\n onSome: (ft) => Duration.millis(now - ft),\n })\n const event = makeEvent<Omit<TimeToCompletion, typeof MetricEventTypeId>>({\n _tag: \"TimeToCompletion\",\n turnIndex: state.turnIndex,\n duration,\n generation,\n measurements: [\n { name: MetricName.duration, kind: \"timer\", value: duration },\n { name: MetricName.generation, kind: \"timer\", value: generation },\n ],\n })\n const next: CompletionState = {\n turnIndex: state.turnIndex + 1,\n turnStart: now,\n firstTokenAt: Option.none(),\n }\n return [next, [ev, event]] as const\n }),\n ),\n ),\n ),\n )\n\n// ---------------------------------------------------------------------------\n// tokenTotals\n// ---------------------------------------------------------------------------\n\nconst emptyUsage: Usage = {}\n\ntype TotalsState = {\n readonly turnIndex: number\n readonly cumulative: Usage\n}\n\nexport const tokenTotals = <A, E, R>(\n self: Stream.Stream<A, E, R>,\n): Stream.Stream<A | MetricEvent, E, R> =>\n self.pipe(\n Stream.mapAccumEffect(\n (): TotalsState => ({ turnIndex: 0, cumulative: emptyUsage }),\n (state, ev) =>\n Effect.sync(() => {\n if (!isTurnCompleteEvent(ev)) return [state, [ev]] as const\n const usage = turnOf(ev).usage\n const cumulative = addUsage(state.cumulative, usage)\n const event = makeEvent<Omit<TokenTotals, typeof MetricEventTypeId>>({\n _tag: \"TokenTotals\",\n turnIndex: state.turnIndex,\n usage,\n cumulative,\n measurements: usageMeasurements(usage),\n })\n return [{ turnIndex: state.turnIndex + 1, cumulative }, [ev, event]] as const\n }),\n ),\n )\n\n// ---------------------------------------------------------------------------\n// throughput\n// ---------------------------------------------------------------------------\n\nexport type ThroughputOptions = {\n /** Tick interval. Default `\"1 second\"`. */\n readonly every?: Duration.Input\n /** What a \"unit\" is. Default `\"char\"` (exact and chunking-independent). */\n readonly unit?: \"char\" | \"token\" | \"event\"\n /** Token counter for `unit: \"token\"`. Absent => approximate 1 per content delta. */\n readonly tokenizer?: (event: TurnEvent) => Effect.Effect<number>\n /** Rate definition. Default `\"windowed\"` (current speed, no cold-start bias). */\n readonly mode?: \"windowed\" | \"cumulative\"\n /** EWMA smoothing. Omitted => off; `\"default\"` => a sensible factor. */\n readonly smooth?: \"default\" | number\n}\n\n/**\n * The throughput accumulator. Mutated by the source per content delta and read\n * by the metronome per tick.\n *\n * @internal\n */\nexport type RateState = {\n readonly units: number\n readonly lastUnits: number\n readonly lastMillis: number\n readonly firstMillis: Option.Option<number>\n readonly smoothed: Option.Option<number>\n readonly turnIndex: number\n}\n\nconst initRateState: RateState = {\n units: 0,\n lastUnits: 0,\n lastMillis: 0,\n firstMillis: Option.none(),\n smoothed: Option.none(),\n turnIndex: 0,\n}\n\nconst resolveSmooth = (smooth: ThroughputOptions[\"smooth\"]): number | undefined =>\n smooth === undefined ? undefined : smooth === \"default\" ? 0.3 : smooth\n\n/**\n * Pure per-tick rate computation. Returns `None` before the first token, else\n * the rate, the window it was measured over, and the next accumulator state.\n * Separated from the metronome so the rate math is unit-testable without time.\n *\n * @internal\n */\nexport const computeThroughputTick = (\n state: RateState,\n now: number,\n mode: \"windowed\" | \"cumulative\",\n smooth: number | undefined,\n): Option.Option<{\n readonly rate: number\n readonly window: Duration.Duration\n readonly next: RateState\n}> =>\n Option.map(state.firstMillis, (first) => {\n const windowMs = mode === \"windowed\" ? now - state.lastMillis : now - first\n const deltaUnits = mode === \"windowed\" ? state.units - state.lastUnits : state.units\n const instant = windowMs > 0 ? (deltaUnits / windowMs) * 1000 : 0\n const rate =\n smooth === undefined\n ? instant\n : Option.match(state.smoothed, {\n onNone: () => instant,\n onSome: (prev) => smooth * instant + (1 - smooth) * prev,\n })\n return {\n rate,\n window: Duration.millis(windowMs),\n next: { ...state, lastUnits: state.units, lastMillis: now, smoothed: Option.some(rate) },\n }\n })\n\nconst addUnits = (state: RateState, n: number, now: number): RateState =>\n Option.isNone(state.firstMillis)\n ? { ...state, units: n, lastUnits: 0, lastMillis: now, firstMillis: Option.some(now) }\n : { ...state, units: state.units + n }\n\nconst resetTurn = (state: RateState): RateState => ({\n ...initRateState,\n turnIndex: state.turnIndex + 1,\n})\n\nconst unitCount = (\n ev: unknown,\n unit: \"char\" | \"token\" | \"event\",\n tokenizer: ThroughputOptions[\"tokenizer\"],\n): Effect.Effect<number> => {\n if (tagOf(ev) !== \"TextDelta\") return Effect.succeed(0)\n const text = (ev as { readonly text: string }).text\n return Match.value(unit).pipe(\n Match.when(\"char\", () => Effect.succeed([...text].length)),\n Match.when(\"event\", () => Effect.succeed(1)),\n Match.when(\"token\", () =>\n tokenizer === undefined ? Effect.succeed(1) : tokenizer(ev as TurnEvent),\n ),\n Match.exhaustive,\n )\n}\n\nconst throughputName = (unit: \"char\" | \"token\" | \"event\"): string =>\n `effect_uai_output_${unit}s_per_second`\n\nexport const throughput =\n (options?: ThroughputOptions) =>\n <A, E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<A | MetricEvent, E, R> => {\n const unit = options?.unit ?? \"char\"\n const mode = options?.mode ?? \"windowed\"\n const every = options?.every ?? Duration.seconds(1)\n const smooth = resolveSmooth(options?.smooth)\n const tokenizer = options?.tokenizer\n const name = throughputName(unit)\n\n return Stream.unwrap(\n Effect.map(Ref.make(initRateState), (ref) => {\n const source = self.pipe(\n Stream.tap((ev) =>\n isTurnCompleteEvent(ev)\n ? Ref.update(ref, resetTurn)\n : tagOf(ev) === \"TextDelta\"\n ? Effect.flatMap(Clock.currentTimeMillis, (now) =>\n Effect.flatMap(unitCount(ev, unit, tokenizer), (n) =>\n Ref.update(ref, (state) => addUnits(state, n, now)),\n ),\n )\n : Effect.void,\n ),\n )\n const metronome: Stream.Stream<MetricEvent> = Stream.fromSchedule(\n Schedule.spaced(every),\n ).pipe(\n Stream.mapEffect(() =>\n Effect.flatMap(Clock.currentTimeMillis, (now) =>\n Ref.modify(ref, (state) =>\n Option.match(computeThroughputTick(state, now, mode, smooth), {\n onNone: () => [Option.none<MetricEvent>(), state] as const,\n onSome: ({ next, rate, window }) =>\n [\n Option.some<MetricEvent>(\n makeEvent<Omit<Throughput, typeof MetricEventTypeId>>({\n _tag: \"Throughput\",\n turnIndex: state.turnIndex,\n ratePerSecond: rate,\n unit,\n window,\n measurements: [{ name, kind: \"histogram\", value: rate }],\n }),\n ),\n next,\n ] as const,\n }),\n ),\n ),\n ),\n Stream.filterMap((o) => (Option.isSome(o) ? Result.succeed(o.value) : Result.failVoid)),\n )\n return source.pipe(Stream.merge(metronome, { haltStrategy: \"left\" }))\n }),\n )\n }\n\n// ---------------------------------------------------------------------------\n// allMetrics\n// ---------------------------------------------------------------------------\n\nexport type AllMetricsOptions = {\n readonly timeToFirstToken?: TimeToFirstTokenOptions\n readonly throughput?: ThroughputOptions\n}\n\nexport const allMetrics =\n (options?: AllMetricsOptions) =>\n <A, E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<A | MetricEvent, E, R> =>\n self.pipe(\n timeToFirstToken(options?.timeToFirstToken),\n throughput(options?.throughput),\n tokenTotals,\n timeToCompletion,\n )\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,187 @@
1
+ import { TurnEvent } from "../domain/Turn.mjs";
2
+ import { computeThroughputTick, isMetricEvent, makeEvent, metricEvents, throughput, timeToCompletion, timeToFirstToken, tokenTotals } from "./Metrics.mjs";
3
+ import { a as describe, n as globalExpect } from "../dist-B04dlFHg.mjs";
4
+ import { t as it } from "../dist-DWgRbDN3.mjs";
5
+ import { Array, Duration, Effect, Fiber, Option, Stream } from "effect";
6
+ import * as TestClock from "effect/testing/TestClock";
7
+ //#region src/observability/Metrics.test.ts
8
+ const turnComplete = (usage) => TurnEvent.TurnComplete({ turn: {
9
+ items: [],
10
+ usage,
11
+ stop_reason: "stop"
12
+ } });
13
+ const text = (s) => TurnEvent.TextDelta({ text: s });
14
+ const samples = (events) => events.filter(isMetricEvent);
15
+ const head = (xs) => Option.getOrThrow(Array.head(xs));
16
+ const tagged = (events, tag) => samples(events).filter((e) => e._tag === tag);
17
+ const timed = (steps) => Stream.fromIterable(steps).pipe(Stream.mapEffect(([delay, ev]) => Effect.as(Effect.sleep(delay), ev)));
18
+ const runTimed = (metered, steps) => Effect.gen(function* () {
19
+ const fiber = yield* Effect.forkChild(Stream.runCollect(metered));
20
+ yield* Effect.forEach(steps, (step) => TestClock.adjust(step), { discard: true });
21
+ return yield* Fiber.join(fiber);
22
+ });
23
+ describe("tokenTotals", () => {
24
+ it.effect("turn level: a single TurnComplete reports that generation's usage", () => Effect.gen(function* () {
25
+ const stream = Stream.make(text("hi"), turnComplete({
26
+ input_tokens: 100,
27
+ output_tokens: 20,
28
+ total_tokens: 120
29
+ }));
30
+ const totals = tagged(yield* Stream.runCollect(stream.pipe(tokenTotals)), "TokenTotals");
31
+ globalExpect(totals).toHaveLength(1);
32
+ globalExpect(head(totals).turnIndex).toBe(0);
33
+ globalExpect(head(totals).usage.output_tokens).toBe(20);
34
+ globalExpect(head(totals).cumulative.output_tokens).toBe(20);
35
+ }));
36
+ it.effect("loop level: cumulative sums across every turn, usage stays per-turn", () => Effect.gen(function* () {
37
+ const stream = Stream.make(text("a"), turnComplete({
38
+ input_tokens: 1e3,
39
+ output_tokens: 120
40
+ }), turnComplete({
41
+ input_tokens: 1300,
42
+ output_tokens: 410
43
+ }));
44
+ const totals = tagged(yield* Stream.runCollect(stream.pipe(tokenTotals)), "TokenTotals");
45
+ globalExpect(totals.map((t) => t.turnIndex)).toEqual([0, 1]);
46
+ globalExpect(totals.map((t) => t.usage.output_tokens)).toEqual([120, 410]);
47
+ globalExpect(totals.map((t) => t.cumulative.input_tokens)).toEqual([1e3, 2300]);
48
+ globalExpect(totals.map((t) => t.cumulative.output_tokens)).toEqual([120, 530]);
49
+ }));
50
+ it.effect("emits an incremental output_tokens counter measurement per turn", () => Effect.gen(function* () {
51
+ const stream = Stream.make(turnComplete({ output_tokens: 7 }));
52
+ globalExpect(head(samples(yield* Stream.runCollect(stream.pipe(tokenTotals)))).measurements.find((m) => m.name === "effect_uai_output_tokens")).toEqual({
53
+ name: "effect_uai_output_tokens",
54
+ kind: "counter",
55
+ value: 7
56
+ });
57
+ }));
58
+ });
59
+ describe("timeToFirstToken", () => {
60
+ it.effect("fires on the first content delta, measured from request (stream init)", () => Effect.gen(function* () {
61
+ const ttft = tagged(yield* runTimed(timed([["300 millis", text("hello")], ["700 millis", turnComplete({})]]).pipe(timeToFirstToken()), ["300 millis", "700 millis"]), "TimeToFirstToken");
62
+ globalExpect(ttft).toHaveLength(1);
63
+ globalExpect(Duration.toMillis(head(ttft).elapsed)).toBe(300);
64
+ globalExpect(head(ttft).kind).toBe("text");
65
+ }));
66
+ it.effect("reports kind reasoning when reasoning arrives first", () => Effect.gen(function* () {
67
+ const ttft = tagged(yield* runTimed(timed([
68
+ ["120 millis", TurnEvent.ReasoningDelta({
69
+ text: "...",
70
+ kind: "trace"
71
+ })],
72
+ ["80 millis", text("answer")],
73
+ ["50 millis", turnComplete({})]
74
+ ]).pipe(timeToFirstToken()), [
75
+ "120 millis",
76
+ "80 millis",
77
+ "50 millis"
78
+ ]), "TimeToFirstToken");
79
+ globalExpect(ttft).toHaveLength(1);
80
+ globalExpect(head(ttft).kind).toBe("reasoning");
81
+ globalExpect(Duration.toMillis(head(ttft).elapsed)).toBe(120);
82
+ }));
83
+ it.effect("loop level: one TTFT per turn, turnIndex increments", () => Effect.gen(function* () {
84
+ const ttft = tagged(yield* runTimed(timed([
85
+ ["200 millis", text("a")],
86
+ ["100 millis", turnComplete({})],
87
+ ["300 millis", text("b")],
88
+ ["100 millis", turnComplete({})]
89
+ ]).pipe(timeToFirstToken()), [
90
+ "200 millis",
91
+ "100 millis",
92
+ "300 millis",
93
+ "100 millis"
94
+ ]), "TimeToFirstToken");
95
+ globalExpect(ttft.map((t) => t.turnIndex)).toEqual([0, 1]);
96
+ globalExpect(ttft.map((t) => Duration.toMillis(t.elapsed))).toEqual([200, 300]);
97
+ }));
98
+ });
99
+ describe("timeToCompletion", () => {
100
+ it.effect("reports duration (request->complete) and generation (first token->complete)", () => Effect.gen(function* () {
101
+ const done = tagged(yield* runTimed(timed([["300 millis", text("hi")], ["700 millis", turnComplete({})]]).pipe(timeToCompletion), ["300 millis", "700 millis"]), "TimeToCompletion");
102
+ globalExpect(done).toHaveLength(1);
103
+ globalExpect(Duration.toMillis(head(done).duration)).toBe(1e3);
104
+ globalExpect(Duration.toMillis(head(done).generation)).toBe(700);
105
+ }));
106
+ });
107
+ describe("computeThroughputTick", () => {
108
+ const base = {
109
+ units: 0,
110
+ lastUnits: 0,
111
+ lastMillis: 0,
112
+ firstMillis: Option.none(),
113
+ smoothed: Option.none(),
114
+ turnIndex: 0
115
+ };
116
+ it("returns None before the first token", () => {
117
+ globalExpect(Option.isNone(computeThroughputTick(base, 500, "windowed", void 0))).toBe(true);
118
+ });
119
+ it("windowed: rate is units in the last interval over that interval", () => {
120
+ const r = computeThroughputTick({
121
+ ...base,
122
+ units: 30,
123
+ lastUnits: 10,
124
+ lastMillis: 500,
125
+ firstMillis: Option.some(100)
126
+ }, 1e3, "windowed", void 0);
127
+ globalExpect(Option.getOrThrow(r).rate).toBe(40);
128
+ });
129
+ it("cumulative: rate is total units over time since first token", () => {
130
+ const r = computeThroughputTick({
131
+ ...base,
132
+ units: 30,
133
+ lastUnits: 10,
134
+ lastMillis: 500,
135
+ firstMillis: Option.some(100)
136
+ }, 1e3, "cumulative", void 0);
137
+ globalExpect(Option.getOrThrow(r).rate).toBeCloseTo(33.333, 2);
138
+ });
139
+ it("smooth: blends the instantaneous rate with the carried average", () => {
140
+ const r = computeThroughputTick({
141
+ ...base,
142
+ units: 20,
143
+ lastUnits: 0,
144
+ lastMillis: 0,
145
+ firstMillis: Option.some(0),
146
+ smoothed: Option.some(100)
147
+ }, 1e3, "windowed", .5);
148
+ globalExpect(Option.getOrThrow(r).rate).toBe(60);
149
+ });
150
+ });
151
+ describe("throughput (metronome)", () => {
152
+ it.effect("emits a live char rate on the tick, over the elapsed window", () => Effect.gen(function* () {
153
+ const rates = (yield* runTimed(Stream.make(text("aaaaa"), text("aaaaa")).pipe(Stream.concat(timed([["10 seconds", turnComplete({})]]))).pipe(throughput({
154
+ every: "1 second",
155
+ unit: "char"
156
+ }), metricEvents, Stream.take(1)), ["1 second"])).filter((e) => e._tag === "Throughput");
157
+ globalExpect(rates).toHaveLength(1);
158
+ globalExpect(head(rates).unit).toBe("char");
159
+ globalExpect(head(rates).turnIndex).toBe(0);
160
+ globalExpect(head(rates).ratePerSecond).toBeGreaterThan(0);
161
+ }));
162
+ });
163
+ describe("custom events and helpers", () => {
164
+ it("makeEvent stamps the brand so isMetricEvent and the recorder see it", () => {
165
+ globalExpect(isMetricEvent(makeEvent({
166
+ _tag: "ToolLatency",
167
+ turnIndex: 0,
168
+ measurements: [{
169
+ name: "myapp_tool_latency",
170
+ kind: "timer",
171
+ value: Duration.millis(42)
172
+ }]
173
+ }))).toBe(true);
174
+ globalExpect(isMetricEvent(text("x"))).toBe(false);
175
+ globalExpect(isMetricEvent(turnComplete({}))).toBe(false);
176
+ });
177
+ it.effect("metricEvents projects a mixed stream onto just the samples", () => Effect.gen(function* () {
178
+ const stream = Stream.make(text("hi"), turnComplete({ output_tokens: 3 })).pipe(tokenTotals, metricEvents);
179
+ const out = yield* Stream.runCollect(stream);
180
+ globalExpect(out.every(isMetricEvent)).toBe(true);
181
+ globalExpect(out.map((e) => e._tag)).toEqual(["TokenTotals"]);
182
+ }));
183
+ });
184
+ //#endregion
185
+ export {};
186
+
187
+ //# sourceMappingURL=Metrics.test.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Metrics.test.mjs","names":["Arr"],"sources":["../../src/observability/Metrics.test.ts"],"sourcesContent":["import { describe, it } from \"@effect/vitest\"\nimport { Array as Arr, Duration, Effect, Fiber, Option, Stream } from \"effect\"\nimport * as TestClock from \"effect/testing/TestClock\"\nimport { expect } from \"vitest\"\nimport type { Usage } from \"../domain/Items.js\"\nimport { TurnEvent } from \"../domain/Turn.js\"\nimport {\n type MetricEvent,\n type Throughput,\n type TimeToCompletion,\n type TimeToFirstToken,\n type TokenTotals,\n computeThroughputTick,\n isMetricEvent,\n makeEvent,\n metricEvents,\n throughput,\n timeToCompletion,\n timeToFirstToken,\n tokenTotals,\n} from \"./Metrics.js\"\n\nconst turnComplete = (usage: Usage): TurnEvent =>\n TurnEvent.TurnComplete({ turn: { items: [], usage, stop_reason: \"stop\" } })\n\nconst text = (s: string): TurnEvent => TurnEvent.TextDelta({ text: s })\n\nconst samples = (events: ReadonlyArray<unknown>): ReadonlyArray<MetricEvent> =>\n events.filter(isMetricEvent)\n\nconst head = <A>(xs: ReadonlyArray<A>): A => Option.getOrThrow(Arr.head(xs))\n\nconst tagged = <T extends MetricEvent[\"_tag\"]>(\n events: ReadonlyArray<unknown>,\n tag: T,\n): ReadonlyArray<Extract<MetricEvent, { readonly _tag: T }>> =>\n samples(events).filter((e): e is Extract<MetricEvent, { readonly _tag: T }> => e._tag === tag)\n\n// A stream that emits each event after the given delay since the previous one.\nconst timed = <A>(steps: ReadonlyArray<readonly [Duration.Input, A]>): Stream.Stream<A> =>\n Stream.fromIterable(steps).pipe(\n Stream.mapEffect(([delay, ev]) => Effect.as(Effect.sleep(delay), ev)),\n )\n\n// Collect a metered stream while advancing the test clock by each step, so\n// every per-event clock read lands at a known virtual time.\nconst runTimed = <A>(\n metered: Stream.Stream<A>,\n steps: ReadonlyArray<Duration.Input>,\n): Effect.Effect<ReadonlyArray<A>> =>\n Effect.gen(function* () {\n const fiber = yield* Effect.forkChild(Stream.runCollect(metered))\n yield* Effect.forEach(steps, (step) => TestClock.adjust(step), { discard: true })\n return yield* Fiber.join(fiber)\n })\n\ndescribe(\"tokenTotals\", () => {\n it.effect(\"turn level: a single TurnComplete reports that generation's usage\", () =>\n Effect.gen(function* () {\n const stream = Stream.make(\n text(\"hi\"),\n turnComplete({ input_tokens: 100, output_tokens: 20, total_tokens: 120 }),\n )\n const out = yield* Stream.runCollect(stream.pipe(tokenTotals))\n const totals = tagged(out, \"TokenTotals\") as ReadonlyArray<TokenTotals>\n expect(totals).toHaveLength(1)\n expect(head(totals).turnIndex).toBe(0)\n expect(head(totals).usage.output_tokens).toBe(20)\n expect(head(totals).cumulative.output_tokens).toBe(20)\n }),\n )\n\n it.effect(\"loop level: cumulative sums across every turn, usage stays per-turn\", () =>\n Effect.gen(function* () {\n const stream = Stream.make(\n text(\"a\"),\n turnComplete({ input_tokens: 1000, output_tokens: 120 }),\n turnComplete({ input_tokens: 1300, output_tokens: 410 }),\n )\n const out = yield* Stream.runCollect(stream.pipe(tokenTotals))\n const totals = tagged(out, \"TokenTotals\") as ReadonlyArray<TokenTotals>\n expect(totals.map((t) => t.turnIndex)).toEqual([0, 1])\n expect(totals.map((t) => t.usage.output_tokens)).toEqual([120, 410])\n expect(totals.map((t) => t.cumulative.input_tokens)).toEqual([1000, 2300])\n expect(totals.map((t) => t.cumulative.output_tokens)).toEqual([120, 530])\n }),\n )\n\n it.effect(\"emits an incremental output_tokens counter measurement per turn\", () =>\n Effect.gen(function* () {\n const stream = Stream.make(turnComplete({ output_tokens: 7 }))\n const out = yield* Stream.runCollect(stream.pipe(tokenTotals))\n const m = head(samples(out)).measurements.find((m) => m.name === \"effect_uai_output_tokens\")\n expect(m).toEqual({ name: \"effect_uai_output_tokens\", kind: \"counter\", value: 7 })\n }),\n )\n})\n\ndescribe(\"timeToFirstToken\", () => {\n it.effect(\"fires on the first content delta, measured from request (stream init)\", () =>\n Effect.gen(function* () {\n const stream = timed<TurnEvent>([\n [\"300 millis\", text(\"hello\")],\n [\"700 millis\", turnComplete({})],\n ]).pipe(timeToFirstToken())\n const out = yield* runTimed(stream, [\"300 millis\", \"700 millis\"])\n const ttft = tagged(out, \"TimeToFirstToken\") as ReadonlyArray<TimeToFirstToken>\n expect(ttft).toHaveLength(1)\n expect(Duration.toMillis(head(ttft).elapsed)).toBe(300)\n expect(head(ttft).kind).toBe(\"text\")\n }),\n )\n\n it.effect(\"reports kind reasoning when reasoning arrives first\", () =>\n Effect.gen(function* () {\n const stream = timed<TurnEvent>([\n [\"120 millis\", TurnEvent.ReasoningDelta({ text: \"...\", kind: \"trace\" })],\n [\"80 millis\", text(\"answer\")],\n [\"50 millis\", turnComplete({})],\n ]).pipe(timeToFirstToken())\n const out = yield* runTimed(stream, [\"120 millis\", \"80 millis\", \"50 millis\"])\n const ttft = tagged(out, \"TimeToFirstToken\") as ReadonlyArray<TimeToFirstToken>\n expect(ttft).toHaveLength(1)\n expect(head(ttft).kind).toBe(\"reasoning\")\n expect(Duration.toMillis(head(ttft).elapsed)).toBe(120)\n }),\n )\n\n it.effect(\"loop level: one TTFT per turn, turnIndex increments\", () =>\n Effect.gen(function* () {\n const stream = timed<TurnEvent>([\n [\"200 millis\", text(\"a\")],\n [\"100 millis\", turnComplete({})],\n [\"300 millis\", text(\"b\")],\n [\"100 millis\", turnComplete({})],\n ]).pipe(timeToFirstToken())\n const out = yield* runTimed(stream, [\"200 millis\", \"100 millis\", \"300 millis\", \"100 millis\"])\n const ttft = tagged(out, \"TimeToFirstToken\") as ReadonlyArray<TimeToFirstToken>\n expect(ttft.map((t) => t.turnIndex)).toEqual([0, 1])\n // turn 0 from init; turn 1 from the previous TurnComplete (loop-scope approximation)\n expect(ttft.map((t) => Duration.toMillis(t.elapsed))).toEqual([200, 300])\n }),\n )\n})\n\ndescribe(\"timeToCompletion\", () => {\n it.effect(\"reports duration (request->complete) and generation (first token->complete)\", () =>\n Effect.gen(function* () {\n const stream = timed<TurnEvent>([\n [\"300 millis\", text(\"hi\")],\n [\"700 millis\", turnComplete({})],\n ]).pipe(timeToCompletion)\n const out = yield* runTimed(stream, [\"300 millis\", \"700 millis\"])\n const done = tagged(out, \"TimeToCompletion\") as ReadonlyArray<TimeToCompletion>\n expect(done).toHaveLength(1)\n expect(Duration.toMillis(head(done).duration)).toBe(1000)\n expect(Duration.toMillis(head(done).generation)).toBe(700)\n }),\n )\n})\n\ndescribe(\"computeThroughputTick\", () => {\n const base = {\n units: 0,\n lastUnits: 0,\n lastMillis: 0,\n firstMillis: Option.none<number>(),\n smoothed: Option.none<number>(),\n turnIndex: 0,\n }\n\n it(\"returns None before the first token\", () => {\n expect(Option.isNone(computeThroughputTick(base, 500, \"windowed\", undefined))).toBe(true)\n })\n\n it(\"windowed: rate is units in the last interval over that interval\", () => {\n const state = {\n ...base,\n units: 30,\n lastUnits: 10,\n lastMillis: 500,\n firstMillis: Option.some(100),\n }\n const r = computeThroughputTick(state, 1000, \"windowed\", undefined)\n // (30 - 10) units over (1000 - 500) ms = 40/s\n expect(Option.getOrThrow(r).rate).toBe(40)\n })\n\n it(\"cumulative: rate is total units over time since first token\", () => {\n const state = {\n ...base,\n units: 30,\n lastUnits: 10,\n lastMillis: 500,\n firstMillis: Option.some(100),\n }\n const r = computeThroughputTick(state, 1000, \"cumulative\", undefined)\n // 30 units over (1000 - 100) ms ~= 33.33/s\n expect(Option.getOrThrow(r).rate).toBeCloseTo(33.333, 2)\n })\n\n it(\"smooth: blends the instantaneous rate with the carried average\", () => {\n const state = {\n ...base,\n units: 20,\n lastUnits: 0,\n lastMillis: 0,\n firstMillis: Option.some(0),\n smoothed: Option.some(100),\n }\n // windowed instant = 20 units / 1000 ms = 20/s; smoothed = 0.5*20 + 0.5*100 = 60\n const r = computeThroughputTick(state, 1000, \"windowed\", 0.5)\n expect(Option.getOrThrow(r).rate).toBe(60)\n })\n})\n\ndescribe(\"throughput (metronome)\", () => {\n it.effect(\"emits a live char rate on the tick, over the elapsed window\", () =>\n Effect.gen(function* () {\n // 10 chars land at t=0; the source then stays open so the metronome can\n // tick. take(1) ends the stream on the first sample, so termination does\n // not depend on the source closing.\n const source = Stream.make(text(\"aaaaa\"), text(\"aaaaa\")).pipe(\n Stream.concat(timed<TurnEvent>([[\"10 seconds\", turnComplete({})]])),\n )\n const stream = source.pipe(\n throughput({ every: \"1 second\", unit: \"char\" }),\n metricEvents,\n Stream.take(1),\n )\n const out = yield* runTimed(stream, [\"1 second\"])\n const rates = out.filter((e): e is Throughput => e._tag === \"Throughput\")\n expect(rates).toHaveLength(1)\n expect(head(rates).unit).toBe(\"char\")\n expect(head(rates).turnIndex).toBe(0)\n // exact rate math is pinned by the computeThroughputTick tests; here we\n // only assert the metronome wired a positive char rate onto the tick.\n expect(head(rates).ratePerSecond).toBeGreaterThan(0)\n }),\n )\n})\n\ndescribe(\"custom events and helpers\", () => {\n it(\"makeEvent stamps the brand so isMetricEvent and the recorder see it\", () => {\n const ev = makeEvent({\n _tag: \"ToolLatency\",\n turnIndex: 0,\n measurements: [{ name: \"myapp_tool_latency\", kind: \"timer\", value: Duration.millis(42) }],\n })\n expect(isMetricEvent(ev)).toBe(true)\n expect(isMetricEvent(text(\"x\"))).toBe(false)\n expect(isMetricEvent(turnComplete({}))).toBe(false)\n })\n\n it.effect(\"metricEvents projects a mixed stream onto just the samples\", () =>\n Effect.gen(function* () {\n const stream = Stream.make(text(\"hi\"), turnComplete({ output_tokens: 3 })).pipe(\n tokenTotals,\n metricEvents,\n )\n const out = yield* Stream.runCollect(stream)\n expect(out.every(isMetricEvent)).toBe(true)\n expect(out.map((e) => e._tag)).toEqual([\"TokenTotals\"])\n }),\n )\n})\n"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { Duration, Layer, Stream } from "effect";
2
+ import { HttpClient } from "effect/unstable/http";
3
+
4
+ //#region src/observability/Telemetry.d.ts
5
+ declare namespace Telemetry_d_exports {
6
+ export { LayerOtlpOptions, RecordOptions, layerOtlp, record };
7
+ }
8
+ type RecordOptions = {
9
+ /** Attributes applied to every recorded metric, e.g. `{ model, provider }`. */readonly attributes?: Readonly<Record<string, string>>;
10
+ };
11
+ /**
12
+ * Record every measurement on every metric event into effect `Metric`
13
+ * instruments, then pass events through unchanged. Matches events structurally
14
+ * (`isMetricEvent`), so it records built-in and custom metrics alike, and
15
+ * dispatches on `measurement.kind`, never on the event tag. Sits mid-pipeline
16
+ * before the transport. Provide an OTLP (or Prometheus) layer to export the
17
+ * instruments it populates.
18
+ */
19
+ declare const record: (options?: RecordOptions) => <A, E, R>(self: Stream.Stream<A, E, R>) => Stream.Stream<A, E, R>;
20
+ type LayerOtlpOptions = {
21
+ readonly url: string;
22
+ readonly resource?: {
23
+ readonly serviceName?: string;
24
+ readonly serviceVersion?: string;
25
+ };
26
+ readonly exportInterval?: Duration.Input;
27
+ };
28
+ /**
29
+ * Convenience layer that exports recorded metrics over OTLP. Provides the JSON
30
+ * serializer and leaves `HttpClient` as a requirement, so the platform's client
31
+ * (FetchHttpClient, NodeHttpClient, ...) is provided at the edge. This keeps
32
+ * the library platform-independent.
33
+ */
34
+ declare const layerOtlp: (options: LayerOtlpOptions) => Layer.Layer<never, never, HttpClient.HttpClient>;
35
+ //#endregion
36
+ export { LayerOtlpOptions, RecordOptions, layerOtlp, record, Telemetry_d_exports as t };
37
+ //# sourceMappingURL=Telemetry.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Telemetry.d.mts","names":[],"sources":["../../src/observability/Telemetry.ts"],"mappings":";;;;;;;KAqDY,aAAA;0FAED,UAAA,GAAa,QAAA,CAAS,MAAA;AAAA;;;;;;AAFjC;;;cAaa,MAAA,GACV,OAAA,GAAU,aAAA,eACD,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA;AAAA,KASnD,gBAAA;EAAA,SACD,GAAA;EAAA,SACA,QAAA;IAAA,SACE,WAAA;IAAA,SACA,cAAA;EAAA;EAAA,SAEF,cAAA,GAAiB,QAAA,CAAS,KAAA;AAAA;;;;;;;cASxB,SAAA,GACX,OAAA,EAAS,gBAAA,KACR,KAAA,CAAM,KAAA,eAAoB,UAAA,CAAW,UAAA"}