@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,157 @@
1
+ import * as Context from "effect/Context"
2
+ import * as Effect from "effect/Effect"
3
+ import type * as Queue from "effect/Queue"
4
+ import * as Ref from "effect/Ref"
5
+ import * as Schema from "effect/Schema"
6
+ import type * as Scope from "effect/Scope"
7
+ import * as Stream from "effect/Stream"
8
+ import { InvalidRequestError, ProviderError, type LlmError } from "./Errors.ts"
9
+
10
+ const emptyLines: ReadonlyArray<string> = Object.freeze([])
11
+ const emptyEnvironment: Readonly<Record<string, string>> = Object.freeze({})
12
+
13
+ export class ProcessResult extends Schema.Class<ProcessResult>("ProcessResult")({
14
+ stdout: Schema.Array(Schema.String),
15
+ exitCode: Schema.Int,
16
+ stderr: Schema.Array(Schema.String).pipe(
17
+ Schema.withConstructorDefault(Effect.succeed(emptyLines))
18
+ )
19
+ }) {}
20
+
21
+ export class ProcessInvocation extends Schema.Class<ProcessInvocation>("ProcessInvocation")({
22
+ argv: Schema.Array(Schema.String),
23
+ cwd: Schema.String,
24
+ envVars: Schema.Record(Schema.String, Schema.String).pipe(
25
+ Schema.withConstructorDefault(Effect.succeed(emptyEnvironment))
26
+ ),
27
+ stdin: Schema.optionalKey(Schema.String),
28
+ streaming: Schema.Boolean
29
+ }) {}
30
+
31
+ export type BidirectionalProcess = readonly [
32
+ stdin: Queue.Queue<string>,
33
+ stdout: Stream.Stream<string, LlmError>
34
+ ]
35
+
36
+ export interface ProcessExecutorShape {
37
+ readonly run: (
38
+ argv: ReadonlyArray<string>,
39
+ cwd: string,
40
+ envVars: Readonly<Record<string, string>>
41
+ ) => Effect.Effect<ProcessResult, LlmError>
42
+ readonly runStreaming: (
43
+ argv: ReadonlyArray<string>,
44
+ cwd: string,
45
+ envVars: Readonly<Record<string, string>>
46
+ ) => Stream.Stream<string, LlmError>
47
+ readonly runWithStdin: (
48
+ argv: ReadonlyArray<string>,
49
+ cwd: string,
50
+ envVars: Readonly<Record<string, string>>,
51
+ stdin: string
52
+ ) => Effect.Effect<ProcessResult, LlmError>
53
+ readonly runStreamingWithStdin: (
54
+ argv: ReadonlyArray<string>,
55
+ cwd: string,
56
+ envVars: Readonly<Record<string, string>>,
57
+ stdin: string
58
+ ) => Stream.Stream<string, LlmError>
59
+ readonly runBidirectional: (
60
+ argv: ReadonlyArray<string>,
61
+ cwd: string,
62
+ envVars: Readonly<Record<string, string>>
63
+ ) => Effect.Effect<BidirectionalProcess, LlmError, Scope.Scope>
64
+ }
65
+
66
+ export class ProcessExecutor extends Context.Service<ProcessExecutor, ProcessExecutorShape>()(
67
+ "@llm4ts/core/ProcessExecutor"
68
+ ) {}
69
+
70
+ export interface ProcessExecutorPrimitives {
71
+ readonly run: ProcessExecutorShape["run"]
72
+ readonly runStreaming: ProcessExecutorShape["runStreaming"]
73
+ readonly runWithStdin?: ProcessExecutorShape["runWithStdin"]
74
+ readonly runStreamingWithStdin?: ProcessExecutorShape["runStreamingWithStdin"]
75
+ readonly runBidirectional?: ProcessExecutorShape["runBidirectional"]
76
+ }
77
+
78
+ export const makeProcessExecutor = (
79
+ primitives: ProcessExecutorPrimitives
80
+ ): ProcessExecutorShape => ({
81
+ run: primitives.run,
82
+ runStreaming: primitives.runStreaming,
83
+ runWithStdin:
84
+ primitives.runWithStdin ?? ((argv, cwd, envVars, _stdin) => primitives.run(argv, cwd, envVars)),
85
+ runStreamingWithStdin:
86
+ primitives.runStreamingWithStdin ??
87
+ ((argv, cwd, envVars, _stdin) => primitives.runStreaming(argv, cwd, envVars)),
88
+ runBidirectional:
89
+ primitives.runBidirectional ??
90
+ ((argv, _cwd, _envVars) =>
91
+ Effect.fail(
92
+ InvalidRequestError.make({
93
+ message: `${argv[0] ?? "process"} does not support bidirectional sessions`
94
+ })
95
+ ))
96
+ })
97
+
98
+ export const processCommandKey = (argv: ReadonlyArray<string>): string => JSON.stringify(argv)
99
+
100
+ export interface FakeProcessExecutor {
101
+ readonly executor: ProcessExecutorShape
102
+ readonly recorded: Effect.Effect<ReadonlyArray<ProcessInvocation>>
103
+ }
104
+
105
+ export interface FakeProcessPlan {
106
+ readonly responses?: ReadonlyMap<string, ProcessResult>
107
+ readonly streamResponses?: ReadonlyMap<string, ReadonlyArray<string>>
108
+ }
109
+
110
+ export const makeFakeProcessExecutor = Effect.fn("@llm4ts/core/ProcessExecutor.makeFake")(
111
+ function* (plan: FakeProcessPlan = {}): Effect.fn.Return<FakeProcessExecutor> {
112
+ const invocations = yield* Ref.make<ReadonlyArray<ProcessInvocation>>([])
113
+ const record = (invocation: ProcessInvocation): Effect.Effect<void> =>
114
+ Ref.update(invocations, (current) => [...current, invocation])
115
+
116
+ const executor = makeProcessExecutor({
117
+ run: (argv, cwd, envVars) => {
118
+ const invocation = ProcessInvocation.make({
119
+ argv,
120
+ cwd,
121
+ envVars,
122
+ streaming: false
123
+ })
124
+ const response = plan.responses?.get(processCommandKey(argv))
125
+ return record(invocation).pipe(
126
+ Effect.andThen(
127
+ response === undefined
128
+ ? Effect.fail(
129
+ ProviderError.make({
130
+ message: `No fake response for argv: ${argv.join(" ")}`
131
+ })
132
+ )
133
+ : Effect.succeed(response)
134
+ )
135
+ )
136
+ },
137
+ runStreaming: (argv, cwd, envVars) => {
138
+ const invocation = ProcessInvocation.make({
139
+ argv,
140
+ cwd,
141
+ envVars,
142
+ streaming: true
143
+ })
144
+ const lines = plan.streamResponses?.get(processCommandKey(argv)) ?? emptyLines
145
+ return Stream.concat(
146
+ Stream.drain(Stream.fromEffect(record(invocation))),
147
+ Stream.fromIterable(lines)
148
+ )
149
+ }
150
+ })
151
+
152
+ return {
153
+ executor,
154
+ recorded: Ref.get(invocations)
155
+ }
156
+ }
157
+ )
@@ -0,0 +1,238 @@
1
+ import * as Clock from "effect/Clock"
2
+ import * as Context from "effect/Context"
3
+ import * as Duration from "effect/Duration"
4
+ import * as Effect from "effect/Effect"
5
+ import * as Layer from "effect/Layer"
6
+ import * as Ref from "effect/Ref"
7
+ import * as Schema from "effect/Schema"
8
+ import type { LlmConfig } from "./Models.ts"
9
+
10
+ export class RateLimiterAcquireTimeout extends Schema.TaggedErrorClass<RateLimiterAcquireTimeout>()(
11
+ "AcquireTimeout",
12
+ {
13
+ timeout: Schema.Duration
14
+ }
15
+ ) {}
16
+
17
+ export class RateLimiterInvalidConfig extends Schema.TaggedErrorClass<RateLimiterInvalidConfig>()(
18
+ "InvalidConfig",
19
+ {
20
+ details: Schema.String
21
+ }
22
+ ) {}
23
+
24
+ export const RateLimiterError = Schema.Union([RateLimiterAcquireTimeout, RateLimiterInvalidConfig])
25
+ export type RateLimiterError = typeof RateLimiterError.Type
26
+
27
+ export class RateLimiterConfig extends Schema.Class<RateLimiterConfig>("RateLimiterConfig")({
28
+ requestsPerMinute: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(60))),
29
+ burstSize: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(10))),
30
+ acquireTimeout: Schema.Duration.pipe(
31
+ Schema.withConstructorDefault(Effect.succeed(Duration.seconds(30)))
32
+ )
33
+ }) {}
34
+
35
+ export const rateLimiterConfigFromLlmConfig = (config: LlmConfig): RateLimiterConfig =>
36
+ RateLimiterConfig.make({
37
+ requestsPerMinute: config.requestsPerMinute,
38
+ burstSize: config.burstSize,
39
+ acquireTimeout: config.acquireTimeout
40
+ })
41
+
42
+ export class RateLimiterMetrics extends Schema.Class<RateLimiterMetrics>("RateLimiterMetrics")({
43
+ totalRequests: Schema.Int,
44
+ throttledRequests: Schema.Int,
45
+ currentTokens: Schema.Int
46
+ }) {}
47
+
48
+ export interface RateLimiterShape {
49
+ readonly acquire: Effect.Effect<void, RateLimiterError>
50
+ readonly tryAcquire: Effect.Effect<boolean>
51
+ readonly metrics: Effect.Effect<RateLimiterMetrics>
52
+ }
53
+
54
+ export class RateLimiter extends Context.Service<RateLimiter, RateLimiterShape>()(
55
+ "@llm4ts/core/RateLimiter"
56
+ ) {}
57
+
58
+ interface BucketState {
59
+ readonly tokens: number
60
+ readonly lastRefillNanos: bigint
61
+ }
62
+
63
+ interface TakeResult {
64
+ readonly acquired: boolean
65
+ readonly tokensAfter: number
66
+ readonly deficit: number
67
+ }
68
+
69
+ const nanosPerSecond = 1_000_000_000
70
+
71
+ const configDetails = (config: RateLimiterConfig): string =>
72
+ `requestsPerMinute=${config.requestsPerMinute}, burstSize=${config.burstSize}, ` +
73
+ `acquireTimeout=${Duration.format(config.acquireTimeout)}`
74
+
75
+ const isConfigValid = (config: RateLimiterConfig): boolean =>
76
+ config.requestsPerMinute > 0 &&
77
+ config.burstSize > 0 &&
78
+ Duration.toMillis(config.acquireTimeout) > 0
79
+
80
+ export const makeRateLimiter = Effect.fn("@llm4ts/core/RateLimiter.make")(function* (
81
+ config: RateLimiterConfig
82
+ ) {
83
+ const initialNanos = yield* Clock.currentTimeNanos
84
+ const state = yield* Ref.make<BucketState>({
85
+ tokens: config.burstSize,
86
+ lastRefillNanos: initialNanos
87
+ })
88
+ const metrics = yield* Ref.make(
89
+ RateLimiterMetrics.make({
90
+ totalRequests: 0,
91
+ throttledRequests: 0,
92
+ currentTokens: config.burstSize
93
+ })
94
+ )
95
+ const capacity = config.burstSize
96
+ const ratePerSecond = config.requestsPerMinute / 60
97
+
98
+ const takeToken: Effect.Effect<TakeResult> = Effect.gen(function* () {
99
+ const now = yield* Clock.currentTimeNanos
100
+
101
+ return yield* Ref.modify(state, (current) => {
102
+ const elapsedNanos = now - current.lastRefillNanos
103
+ const tokensToAdd =
104
+ elapsedNanos <= 0n ? 0 : (Number(elapsedNanos) / nanosPerSecond) * ratePerSecond
105
+ const refilled =
106
+ tokensToAdd <= 0 ? current.tokens : Math.min(capacity, current.tokens + tokensToAdd)
107
+ const lastRefillNanos = tokensToAdd <= 0 ? current.lastRefillNanos : now
108
+
109
+ if (refilled >= 1) {
110
+ const remaining = refilled - 1
111
+ return [
112
+ {
113
+ acquired: true,
114
+ tokensAfter: remaining,
115
+ deficit: 0
116
+ },
117
+ {
118
+ tokens: remaining,
119
+ lastRefillNanos
120
+ }
121
+ ]
122
+ }
123
+
124
+ return [
125
+ {
126
+ acquired: false,
127
+ tokensAfter: refilled,
128
+ deficit: 1 - refilled
129
+ },
130
+ {
131
+ tokens: refilled,
132
+ lastRefillNanos
133
+ }
134
+ ]
135
+ })
136
+ })
137
+
138
+ const updateCurrentTokens = (tokens: number): Effect.Effect<void> =>
139
+ Ref.update(metrics, (current) =>
140
+ RateLimiterMetrics.make({
141
+ ...current,
142
+ currentTokens: Math.trunc(tokens)
143
+ })
144
+ )
145
+
146
+ const waitForToken = (deficit: number): Effect.Effect<void, RateLimiterInvalidConfig> => {
147
+ if (ratePerSecond <= 0) {
148
+ return Effect.fail(
149
+ RateLimiterInvalidConfig.make({
150
+ details: configDetails(config)
151
+ })
152
+ )
153
+ }
154
+
155
+ const nanoseconds = BigInt(Math.max(0, Math.ceil((deficit / ratePerSecond) * nanosPerSecond)))
156
+ return Effect.sleep(Duration.nanos(nanoseconds))
157
+ }
158
+
159
+ const acquireLoop = (alreadyThrottled: boolean): Effect.Effect<void, RateLimiterError> =>
160
+ Effect.gen(function* () {
161
+ const result = yield* takeToken
162
+ yield* updateCurrentTokens(result.tokensAfter)
163
+
164
+ if (result.acquired) {
165
+ return
166
+ }
167
+
168
+ if (!alreadyThrottled) {
169
+ yield* Ref.update(metrics, (current) =>
170
+ RateLimiterMetrics.make({
171
+ ...current,
172
+ throttledRequests: current.throttledRequests + 1
173
+ })
174
+ )
175
+ }
176
+
177
+ yield* waitForToken(result.deficit)
178
+ return yield* Effect.suspend(() => acquireLoop(true))
179
+ })
180
+
181
+ const acquire: Effect.Effect<void, RateLimiterError> = Effect.suspend(() => {
182
+ if (!isConfigValid(config)) {
183
+ return Effect.fail(
184
+ RateLimiterInvalidConfig.make({
185
+ details: configDetails(config)
186
+ })
187
+ )
188
+ }
189
+
190
+ return Ref.update(metrics, (current) =>
191
+ RateLimiterMetrics.make({
192
+ ...current,
193
+ totalRequests: current.totalRequests + 1
194
+ })
195
+ ).pipe(
196
+ Effect.andThen(acquireLoop(false)),
197
+ Effect.timeoutOrElse({
198
+ duration: config.acquireTimeout,
199
+ orElse: () =>
200
+ Effect.fail(
201
+ RateLimiterAcquireTimeout.make({
202
+ timeout: config.acquireTimeout
203
+ })
204
+ )
205
+ })
206
+ )
207
+ })
208
+
209
+ const tryAcquire: Effect.Effect<boolean> = Effect.suspend(() => {
210
+ if (!isConfigValid(config)) {
211
+ return Effect.logWarning(`Rate limiter config invalid: ${configDetails(config)}`).pipe(
212
+ Effect.as(false)
213
+ )
214
+ }
215
+
216
+ return Effect.gen(function* () {
217
+ const result = yield* takeToken
218
+ yield* updateCurrentTokens(result.tokensAfter)
219
+ yield* Ref.update(metrics, (current) =>
220
+ RateLimiterMetrics.make({
221
+ ...current,
222
+ totalRequests: current.totalRequests + 1,
223
+ throttledRequests: current.throttledRequests + (result.acquired ? 0 : 1)
224
+ })
225
+ )
226
+ return result.acquired
227
+ })
228
+ })
229
+
230
+ return RateLimiter.of({
231
+ acquire,
232
+ tryAcquire,
233
+ metrics: Ref.get(metrics)
234
+ })
235
+ })
236
+
237
+ export const rateLimiterLayer = (config: RateLimiterConfig): Layer.Layer<RateLimiter> =>
238
+ Layer.effect(RateLimiter, makeRateLimiter(config))
@@ -0,0 +1,164 @@
1
+ import * as Clock from "effect/Clock"
2
+ import * as Deferred from "effect/Deferred"
3
+ import * as Duration from "effect/Duration"
4
+ import * as Effect from "effect/Effect"
5
+ import * as Schema from "effect/Schema"
6
+ import * as Stream from "effect/Stream"
7
+ import { ParseError, TimeoutError, type LlmError } from "./Errors.ts"
8
+ import { LlmChunk, LlmResponse, StreamProgress } from "./Models.ts"
9
+
10
+ const appendChunk = (response: LlmResponse, chunk: LlmChunk): LlmResponse => {
11
+ const usage = chunk.usage ?? response.usage
12
+
13
+ return LlmResponse.make({
14
+ content: response.content + chunk.delta,
15
+ metadata: {
16
+ ...response.metadata,
17
+ ...chunk.metadata
18
+ },
19
+ ...(usage === undefined ? {} : { usage })
20
+ })
21
+ }
22
+
23
+ export const collect = Effect.fn("@llm4ts/core/Streaming.collect")(
24
+ <R>(stream: Stream.Stream<LlmChunk, LlmError, R>) =>
25
+ Stream.runFold(stream, () => LlmResponse.make({ content: "" }), appendChunk)
26
+ )
27
+
28
+ const estimateTokens = (text: string): number => Math.max(1, Math.floor(text.length / 4))
29
+
30
+ export const trackProgress = <R, R2>(
31
+ stream: Stream.Stream<LlmChunk, LlmError, R>,
32
+ onProgress: (progress: StreamProgress) => Effect.Effect<void, never, R2>
33
+ ): Stream.Stream<LlmChunk, LlmError, R | R2> =>
34
+ Stream.unwrap(
35
+ Effect.map(Clock.currentTimeMillis, (startTime) =>
36
+ stream.pipe(
37
+ Stream.mapAccumEffect(
38
+ () => 0,
39
+ (tokensProcessed, chunk) =>
40
+ Effect.gen(function* () {
41
+ const now = yield* Clock.currentTimeMillis
42
+ const elapsedMs = now - startTime
43
+ const nextTokensProcessed = tokensProcessed + estimateTokens(chunk.delta)
44
+ const elapsedSeconds = elapsedMs / 1_000
45
+ const tokensPerSecond = elapsedSeconds > 0 ? nextTokensProcessed / elapsedSeconds : 0
46
+
47
+ yield* onProgress(
48
+ StreamProgress.make({
49
+ tokensProcessed: nextTokensProcessed,
50
+ tokensPerSecond,
51
+ elapsedMs
52
+ })
53
+ )
54
+
55
+ return [nextTokensProcessed, [chunk]] as const
56
+ })
57
+ )
58
+ )
59
+ )
60
+ )
61
+
62
+ export const parsePartialJson = <A, E, RD, RE, R>(
63
+ stream: Stream.Stream<string, LlmError, R>,
64
+ schema: Schema.ConstraintCodec<A, E, RD, RE>
65
+ ): Stream.Stream<A, LlmError, R | RD> =>
66
+ stream.pipe(
67
+ Stream.mapAccumEffect(
68
+ () => "",
69
+ (accumulated, chunk) => {
70
+ const next = accumulated + chunk
71
+ return Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(next).pipe(
72
+ Effect.match({
73
+ onFailure: () => [next, []] as const,
74
+ onSuccess: (value) => [next, [value]] as const
75
+ })
76
+ )
77
+ }
78
+ )
79
+ )
80
+
81
+ const timeoutError = (duration: Duration.Input): TimeoutError =>
82
+ TimeoutError.make({ duration: Duration.fromInputUnsafe(duration) })
83
+
84
+ export const withTimeout = <R>(
85
+ stream: Stream.Stream<LlmChunk, LlmError, R>,
86
+ duration: Duration.Input
87
+ ): Stream.Stream<LlmChunk, LlmError, R> =>
88
+ Stream.timeoutOrElse(stream, {
89
+ duration,
90
+ orElse: () => Stream.fail(timeoutError(duration))
91
+ })
92
+
93
+ export const cancellable = <R>(
94
+ stream: Stream.Stream<LlmChunk, LlmError, R>
95
+ ): Effect.Effect<
96
+ readonly [stream: Stream.Stream<LlmChunk, LlmError, R>, cancel: Effect.Effect<void>]
97
+ > =>
98
+ Effect.map(Deferred.make<void>(), (deferred) => [
99
+ Stream.interruptWhen(stream, Deferred.await(deferred)),
100
+ Deferred.succeed(deferred, undefined).pipe(Effect.asVoid)
101
+ ])
102
+
103
+ export const withSnapshots = <R>(
104
+ stream: Stream.Stream<LlmChunk, LlmError, R>,
105
+ snapshotInterval: Duration.Input = "500 millis"
106
+ ): Stream.Stream<string, LlmError, R> =>
107
+ stream.pipe(
108
+ Stream.scan("", (content, chunk) => content + chunk.delta),
109
+ Stream.debounce(snapshotInterval)
110
+ )
111
+
112
+ const chunkJson = Schema.fromJsonString(Schema.toCodecJson(LlmChunk))
113
+
114
+ export const toSSE = <R>(
115
+ stream: Stream.Stream<LlmChunk, LlmError, R>
116
+ ): Stream.Stream<string, LlmError, R> =>
117
+ Stream.mapEffect(stream, (chunk) =>
118
+ Schema.encodeEffect(chunkJson)(chunk).pipe(
119
+ Effect.map((encoded) => `data: ${encoded}\n\n`),
120
+ Effect.mapError((error) =>
121
+ ParseError.make({
122
+ message: `Failed to encode SSE chunk: ${String(error)}`,
123
+ raw: chunk.delta
124
+ })
125
+ )
126
+ )
127
+ )
128
+
129
+ export const fromSSE = <R>(
130
+ stream: Stream.Stream<string, LlmError, R>
131
+ ): Stream.Stream<LlmChunk, LlmError, R> =>
132
+ stream.pipe(
133
+ Stream.filter((line) => line.startsWith("data: ")),
134
+ Stream.map((line) => line.slice("data: ".length).trim()),
135
+ Stream.filter((payload) => payload.length > 0 && payload !== "[DONE]"),
136
+ Stream.mapEffect((payload) =>
137
+ Schema.decodeUnknownEffect(chunkJson)(payload).pipe(
138
+ Effect.mapError((error) =>
139
+ ParseError.make({
140
+ message: `Failed to parse SSE chunk: ${String(error)}`,
141
+ raw: payload
142
+ })
143
+ )
144
+ )
145
+ )
146
+ )
147
+
148
+ export const withHeartbeat = <R>(
149
+ stream: Stream.Stream<LlmChunk, LlmError, R>,
150
+ heartbeatTimeout: Duration.Input
151
+ ): Stream.Stream<LlmChunk, LlmError, R> =>
152
+ withTimeout(stream, heartbeatTimeout).pipe(Stream.rechunk(1))
153
+
154
+ export const parallelStream = <A, B, R>(
155
+ inputs: Iterable<A>,
156
+ parallelism: number,
157
+ f: (input: A) => Stream.Stream<B, LlmError, R>
158
+ ): Stream.Stream<B, LlmError, R> =>
159
+ Stream.fromIterable(inputs).pipe(
160
+ Stream.mapEffect((input) => Stream.runCollect(f(input)), {
161
+ concurrency: parallelism
162
+ }),
163
+ Stream.flatMap(Stream.fromIterable)
164
+ )
@@ -0,0 +1,138 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Schema from "effect/Schema"
3
+ import type { SchemaError } from "effect/SchemaError"
4
+ import { ParseError, ProviderError, type LlmError } from "./Errors.ts"
5
+ import type { JsonSchema } from "./Models.ts"
6
+
7
+ const snippet = (text: string): string => {
8
+ const trimmed = text.trim()
9
+ return trimmed.length <= 120 ? trimmed : `${trimmed.slice(0, 117)}...`
10
+ }
11
+
12
+ const extractJsonFromMarkdownFences = (raw: string): ReadonlyArray<string> => {
13
+ const pattern = /```(?:[\w.+-]+)?\s*(.*?)\s*```/gis
14
+ const results: Array<string> = []
15
+
16
+ for (const match of raw.matchAll(pattern)) {
17
+ const content = match[1]?.trim()
18
+ if (content !== undefined && content.length > 0) {
19
+ results.push(content)
20
+ }
21
+ }
22
+
23
+ return results
24
+ }
25
+
26
+ const extractBalancedJsonObjects = (raw: string): ReadonlyArray<string> => {
27
+ const results: Array<string> = []
28
+ let startIndex: number | undefined
29
+ let depth = 0
30
+ let inString = false
31
+ let escaping = false
32
+
33
+ for (let index = 0; index < raw.length; index += 1) {
34
+ const character = raw.charAt(index)
35
+
36
+ if (inString) {
37
+ if (escaping) {
38
+ escaping = false
39
+ } else if (character === "\\") {
40
+ escaping = true
41
+ } else if (character === '"') {
42
+ inString = false
43
+ }
44
+ continue
45
+ }
46
+
47
+ if (character === '"') {
48
+ inString = true
49
+ } else if (character === "{") {
50
+ startIndex ??= index
51
+ depth += 1
52
+ } else if (character === "}" && depth > 0) {
53
+ depth -= 1
54
+ if (depth === 0 && startIndex !== undefined) {
55
+ results.push(raw.slice(startIndex, index + 1))
56
+ startIndex = undefined
57
+ }
58
+ }
59
+ }
60
+
61
+ return results
62
+ }
63
+
64
+ export const jsonCandidates = (raw: string): ReadonlyArray<string> => {
65
+ const fenced = extractJsonFromMarkdownFences(raw)
66
+ const balanced = [
67
+ ...extractBalancedJsonObjects(raw),
68
+ ...fenced.flatMap(extractBalancedJsonObjects)
69
+ ]
70
+ const seen = new Set<string>()
71
+ const candidates: Array<string> = []
72
+
73
+ for (const candidate of [raw, ...fenced, ...balanced]) {
74
+ const trimmed = candidate.trim()
75
+ if (trimmed.length > 0 && !seen.has(trimmed)) {
76
+ seen.add(trimmed)
77
+ candidates.push(trimmed)
78
+ }
79
+ }
80
+
81
+ return candidates
82
+ }
83
+
84
+ export const withSchemaHint = (prompt: string, schema: JsonSchema): string => {
85
+ if (Object.keys(schema).length === 0) {
86
+ return prompt
87
+ }
88
+
89
+ const encoded = JSON.stringify(schema) ?? "{}"
90
+ return `${prompt}\n\nReturn JSON matching this schema:\n${encoded}`
91
+ }
92
+
93
+ interface CandidateFailure {
94
+ readonly candidate: string
95
+ readonly error: SchemaError
96
+ }
97
+
98
+ export const parseFromText = Effect.fn("@llm4ts/core/StructuredOutput.parseFromText")(<
99
+ A,
100
+ E,
101
+ RD,
102
+ RE
103
+ >(
104
+ raw: string,
105
+ schema: Schema.ConstraintCodec<A, E, RD, RE>,
106
+ _jsonSchema: JsonSchema
107
+ ): Effect.Effect<A, LlmError, RD> => {
108
+ if (raw.trim().length === 0) {
109
+ return Effect.fail(
110
+ ProviderError.make({
111
+ message: "empty response from provider — no text to parse as structured output"
112
+ })
113
+ )
114
+ }
115
+
116
+ const candidates = jsonCandidates(raw)
117
+ const attempts = candidates.map((candidate) =>
118
+ Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(candidate).pipe(
119
+ Effect.mapError(
120
+ (error): CandidateFailure => ({
121
+ candidate,
122
+ error
123
+ })
124
+ )
125
+ )
126
+ )
127
+
128
+ return Effect.firstSuccessOf(attempts).pipe(
129
+ Effect.mapError(({ candidate, error }) =>
130
+ ParseError.make({
131
+ message:
132
+ `Failed to parse response as structured output: ${String(error)} ` +
133
+ `(tried ${attempts.length} candidate(s); last: ${snippet(candidate)})`,
134
+ raw
135
+ })
136
+ )
137
+ )
138
+ })