@llm4ts/core 0.1.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 (284) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/AgentSession.d.ts +66 -0
  4. package/dist/AgentSession.d.ts.map +1 -0
  5. package/dist/AgentSession.js +53 -0
  6. package/dist/AgentSession.js.map +1 -0
  7. package/dist/Capability.d.ts +101 -0
  8. package/dist/Capability.d.ts.map +1 -0
  9. package/dist/Capability.js +210 -0
  10. package/dist/Capability.js.map +1 -0
  11. package/dist/Connector.d.ts +69 -0
  12. package/dist/Connector.d.ts.map +1 -0
  13. package/dist/Connector.js +86 -0
  14. package/dist/Connector.js.map +1 -0
  15. package/dist/ConnectorConfig.d.ts +70 -0
  16. package/dist/ConnectorConfig.d.ts.map +1 -0
  17. package/dist/ConnectorConfig.js +110 -0
  18. package/dist/ConnectorConfig.js.map +1 -0
  19. package/dist/ConnectorRegistry.d.ts +28 -0
  20. package/dist/ConnectorRegistry.d.ts.map +1 -0
  21. package/dist/ConnectorRegistry.js +87 -0
  22. package/dist/ConnectorRegistry.js.map +1 -0
  23. package/dist/ContextManagement.d.ts +78 -0
  24. package/dist/ContextManagement.d.ts.map +1 -0
  25. package/dist/ContextManagement.js +221 -0
  26. package/dist/ContextManagement.js.map +1 -0
  27. package/dist/Conversation.d.ts +18 -0
  28. package/dist/Conversation.d.ts.map +1 -0
  29. package/dist/Conversation.js +17 -0
  30. package/dist/Conversation.js.map +1 -0
  31. package/dist/Errors.d.ts +64 -0
  32. package/dist/Errors.d.ts.map +1 -0
  33. package/dist/Errors.js +76 -0
  34. package/dist/Errors.js.map +1 -0
  35. package/dist/HttpClient.d.ts +43 -0
  36. package/dist/HttpClient.d.ts.map +1 -0
  37. package/dist/HttpClient.js +99 -0
  38. package/dist/HttpClient.js.map +1 -0
  39. package/dist/LlmService.d.ts +24 -0
  40. package/dist/LlmService.d.ts.map +1 -0
  41. package/dist/LlmService.js +4 -0
  42. package/dist/LlmService.js.map +1 -0
  43. package/dist/Models.d.ts +138 -0
  44. package/dist/Models.d.ts.map +1 -0
  45. package/dist/Models.js +209 -0
  46. package/dist/Models.js.map +1 -0
  47. package/dist/ProcessExecutor.d.ts +57 -0
  48. package/dist/ProcessExecutor.d.ts.map +1 -0
  49. package/dist/ProcessExecutor.js +71 -0
  50. package/dist/ProcessExecutor.js.map +1 -0
  51. package/dist/RateLimiter.d.ts +44 -0
  52. package/dist/RateLimiter.d.ts.map +1 -0
  53. package/dist/RateLimiter.js +155 -0
  54. package/dist/RateLimiter.js.map +1 -0
  55. package/dist/Streaming.d.ts +17 -0
  56. package/dist/Streaming.d.ts.map +1 -0
  57. package/dist/Streaming.js +65 -0
  58. package/dist/Streaming.js.map +1 -0
  59. package/dist/StructuredOutput.d.ts +10 -0
  60. package/dist/StructuredOutput.d.ts.map +1 -0
  61. package/dist/StructuredOutput.js +97 -0
  62. package/dist/StructuredOutput.js.map +1 -0
  63. package/dist/TemporaryFiles.d.ts +20 -0
  64. package/dist/TemporaryFiles.d.ts.map +1 -0
  65. package/dist/TemporaryFiles.js +18 -0
  66. package/dist/TemporaryFiles.js.map +1 -0
  67. package/dist/UsageLimits.d.ts +5 -0
  68. package/dist/UsageLimits.d.ts.map +1 -0
  69. package/dist/UsageLimits.js +187 -0
  70. package/dist/UsageLimits.js.map +1 -0
  71. package/dist/eval/Checks.d.ts +6 -0
  72. package/dist/eval/Checks.d.ts.map +1 -0
  73. package/dist/eval/Checks.js +39 -0
  74. package/dist/eval/Checks.js.map +1 -0
  75. package/dist/eval/Eval.d.ts +57 -0
  76. package/dist/eval/Eval.d.ts.map +1 -0
  77. package/dist/eval/Eval.js +91 -0
  78. package/dist/eval/Eval.js.map +1 -0
  79. package/dist/eval/EvalSuite.d.ts +21 -0
  80. package/dist/eval/EvalSuite.d.ts.map +1 -0
  81. package/dist/eval/EvalSuite.js +30 -0
  82. package/dist/eval/EvalSuite.js.map +1 -0
  83. package/dist/eval/Evaluator.d.ts +10 -0
  84. package/dist/eval/Evaluator.d.ts.map +1 -0
  85. package/dist/eval/Evaluator.js +14 -0
  86. package/dist/eval/Evaluator.js.map +1 -0
  87. package/dist/eval/Judge.d.ts +15 -0
  88. package/dist/eval/Judge.d.ts.map +1 -0
  89. package/dist/eval/Judge.js +73 -0
  90. package/dist/eval/Judge.js.map +1 -0
  91. package/dist/observability/MeteredLlmService.d.ts +16 -0
  92. package/dist/observability/MeteredLlmService.d.ts.map +1 -0
  93. package/dist/observability/MeteredLlmService.js +66 -0
  94. package/dist/observability/MeteredLlmService.js.map +1 -0
  95. package/dist/observability/Metrics.d.ts +71 -0
  96. package/dist/observability/Metrics.d.ts.map +1 -0
  97. package/dist/observability/Metrics.js +147 -0
  98. package/dist/observability/Metrics.js.map +1 -0
  99. package/dist/observability/Redaction.d.ts +8 -0
  100. package/dist/observability/Redaction.d.ts.map +1 -0
  101. package/dist/observability/Redaction.js +38 -0
  102. package/dist/observability/Redaction.js.map +1 -0
  103. package/dist/observability/StreamRecorder.d.ts +9 -0
  104. package/dist/observability/StreamRecorder.d.ts.map +1 -0
  105. package/dist/observability/StreamRecorder.js +10 -0
  106. package/dist/observability/StreamRecorder.js.map +1 -0
  107. package/dist/observability/StructuredLogger.d.ts +31 -0
  108. package/dist/observability/StructuredLogger.d.ts.map +1 -0
  109. package/dist/observability/StructuredLogger.js +34 -0
  110. package/dist/observability/StructuredLogger.js.map +1 -0
  111. package/dist/observability/Tracing.d.ts +38 -0
  112. package/dist/observability/Tracing.d.ts.map +1 -0
  113. package/dist/observability/Tracing.js +75 -0
  114. package/dist/observability/Tracing.js.map +1 -0
  115. package/dist/providers/AnthropicModels.d.ts +95 -0
  116. package/dist/providers/AnthropicModels.d.ts.map +1 -0
  117. package/dist/providers/AnthropicModels.js +83 -0
  118. package/dist/providers/AnthropicModels.js.map +1 -0
  119. package/dist/providers/AnthropicProvider.d.ts +13 -0
  120. package/dist/providers/AnthropicProvider.d.ts.map +1 -0
  121. package/dist/providers/AnthropicProvider.js +177 -0
  122. package/dist/providers/AnthropicProvider.js.map +1 -0
  123. package/dist/providers/AntigravityConnector.d.ts +7 -0
  124. package/dist/providers/AntigravityConnector.d.ts.map +1 -0
  125. package/dist/providers/AntigravityConnector.js +52 -0
  126. package/dist/providers/AntigravityConnector.js.map +1 -0
  127. package/dist/providers/ClaudeAgentSession.d.ts +11 -0
  128. package/dist/providers/ClaudeAgentSession.d.ts.map +1 -0
  129. package/dist/providers/ClaudeAgentSession.js +158 -0
  130. package/dist/providers/ClaudeAgentSession.js.map +1 -0
  131. package/dist/providers/ClaudeCliConnector.d.ts +9 -0
  132. package/dist/providers/ClaudeCliConnector.d.ts.map +1 -0
  133. package/dist/providers/ClaudeCliConnector.js +123 -0
  134. package/dist/providers/ClaudeCliConnector.js.map +1 -0
  135. package/dist/providers/CliSupport.d.ts +21 -0
  136. package/dist/providers/CliSupport.d.ts.map +1 -0
  137. package/dist/providers/CliSupport.js +62 -0
  138. package/dist/providers/CliSupport.js.map +1 -0
  139. package/dist/providers/CodexConnector.d.ts +12 -0
  140. package/dist/providers/CodexConnector.d.ts.map +1 -0
  141. package/dist/providers/CodexConnector.js +175 -0
  142. package/dist/providers/CodexConnector.js.map +1 -0
  143. package/dist/providers/ConnectorFactories.d.ts +15 -0
  144. package/dist/providers/ConnectorFactories.d.ts.map +1 -0
  145. package/dist/providers/ConnectorFactories.js +74 -0
  146. package/dist/providers/ConnectorFactories.js.map +1 -0
  147. package/dist/providers/CopilotConnector.d.ts +6 -0
  148. package/dist/providers/CopilotConnector.d.ts.map +1 -0
  149. package/dist/providers/CopilotConnector.js +35 -0
  150. package/dist/providers/CopilotConnector.js.map +1 -0
  151. package/dist/providers/CursorConnector.d.ts +8 -0
  152. package/dist/providers/CursorConnector.d.ts.map +1 -0
  153. package/dist/providers/CursorConnector.js +82 -0
  154. package/dist/providers/CursorConnector.js.map +1 -0
  155. package/dist/providers/GeminiApiProvider.d.ts +9 -0
  156. package/dist/providers/GeminiApiProvider.d.ts.map +1 -0
  157. package/dist/providers/GeminiApiProvider.js +181 -0
  158. package/dist/providers/GeminiApiProvider.js.map +1 -0
  159. package/dist/providers/GeminiCliProvider.d.ts +106 -0
  160. package/dist/providers/GeminiCliProvider.d.ts.map +1 -0
  161. package/dist/providers/GeminiCliProvider.js +596 -0
  162. package/dist/providers/GeminiCliProvider.js.map +1 -0
  163. package/dist/providers/GeminiModels.d.ts +92 -0
  164. package/dist/providers/GeminiModels.d.ts.map +1 -0
  165. package/dist/providers/GeminiModels.js +80 -0
  166. package/dist/providers/GeminiModels.js.map +1 -0
  167. package/dist/providers/GrokCliConnector.d.ts +8 -0
  168. package/dist/providers/GrokCliConnector.d.ts.map +1 -0
  169. package/dist/providers/GrokCliConnector.js +82 -0
  170. package/dist/providers/GrokCliConnector.js.map +1 -0
  171. package/dist/providers/LmStudioModels.d.ts +114 -0
  172. package/dist/providers/LmStudioModels.d.ts.map +1 -0
  173. package/dist/providers/LmStudioModels.js +102 -0
  174. package/dist/providers/LmStudioModels.js.map +1 -0
  175. package/dist/providers/LmStudioProvider.d.ts +13 -0
  176. package/dist/providers/LmStudioProvider.d.ts.map +1 -0
  177. package/dist/providers/LmStudioProvider.js +158 -0
  178. package/dist/providers/LmStudioProvider.js.map +1 -0
  179. package/dist/providers/MockProvider.d.ts +6 -0
  180. package/dist/providers/MockProvider.d.ts.map +1 -0
  181. package/dist/providers/MockProvider.js +271 -0
  182. package/dist/providers/MockProvider.js.map +1 -0
  183. package/dist/providers/OllamaModels.d.ts +68 -0
  184. package/dist/providers/OllamaModels.d.ts.map +1 -0
  185. package/dist/providers/OllamaModels.js +59 -0
  186. package/dist/providers/OllamaModels.js.map +1 -0
  187. package/dist/providers/OllamaProvider.d.ts +7 -0
  188. package/dist/providers/OllamaProvider.d.ts.map +1 -0
  189. package/dist/providers/OllamaProvider.js +134 -0
  190. package/dist/providers/OllamaProvider.js.map +1 -0
  191. package/dist/providers/OpenAIModels.d.ts +139 -0
  192. package/dist/providers/OpenAIModels.d.ts.map +1 -0
  193. package/dist/providers/OpenAIModels.js +123 -0
  194. package/dist/providers/OpenAIModels.js.map +1 -0
  195. package/dist/providers/OpenAIProvider.d.ts +9 -0
  196. package/dist/providers/OpenAIProvider.d.ts.map +1 -0
  197. package/dist/providers/OpenAIProvider.js +185 -0
  198. package/dist/providers/OpenAIProvider.js.map +1 -0
  199. package/dist/providers/OpenCodeCliConnector.d.ts +8 -0
  200. package/dist/providers/OpenCodeCliConnector.d.ts.map +1 -0
  201. package/dist/providers/OpenCodeCliConnector.js +93 -0
  202. package/dist/providers/OpenCodeCliConnector.js.map +1 -0
  203. package/dist/providers/OpenCodeModels.d.ts +61 -0
  204. package/dist/providers/OpenCodeModels.d.ts.map +1 -0
  205. package/dist/providers/OpenCodeModels.js +53 -0
  206. package/dist/providers/OpenCodeModels.js.map +1 -0
  207. package/dist/providers/OpenCodeProvider.d.ts +10 -0
  208. package/dist/providers/OpenCodeProvider.d.ts.map +1 -0
  209. package/dist/providers/OpenCodeProvider.js +154 -0
  210. package/dist/providers/OpenCodeProvider.js.map +1 -0
  211. package/dist/providers/PiConnector.d.ts +8 -0
  212. package/dist/providers/PiConnector.d.ts.map +1 -0
  213. package/dist/providers/PiConnector.js +99 -0
  214. package/dist/providers/PiConnector.js.map +1 -0
  215. package/dist/tools/Tool.d.ts +83 -0
  216. package/dist/tools/Tool.d.ts.map +1 -0
  217. package/dist/tools/Tool.js +278 -0
  218. package/dist/tools/Tool.js.map +1 -0
  219. package/dist/tools/ToolCallingExecutor.d.ts +18 -0
  220. package/dist/tools/ToolCallingExecutor.d.ts.map +1 -0
  221. package/dist/tools/ToolCallingExecutor.js +64 -0
  222. package/dist/tools/ToolCallingExecutor.js.map +1 -0
  223. package/dist/tools/ToolRegistry.d.ts +45 -0
  224. package/dist/tools/ToolRegistry.d.ts.map +1 -0
  225. package/dist/tools/ToolRegistry.js +133 -0
  226. package/dist/tools/ToolRegistry.js.map +1 -0
  227. package/package.json +105 -0
  228. package/src/.gitkeep +1 -0
  229. package/src/AgentSession.ts +69 -0
  230. package/src/Capability.ts +240 -0
  231. package/src/Connector.ts +220 -0
  232. package/src/ConnectorConfig.ts +146 -0
  233. package/src/ConnectorRegistry.ts +184 -0
  234. package/src/ContextManagement.ts +372 -0
  235. package/src/Conversation.ts +21 -0
  236. package/src/Errors.ts +87 -0
  237. package/src/HttpClient.ts +202 -0
  238. package/src/LlmService.ts +45 -0
  239. package/src/Models.ts +250 -0
  240. package/src/ProcessExecutor.ts +157 -0
  241. package/src/RateLimiter.ts +238 -0
  242. package/src/Streaming.ts +164 -0
  243. package/src/StructuredOutput.ts +138 -0
  244. package/src/TemporaryFiles.ts +51 -0
  245. package/src/UsageLimits.ts +242 -0
  246. package/src/eval/Checks.ts +68 -0
  247. package/src/eval/Eval.ts +112 -0
  248. package/src/eval/EvalSuite.ts +63 -0
  249. package/src/eval/Evaluator.ts +31 -0
  250. package/src/eval/Judge.ts +92 -0
  251. package/src/observability/MeteredLlmService.ts +156 -0
  252. package/src/observability/Metrics.ts +195 -0
  253. package/src/observability/Redaction.ts +55 -0
  254. package/src/observability/StreamRecorder.ts +32 -0
  255. package/src/observability/StructuredLogger.ts +62 -0
  256. package/src/observability/Tracing.ts +141 -0
  257. package/src/providers/AnthropicModels.ts +96 -0
  258. package/src/providers/AnthropicProvider.ts +320 -0
  259. package/src/providers/AntigravityConnector.ts +77 -0
  260. package/src/providers/ClaudeAgentSession.ts +226 -0
  261. package/src/providers/ClaudeCliConnector.ts +201 -0
  262. package/src/providers/CliSupport.ts +100 -0
  263. package/src/providers/CodexConnector.ts +251 -0
  264. package/src/providers/ConnectorFactories.ts +142 -0
  265. package/src/providers/CopilotConnector.ts +47 -0
  266. package/src/providers/CursorConnector.ts +120 -0
  267. package/src/providers/GeminiApiProvider.ts +337 -0
  268. package/src/providers/GeminiCliProvider.ts +793 -0
  269. package/src/providers/GeminiModels.ts +97 -0
  270. package/src/providers/GrokCliConnector.ts +117 -0
  271. package/src/providers/LmStudioModels.ts +117 -0
  272. package/src/providers/LmStudioProvider.ts +260 -0
  273. package/src/providers/MockProvider.ts +322 -0
  274. package/src/providers/OllamaModels.ts +64 -0
  275. package/src/providers/OllamaProvider.ts +236 -0
  276. package/src/providers/OpenAIModels.ts +152 -0
  277. package/src/providers/OpenAIProvider.ts +331 -0
  278. package/src/providers/OpenCodeCliConnector.ts +135 -0
  279. package/src/providers/OpenCodeModels.ts +60 -0
  280. package/src/providers/OpenCodeProvider.ts +278 -0
  281. package/src/providers/PiConnector.ts +142 -0
  282. package/src/tools/Tool.ts +379 -0
  283. package/src/tools/ToolCallingExecutor.ts +106 -0
  284. package/src/tools/ToolRegistry.ts +224 -0
