@llm4ts/core 2.54.0 → 3.0.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 (142) hide show
  1. package/README.md +1 -1
  2. package/dist/Capability.d.ts +0 -9
  3. package/dist/Capability.d.ts.map +1 -1
  4. package/dist/Capability.js +0 -38
  5. package/dist/Capability.js.map +1 -1
  6. package/dist/Connector.d.ts +2 -3
  7. package/dist/Connector.d.ts.map +1 -1
  8. package/dist/Connector.js +0 -5
  9. package/dist/Connector.js.map +1 -1
  10. package/dist/ConnectorRegistry.d.ts +0 -3
  11. package/dist/ConnectorRegistry.d.ts.map +1 -1
  12. package/dist/ConnectorRegistry.js +0 -3
  13. package/dist/ConnectorRegistry.js.map +1 -1
  14. package/dist/HttpClient.d.ts +0 -2
  15. package/dist/HttpClient.d.ts.map +1 -1
  16. package/dist/HttpClient.js +0 -2
  17. package/dist/HttpClient.js.map +1 -1
  18. package/dist/LlmService.d.ts +1 -2
  19. package/dist/LlmService.d.ts.map +1 -1
  20. package/dist/LlmService.js.map +1 -1
  21. package/dist/Models.d.ts +0 -21
  22. package/dist/Models.d.ts.map +1 -1
  23. package/dist/Models.js +0 -19
  24. package/dist/Models.js.map +1 -1
  25. package/dist/Streaming.d.ts +4 -11
  26. package/dist/Streaming.d.ts.map +1 -1
  27. package/dist/Streaming.js +2 -29
  28. package/dist/Streaming.js.map +1 -1
  29. package/dist/providers/AnthropicModels.d.ts +0 -43
  30. package/dist/providers/AnthropicModels.d.ts.map +1 -1
  31. package/dist/providers/AnthropicModels.js +0 -39
  32. package/dist/providers/AnthropicModels.js.map +1 -1
  33. package/dist/providers/AnthropicProvider.d.ts +2 -3
  34. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  35. package/dist/providers/AnthropicProvider.js +2 -49
  36. package/dist/providers/AnthropicProvider.js.map +1 -1
  37. package/dist/providers/GeminiApiProvider.d.ts +2 -3
  38. package/dist/providers/GeminiApiProvider.d.ts.map +1 -1
  39. package/dist/providers/GeminiApiProvider.js +2 -44
  40. package/dist/providers/GeminiApiProvider.js.map +1 -1
  41. package/dist/providers/GeminiModels.d.ts +0 -48
  42. package/dist/providers/GeminiModels.d.ts.map +1 -1
  43. package/dist/providers/GeminiModels.js +0 -40
  44. package/dist/providers/GeminiModels.js.map +1 -1
  45. package/dist/providers/LmStudioProvider.d.ts.map +1 -1
  46. package/dist/providers/LmStudioProvider.js +1 -4
  47. package/dist/providers/LmStudioProvider.js.map +1 -1
  48. package/dist/providers/MlxLmProvider.d.ts.map +1 -1
  49. package/dist/providers/MlxLmProvider.js +1 -2
  50. package/dist/providers/MlxLmProvider.js.map +1 -1
  51. package/dist/providers/MockProvider.d.ts.map +1 -1
  52. package/dist/providers/MockProvider.js +1 -6
  53. package/dist/providers/MockProvider.js.map +1 -1
  54. package/dist/providers/OllamaProvider.d.ts.map +1 -1
  55. package/dist/providers/OllamaProvider.js +1 -4
  56. package/dist/providers/OllamaProvider.js.map +1 -1
  57. package/dist/providers/OpenAIModels.d.ts +0 -58
  58. package/dist/providers/OpenAIModels.d.ts.map +1 -1
  59. package/dist/providers/OpenAIModels.js +0 -51
  60. package/dist/providers/OpenAIModels.js.map +1 -1
  61. package/dist/providers/OpenAIProvider.d.ts +2 -3
  62. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  63. package/dist/providers/OpenAIProvider.js +2 -46
  64. package/dist/providers/OpenAIProvider.js.map +1 -1
  65. package/package.json +2 -14
  66. package/src/Capability.ts +0 -50
  67. package/src/Connector.ts +2 -20
  68. package/src/ConnectorRegistry.ts +0 -15
  69. package/src/HttpClient.ts +0 -15
  70. package/src/LlmService.ts +1 -13
  71. package/src/Models.ts +0 -22
  72. package/src/Streaming.ts +3 -99
  73. package/src/providers/AnthropicModels.ts +0 -47
  74. package/src/providers/AnthropicProvider.ts +1 -87
  75. package/src/providers/GeminiApiProvider.ts +2 -83
  76. package/src/providers/GeminiModels.ts +0 -46
  77. package/src/providers/LmStudioProvider.ts +1 -7
  78. package/src/providers/MlxLmProvider.ts +1 -3
  79. package/src/providers/MockProvider.ts +0 -9
  80. package/src/providers/OllamaProvider.ts +1 -7
  81. package/src/providers/OpenAIModels.ts +0 -64
  82. package/src/providers/OpenAIProvider.ts +2 -83
  83. package/dist/ContextManagement.d.ts +0 -78
  84. package/dist/ContextManagement.d.ts.map +0 -1
  85. package/dist/ContextManagement.js +0 -222
  86. package/dist/ContextManagement.js.map +0 -1
  87. package/dist/Conversation.d.ts +0 -18
  88. package/dist/Conversation.d.ts.map +0 -1
  89. package/dist/Conversation.js +0 -17
  90. package/dist/Conversation.js.map +0 -1
  91. package/dist/RateLimiter.d.ts +0 -44
  92. package/dist/RateLimiter.d.ts.map +0 -1
  93. package/dist/RateLimiter.js +0 -155
  94. package/dist/RateLimiter.js.map +0 -1
  95. package/dist/observability/MeteredLlmService.d.ts +0 -16
  96. package/dist/observability/MeteredLlmService.d.ts.map +0 -1
  97. package/dist/observability/MeteredLlmService.js +0 -72
  98. package/dist/observability/MeteredLlmService.js.map +0 -1
  99. package/dist/observability/Metrics.d.ts +0 -76
  100. package/dist/observability/Metrics.d.ts.map +0 -1
  101. package/dist/observability/Metrics.js +0 -152
  102. package/dist/observability/Metrics.js.map +0 -1
  103. package/dist/observability/StructuredLogger.d.ts +0 -31
  104. package/dist/observability/StructuredLogger.d.ts.map +0 -1
  105. package/dist/observability/StructuredLogger.js +0 -39
  106. package/dist/observability/StructuredLogger.js.map +0 -1
  107. package/dist/observability/Tracing.d.ts +0 -38
  108. package/dist/observability/Tracing.d.ts.map +0 -1
  109. package/dist/observability/Tracing.js +0 -80
  110. package/dist/observability/Tracing.js.map +0 -1
  111. package/dist/providers/OpenCodeModels.d.ts +0 -61
  112. package/dist/providers/OpenCodeModels.d.ts.map +0 -1
  113. package/dist/providers/OpenCodeModels.js +0 -53
  114. package/dist/providers/OpenCodeModels.js.map +0 -1
  115. package/dist/providers/OpenCodeProvider.d.ts +0 -10
  116. package/dist/providers/OpenCodeProvider.d.ts.map +0 -1
  117. package/dist/providers/OpenCodeProvider.js +0 -154
  118. package/dist/providers/OpenCodeProvider.js.map +0 -1
  119. package/dist/tools/Tool.d.ts +0 -83
  120. package/dist/tools/Tool.d.ts.map +0 -1
  121. package/dist/tools/Tool.js +0 -278
  122. package/dist/tools/Tool.js.map +0 -1
  123. package/dist/tools/ToolCallingExecutor.d.ts +0 -18
  124. package/dist/tools/ToolCallingExecutor.d.ts.map +0 -1
  125. package/dist/tools/ToolCallingExecutor.js +0 -64
  126. package/dist/tools/ToolCallingExecutor.js.map +0 -1
  127. package/dist/tools/ToolRegistry.d.ts +0 -45
  128. package/dist/tools/ToolRegistry.d.ts.map +0 -1
  129. package/dist/tools/ToolRegistry.js +0 -133
  130. package/dist/tools/ToolRegistry.js.map +0 -1
  131. package/src/ContextManagement.ts +0 -367
  132. package/src/Conversation.ts +0 -21
  133. package/src/RateLimiter.ts +0 -238
  134. package/src/observability/MeteredLlmService.ts +0 -168
  135. package/src/observability/Metrics.ts +0 -200
  136. package/src/observability/StructuredLogger.ts +0 -67
  137. package/src/observability/Tracing.ts +0 -146
  138. package/src/providers/OpenCodeModels.ts +0 -60
  139. package/src/providers/OpenCodeProvider.ts +0 -278
  140. package/src/tools/Tool.ts +0 -376
  141. package/src/tools/ToolCallingExecutor.ts +0 -106
  142. package/src/tools/ToolRegistry.ts +0 -224
