@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,379 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Schema from "effect/Schema"
3
+ import { Capability as CapabilitySchema, type Capability } from "../Capability.ts"
4
+ import { ToolDefinition, type JsonSchema, type LlmProvider } from "../Models.ts"
5
+ import {
6
+ isJsonRecord,
7
+ jsonArray,
8
+ jsonField,
9
+ jsonStringField,
10
+ type JsonValue
11
+ } from "../providers/CliSupport.ts"
12
+
13
+ export class InvalidToolSchema extends Schema.TaggedErrorClass<InvalidToolSchema>()(
14
+ "InvalidSchema",
15
+ {
16
+ message: Schema.String
17
+ }
18
+ ) {}
19
+
20
+ export class InvalidToolParameters extends Schema.TaggedErrorClass<InvalidToolParameters>()(
21
+ "InvalidParameters",
22
+ {
23
+ message: Schema.String
24
+ }
25
+ ) {}
26
+
27
+ export class DuplicateToolName extends Schema.TaggedErrorClass<DuplicateToolName>()(
28
+ "DuplicateToolName",
29
+ {
30
+ name: Schema.String
31
+ }
32
+ ) {
33
+ get message(): string {
34
+ return `Tool already registered: ${this.name}`
35
+ }
36
+ }
37
+
38
+ export class ToolSandboxViolation extends Schema.TaggedErrorClass<ToolSandboxViolation>()(
39
+ "SandboxViolation",
40
+ {
41
+ message: Schema.String
42
+ }
43
+ ) {}
44
+
45
+ export class ToolExecutionFailed extends Schema.TaggedErrorClass<ToolExecutionFailed>()(
46
+ "ExecutionFailed",
47
+ {
48
+ message: Schema.String
49
+ }
50
+ ) {}
51
+
52
+ export class ToolSchemaGenerationFailed extends Schema.TaggedErrorClass<ToolSchemaGenerationFailed>()(
53
+ "SchemaGenerationFailed",
54
+ {
55
+ message: Schema.String
56
+ }
57
+ ) {}
58
+
59
+ export const ToolExecutionError = Schema.Union([
60
+ InvalidToolSchema,
61
+ InvalidToolParameters,
62
+ DuplicateToolName,
63
+ ToolSandboxViolation,
64
+ ToolExecutionFailed,
65
+ ToolSchemaGenerationFailed
66
+ ])
67
+ export type ToolExecutionError = typeof ToolExecutionError.Type
68
+
69
+ export const ToolSandbox = Schema.Literals([
70
+ "WorkspaceReadWrite",
71
+ "WorkspaceReadOnly",
72
+ "Unrestricted"
73
+ ])
74
+ export type ToolSandbox = typeof ToolSandbox.Type
75
+
76
+ export class ToolDescriptor extends Schema.Class<ToolDescriptor>("ToolDescriptor")({
77
+ name: Schema.String,
78
+ description: Schema.String,
79
+ parameters: Schema.Record(Schema.String, Schema.Json),
80
+ tags: Schema.ReadonlySet(Schema.String).pipe(
81
+ Schema.withConstructorDefault(Effect.succeed(new Set<string>()))
82
+ ),
83
+ sandbox: ToolSandbox.pipe(Schema.withConstructorDefault(Effect.succeed("WorkspaceReadWrite"))),
84
+ requires: Schema.Array(CapabilitySchema).pipe(
85
+ Schema.withConstructorDefault(Effect.succeed(Object.freeze([])))
86
+ )
87
+ }) {}
88
+
89
+ export interface Tool {
90
+ readonly name: string
91
+ readonly description: string
92
+ readonly parameters: JsonSchema
93
+ readonly execute: (parameters: JsonValue) => Effect.Effect<JsonValue, ToolExecutionError>
94
+ readonly tags: ReadonlySet<string>
95
+ readonly sandbox: ToolSandbox
96
+ readonly requires: ReadonlyArray<Capability>
97
+ }
98
+
99
+ export interface ToolOptions {
100
+ readonly name: string
101
+ readonly description: string
102
+ readonly parameters: JsonSchema
103
+ readonly execute: Tool["execute"]
104
+ readonly tags?: Iterable<string>
105
+ readonly sandbox?: ToolSandbox
106
+ readonly requires?: ReadonlyArray<Capability>
107
+ }
108
+
109
+ export const makeTool = (options: ToolOptions): Tool => ({
110
+ name: options.name,
111
+ description: options.description,
112
+ parameters: options.parameters,
113
+ execute: options.execute,
114
+ tags: new Set(options.tags ?? []),
115
+ sandbox: options.sandbox ?? "WorkspaceReadWrite",
116
+ requires: options.requires ?? []
117
+ })
118
+
119
+ export const toolDefinition = (tool: Tool): ToolDefinition =>
120
+ ToolDefinition.make({
121
+ name: tool.name,
122
+ description: tool.description,
123
+ parameters: tool.parameters
124
+ })
125
+
126
+ const splitParameters = (raw: string): ReadonlyArray<string> => {
127
+ const parameters: Array<string> = []
128
+ let start = 0
129
+ let depth = 0
130
+ for (let index = 0; index < raw.length; index += 1) {
131
+ const character = raw.charAt(index)
132
+ if (character === "[" || character === "<" || character === "(") {
133
+ depth += 1
134
+ } else if (character === "]" || character === ">" || character === ")") {
135
+ depth -= 1
136
+ } else if (character === "," && depth === 0) {
137
+ parameters.push(raw.slice(start, index).trim())
138
+ start = index + 1
139
+ }
140
+ }
141
+ const last = raw.slice(start).trim()
142
+ return last.length === 0 ? parameters : [...parameters, last]
143
+ }
144
+
145
+ interface ParsedParameter {
146
+ readonly name: string
147
+ readonly type: string
148
+ readonly required: boolean
149
+ }
150
+
151
+ const parseParameter = (raw: string): ParsedParameter | undefined => {
152
+ const match = /^(?:readonly\s+)?([A-Za-z_$][\w$]*)(\?)?\s*:\s*(.+?)(?:\s*=\s*.+)?$/.exec(raw)
153
+ const name = match?.[1]
154
+ const type = match?.[3]?.trim()
155
+ if (name === undefined || type === undefined) {
156
+ return undefined
157
+ }
158
+ const optional = match?.[2] === "?" || raw.includes("=") || /^(?:Option|Optional)[[<]/.test(type)
159
+ return {
160
+ name,
161
+ type,
162
+ required: !optional
163
+ }
164
+ }
165
+
166
+ const genericInner = (type: string, names: ReadonlyArray<string>): string | undefined => {
167
+ for (const name of names) {
168
+ const squarePrefix = `${name}[`
169
+ const anglePrefix = `${name}<`
170
+ if (type.startsWith(squarePrefix) && type.endsWith("]")) {
171
+ return type.slice(squarePrefix.length, -1).trim()
172
+ }
173
+ if (type.startsWith(anglePrefix) && type.endsWith(">")) {
174
+ return type.slice(anglePrefix.length, -1).trim()
175
+ }
176
+ }
177
+ return undefined
178
+ }
179
+
180
+ const schemaForType = (rawType: string): JsonValue => {
181
+ const type = rawType.trim()
182
+ switch (type) {
183
+ case "String":
184
+ case "string":
185
+ return { type: "string" }
186
+ case "Boolean":
187
+ case "boolean":
188
+ return { type: "boolean" }
189
+ case "Int":
190
+ case "Long":
191
+ case "Short":
192
+ case "Byte":
193
+ case "BigInt":
194
+ case "int":
195
+ case "integer":
196
+ return { type: "integer" }
197
+ case "Double":
198
+ case "Float":
199
+ case "BigDecimal":
200
+ case "number":
201
+ return { type: "number" }
202
+ }
203
+
204
+ const optional = genericInner(type, ["Option", "Optional"])
205
+ if (optional !== undefined) {
206
+ return schemaForType(optional)
207
+ }
208
+ const array = genericInner(type, ["List", "Seq", "Vector", "Set", "Array", "ReadonlyArray"])
209
+ if (array !== undefined) {
210
+ return {
211
+ type: "array",
212
+ items: schemaForType(array)
213
+ }
214
+ }
215
+ const map = genericInner(type, ["Map", "Record"])
216
+ if (map !== undefined) {
217
+ const parts = splitParameters(map)
218
+ return {
219
+ type: "object",
220
+ additionalProperties: schemaForType(parts.at(-1) ?? "object")
221
+ }
222
+ }
223
+ return { type: "object" }
224
+ }
225
+
226
+ export const schemaFromMethodSignature = (
227
+ signature: string
228
+ ): Effect.Effect<JsonSchema, ToolSchemaGenerationFailed> => {
229
+ const parametersText = signature.match(
230
+ /(?:def|function)?\s*[A-Za-z_$][\w$]*\s*\((.*)\)\s*(?::.*)?$/
231
+ )?.[1]
232
+ if (parametersText === undefined) {
233
+ return Effect.fail(
234
+ ToolSchemaGenerationFailed.make({
235
+ message: "Unsupported signature. Expected a method declaration or definition"
236
+ })
237
+ )
238
+ }
239
+ const parameters = splitParameters(parametersText).map(parseParameter)
240
+ if (parameters.some((parameter) => parameter === undefined)) {
241
+ return Effect.fail(
242
+ ToolSchemaGenerationFailed.make({
243
+ message: "Failed to parse one or more method parameters"
244
+ })
245
+ )
246
+ }
247
+
248
+ const properties: Record<string, JsonValue> = {}
249
+ const required: Array<string> = []
250
+ for (const parameter of parameters) {
251
+ if (parameter !== undefined) {
252
+ properties[parameter.name] = schemaForType(parameter.type)
253
+ if (parameter.required) {
254
+ required.push(parameter.name)
255
+ }
256
+ }
257
+ }
258
+ return Effect.succeed({
259
+ type: "object",
260
+ properties,
261
+ required,
262
+ additionalProperties: false
263
+ })
264
+ }
265
+
266
+ export const toolFromMethodSignature = Effect.fn("@llm4ts/core/tools/Tool.fromMethodSignature")(
267
+ function* (
268
+ options: Omit<ToolOptions, "parameters"> & {
269
+ readonly signature: string
270
+ }
271
+ ) {
272
+ const parameters = yield* schemaFromMethodSignature(options.signature)
273
+ return makeTool({
274
+ ...options,
275
+ parameters
276
+ })
277
+ }
278
+ )
279
+
280
+ const valueMatchesType = (value: JsonValue, expected: string): boolean => {
281
+ switch (expected) {
282
+ case "string":
283
+ return typeof value === "string"
284
+ case "boolean":
285
+ return typeof value === "boolean"
286
+ case "integer":
287
+ return typeof value === "number" && Number.isInteger(value)
288
+ case "number":
289
+ return typeof value === "number"
290
+ case "array":
291
+ return Array.isArray(value)
292
+ case "object":
293
+ return isJsonRecord(value)
294
+ default:
295
+ return true
296
+ }
297
+ }
298
+
299
+ export const validateToolParameters = Effect.fn("@llm4ts/core/tools/Tool.validateParameters")(
300
+ function* (
301
+ schema: JsonSchema,
302
+ parameters: JsonValue
303
+ ): Effect.fn.Return<void, ToolExecutionError> {
304
+ if (!isJsonRecord(schema)) {
305
+ return yield* InvalidToolParameters.make({
306
+ message: "Tool schema must be a JSON object"
307
+ })
308
+ }
309
+ if (!isJsonRecord(parameters)) {
310
+ return yield* InvalidToolParameters.make({
311
+ message: "Tool arguments must be a JSON object"
312
+ })
313
+ }
314
+
315
+ const required = jsonArray(jsonField(schema, "required")).flatMap((value) =>
316
+ typeof value === "string" ? [value] : []
317
+ )
318
+ const missing = required.filter((name) => parameters[name] === undefined).sort()
319
+ if (missing.length > 0) {
320
+ return yield* InvalidToolParameters.make({
321
+ message: `Missing required parameters: ${missing.join(", ")}`
322
+ })
323
+ }
324
+
325
+ const properties = jsonField(schema, "properties")
326
+ for (const [name, value] of Object.entries(parameters)) {
327
+ const property = jsonField(properties, name)
328
+ if (property !== undefined) {
329
+ const expected = jsonStringField(property, "type") ?? "object"
330
+ if (!valueMatchesType(value, expected)) {
331
+ return yield* InvalidToolParameters.make({
332
+ message: `Invalid type for '${name}'. Expected: ${expected}`
333
+ })
334
+ }
335
+ }
336
+ }
337
+ }
338
+ )
339
+
340
+ export const toolsToOpenAI = (tools: ReadonlyArray<Tool>): JsonValue =>
341
+ tools.map((tool) => ({
342
+ type: "function",
343
+ function: {
344
+ name: tool.name,
345
+ description: tool.description,
346
+ parameters: tool.parameters
347
+ }
348
+ }))
349
+
350
+ export const toolsToAnthropic = (tools: ReadonlyArray<Tool>): JsonValue =>
351
+ tools.map((tool) => ({
352
+ name: tool.name,
353
+ description: tool.description,
354
+ input_schema: tool.parameters
355
+ }))
356
+
357
+ export const toolsToGemini = (tools: ReadonlyArray<Tool>): JsonValue => ({
358
+ functionDeclarations: tools.map((tool) => ({
359
+ name: tool.name,
360
+ description: tool.description,
361
+ parameters: tool.parameters
362
+ }))
363
+ })
364
+
365
+ export const toolsToProviderFormat = (
366
+ provider: LlmProvider,
367
+ tools: ReadonlyArray<Tool>
368
+ ): JsonValue => {
369
+ switch (provider) {
370
+ case "OpenAI":
371
+ return toolsToOpenAI(tools)
372
+ case "Anthropic":
373
+ return toolsToAnthropic(tools)
374
+ case "GeminiApi":
375
+ return toolsToGemini(tools)
376
+ default:
377
+ return []
378
+ }
379
+ }
@@ -0,0 +1,106 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Schema from "effect/Schema"
3
+ import { InvalidRequestError, type LlmError } from "../Errors.ts"
4
+ import type { LlmServiceShape } from "../LlmService.ts"
5
+ import { LlmResponse, type ToolCallResponse } from "../Models.ts"
6
+ import { jsonText } from "../providers/CliSupport.ts"
7
+ import { toolDefinition, type Tool } from "./Tool.ts"
8
+ import { toolResultValue, type ToolRegistryShape, type ToolResult } from "./ToolRegistry.ts"
9
+
10
+ export class ToolLoopConfig extends Schema.Class<ToolLoopConfig>("ToolLoopConfig")({
11
+ maxIterations: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(8))),
12
+ maxDenials: Schema.optionalKey(Schema.Int),
13
+ maxConcurrency: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(1)))
14
+ }) {}
15
+
16
+ export const buildToolFollowUpPrompt = (
17
+ previousPrompt: string,
18
+ response: ToolCallResponse,
19
+ results: ReadonlyArray<ToolResult>
20
+ ): string => {
21
+ const renderedResults = results
22
+ .map(
23
+ (result) => `tool_call_id=${result.toolCallId} result=${jsonText(toolResultValue(result))}`
24
+ )
25
+ .join("\n")
26
+ return (
27
+ `${previousPrompt}\n\n` +
28
+ `Assistant requested tool execution with finishReason='${response.finishReason}'.\n` +
29
+ `Tool results:\n${renderedResults}\n\n` +
30
+ "Continue the task. If no further tools are needed, respond with final user-facing content.\n"
31
+ )
32
+ }
33
+
34
+ export const runToolCallingLoop = Effect.fn("@llm4ts/core/tools/ToolCallingExecutor.run")(
35
+ function* (
36
+ prompt: string,
37
+ tools: ReadonlyArray<Tool>,
38
+ llmService: LlmServiceShape,
39
+ registry: ToolRegistryShape,
40
+ config: ToolLoopConfig = ToolLoopConfig.make({})
41
+ ): Effect.fn.Return<LlmResponse, LlmError> {
42
+ if (config.maxIterations < 0) {
43
+ return yield* InvalidRequestError.make({
44
+ message: `Tool loop maxIterations must be non-negative; received ${config.maxIterations}`
45
+ })
46
+ }
47
+ if (config.maxDenials !== undefined && config.maxDenials < 0) {
48
+ return yield* InvalidRequestError.make({
49
+ message: `Tool loop maxDenials must be non-negative; received ${config.maxDenials}`
50
+ })
51
+ }
52
+
53
+ const definitions = tools.map(toolDefinition)
54
+ const loop = (
55
+ currentPrompt: string,
56
+ iteration: number,
57
+ denials: number
58
+ ): Effect.Effect<LlmResponse, LlmError> => {
59
+ if (iteration >= config.maxIterations) {
60
+ return Effect.fail(
61
+ InvalidRequestError.make({
62
+ message: `Tool call loop reached max iterations (${config.maxIterations})`
63
+ })
64
+ )
65
+ }
66
+
67
+ return Effect.flatMap(llmService.executeWithTools(currentPrompt, definitions), (response) => {
68
+ if (response.toolCalls.length === 0) {
69
+ return Effect.succeed(
70
+ LlmResponse.make({
71
+ content: response.content ?? "",
72
+ metadata: {
73
+ finish_reason: response.finishReason,
74
+ tool_iterations: String(iteration)
75
+ }
76
+ })
77
+ )
78
+ }
79
+
80
+ return Effect.flatMap(
81
+ registry.executeAll(response.toolCalls, config.maxConcurrency),
82
+ (results) => {
83
+ const totalDenials = denials + results.filter((result) => result.denied).length
84
+ if (config.maxDenials !== undefined && totalDenials >= config.maxDenials) {
85
+ return Effect.fail(
86
+ InvalidRequestError.make({
87
+ message:
88
+ "Tool loop exceeded the capability-denial budget " + `(${config.maxDenials})`
89
+ })
90
+ )
91
+ }
92
+ return Effect.suspend(() =>
93
+ loop(
94
+ buildToolFollowUpPrompt(currentPrompt, response, results),
95
+ iteration + 1,
96
+ totalDenials
97
+ )
98
+ )
99
+ }
100
+ )
101
+ })
102
+ }
103
+
104
+ return yield* loop(prompt, 0, 0)
105
+ }
106
+ )
@@ -0,0 +1,224 @@
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 { allows, currentGrants, type Capability } from "../Capability.ts"
7
+ import { InvalidRequestError, ToolError } from "../Errors.ts"
8
+ import type { LlmError } from "../Errors.ts"
9
+ import type { ToolCall } from "../Models.ts"
10
+ import type { JsonValue } from "../providers/CliSupport.ts"
11
+ import {
12
+ DuplicateToolName,
13
+ validateToolParameters,
14
+ type Tool,
15
+ type ToolExecutionError
16
+ } from "./Tool.ts"
17
+
18
+ export class ToolSucceeded extends Schema.TaggedClass<ToolSucceeded>()("Succeeded", {
19
+ value: Schema.Json
20
+ }) {}
21
+
22
+ export class ToolFailed extends Schema.TaggedClass<ToolFailed>()("Failed", {
23
+ message: Schema.String
24
+ }) {}
25
+
26
+ export const ToolResultValue = Schema.Union([ToolSucceeded, ToolFailed])
27
+ export type ToolResultValue = typeof ToolResultValue.Type
28
+
29
+ export class ToolResult extends Schema.Class<ToolResult>("ToolResult")({
30
+ toolCallId: Schema.String,
31
+ result: ToolResultValue,
32
+ denied: Schema.Boolean.pipe(Schema.withConstructorDefault(Effect.succeed(false)))
33
+ }) {}
34
+
35
+ export interface ToolRegistryShape {
36
+ readonly register: (tool: Tool) => Effect.Effect<void, ToolExecutionError>
37
+ readonly registerAll: (tools: ReadonlyArray<Tool>) => Effect.Effect<void, ToolExecutionError>
38
+ readonly get: (name: string) => Effect.Effect<Tool, LlmError>
39
+ readonly list: Effect.Effect<ReadonlyArray<Tool>>
40
+ readonly select: (prompt: string, limit?: number) => Effect.Effect<ReadonlyArray<Tool>>
41
+ readonly validate: (
42
+ call: ToolCall
43
+ ) => Effect.Effect<readonly [tool: Tool, parameters: JsonValue], LlmError>
44
+ readonly execute: (call: ToolCall) => Effect.Effect<ToolResult, LlmError>
45
+ readonly executeAll: (
46
+ calls: ReadonlyArray<ToolCall>,
47
+ concurrency?: number
48
+ ) => Effect.Effect<ReadonlyArray<ToolResult>, LlmError>
49
+ }
50
+
51
+ export class ToolRegistry extends Context.Service<ToolRegistry, ToolRegistryShape>()(
52
+ "@llm4ts/core/tools/ToolRegistry"
53
+ ) {}
54
+
55
+ const capabilityName = (capability: Capability): string =>
56
+ capability._tag === "Exec" ? `Exec(${capability.command})` : capability._tag
57
+
58
+ const promptWords = (prompt: string): ReadonlySet<string> =>
59
+ new Set(
60
+ prompt
61
+ .toLowerCase()
62
+ .split(/[^a-z0-9_]+/)
63
+ .filter((word) => word.length > 0)
64
+ )
65
+
66
+ const toolScore = (tool: Tool, words: ReadonlySet<string>): number => {
67
+ let score = 0
68
+ for (const word of words) {
69
+ score += tool.name.toLowerCase().includes(word) ? 5 : 0
70
+ score += [...tool.tags].some((tag) => tag.toLowerCase() === word) ? 4 : 0
71
+ score += tool.description.toLowerCase().includes(word) ? 2 : 0
72
+ }
73
+ return score
74
+ }
75
+
76
+ const parseArguments = (call: ToolCall): Effect.Effect<JsonValue, ToolError> =>
77
+ Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json))(call.arguments).pipe(
78
+ Effect.mapError((error) =>
79
+ ToolError.make({
80
+ toolName: call.name,
81
+ detail: `Invalid arguments JSON: ${String(error)}`
82
+ })
83
+ )
84
+ )
85
+
86
+ export const makeToolRegistry = Effect.fn("@llm4ts/core/tools/ToolRegistry.make")(
87
+ function* (): Effect.fn.Return<ToolRegistryShape> {
88
+ const state = yield* Ref.make<ReadonlyMap<string, Tool>>(new Map())
89
+
90
+ const register = (tool: Tool): Effect.Effect<void, ToolExecutionError> =>
91
+ Ref.modify(state, (current) => {
92
+ if (current.has(tool.name)) {
93
+ return [false, current] as const
94
+ }
95
+ const next = new Map(current)
96
+ next.set(tool.name, tool)
97
+ return [true, next] as const
98
+ }).pipe(
99
+ Effect.flatMap((registered) =>
100
+ registered ? Effect.void : Effect.fail(DuplicateToolName.make({ name: tool.name }))
101
+ )
102
+ )
103
+
104
+ const registerAll = (tools: ReadonlyArray<Tool>): Effect.Effect<void, ToolExecutionError> =>
105
+ Effect.forEach(tools, register, {
106
+ discard: true
107
+ })
108
+
109
+ const get = (name: string): Effect.Effect<Tool, LlmError> =>
110
+ Effect.flatMap(Ref.get(state), (tools) => {
111
+ const tool = tools.get(name)
112
+ return tool === undefined
113
+ ? Effect.fail(
114
+ ToolError.make({
115
+ toolName: name,
116
+ detail: `Tool not found: ${name}`
117
+ })
118
+ )
119
+ : Effect.succeed(tool)
120
+ })
121
+
122
+ const list = Ref.get(state).pipe(
123
+ Effect.map((tools) =>
124
+ [...tools.values()].sort((left, right) => left.name.localeCompare(right.name))
125
+ )
126
+ )
127
+
128
+ const select = (prompt: string, limit = 8): Effect.Effect<ReadonlyArray<Tool>> =>
129
+ Effect.map(list, (tools) =>
130
+ tools
131
+ .map((tool) => ({
132
+ tool,
133
+ score: toolScore(tool, promptWords(prompt))
134
+ }))
135
+ .filter(({ score }) => score > 0)
136
+ .sort(
137
+ (left, right) =>
138
+ right.score - left.score || left.tool.name.localeCompare(right.tool.name)
139
+ )
140
+ .slice(0, Math.max(0, limit))
141
+ .map(({ tool }) => tool)
142
+ )
143
+
144
+ const validate = Effect.fn("@llm4ts/core/tools/ToolRegistry.validate")(function* (
145
+ call: ToolCall
146
+ ): Effect.fn.Return<readonly [tool: Tool, parameters: JsonValue], LlmError> {
147
+ const tool = yield* get(call.name)
148
+ const parameters = yield* parseArguments(call)
149
+ yield* validateToolParameters(tool.parameters, parameters).pipe(
150
+ Effect.mapError((error) =>
151
+ ToolError.make({
152
+ toolName: tool.name,
153
+ detail: error.message
154
+ })
155
+ )
156
+ )
157
+ return [tool, parameters]
158
+ })
159
+
160
+ const execute = Effect.fn("@llm4ts/core/tools/ToolRegistry.execute")(function* (
161
+ call: ToolCall
162
+ ): Effect.fn.Return<ToolResult, LlmError> {
163
+ const [tool, parameters] = yield* validate(call)
164
+ const grants = yield* currentGrants
165
+ const missing = tool.requires
166
+ .filter((capability) => !allows(grants, capability))
167
+ .sort((left, right) => capabilityName(left).localeCompare(capabilityName(right)))
168
+ if (missing.length > 0) {
169
+ return ToolResult.make({
170
+ toolCallId: call.id,
171
+ denied: true,
172
+ result: ToolFailed.make({
173
+ message:
174
+ `Tool '${tool.name}' requires ungranted capabilities: ` +
175
+ missing.map(capabilityName).join(", ")
176
+ })
177
+ })
178
+ }
179
+
180
+ const result = yield* Effect.result(tool.execute(parameters))
181
+ return ToolResult.make({
182
+ toolCallId: call.id,
183
+ result:
184
+ result._tag === "Success"
185
+ ? ToolSucceeded.make({ value: result.success })
186
+ : ToolFailed.make({ message: result.failure.message })
187
+ })
188
+ })
189
+
190
+ const executeAll = (
191
+ calls: ReadonlyArray<ToolCall>,
192
+ concurrency = 1
193
+ ): Effect.Effect<ReadonlyArray<ToolResult>, LlmError> =>
194
+ !Number.isInteger(concurrency) || concurrency <= 0
195
+ ? Effect.fail(
196
+ InvalidRequestError.make({
197
+ message: `Tool execution concurrency must be a positive integer; received ${concurrency}`
198
+ })
199
+ )
200
+ : Effect.forEach(calls, execute, {
201
+ concurrency
202
+ })
203
+
204
+ return {
205
+ register,
206
+ registerAll,
207
+ get,
208
+ list,
209
+ select,
210
+ validate,
211
+ execute,
212
+ executeAll
213
+ }
214
+ }
215
+ )
216
+
217
+ export const ToolRegistryLive = Layer.effect(ToolRegistry, makeToolRegistry())
218
+
219
+ export const toolResultValue = (result: ToolResult): JsonValue =>
220
+ result.result._tag === "Succeeded"
221
+ ? result.result.value
222
+ : {
223
+ error: result.result.message
224
+ }