@@ -0,0 +1,156 @@
1
+ import * as Clock from "effect/Clock"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Ref from "effect/Ref"
4
+ import * as Schema from "effect/Schema"
5
+ import * as Stream from "effect/Stream"
6
+ import type { LlmError } from "../Errors.ts"
7
+ import type { LlmServiceShape, StructuredResult } from "../LlmService.ts"
8
+ import type { LlmChunk, TokenUsage } from "../Models.ts"
9
+ import { RequestMetrics } from "./Metrics.ts"
10
+ import type { MetricsCollectorShape, RequestLabels } from "./Metrics.ts"
11
+
12
+ export class LlmPricing extends Schema.Class<LlmPricing>("LlmPricing")({
13
+ inputUsdPer1k: Schema.Number,
14
+ outputUsdPer1k: Schema.Number
15
+ }) {}
16
+
17
+ export interface MeteredLlmOptions {
18
+ readonly labels: RequestLabels
19
+ readonly pricing?: LlmPricing
20
+ }
21
+
22
+ const estimatedCost = (
23
+ usage: TokenUsage | undefined,
24
+ pricing: LlmPricing | undefined
25
+ ): number | undefined =>
26
+ usage === undefined || pricing === undefined
27
+ ? undefined
28
+ : (usage.prompt / 1_000) * pricing.inputUsdPer1k +
29
+ (usage.completion / 1_000) * pricing.outputUsdPer1k
30
+
31
+ const complete = (
32
+ collector: MetricsCollectorShape,
33
+ options: MeteredLlmOptions,
34
+ startedAt: number,
35
+ endedAt: number,
36
+ success: boolean,
37
+ errorType: string | undefined,
38
+ usage: TokenUsage | undefined
39
+ ): Effect.Effect<void> => {
40
+ const cost = estimatedCost(usage, options.pricing)
41
+ return collector.recordCompleted(
42
+ RequestMetrics.make({
43
+ labels: options.labels,
44
+ latencyMs: Math.max(0, endedAt - startedAt),
45
+ success,
46
+ ...(errorType === undefined ? {} : { errorType }),
47
+ ...(usage === undefined ? {} : { tokenUsage: usage }),
48
+ ...(cost === undefined ? {} : { estimatedCostUsd: cost })
49
+ })
50
+ )
51
+ }
52
+
53
+ const meterEffect = <A, R>(
54
+ effect: Effect.Effect<A, LlmError, R>,
55
+ collector: MetricsCollectorShape,
56
+ options: MeteredLlmOptions,
57
+ usageOf: (value: A) => TokenUsage | undefined
58
+ ): Effect.Effect<A, LlmError, R> =>
59
+ Effect.gen(function* () {
60
+ const startedAt = yield* Clock.currentTimeMillis
61
+ yield* collector.markRequestStarted(options.labels)
62
+ const result = yield* Effect.result(effect)
63
+ const endedAt = yield* Clock.currentTimeMillis
64
+ yield* complete(
65
+ collector,
66
+ options,
67
+ startedAt,
68
+ endedAt,
69
+ result._tag === "Success",
70
+ result._tag === "Failure" ? result.failure._tag : undefined,
71
+ result._tag === "Success" ? usageOf(result.success) : undefined
72
+ )
73
+ return yield* Effect.fromResult(result)
74
+ })
75
+
76
+ interface StreamMeasurement {
77
+ readonly success: boolean
78
+ readonly errorType: string | undefined
79
+ readonly usage: TokenUsage | undefined
80
+ }
81
+
82
+ const meterStream = (
83
+ stream: Stream.Stream<LlmChunk, LlmError>,
84
+ collector: MetricsCollectorShape,
85
+ options: MeteredLlmOptions
86
+ ): Stream.Stream<LlmChunk, LlmError> =>
87
+ Stream.unwrap(
88
+ Effect.gen(function* () {
89
+ const startedAt = yield* Clock.currentTimeMillis
90
+ yield* collector.markRequestStarted(options.labels)
91
+ const measurement = yield* Ref.make<StreamMeasurement>({
92
+ success: true,
93
+ errorType: undefined,
94
+ usage: undefined
95
+ })
96
+ return stream.pipe(
97
+ Stream.tap((chunk) =>
98
+ chunk.usage === undefined
99
+ ? Effect.void
100
+ : Ref.update(measurement, (current) => ({
101
+ ...current,
102
+ usage: chunk.usage
103
+ }))
104
+ ),
105
+ Stream.tapError((error) =>
106
+ Ref.update(measurement, (current) => ({
107
+ ...current,
108
+ success: false,
109
+ errorType: error._tag
110
+ }))
111
+ ),
112
+ Stream.ensuring(
113
+ Effect.gen(function* () {
114
+ const current = yield* Ref.get(measurement)
115
+ const endedAt = yield* Clock.currentTimeMillis
116
+ yield* complete(
117
+ collector,
118
+ options,
119
+ startedAt,
120
+ endedAt,
121
+ current.success,
122
+ current.errorType,
123
+ current.usage
124
+ )
125
+ })
126
+ )
127
+ )
128
+ })
129
+ )
130
+
131
+ export const meterLlmService = (
132
+ service: LlmServiceShape,
133
+ collector: MetricsCollectorShape,
134
+ options: MeteredLlmOptions
135
+ ): LlmServiceShape => ({
136
+ executeStream: (prompt) => meterStream(service.executeStream(prompt), collector, options),
137
+ executeStreamWithHistory: (messages) =>
138
+ meterStream(service.executeStreamWithHistory(messages), collector, options),
139
+ executeWithTools: (prompt, tools) =>
140
+ meterEffect(service.executeWithTools(prompt, tools), collector, options, () => undefined),
141
+ executeStructured: (prompt, schema, jsonSchema) =>
142
+ meterEffect(
143
+ service.executeStructured(prompt, schema, jsonSchema),
144
+ collector,
145
+ options,
146
+ () => undefined
147
+ ),
148
+ executeStructuredWithUsage: (prompt, schema, jsonSchema) =>
149
+ meterEffect(
150
+ service.executeStructuredWithUsage(prompt, schema, jsonSchema),
151
+ collector,
152
+ options,
153
+ <A>(result: StructuredResult<A>) => result[1]
154
+ ),
155
+ isAvailable: service.isAvailable
156
+ })
@@ -0,0 +1,195 @@
1
+ import * as Context from "effect/Context"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Layer from "effect/Layer"
4
+ import * as Ref from "effect/Ref"
5
+ import * as Schema from "effect/Schema"
6
+ import { TokenUsage } from "../Models.ts"
7
+
8
+ export class RequestLabels extends Schema.Class<RequestLabels>("RequestLabels")({
9
+ provider: Schema.String,
10
+ model: Schema.String,
11
+ agent: Schema.optionalKey(Schema.String),
12
+ runId: Schema.optionalKey(Schema.String),
13
+ workflowStep: Schema.optionalKey(Schema.String)
14
+ }) {}
15
+
16
+ export class RequestMetrics extends Schema.Class<RequestMetrics>("RequestMetrics")({
17
+ labels: RequestLabels,
18
+ tokenUsage: Schema.optionalKey(TokenUsage),
19
+ latencyMs: Schema.Number,
20
+ success: Schema.Boolean,
21
+ errorType: Schema.optionalKey(Schema.String),
22
+ estimatedCostUsd: Schema.optionalKey(Schema.Number)
23
+ }) {}
24
+
25
+ export class ProviderHealth extends Schema.Class<ProviderHealth>("ProviderHealth")({
26
+ provider: Schema.String,
27
+ requestCount: Schema.Int,
28
+ successRate: Schema.Number,
29
+ avgLatencyMs: Schema.Number,
30
+ p95LatencyMs: Schema.Number,
31
+ estimatedCostUsd: Schema.Number
32
+ }) {}
33
+
34
+ export class ObservabilitySnapshot extends Schema.Class<ObservabilitySnapshot>(
35
+ "ObservabilitySnapshot"
36
+ )({
37
+ totalRequests: Schema.Int,
38
+ totalErrors: Schema.Int,
39
+ activeRequests: Schema.Int,
40
+ promptTokens: Schema.Int,
41
+ completionTokens: Schema.Int,
42
+ totalTokens: Schema.Int,
43
+ estimatedCostUsd: Schema.Number,
44
+ p50LatencyMs: Schema.Number,
45
+ p95LatencyMs: Schema.Number,
46
+ byProvider: Schema.Array(ProviderHealth),
47
+ byAgentRequests: Schema.Record(Schema.String, Schema.Int)
48
+ }) {}
49
+
50
+ export class DashboardMetricsSnapshot extends Schema.Class<DashboardMetricsSnapshot>(
51
+ "DashboardMetricsSnapshot"
52
+ )({
53
+ providerHealth: Schema.Array(ProviderHealth),
54
+ totalTokens: Schema.Int,
55
+ totalCostUsd: Schema.Number,
56
+ totalRequests: Schema.Int,
57
+ errorRate: Schema.Number
58
+ }) {}
59
+
60
+ export interface MetricsCollectorShape {
61
+ readonly markRequestStarted: (labels: RequestLabels) => Effect.Effect<void>
62
+ readonly recordCompleted: (metrics: RequestMetrics) => Effect.Effect<void>
63
+ readonly snapshot: Effect.Effect<ObservabilitySnapshot>
64
+ readonly dashboardSnapshot: Effect.Effect<DashboardMetricsSnapshot>
65
+ }
66
+
67
+ export class MetricsCollector extends Context.Service<MetricsCollector, MetricsCollectorShape>()(
68
+ "@llm4ts/core/observability/MetricsCollector"
69
+ ) {}
70
+
71
+ interface MetricsState {
72
+ readonly totalRequests: number
73
+ readonly activeRequests: number
74
+ readonly completed: ReadonlyArray<RequestMetrics>
75
+ readonly byAgentRequests: ReadonlyMap<string, number>
76
+ }
77
+
78
+ const percentile = (values: ReadonlyArray<number>, quantile: number): number => {
79
+ if (values.length === 0) {
80
+ return 0
81
+ }
82
+ const sorted = [...values].sort((left, right) => left - right)
83
+ const index = Math.max(0, Math.ceil(quantile * sorted.length) - 1)
84
+ return sorted[index] ?? 0
85
+ }
86
+
87
+ const providerHealth = (
88
+ completed: ReadonlyArray<RequestMetrics>
89
+ ): ReadonlyArray<ProviderHealth> => {
90
+ const providers = new Map<string, Array<RequestMetrics>>()
91
+ for (const metric of completed) {
92
+ const current = providers.get(metric.labels.provider) ?? []
93
+ current.push(metric)
94
+ providers.set(metric.labels.provider, current)
95
+ }
96
+ return [...providers.entries()]
97
+ .sort(([left], [right]) => left.localeCompare(right))
98
+ .map(([provider, metrics]) => {
99
+ const latencies = metrics.map((metric) => metric.latencyMs)
100
+ const successes = metrics.filter((metric) => metric.success).length
101
+ return ProviderHealth.make({
102
+ provider,
103
+ requestCount: metrics.length,
104
+ successRate: metrics.length === 0 ? 1 : successes / metrics.length,
105
+ avgLatencyMs:
106
+ metrics.length === 0
107
+ ? 0
108
+ : latencies.reduce((sum, latency) => sum + latency, 0) / metrics.length,
109
+ p95LatencyMs: percentile(latencies, 0.95),
110
+ estimatedCostUsd: metrics.reduce((sum, metric) => sum + (metric.estimatedCostUsd ?? 0), 0)
111
+ })
112
+ })
113
+ }
114
+
115
+ const toSnapshot = (state: MetricsState): ObservabilitySnapshot => {
116
+ const completed = state.completed
117
+ const promptTokens = completed.reduce((sum, metric) => sum + (metric.tokenUsage?.prompt ?? 0), 0)
118
+ const completionTokens = completed.reduce(
119
+ (sum, metric) => sum + (metric.tokenUsage?.completion ?? 0),
120
+ 0
121
+ )
122
+ return ObservabilitySnapshot.make({
123
+ totalRequests: state.totalRequests,
124
+ totalErrors: completed.filter((metric) => !metric.success).length,
125
+ activeRequests: state.activeRequests,
126
+ promptTokens,
127
+ completionTokens,
128
+ totalTokens: promptTokens + completionTokens,
129
+ estimatedCostUsd: completed.reduce((sum, metric) => sum + (metric.estimatedCostUsd ?? 0), 0),
130
+ p50LatencyMs: percentile(
131
+ completed.map((metric) => metric.latencyMs),
132
+ 0.5
133
+ ),
134
+ p95LatencyMs: percentile(
135
+ completed.map((metric) => metric.latencyMs),
136
+ 0.95
137
+ ),
138
+ byProvider: providerHealth(completed),
139
+ byAgentRequests: Object.fromEntries(state.byAgentRequests)
140
+ })
141
+ }
142
+
143
+ export const makeMetricsCollector = Effect.fn("@llm4ts/core/observability/MetricsCollector.make")(
144
+ function* (): Effect.fn.Return<MetricsCollectorShape> {
145
+ const state = yield* Ref.make<MetricsState>({
146
+ totalRequests: 0,
147
+ activeRequests: 0,
148
+ completed: [],
149
+ byAgentRequests: new Map()
150
+ })
151
+
152
+ const markRequestStarted = (labels: RequestLabels): Effect.Effect<void> =>
153
+ Ref.update(state, (current) => {
154
+ const agents = new Map(current.byAgentRequests)
155
+ if (labels.agent !== undefined) {
156
+ agents.set(labels.agent, (agents.get(labels.agent) ?? 0) + 1)
157
+ }
158
+ return {
159
+ totalRequests: current.totalRequests + 1,
160
+ activeRequests: current.activeRequests + 1,
161
+ completed: current.completed,
162
+ byAgentRequests: agents
163
+ }
164
+ })
165
+
166
+ const recordCompleted = (metrics: RequestMetrics): Effect.Effect<void> =>
167
+ Ref.update(state, (current) => ({
168
+ ...current,
169
+ activeRequests: Math.max(0, current.activeRequests - 1),
170
+ completed: [...current.completed, metrics]
171
+ }))
172
+
173
+ const snapshot = Ref.get(state).pipe(Effect.map(toSnapshot))
174
+ const dashboardSnapshot = snapshot.pipe(
175
+ Effect.map((current) =>
176
+ DashboardMetricsSnapshot.make({
177
+ providerHealth: current.byProvider,
178
+ totalTokens: current.totalTokens,
179
+ totalCostUsd: current.estimatedCostUsd,
180
+ totalRequests: current.totalRequests,
181
+ errorRate: current.totalRequests === 0 ? 0 : current.totalErrors / current.totalRequests
182
+ })
183
+ )
184
+ )
185
+
186
+ return {
187
+ markRequestStarted,
188
+ recordCompleted,
189
+ snapshot,
190
+ dashboardSnapshot
191
+ }
192
+ }
193
+ )
194
+
195
+ export const MetricsCollectorLive = Layer.effect(MetricsCollector, makeMetricsCollector())
@@ -0,0 +1,55 @@
1
+ import type { JsonValue } from "../providers/CliSupport.ts"
2
+
3
+ export interface RedactionOptions {
4
+ readonly secrets?: ReadonlyArray<string>
5
+ readonly maxLength?: number
6
+ }
7
+
8
+ const sensitiveKey = /(?:api[-_]?key|authorization|cookie|password|secret|token)/i
9
+ const credentialPatterns: ReadonlyArray<RegExp> = [
10
+ /\bBearer\s+[A-Za-z0-9._~+/=-]+\b/gi,
11
+ /\b(?:sk|xox[baprs]|gh[opusr])[-_][A-Za-z0-9_-]{8,}\b/g
12
+ ]
13
+
14
+ export const redactText = (text: string, options: RedactionOptions = {}): string => {
15
+ let redacted = text
16
+ for (const secret of options.secrets ?? []) {
17
+ if (secret.length > 0) {
18
+ redacted = redacted.split(secret).join("[REDACTED]")
19
+ }
20
+ }
21
+ for (const pattern of credentialPatterns) {
22
+ redacted = redacted.replace(pattern, "[REDACTED]")
23
+ }
24
+ const maxLength = Math.max(0, options.maxLength ?? 2_048)
25
+ return redacted.length <= maxLength ? redacted : `${redacted.slice(0, maxLength)}…[TRUNCATED]`
26
+ }
27
+
28
+ const sanitizeValue = (value: JsonValue, options: RedactionOptions): JsonValue => {
29
+ if (typeof value === "string") {
30
+ return redactText(value, options)
31
+ }
32
+ if (Array.isArray(value)) {
33
+ return value.map((item) => sanitizeValue(item, options))
34
+ }
35
+ if (value !== null && typeof value === "object") {
36
+ return Object.fromEntries(
37
+ Object.entries(value).map(([key, child]) => [
38
+ key,
39
+ sensitiveKey.test(key) ? "[REDACTED]" : sanitizeValue(child, options)
40
+ ])
41
+ )
42
+ }
43
+ return value
44
+ }
45
+
46
+ export const sanitizeFields = (
47
+ fields: Readonly<Record<string, JsonValue>>,
48
+ options: RedactionOptions = {}
49
+ ): Readonly<Record<string, JsonValue>> =>
50
+ Object.fromEntries(
51
+ Object.entries(fields).map(([key, value]) => [
52
+ key,
53
+ sensitiveKey.test(key) ? "[REDACTED]" : sanitizeValue(value, options)
54
+ ])
55
+ )
@@ -0,0 +1,32 @@
1
+ import * as Context from "effect/Context"
2
+ import * as Effect from "effect/Effect"
3
+
4
+ export interface StreamRecorder {
5
+ readonly rawLine: (
6
+ provider: string,
7
+ model: string | undefined,
8
+ line: string
9
+ ) => Effect.Effect<void>
10
+ readonly streamError: (
11
+ provider: string,
12
+ model: string | undefined,
13
+ message: string
14
+ ) => Effect.Effect<void>
15
+ }
16
+
17
+ export const noopStreamRecorder: StreamRecorder = {
18
+ rawLine: (_provider, _model, _line) => Effect.void,
19
+ streamError: (_provider, _model, _message) => Effect.void
20
+ }
21
+
22
+ const CurrentStreamRecorder = Context.Reference<StreamRecorder>(
23
+ "@llm4ts/core/observability/CurrentStreamRecorder",
24
+ { defaultValue: () => noopStreamRecorder }
25
+ )
26
+
27
+ export const currentStreamRecorder: Effect.Effect<StreamRecorder> = CurrentStreamRecorder
28
+
29
+ export const withStreamRecorder =
30
+ (recorder: StreamRecorder) =>
31
+ <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
32
+ Effect.provideService(effect, CurrentStreamRecorder, recorder)
@@ -0,0 +1,62 @@
1
+ import * as Clock from "effect/Clock"
2
+ import * as Context from "effect/Context"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Layer from "effect/Layer"
5
+ import * as Schema from "effect/Schema"
6
+ import type { JsonValue } from "../providers/CliSupport.ts"
7
+ import { redactText, sanitizeFields, type RedactionOptions } from "./Redaction.ts"
8
+ import type { TracingServiceShape } from "./Tracing.ts"
9
+
10
+ export const LogLevel = Schema.Literals(["Debug", "Info", "Warning", "Error"])
11
+ export type LogLevel = typeof LogLevel.Type
12
+
13
+ export class StructuredLogEvent extends Schema.Class<StructuredLogEvent>("StructuredLogEvent")({
14
+ level: LogLevel,
15
+ message: Schema.String,
16
+ correlationId: Schema.String,
17
+ timestamp: Schema.Number,
18
+ fields: Schema.Record(Schema.String, Schema.Json)
19
+ }) {}
20
+
21
+ export interface StructuredLoggerShape {
22
+ readonly log: (
23
+ level: LogLevel,
24
+ message: string,
25
+ fields?: Readonly<Record<string, JsonValue>>
26
+ ) => Effect.Effect<void>
27
+ }
28
+
29
+ export class StructuredLogger extends Context.Service<StructuredLogger, StructuredLoggerShape>()(
30
+ "@llm4ts/core/observability/StructuredLogger"
31
+ ) {}
32
+
33
+ export interface StructuredLoggerOptions extends RedactionOptions {
34
+ readonly sink: (event: StructuredLogEvent) => Effect.Effect<void>
35
+ }
36
+
37
+ export const makeStructuredLogger = (
38
+ tracing: TracingServiceShape,
39
+ options: StructuredLoggerOptions
40
+ ): StructuredLoggerShape => ({
41
+ log: Effect.fn("@llm4ts/core/observability/StructuredLogger.log")(function* (
42
+ level: LogLevel,
43
+ message: string,
44
+ fields: Readonly<Record<string, JsonValue>> = {}
45
+ ) {
46
+ const correlationId = yield* tracing.correlationId
47
+ const timestamp = yield* Clock.currentTimeMillis
48
+ yield* options.sink(
49
+ StructuredLogEvent.make({
50
+ level,
51
+ message: redactText(message, options),
52
+ correlationId,
53
+ timestamp,
54
+ fields: sanitizeFields(fields, options)
55
+ })
56
+ )
57
+ })
58
+ })
59
+
60
+ export const StructuredLoggerNoop = Layer.succeed(StructuredLogger)({
61
+ log: (_level, _message, _fields) => Effect.void
62
+ })
@@ -0,0 +1,141 @@
1
+ import * as Clock from "effect/Clock"
2
+ import * as Context from "effect/Context"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Exit from "effect/Exit"
5
+ import * as Layer from "effect/Layer"
6
+ import * as Ref from "effect/Ref"
7
+ import * as Schema from "effect/Schema"
8
+ import type { JsonValue } from "../providers/CliSupport.ts"
9
+ import { redactText, sanitizeFields, type RedactionOptions } from "./Redaction.ts"
10
+
11
+ export const TraceStatus = Schema.Literals(["Ok", "Error"])
12
+ export type TraceStatus = typeof TraceStatus.Type
13
+
14
+ export class TraceSpan extends Schema.Class<TraceSpan>("TraceSpan")({
15
+ traceId: Schema.String,
16
+ spanId: Schema.String,
17
+ parentSpanId: Schema.optionalKey(Schema.String),
18
+ correlationId: Schema.String,
19
+ name: Schema.String,
20
+ status: TraceStatus,
21
+ startedAt: Schema.Number,
22
+ endedAt: Schema.Number,
23
+ attributes: Schema.Record(Schema.String, Schema.Json),
24
+ errorMessage: Schema.optionalKey(Schema.String)
25
+ }) {}
26
+
27
+ interface TraceContext {
28
+ readonly traceId: string
29
+ readonly spanId: string
30
+ readonly correlationId: string
31
+ }
32
+
33
+ const CurrentTraceContext = Context.Reference<TraceContext | undefined>(
34
+ "@llm4ts/core/observability/CurrentTraceContext",
35
+ { defaultValue: () => undefined }
36
+ )
37
+
38
+ export interface TracingServiceShape {
39
+ readonly withCorrelationId: <A, E, R>(
40
+ id: string,
41
+ effect: Effect.Effect<A, E, R>
42
+ ) => Effect.Effect<A, E, R>
43
+ readonly correlationId: Effect.Effect<string>
44
+ readonly inSpan: <A, E, R>(
45
+ name: string,
46
+ attributes: Readonly<Record<string, JsonValue>>,
47
+ effect: Effect.Effect<A, E, R>
48
+ ) => Effect.Effect<A, E, R>
49
+ readonly recordedSpans: Effect.Effect<ReadonlyArray<TraceSpan>>
50
+ }
51
+
52
+ export class TracingService extends Context.Service<TracingService, TracingServiceShape>()(
53
+ "@llm4ts/core/observability/TracingService"
54
+ ) {}
55
+
56
+ export interface TracingOptions extends RedactionOptions {
57
+ readonly makeId?: Effect.Effect<string>
58
+ }
59
+
60
+ const defaultId = Effect.sync(() => globalThis.crypto.randomUUID())
61
+
62
+ export const makeTracingService = Effect.fn("@llm4ts/core/observability/TracingService.make")(
63
+ function* (options: TracingOptions = {}): Effect.fn.Return<TracingServiceShape> {
64
+ const spans = yield* Ref.make<ReadonlyArray<TraceSpan>>([])
65
+ const makeId = options.makeId ?? defaultId
66
+
67
+ const correlationId = Effect.flatMap(CurrentTraceContext, (current) =>
68
+ current === undefined ? makeId : Effect.succeed(current.correlationId)
69
+ )
70
+
71
+ const withCorrelationId = <A, E, R>(
72
+ id: string,
73
+ effect: Effect.Effect<A, E, R>
74
+ ): Effect.Effect<A, E, R> =>
75
+ Effect.flatMap(CurrentTraceContext, (current) =>
76
+ Effect.provideService(effect, CurrentTraceContext, {
77
+ traceId: current?.traceId ?? id,
78
+ spanId: current?.spanId ?? id,
79
+ correlationId: id
80
+ })
81
+ )
82
+
83
+ const inSpan = <A, E, R>(
84
+ name: string,
85
+ attributes: Readonly<Record<string, JsonValue>>,
86
+ effect: Effect.Effect<A, E, R>
87
+ ): Effect.Effect<A, E, R> =>
88
+ Effect.gen(function* () {
89
+ const parent = yield* CurrentTraceContext
90
+ const correlation = yield* correlationId
91
+ const traceId = parent?.traceId ?? (yield* makeId)
92
+ const spanId = yield* makeId
93
+ const startedAt = yield* Clock.currentTimeMillis
94
+ const next: TraceContext = {
95
+ traceId,
96
+ spanId,
97
+ correlationId: correlation
98
+ }
99
+ return yield* Effect.provideService(
100
+ effect.pipe(
101
+ Effect.onExit((exit) =>
102
+ Clock.currentTimeMillis.pipe(
103
+ Effect.flatMap((endedAt) =>
104
+ Ref.update(spans, (current) => [
105
+ ...current,
106
+ TraceSpan.make({
107
+ traceId,
108
+ spanId,
109
+ ...(parent === undefined ? {} : { parentSpanId: parent.spanId }),
110
+ correlationId: correlation,
111
+ name,
112
+ status: Exit.isSuccess(exit) ? "Ok" : "Error",
113
+ startedAt,
114
+ endedAt,
115
+ attributes: sanitizeFields(attributes, options),
116
+ ...(Exit.isFailure(exit)
117
+ ? {
118
+ errorMessage: redactText(String(exit.cause), options)
119
+ }
120
+ : {})
121
+ })
122
+ ])
123
+ )
124
+ )
125
+ )
126
+ ),
127
+ CurrentTraceContext,
128
+ next
129
+ )
130
+ })
131
+
132
+ return {
133
+ withCorrelationId,
134
+ correlationId,
135
+ inSpan,
136
+ recordedSpans: Ref.get(spans)
137
+ }
138
+ }
139
+ )
140
+
141
+ export const TracingServiceLive = Layer.effect(TracingService, makeTracingService())