package/src/tools/Tool.ts DELETED
@@ -1,376 +0,0 @@
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.TaggedError<InvalidToolSchema>()("InvalidSchema", {
14
- message: Schema.String
15
- }) {}
16
-
17
- export class InvalidToolParameters extends Schema.TaggedError<InvalidToolParameters>()(
18
- "InvalidParameters",
19
- {
20
- message: Schema.String
21
- }
22
- ) {}
23
-
24
- export class DuplicateToolName extends Schema.TaggedError<DuplicateToolName>()(
25
- "DuplicateToolName",
26
- {
27
- name: Schema.String
28
- }
29
- ) {
30
- get message(): string {
31
- return `Tool already registered: ${this.name}`
32
- }
33
- }
34
-
35
- export class ToolSandboxViolation extends Schema.TaggedError<ToolSandboxViolation>()(
36
- "SandboxViolation",
37
- {
38
- message: Schema.String
39
- }
40
- ) {}
41
-
42
- export class ToolExecutionFailed extends Schema.TaggedError<ToolExecutionFailed>()(
43
- "ExecutionFailed",
44
- {
45
- message: Schema.String
46
- }
47
- ) {}
48
-
49
- export class ToolSchemaGenerationFailed extends Schema.TaggedError<ToolSchemaGenerationFailed>()(
50
- "SchemaGenerationFailed",
51
- {
52
- message: Schema.String
53
- }
54
- ) {}
55
-
56
- export const ToolExecutionError = Schema.Union([
57
- InvalidToolSchema,
58
- InvalidToolParameters,
59
- DuplicateToolName,
60
- ToolSandboxViolation,
61
- ToolExecutionFailed,
62
- ToolSchemaGenerationFailed
63
- ])
64
- export type ToolExecutionError = typeof ToolExecutionError.Type
65
-
66
- export const ToolSandbox = Schema.Literals([
67
- "WorkspaceReadWrite",
68
- "WorkspaceReadOnly",
69
- "Unrestricted"
70
- ])
71
- export type ToolSandbox = typeof ToolSandbox.Type
72
-
73
- export class ToolDescriptor extends Schema.Class<ToolDescriptor>("ToolDescriptor")({
74
- name: Schema.String,
75
- description: Schema.String,
76
- parameters: Schema.Record(Schema.String, Schema.Json),
77
- tags: Schema.ReadonlySet(Schema.String).pipe(
78
- Schema.withConstructorDefault(Effect.succeed(new Set<string>()))
79
- ),
80
- sandbox: ToolSandbox.pipe(Schema.withConstructorDefault(Effect.succeed("WorkspaceReadWrite"))),
81
- requires: Schema.Array(CapabilitySchema).pipe(
82
- Schema.withConstructorDefault(Effect.succeed(Object.freeze([])))
83
- )
84
- }) {}
85
-
86
- export interface Tool {
87
- readonly name: string
88
- readonly description: string
89
- readonly parameters: JsonSchema
90
- readonly execute: (parameters: JsonValue) => Effect.Effect<JsonValue, ToolExecutionError>
91
- readonly tags: ReadonlySet<string>
92
- readonly sandbox: ToolSandbox
93
- readonly requires: ReadonlyArray<Capability>
94
- }
95
-
96
- export interface ToolOptions {
97
- readonly name: string
98
- readonly description: string
99
- readonly parameters: JsonSchema
100
- readonly execute: Tool["execute"]
101
- readonly tags?: Iterable<string>
102
- readonly sandbox?: ToolSandbox
103
- readonly requires?: ReadonlyArray<Capability>
104
- }
105
-
106
- export const makeTool = (options: ToolOptions): Tool => ({
107
- name: options.name,
108
- description: options.description,
109
- parameters: options.parameters,
110
- execute: options.execute,
111
- tags: new Set(options.tags ?? []),
112
- sandbox: options.sandbox ?? "WorkspaceReadWrite",
113
- requires: options.requires ?? []
114
- })
115
-
116
- export const toolDefinition = (tool: Tool): ToolDefinition =>
117
- ToolDefinition.make({
118
- name: tool.name,
119
- description: tool.description,
120
- parameters: tool.parameters
121
- })
122
-
123
- const splitParameters = (raw: string): ReadonlyArray<string> => {
124
- const parameters: Array<string> = []
125
- let start = 0
126
- let depth = 0
127
- for (let index = 0; index < raw.length; index += 1) {
128
- const character = raw.charAt(index)
129
- if (character === "[" || character === "<" || character === "(") {
130
- depth += 1
131
- } else if (character === "]" || character === ">" || character === ")") {
132
- depth -= 1
133
- } else if (character === "," && depth === 0) {
134
- parameters.push(raw.slice(start, index).trim())
135
- start = index + 1
136
- }
137
- }
138
- const last = raw.slice(start).trim()
139
- return last.length === 0 ? parameters : [...parameters, last]
140
- }
141
-
142
- interface ParsedParameter {
143
- readonly name: string
144
- readonly type: string
145
- readonly required: boolean
146
- }
147
-
148
- const parseParameter = (raw: string): ParsedParameter | undefined => {
149
- const match = /^(?:readonly\s+)?([A-Za-z_$][\w$]*)(\?)?\s*:\s*(.+?)(?:\s*=\s*.+)?$/.exec(raw)
150
- const name = match?.[1]
151
- const type = match?.[3]?.trim()
152
- if (name === undefined || type === undefined) {
153
- return undefined
154
- }
155
- const optional = match?.[2] === "?" || raw.includes("=") || /^(?:Option|Optional)[[<]/.test(type)
156
- return {
157
- name,
158
- type,
159
- required: !optional
160
- }
161
- }
162
-
163
- const genericInner = (type: string, names: ReadonlyArray<string>): string | undefined => {
164
- for (const name of names) {
165
- const squarePrefix = `${name}[`
166
- const anglePrefix = `${name}<`
167
- if (type.startsWith(squarePrefix) && type.endsWith("]")) {
168
- return type.slice(squarePrefix.length, -1).trim()
169
- }
170
- if (type.startsWith(anglePrefix) && type.endsWith(">")) {
171
- return type.slice(anglePrefix.length, -1).trim()
172
- }
173
- }
174
- return undefined
175
- }
176
-
177
- const schemaForType = (rawType: string): JsonValue => {
178
- const type = rawType.trim()
179
- switch (type) {
180
- case "String":
181
- case "string":
182
- return { type: "string" }
183
- case "Boolean":
184
- case "boolean":
185
- return { type: "boolean" }
186
- case "Int":
187
- case "Long":
188
- case "Short":
189
- case "Byte":
190
- case "BigInt":
191
- case "int":
192
- case "integer":
193
- return { type: "integer" }
194
- case "Double":
195
- case "Float":
196
- case "BigDecimal":
197
- case "number":
198
- return { type: "number" }
199
- }
200
-
201
- const optional = genericInner(type, ["Option", "Optional"])
202
- if (optional !== undefined) {
203
- return schemaForType(optional)
204
- }
205
- const array = genericInner(type, ["List", "Seq", "Vector", "Set", "Array", "ReadonlyArray"])
206
- if (array !== undefined) {
207
- return {
208
- type: "array",
209
- items: schemaForType(array)
210
- }
211
- }
212
- const map = genericInner(type, ["Map", "Record"])
213
- if (map !== undefined) {
214
- const parts = splitParameters(map)
215
- return {
216
- type: "object",
217
- additionalProperties: schemaForType(parts.at(-1) ?? "object")
218
- }
219
- }
220
- return { type: "object" }
221
- }
222
-
223
- export const schemaFromMethodSignature = (
224
- signature: string
225
- ): Effect.Effect<JsonSchema, ToolSchemaGenerationFailed> => {
226
- const parametersText = signature.match(
227
- /(?:def|function)?\s*[A-Za-z_$][\w$]*\s*\((.*)\)\s*(?::.*)?$/
228
- )?.[1]
229
- if (parametersText === undefined) {
230
- return Effect.fail(
231
- ToolSchemaGenerationFailed.make({
232
- message: "Unsupported signature. Expected a method declaration or definition"
233
- })
234
- )
235
- }
236
- const parameters = splitParameters(parametersText).map(parseParameter)
237
- if (parameters.some((parameter) => parameter === undefined)) {
238
- return Effect.fail(
239
- ToolSchemaGenerationFailed.make({
240
- message: "Failed to parse one or more method parameters"
241
- })
242
- )
243
- }
244
-
245
- const properties: Record<string, JsonValue> = {}
246
- const required: Array<string> = []
247
- for (const parameter of parameters) {
248
- if (parameter !== undefined) {
249
- properties[parameter.name] = schemaForType(parameter.type)
250
- if (parameter.required) {
251
- required.push(parameter.name)
252
- }
253
- }
254
- }
255
- return Effect.succeed({
256
- type: "object",
257
- properties,
258
- required,
259
- additionalProperties: false
260
- })
261
- }
262
-
263
- export const toolFromMethodSignature = Effect.fn("@llm4ts/core/tools/Tool.fromMethodSignature")(
264
- function* (
265
- options: Omit<ToolOptions, "parameters"> & {
266
- readonly signature: string
267
- }
268
- ) {
269
- const parameters = yield* schemaFromMethodSignature(options.signature)
270
- return makeTool({
271
- ...options,
272
- parameters
273
- })
274
- }
275
- )
276
-
277
- const valueMatchesType = (value: JsonValue, expected: string): boolean => {
278
- switch (expected) {
279
- case "string":
280
- return typeof value === "string"
281
- case "boolean":
282
- return typeof value === "boolean"
283
- case "integer":
284
- return typeof value === "number" && Number.isInteger(value)
285
- case "number":
286
- return typeof value === "number"
287
- case "array":
288
- return Array.isArray(value)
289
- case "object":
290
- return isJsonRecord(value)
291
- default:
292
- return true
293
- }
294
- }
295
-
296
- export const validateToolParameters = Effect.fn("@llm4ts/core/tools/Tool.validateParameters")(
297
- function* (
298
- schema: JsonSchema,
299
- parameters: JsonValue
300
- ): Effect.fn.Return<void, ToolExecutionError> {
301
- if (!isJsonRecord(schema)) {
302
- return yield* InvalidToolParameters.make({
303
- message: "Tool schema must be a JSON object"
304
- })
305
- }
306
- if (!isJsonRecord(parameters)) {
307
- return yield* InvalidToolParameters.make({
308
- message: "Tool arguments must be a JSON object"
309
- })
310
- }
311
-
312
- const required = jsonArray(jsonField(schema, "required")).flatMap((value) =>
313
- typeof value === "string" ? [value] : []
314
- )
315
- const missing = required.filter((name) => parameters[name] === undefined).sort()
316
- if (missing.length > 0) {
317
- return yield* InvalidToolParameters.make({
318
- message: `Missing required parameters: ${missing.join(", ")}`
319
- })
320
- }
321
-
322
- const properties = jsonField(schema, "properties")
323
- for (const [name, value] of Object.entries(parameters)) {
324
- const property = jsonField(properties, name)
325
- if (property !== undefined) {
326
- const expected = jsonStringField(property, "type") ?? "object"
327
- if (!valueMatchesType(value, expected)) {
328
- return yield* InvalidToolParameters.make({
329
- message: `Invalid type for '${name}'. Expected: ${expected}`
330
- })
331
- }
332
- }
333
- }
334
- }
335
- )
336
-
337
- export const toolsToOpenAI = (tools: ReadonlyArray<Tool>): JsonValue =>
338
- tools.map((tool) => ({
339
- type: "function",
340
- function: {
341
- name: tool.name,
342
- description: tool.description,
343
- parameters: tool.parameters
344
- }
345
- }))
346
-
347
- export const toolsToAnthropic = (tools: ReadonlyArray<Tool>): JsonValue =>
348
- tools.map((tool) => ({
349
- name: tool.name,
350
- description: tool.description,
351
- input_schema: tool.parameters
352
- }))
353
-
354
- export const toolsToGemini = (tools: ReadonlyArray<Tool>): JsonValue => ({
355
- functionDeclarations: tools.map((tool) => ({
356
- name: tool.name,
357
- description: tool.description,
358
- parameters: tool.parameters
359
- }))
360
- })
361
-
362
- export const toolsToProviderFormat = (
363
- provider: LlmProvider,
364
- tools: ReadonlyArray<Tool>
365
- ): JsonValue => {
366
- switch (provider) {
367
- case "OpenAI":
368
- return toolsToOpenAI(tools)
369
- case "Anthropic":
370
- return toolsToAnthropic(tools)
371
- case "GeminiApi":
372
- return toolsToGemini(tools)
373
- default:
374
- return []
375
- }
376
- }
@@ -1,106 +0,0 @@
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
- )
@@ -1,224 +0,0 @@
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
- }