@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,146 @@
1
+ import * as Duration from "effect/Duration"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Schema from "effect/Schema"
4
+ import { ConnectorId, ConnectorIds, LlmConfig, type LlmProvider } from "./Models.ts"
5
+
6
+ export class DockerSandbox extends Schema.TaggedClass<DockerSandbox>()("Docker", {
7
+ image: Schema.String,
8
+ mount: Schema.Boolean.pipe(Schema.withConstructorDefault(Effect.succeed(true))),
9
+ network: Schema.optionalKey(Schema.String)
10
+ }) {}
11
+
12
+ export class PodmanSandbox extends Schema.TaggedClass<PodmanSandbox>()("Podman", {}) {}
13
+ export class SeatbeltMacOsSandbox extends Schema.TaggedClass<SeatbeltMacOsSandbox>()(
14
+ "SeatbeltMacOS",
15
+ {}
16
+ ) {}
17
+ export class RunscSandbox extends Schema.TaggedClass<RunscSandbox>()("Runsc", {}) {}
18
+ export class LxcSandbox extends Schema.TaggedClass<LxcSandbox>()("Lxc", {}) {}
19
+
20
+ export const CliSandbox = Schema.Union([
21
+ DockerSandbox,
22
+ PodmanSandbox,
23
+ SeatbeltMacOsSandbox,
24
+ RunscSandbox,
25
+ LxcSandbox
26
+ ])
27
+ export type CliSandbox = typeof CliSandbox.Type
28
+
29
+ const emptyStringRecord: Readonly<Record<string, string>> = Object.freeze({})
30
+
31
+ export class ApiConnectorConfig extends Schema.TaggedClass<ApiConnectorConfig>()(
32
+ "ApiConnectorConfig",
33
+ {
34
+ connectorId: ConnectorId,
35
+ model: Schema.optionalKey(Schema.String),
36
+ baseUrl: Schema.optionalKey(Schema.String),
37
+ apiKey: Schema.optionalKey(
38
+ Schema.Redacted(Schema.String, {
39
+ disallowJsonEncode: true
40
+ })
41
+ ),
42
+ timeout: Schema.Duration.pipe(
43
+ Schema.withConstructorDefault(Effect.succeed(Duration.seconds(300)))
44
+ ),
45
+ maxRetries: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(3))),
46
+ requestsPerMinute: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(60))),
47
+ burstSize: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(10))),
48
+ acquireTimeout: Schema.Duration.pipe(
49
+ Schema.withConstructorDefault(Effect.succeed(Duration.seconds(30)))
50
+ ),
51
+ temperature: Schema.optionalKey(Schema.Number),
52
+ maxTokens: Schema.optionalKey(Schema.Int)
53
+ }
54
+ ) {}
55
+
56
+ export class CliConnectorConfig extends Schema.TaggedClass<CliConnectorConfig>()(
57
+ "CliConnectorConfig",
58
+ {
59
+ connectorId: ConnectorId,
60
+ model: Schema.optionalKey(Schema.String),
61
+ timeout: Schema.Duration.pipe(
62
+ Schema.withConstructorDefault(Effect.succeed(Duration.seconds(300)))
63
+ ),
64
+ maxRetries: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(3))),
65
+ flags: Schema.Record(Schema.String, Schema.String).pipe(
66
+ Schema.withConstructorDefault(Effect.succeed(emptyStringRecord))
67
+ ),
68
+ sandbox: Schema.optionalKey(CliSandbox),
69
+ turnLimit: Schema.optionalKey(Schema.Int),
70
+ envVars: Schema.Record(Schema.String, Schema.String).pipe(
71
+ Schema.withConstructorDefault(Effect.succeed(emptyStringRecord))
72
+ ),
73
+ workingDir: Schema.optionalKey(Schema.String),
74
+ readOnly: Schema.Boolean.pipe(Schema.withConstructorDefault(Effect.succeed(false)))
75
+ }
76
+ ) {}
77
+
78
+ export const ConnectorConfig = Schema.Union([ApiConnectorConfig, CliConnectorConfig])
79
+ export type ConnectorConfig = typeof ConnectorConfig.Type
80
+
81
+ const providerForConnector = (connectorId: ConnectorId): LlmProvider => {
82
+ switch (connectorId.value) {
83
+ case "anthropic":
84
+ return "Anthropic"
85
+ case "gemini-api":
86
+ return "GeminiApi"
87
+ case "lm-studio":
88
+ return "LmStudio"
89
+ case "ollama":
90
+ return "Ollama"
91
+ case "mock":
92
+ return "Mock"
93
+ default:
94
+ return "OpenAI"
95
+ }
96
+ }
97
+
98
+ export const toLlmConfig = (config: ApiConnectorConfig): LlmConfig =>
99
+ new LlmConfig({
100
+ provider: providerForConnector(config.connectorId),
101
+ model: config.model ?? "",
102
+ timeout: config.timeout,
103
+ maxRetries: config.maxRetries,
104
+ requestsPerMinute: config.requestsPerMinute,
105
+ burstSize: config.burstSize,
106
+ acquireTimeout: config.acquireTimeout,
107
+ ...(config.baseUrl === undefined ? {} : { baseUrl: config.baseUrl }),
108
+ ...(config.apiKey === undefined ? {} : { apiKey: config.apiKey }),
109
+ ...(config.temperature === undefined ? {} : { temperature: config.temperature }),
110
+ ...(config.maxTokens === undefined ? {} : { maxTokens: config.maxTokens })
111
+ })
112
+
113
+ const emptyConnectors: ReadonlyArray<ConnectorConfig> = Object.freeze([])
114
+
115
+ export class FallbackChain extends Schema.Class<FallbackChain>("FallbackChain")({
116
+ connectors: Schema.Array(ConnectorConfig).pipe(
117
+ Schema.withConstructorDefault(Effect.succeed(emptyConnectors))
118
+ )
119
+ }) {
120
+ get isEmpty(): boolean {
121
+ return this.connectors.length === 0
122
+ }
123
+
124
+ get nonEmpty(): boolean {
125
+ return this.connectors.length > 0
126
+ }
127
+ }
128
+
129
+ export const emptyFallbackChain = new FallbackChain()
130
+
131
+ export const defaultCliConnectorConfigs = Object.freeze({
132
+ Claude: new CliConnectorConfig({ connectorId: ConnectorIds.ClaudeCli }),
133
+ Codex: new CliConnectorConfig({ connectorId: ConnectorIds.Codex })
134
+ })
135
+
136
+ export const defaultReasoningConfig = (
137
+ coder: ConnectorConfig,
138
+ explicit?: ConnectorConfig
139
+ ): ConnectorConfig => {
140
+ if (explicit !== undefined) {
141
+ return explicit
142
+ }
143
+ return coder instanceof CliConnectorConfig
144
+ ? CliConnectorConfig.make({ ...coder, readOnly: true })
145
+ : coder
146
+ }
@@ -0,0 +1,184 @@
1
+ import * as Context from "effect/Context"
2
+ import * as Effect from "effect/Effect"
3
+ import type {
4
+ ApiConnectorConfig,
5
+ CliConnectorConfig,
6
+ ConnectorConfig,
7
+ FallbackChain
8
+ } from "./ConnectorConfig.ts"
9
+ import {
10
+ ApiConnectorConfig as ApiConfig,
11
+ CliConnectorConfig as CliConfig
12
+ } from "./ConnectorConfig.ts"
13
+ import type { ApiConnectorShape, CliConnectorShape, ConnectorShape } from "./Connector.ts"
14
+ import { ConfigError, type LlmError } from "./Errors.ts"
15
+ import { HealthStatus, type ConnectorId, type ConnectorKind } from "./Models.ts"
16
+
17
+ export interface ConnectorFactory {
18
+ readonly connectorId: ConnectorId
19
+ readonly kind: ConnectorKind
20
+ readonly create: (config: ConnectorConfig) => Effect.Effect<ConnectorShape, LlmError>
21
+ }
22
+
23
+ export interface ConnectorRegistryShape {
24
+ readonly resolve: (config: ConnectorConfig) => Effect.Effect<ConnectorShape, LlmError>
25
+ readonly resolveApi: (config: ApiConnectorConfig) => Effect.Effect<ApiConnectorShape, LlmError>
26
+ readonly resolveCli: (config: CliConnectorConfig) => Effect.Effect<CliConnectorShape, LlmError>
27
+ readonly resolveFallback: (chain: FallbackChain) => Effect.Effect<ConnectorShape, LlmError>
28
+ readonly available: Effect.Effect<ReadonlyArray<ConnectorId>>
29
+ readonly healthCheckAll: Effect.Effect<ReadonlyMap<ConnectorId, HealthStatus>>
30
+ }
31
+
32
+ export class ConnectorRegistry extends Context.Service<ConnectorRegistry, ConnectorRegistryShape>()(
33
+ "@llm4ts/core/ConnectorRegistry"
34
+ ) {}
35
+
36
+ const unknownHealth = (): HealthStatus =>
37
+ HealthStatus.make({
38
+ availability: "Unknown",
39
+ authStatus: "Unknown"
40
+ })
41
+
42
+ const isApiConnector = (connector: ConnectorShape): connector is ApiConnectorShape =>
43
+ connector.kind === "Api"
44
+
45
+ const isCliConnector = (connector: ConnectorShape): connector is CliConnectorShape =>
46
+ connector.kind === "Cli" &&
47
+ "interactionSupport" in connector &&
48
+ "buildArgv" in connector &&
49
+ "buildInteractiveArgv" in connector &&
50
+ "complete" in connector &&
51
+ "completeStream" in connector
52
+
53
+ const healthEntry = (
54
+ id: ConnectorId,
55
+ status: HealthStatus
56
+ ): readonly [ConnectorId, HealthStatus] => [id, status]
57
+
58
+ export const makeConnectorRegistry = (
59
+ factories: Iterable<ConnectorFactory>
60
+ ): ConnectorRegistryShape => {
61
+ const byId = new Map<string, ConnectorFactory>()
62
+ for (const factory of factories) {
63
+ byId.set(factory.connectorId.value, factory)
64
+ }
65
+
66
+ const resolve = (config: ConnectorConfig): Effect.Effect<ConnectorShape, LlmError> => {
67
+ const factory = byId.get(config.connectorId.value)
68
+ return factory === undefined
69
+ ? Effect.fail(
70
+ ConfigError.make({
71
+ message: `Unknown connector: ${config.connectorId.value}`
72
+ })
73
+ )
74
+ : factory.create(config)
75
+ }
76
+
77
+ const resolveApi = (config: ApiConnectorConfig): Effect.Effect<ApiConnectorShape, LlmError> =>
78
+ Effect.flatMap(resolve(config), (connector) =>
79
+ isApiConnector(connector)
80
+ ? Effect.succeed(connector)
81
+ : Effect.fail(
82
+ ConfigError.make({
83
+ message: `Expected ApiConnector, got ${connector.kind}`
84
+ })
85
+ )
86
+ )
87
+
88
+ const resolveCli = (config: CliConnectorConfig): Effect.Effect<CliConnectorShape, LlmError> =>
89
+ Effect.flatMap(resolve(config), (connector) =>
90
+ isCliConnector(connector)
91
+ ? Effect.succeed(connector)
92
+ : Effect.fail(
93
+ ConfigError.make({
94
+ message: `Expected CliConnector, got ${connector.kind}`
95
+ })
96
+ )
97
+ )
98
+
99
+ const resolveFallback = (chain: FallbackChain): Effect.Effect<ConnectorShape, LlmError> => {
100
+ const loop = (
101
+ remaining: ReadonlyArray<ConnectorConfig>,
102
+ failures: ReadonlyArray<string>
103
+ ): Effect.Effect<ConnectorShape, LlmError> => {
104
+ const [config, ...tail] = remaining
105
+ if (config === undefined) {
106
+ return Effect.fail(
107
+ ConfigError.make({
108
+ message:
109
+ failures.length === 0
110
+ ? "Fallback chain is empty"
111
+ : `No fallback connector is available: ${failures.join("; ")}`
112
+ })
113
+ )
114
+ }
115
+
116
+ return Effect.result(
117
+ Effect.flatMap(resolve(config), (connector) =>
118
+ Effect.map(connector.isAvailable, (available) => ({
119
+ connector,
120
+ available
121
+ }))
122
+ )
123
+ ).pipe(
124
+ Effect.flatMap((result) => {
125
+ if (result._tag === "Success" && result.success.available) {
126
+ return Effect.succeed(result.success.connector)
127
+ }
128
+
129
+ const reason =
130
+ result._tag === "Failure"
131
+ ? `${config.connectorId.value}: ${result.failure.message}`
132
+ : `${config.connectorId.value}: unavailable`
133
+ return Effect.suspend(() => loop(tail, [...failures, reason]))
134
+ })
135
+ )
136
+ }
137
+
138
+ return loop(chain.connectors, [])
139
+ }
140
+
141
+ const available = Effect.sync(() => Array.from(byId.values(), (factory) => factory.connectorId))
142
+
143
+ const healthCheckAll = Effect.forEach(Array.from(byId.values()), (factory) => {
144
+ const defaultConfig: ConnectorConfig =
145
+ factory.kind === "Api"
146
+ ? ApiConfig.make({ connectorId: factory.connectorId })
147
+ : CliConfig.make({ connectorId: factory.connectorId })
148
+
149
+ return Effect.result(
150
+ Effect.flatMap(factory.create(defaultConfig), (connector) => connector.healthCheck)
151
+ ).pipe(
152
+ Effect.map((result) =>
153
+ healthEntry(
154
+ factory.connectorId,
155
+ result._tag === "Success" ? result.success : unknownHealth()
156
+ )
157
+ )
158
+ )
159
+ }).pipe(Effect.map((entries) => new Map<ConnectorId, HealthStatus>(entries)))
160
+
161
+ return {
162
+ resolve,
163
+ resolveApi,
164
+ resolveCli,
165
+ resolveFallback,
166
+ available,
167
+ healthCheckAll
168
+ }
169
+ }
170
+
171
+ export const resolveConnector = (
172
+ config: ConnectorConfig
173
+ ): Effect.Effect<ConnectorShape, LlmError, ConnectorRegistry> =>
174
+ Effect.flatMap(ConnectorRegistry, (registry) => registry.resolve(config))
175
+
176
+ export const resolveApiConnector = (
177
+ config: ApiConnectorConfig
178
+ ): Effect.Effect<ApiConnectorShape, LlmError, ConnectorRegistry> =>
179
+ Effect.flatMap(ConnectorRegistry, (registry) => registry.resolveApi(config))
180
+
181
+ export const resolveCliConnector = (
182
+ config: CliConnectorConfig
183
+ ): Effect.Effect<CliConnectorShape, LlmError, ConnectorRegistry> =>
184
+ Effect.flatMap(ConnectorRegistry, (registry) => registry.resolveCli(config))
@@ -0,0 +1,372 @@
1
+ import * as Crypto from "effect/Crypto"
2
+ import * as DateTime from "effect/DateTime"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Schema from "effect/Schema"
5
+ import { ConversationMessage, type PromptRole } from "./Conversation.ts"
6
+ import type { LlmProvider } from "./Models.ts"
7
+
8
+ export class ContextInvalidInput extends Schema.TaggedErrorClass<ContextInvalidInput>()(
9
+ "InvalidInput",
10
+ {
11
+ message: Schema.String
12
+ }
13
+ ) {}
14
+
15
+ export class ContextParseFailed extends Schema.TaggedErrorClass<ContextParseFailed>()(
16
+ "ParseFailed",
17
+ {
18
+ message: Schema.String
19
+ }
20
+ ) {}
21
+
22
+ export class ContextSummarizationFailed extends Schema.TaggedErrorClass<ContextSummarizationFailed>()(
23
+ "SummarizationFailed",
24
+ {
25
+ message: Schema.String
26
+ }
27
+ ) {}
28
+
29
+ export class ContextToolLoopFailed extends Schema.TaggedErrorClass<ContextToolLoopFailed>()(
30
+ "ToolLoopFailed",
31
+ {
32
+ message: Schema.String
33
+ }
34
+ ) {}
35
+
36
+ export const ContextError = Schema.Union([
37
+ ContextInvalidInput,
38
+ ContextParseFailed,
39
+ ContextSummarizationFailed,
40
+ ContextToolLoopFailed
41
+ ])
42
+ export type ContextError = typeof ContextError.Type
43
+
44
+ export class ContextLimits extends Schema.Class<ContextLimits>("ContextLimits")({
45
+ maxTokens: Schema.Int,
46
+ warningThresholdPct: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(0.85)))
47
+ }) {}
48
+
49
+ export class ContextWindow extends Schema.Class<ContextWindow>("ContextWindow")({
50
+ messages: Schema.Array(ConversationMessage),
51
+ totalTokens: Schema.Int,
52
+ approachingLimit: Schema.Boolean,
53
+ trimmed: Schema.Boolean
54
+ }) {}
55
+
56
+ export class DropOldestFifo extends Schema.TaggedClass<DropOldestFifo>()("DropOldestFifo", {}) {}
57
+
58
+ export class SlidingWindow extends Schema.TaggedClass<SlidingWindow>()("SlidingWindow", {}) {}
59
+
60
+ export class PriorityBased extends Schema.TaggedClass<PriorityBased>()("PriorityBased", {}) {}
61
+
62
+ export class SummarizeOldMessages extends Schema.TaggedClass<SummarizeOldMessages>()(
63
+ "SummarizeOldMessages",
64
+ {
65
+ summaryTargetTokens: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(256)))
66
+ }
67
+ ) {}
68
+
69
+ export const ContextTrimmingStrategy = Schema.Union([
70
+ DropOldestFifo,
71
+ SlidingWindow,
72
+ PriorityBased,
73
+ SummarizeOldMessages
74
+ ])
75
+ export type ContextTrimmingStrategy = typeof ContextTrimmingStrategy.Type
76
+
77
+ export const ContextTrimmingStrategies = Object.freeze({
78
+ DropOldestFifo: DropOldestFifo.make({}),
79
+ SlidingWindow: SlidingWindow.make({}),
80
+ PriorityBased: PriorityBased.make({}),
81
+ SummarizeOldMessages: (summaryTargetTokens = 256): SummarizeOldMessages =>
82
+ SummarizeOldMessages.make({ summaryTargetTokens })
83
+ })
84
+
85
+ export interface TokenCounter {
86
+ readonly countText: (provider: LlmProvider, text: string) => number
87
+ readonly countMessage: (provider: LlmProvider, message: ConversationMessage) => number
88
+ readonly countMessages: (provider: LlmProvider, messages: Iterable<ConversationMessage>) => number
89
+ }
90
+
91
+ const roleOverhead = (role: PromptRole): number => {
92
+ switch (role) {
93
+ case "System":
94
+ return 6
95
+ case "User":
96
+ case "Assistant":
97
+ return 5
98
+ case "Tool":
99
+ return 8
100
+ }
101
+ }
102
+
103
+ const providerCharactersPerToken = (provider: LlmProvider): number => {
104
+ switch (provider) {
105
+ case "OpenAI":
106
+ case "OpenCode":
107
+ return 4
108
+ case "Anthropic":
109
+ return 3.8
110
+ case "GeminiApi":
111
+ case "GeminiCli":
112
+ return 4.2
113
+ case "LmStudio":
114
+ case "Ollama":
115
+ case "Mock":
116
+ return 4.5
117
+ }
118
+ }
119
+
120
+ const countText = (provider: LlmProvider, text: string): number =>
121
+ Math.max(1, Math.ceil(Math.max(text.length, 1) / providerCharactersPerToken(provider)))
122
+
123
+ const countMessage = (provider: LlmProvider, message: ConversationMessage): number =>
124
+ countText(provider, message.content) + roleOverhead(message.role)
125
+
126
+ export const defaultTokenCounter: TokenCounter = {
127
+ countText,
128
+ countMessage,
129
+ countMessages: (provider, messages) =>
130
+ Array.from(messages).reduce((total, message) => total + countMessage(provider, message), 0)
131
+ }
132
+
133
+ export interface MessageSummarizer<R = never> {
134
+ readonly summarize: (
135
+ messages: ReadonlyArray<ConversationMessage>,
136
+ targetTokens: number
137
+ ) => Effect.Effect<ConversationMessage, ContextError, R>
138
+ }
139
+
140
+ export const simpleMessageSummarizer: MessageSummarizer<Crypto.Crypto> = {
141
+ summarize: (messages, targetTokens) => {
142
+ if (messages.length === 0) {
143
+ return Effect.fail(
144
+ ContextInvalidInput.make({
145
+ message: "Cannot summarize empty messages"
146
+ })
147
+ )
148
+ }
149
+
150
+ const summaryText = messages
151
+ .slice(0, 8)
152
+ .map((message) => `${message.role.toLowerCase()}: ${message.content.slice(0, 160)}`)
153
+ .join("\n")
154
+
155
+ return Effect.gen(function* () {
156
+ const crypto = yield* Crypto.Crypto
157
+ const id = yield* crypto.randomUUIDv4.pipe(
158
+ Effect.mapError((error) =>
159
+ ContextSummarizationFailed.make({
160
+ message: `Failed to generate summary id: ${error.message}`
161
+ })
162
+ )
163
+ )
164
+ const timestamp = yield* DateTime.now
165
+
166
+ return ConversationMessage.make({
167
+ id,
168
+ role: "Assistant",
169
+ content: `Summary of previous context:\n${summaryText}`,
170
+ timestamp,
171
+ tokens: Math.max(1, targetTokens),
172
+ metadata: { summary: "true" },
173
+ important: true
174
+ })
175
+ })
176
+ }
177
+ }
178
+
179
+ const annotateTokens = (
180
+ messages: ReadonlyArray<ConversationMessage>,
181
+ provider: LlmProvider,
182
+ tokenCounter: TokenCounter
183
+ ): ReadonlyArray<ConversationMessage> =>
184
+ messages.map((message) =>
185
+ message.tokens > 0
186
+ ? message
187
+ : ConversationMessage.make({
188
+ ...message,
189
+ tokens: tokenCounter.countMessage(provider, message)
190
+ })
191
+ )
192
+
193
+ const dropOldest = (
194
+ messages: ReadonlyArray<ConversationMessage>,
195
+ maxTokens: number
196
+ ): ReadonlyArray<ConversationMessage> => {
197
+ let used = 0
198
+ let kept: ReadonlyArray<ConversationMessage> = []
199
+
200
+ for (const message of Array.from(messages).reverse()) {
201
+ if (used + message.tokens <= maxTokens) {
202
+ used += message.tokens
203
+ kept = [message, ...kept]
204
+ }
205
+ }
206
+
207
+ return kept
208
+ }
209
+
210
+ const slidingWindow = (
211
+ messages: ReadonlyArray<ConversationMessage>,
212
+ maxTokens: number
213
+ ): ReadonlyArray<ConversationMessage> => {
214
+ const system = messages.filter((message) => message.role === "System")
215
+ const recentNonSystem = messages.filter((message) => message.role !== "System")
216
+ const systemTokens = system.reduce((total, message) => total + message.tokens, 0)
217
+ const tail = dropOldest(recentNonSystem, Math.max(0, maxTokens - systemTokens))
218
+ return [...system, ...tail].slice(-messages.length)
219
+ }
220
+
221
+ const priorityWindow = (
222
+ messages: ReadonlyArray<ConversationMessage>,
223
+ maxTokens: number
224
+ ): ReadonlyArray<ConversationMessage> => {
225
+ const scored = messages.map((message, index) => ({
226
+ message,
227
+ score:
228
+ (message.role === "System" ? 1_000 : 0) +
229
+ (message.role === "Tool" ? 600 : 0) +
230
+ (message.important ? 400 : 0) +
231
+ index
232
+ }))
233
+ scored.sort(
234
+ (left, right) =>
235
+ right.score - left.score ||
236
+ DateTime.toEpochMillis(left.message.timestamp) -
237
+ DateTime.toEpochMillis(right.message.timestamp)
238
+ )
239
+
240
+ let used = 0
241
+ const selected: Array<ConversationMessage> = []
242
+ for (const candidate of scored) {
243
+ if (used + candidate.message.tokens <= maxTokens) {
244
+ used += candidate.message.tokens
245
+ selected.push(candidate.message)
246
+ }
247
+ }
248
+
249
+ return selected.sort(
250
+ (left, right) =>
251
+ DateTime.toEpochMillis(left.timestamp) - DateTime.toEpochMillis(right.timestamp)
252
+ )
253
+ }
254
+
255
+ const summarizeOldMessages = <R>(
256
+ messages: ReadonlyArray<ConversationMessage>,
257
+ maxTokens: number,
258
+ summaryTargetTokens: number,
259
+ provider: LlmProvider,
260
+ tokenCounter: TokenCounter,
261
+ summarizer: MessageSummarizer<R>
262
+ ): Effect.Effect<ReadonlyArray<ConversationMessage>, ContextError, R> => {
263
+ if (messages.length <= 2) {
264
+ return Effect.succeed(dropOldest(messages, maxTokens))
265
+ }
266
+
267
+ const splitIndex = Math.max(1, Math.floor(messages.length / 2))
268
+ const oldMessages = messages.slice(0, splitIndex)
269
+ const recentMessages = messages.slice(splitIndex)
270
+
271
+ return Effect.map(summarizer.summarize(oldMessages, summaryTargetTokens), (summary) => {
272
+ const annotatedSummary = ConversationMessage.make({
273
+ ...summary,
274
+ tokens: tokenCounter.countMessage(provider, summary)
275
+ })
276
+ return dropOldest([annotatedSummary, ...recentMessages], maxTokens)
277
+ })
278
+ }
279
+
280
+ export function applyContextWindow(
281
+ messages: ReadonlyArray<ConversationMessage>,
282
+ provider: LlmProvider,
283
+ limits: ContextLimits,
284
+ strategy: DropOldestFifo | SlidingWindow | PriorityBased,
285
+ tokenCounter?: TokenCounter
286
+ ): Effect.Effect<ContextWindow, ContextError>
287
+ export function applyContextWindow(
288
+ messages: ReadonlyArray<ConversationMessage>,
289
+ provider: LlmProvider,
290
+ limits: ContextLimits,
291
+ strategy: ContextTrimmingStrategy,
292
+ tokenCounter?: TokenCounter
293
+ ): Effect.Effect<ContextWindow, ContextError, Crypto.Crypto>
294
+ export function applyContextWindow<R>(
295
+ messages: ReadonlyArray<ConversationMessage>,
296
+ provider: LlmProvider,
297
+ limits: ContextLimits,
298
+ strategy: ContextTrimmingStrategy,
299
+ tokenCounter: TokenCounter,
300
+ summarizer: MessageSummarizer<R>
301
+ ): Effect.Effect<ContextWindow, ContextError, R>
302
+ export function applyContextWindow<R>(
303
+ messages: ReadonlyArray<ConversationMessage>,
304
+ provider: LlmProvider,
305
+ limits: ContextLimits,
306
+ strategy: ContextTrimmingStrategy,
307
+ tokenCounter: TokenCounter = defaultTokenCounter,
308
+ summarizer?: MessageSummarizer<R>
309
+ ): Effect.Effect<ContextWindow, ContextError, R | Crypto.Crypto> {
310
+ if (limits.maxTokens <= 0) {
311
+ return Effect.fail(
312
+ ContextInvalidInput.make({
313
+ message: "maxTokens must be > 0"
314
+ })
315
+ )
316
+ }
317
+
318
+ const withTokens = annotateTokens(messages, provider, tokenCounter)
319
+ const totalBefore = withTokens.reduce((total, message) => total + message.tokens, 0)
320
+
321
+ const trimmed: Effect.Effect<
322
+ readonly [messages: ReadonlyArray<ConversationMessage>, wasTrimmed: boolean],
323
+ ContextError,
324
+ R | Crypto.Crypto
325
+ > =
326
+ totalBefore <= limits.maxTokens
327
+ ? Effect.succeed([withTokens, false])
328
+ : (() => {
329
+ switch (strategy._tag) {
330
+ case "DropOldestFifo":
331
+ return Effect.succeed([dropOldest(withTokens, limits.maxTokens), true])
332
+ case "SlidingWindow":
333
+ return Effect.succeed([slidingWindow(withTokens, limits.maxTokens), true])
334
+ case "PriorityBased":
335
+ return Effect.succeed([priorityWindow(withTokens, limits.maxTokens), true])
336
+ case "SummarizeOldMessages": {
337
+ const summarized: Effect.Effect<
338
+ ReadonlyArray<ConversationMessage>,
339
+ ContextError,
340
+ R | Crypto.Crypto
341
+ > = summarizer === undefined
342
+ ? summarizeOldMessages(
343
+ withTokens,
344
+ limits.maxTokens,
345
+ strategy.summaryTargetTokens,
346
+ provider,
347
+ tokenCounter,
348
+ simpleMessageSummarizer
349
+ )
350
+ : summarizeOldMessages(
351
+ withTokens,
352
+ limits.maxTokens,
353
+ strategy.summaryTargetTokens,
354
+ provider,
355
+ tokenCounter,
356
+ summarizer
357
+ )
358
+ return Effect.map(summarized, (messages) => [messages, true])
359
+ }
360
+ }
361
+ })()
362
+
363
+ return Effect.map(trimmed, ([finalMessages, wasTrimmed]) => {
364
+ const totalTokens = finalMessages.reduce((total, message) => total + message.tokens, 0)
365
+ return ContextWindow.make({
366
+ messages: finalMessages,
367
+ totalTokens,
368
+ approachingLimit: totalTokens >= limits.maxTokens * limits.warningThresholdPct,
369
+ trimmed: wasTrimmed
370
+ })
371
+ })
372
+ }