@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.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/AgentSession.d.ts +66 -0
- package/dist/AgentSession.d.ts.map +1 -0
- package/dist/AgentSession.js +53 -0
- package/dist/AgentSession.js.map +1 -0
- package/dist/Capability.d.ts +101 -0
- package/dist/Capability.d.ts.map +1 -0
- package/dist/Capability.js +210 -0
- package/dist/Capability.js.map +1 -0
- package/dist/Connector.d.ts +69 -0
- package/dist/Connector.d.ts.map +1 -0
- package/dist/Connector.js +86 -0
- package/dist/Connector.js.map +1 -0
- package/dist/ConnectorConfig.d.ts +70 -0
- package/dist/ConnectorConfig.d.ts.map +1 -0
- package/dist/ConnectorConfig.js +110 -0
- package/dist/ConnectorConfig.js.map +1 -0
- package/dist/ConnectorRegistry.d.ts +28 -0
- package/dist/ConnectorRegistry.d.ts.map +1 -0
- package/dist/ConnectorRegistry.js +87 -0
- package/dist/ConnectorRegistry.js.map +1 -0
- package/dist/ContextManagement.d.ts +78 -0
- package/dist/ContextManagement.d.ts.map +1 -0
- package/dist/ContextManagement.js +221 -0
- package/dist/ContextManagement.js.map +1 -0
- package/dist/Conversation.d.ts +18 -0
- package/dist/Conversation.d.ts.map +1 -0
- package/dist/Conversation.js +17 -0
- package/dist/Conversation.js.map +1 -0
- package/dist/Errors.d.ts +64 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +76 -0
- package/dist/Errors.js.map +1 -0
- package/dist/HttpClient.d.ts +43 -0
- package/dist/HttpClient.d.ts.map +1 -0
- package/dist/HttpClient.js +99 -0
- package/dist/HttpClient.js.map +1 -0
- package/dist/LlmService.d.ts +24 -0
- package/dist/LlmService.d.ts.map +1 -0
- package/dist/LlmService.js +4 -0
- package/dist/LlmService.js.map +1 -0
- package/dist/Models.d.ts +138 -0
- package/dist/Models.d.ts.map +1 -0
- package/dist/Models.js +209 -0
- package/dist/Models.js.map +1 -0
- package/dist/ProcessExecutor.d.ts +57 -0
- package/dist/ProcessExecutor.d.ts.map +1 -0
- package/dist/ProcessExecutor.js +71 -0
- package/dist/ProcessExecutor.js.map +1 -0
- package/dist/RateLimiter.d.ts +44 -0
- package/dist/RateLimiter.d.ts.map +1 -0
- package/dist/RateLimiter.js +155 -0
- package/dist/RateLimiter.js.map +1 -0
- package/dist/Streaming.d.ts +17 -0
- package/dist/Streaming.d.ts.map +1 -0
- package/dist/Streaming.js +65 -0
- package/dist/Streaming.js.map +1 -0
- package/dist/StructuredOutput.d.ts +10 -0
- package/dist/StructuredOutput.d.ts.map +1 -0
- package/dist/StructuredOutput.js +97 -0
- package/dist/StructuredOutput.js.map +1 -0
- package/dist/TemporaryFiles.d.ts +20 -0
- package/dist/TemporaryFiles.d.ts.map +1 -0
- package/dist/TemporaryFiles.js +18 -0
- package/dist/TemporaryFiles.js.map +1 -0
- package/dist/UsageLimits.d.ts +5 -0
- package/dist/UsageLimits.d.ts.map +1 -0
- package/dist/UsageLimits.js +187 -0
- package/dist/UsageLimits.js.map +1 -0
- package/dist/eval/Checks.d.ts +6 -0
- package/dist/eval/Checks.d.ts.map +1 -0
- package/dist/eval/Checks.js +39 -0
- package/dist/eval/Checks.js.map +1 -0
- package/dist/eval/Eval.d.ts +57 -0
- package/dist/eval/Eval.d.ts.map +1 -0
- package/dist/eval/Eval.js +91 -0
- package/dist/eval/Eval.js.map +1 -0
- package/dist/eval/EvalSuite.d.ts +21 -0
- package/dist/eval/EvalSuite.d.ts.map +1 -0
- package/dist/eval/EvalSuite.js +30 -0
- package/dist/eval/EvalSuite.js.map +1 -0
- package/dist/eval/Evaluator.d.ts +10 -0
- package/dist/eval/Evaluator.d.ts.map +1 -0
- package/dist/eval/Evaluator.js +14 -0
- package/dist/eval/Evaluator.js.map +1 -0
- package/dist/eval/Judge.d.ts +15 -0
- package/dist/eval/Judge.d.ts.map +1 -0
- package/dist/eval/Judge.js +73 -0
- package/dist/eval/Judge.js.map +1 -0
- package/dist/observability/MeteredLlmService.d.ts +16 -0
- package/dist/observability/MeteredLlmService.d.ts.map +1 -0
- package/dist/observability/MeteredLlmService.js +66 -0
- package/dist/observability/MeteredLlmService.js.map +1 -0
- package/dist/observability/Metrics.d.ts +71 -0
- package/dist/observability/Metrics.d.ts.map +1 -0
- package/dist/observability/Metrics.js +147 -0
- package/dist/observability/Metrics.js.map +1 -0
- package/dist/observability/Redaction.d.ts +8 -0
- package/dist/observability/Redaction.d.ts.map +1 -0
- package/dist/observability/Redaction.js +38 -0
- package/dist/observability/Redaction.js.map +1 -0
- package/dist/observability/StreamRecorder.d.ts +9 -0
- package/dist/observability/StreamRecorder.d.ts.map +1 -0
- package/dist/observability/StreamRecorder.js +10 -0
- package/dist/observability/StreamRecorder.js.map +1 -0
- package/dist/observability/StructuredLogger.d.ts +31 -0
- package/dist/observability/StructuredLogger.d.ts.map +1 -0
- package/dist/observability/StructuredLogger.js +34 -0
- package/dist/observability/StructuredLogger.js.map +1 -0
- package/dist/observability/Tracing.d.ts +38 -0
- package/dist/observability/Tracing.d.ts.map +1 -0
- package/dist/observability/Tracing.js +75 -0
- package/dist/observability/Tracing.js.map +1 -0
- package/dist/providers/AnthropicModels.d.ts +95 -0
- package/dist/providers/AnthropicModels.d.ts.map +1 -0
- package/dist/providers/AnthropicModels.js +83 -0
- package/dist/providers/AnthropicModels.js.map +1 -0
- package/dist/providers/AnthropicProvider.d.ts +13 -0
- package/dist/providers/AnthropicProvider.d.ts.map +1 -0
- package/dist/providers/AnthropicProvider.js +177 -0
- package/dist/providers/AnthropicProvider.js.map +1 -0
- package/dist/providers/AntigravityConnector.d.ts +7 -0
- package/dist/providers/AntigravityConnector.d.ts.map +1 -0
- package/dist/providers/AntigravityConnector.js +52 -0
- package/dist/providers/AntigravityConnector.js.map +1 -0
- package/dist/providers/ClaudeAgentSession.d.ts +11 -0
- package/dist/providers/ClaudeAgentSession.d.ts.map +1 -0
- package/dist/providers/ClaudeAgentSession.js +158 -0
- package/dist/providers/ClaudeAgentSession.js.map +1 -0
- package/dist/providers/ClaudeCliConnector.d.ts +9 -0
- package/dist/providers/ClaudeCliConnector.d.ts.map +1 -0
- package/dist/providers/ClaudeCliConnector.js +123 -0
- package/dist/providers/ClaudeCliConnector.js.map +1 -0
- package/dist/providers/CliSupport.d.ts +21 -0
- package/dist/providers/CliSupport.d.ts.map +1 -0
- package/dist/providers/CliSupport.js +62 -0
- package/dist/providers/CliSupport.js.map +1 -0
- package/dist/providers/CodexConnector.d.ts +12 -0
- package/dist/providers/CodexConnector.d.ts.map +1 -0
- package/dist/providers/CodexConnector.js +175 -0
- package/dist/providers/CodexConnector.js.map +1 -0
- package/dist/providers/ConnectorFactories.d.ts +15 -0
- package/dist/providers/ConnectorFactories.d.ts.map +1 -0
- package/dist/providers/ConnectorFactories.js +74 -0
- package/dist/providers/ConnectorFactories.js.map +1 -0
- package/dist/providers/CopilotConnector.d.ts +6 -0
- package/dist/providers/CopilotConnector.d.ts.map +1 -0
- package/dist/providers/CopilotConnector.js +35 -0
- package/dist/providers/CopilotConnector.js.map +1 -0
- package/dist/providers/CursorConnector.d.ts +8 -0
- package/dist/providers/CursorConnector.d.ts.map +1 -0
- package/dist/providers/CursorConnector.js +82 -0
- package/dist/providers/CursorConnector.js.map +1 -0
- package/dist/providers/GeminiApiProvider.d.ts +9 -0
- package/dist/providers/GeminiApiProvider.d.ts.map +1 -0
- package/dist/providers/GeminiApiProvider.js +181 -0
- package/dist/providers/GeminiApiProvider.js.map +1 -0
- package/dist/providers/GeminiCliProvider.d.ts +106 -0
- package/dist/providers/GeminiCliProvider.d.ts.map +1 -0
- package/dist/providers/GeminiCliProvider.js +596 -0
- package/dist/providers/GeminiCliProvider.js.map +1 -0
- package/dist/providers/GeminiModels.d.ts +92 -0
- package/dist/providers/GeminiModels.d.ts.map +1 -0
- package/dist/providers/GeminiModels.js +80 -0
- package/dist/providers/GeminiModels.js.map +1 -0
- package/dist/providers/GrokCliConnector.d.ts +8 -0
- package/dist/providers/GrokCliConnector.d.ts.map +1 -0
- package/dist/providers/GrokCliConnector.js +82 -0
- package/dist/providers/GrokCliConnector.js.map +1 -0
- package/dist/providers/LmStudioModels.d.ts +114 -0
- package/dist/providers/LmStudioModels.d.ts.map +1 -0
- package/dist/providers/LmStudioModels.js +102 -0
- package/dist/providers/LmStudioModels.js.map +1 -0
- package/dist/providers/LmStudioProvider.d.ts +13 -0
- package/dist/providers/LmStudioProvider.d.ts.map +1 -0
- package/dist/providers/LmStudioProvider.js +158 -0
- package/dist/providers/LmStudioProvider.js.map +1 -0
- package/dist/providers/MockProvider.d.ts +6 -0
- package/dist/providers/MockProvider.d.ts.map +1 -0
- package/dist/providers/MockProvider.js +271 -0
- package/dist/providers/MockProvider.js.map +1 -0
- package/dist/providers/OllamaModels.d.ts +68 -0
- package/dist/providers/OllamaModels.d.ts.map +1 -0
- package/dist/providers/OllamaModels.js +59 -0
- package/dist/providers/OllamaModels.js.map +1 -0
- package/dist/providers/OllamaProvider.d.ts +7 -0
- package/dist/providers/OllamaProvider.d.ts.map +1 -0
- package/dist/providers/OllamaProvider.js +134 -0
- package/dist/providers/OllamaProvider.js.map +1 -0
- package/dist/providers/OpenAIModels.d.ts +139 -0
- package/dist/providers/OpenAIModels.d.ts.map +1 -0
- package/dist/providers/OpenAIModels.js +123 -0
- package/dist/providers/OpenAIModels.js.map +1 -0
- package/dist/providers/OpenAIProvider.d.ts +9 -0
- package/dist/providers/OpenAIProvider.d.ts.map +1 -0
- package/dist/providers/OpenAIProvider.js +185 -0
- package/dist/providers/OpenAIProvider.js.map +1 -0
- package/dist/providers/OpenCodeCliConnector.d.ts +8 -0
- package/dist/providers/OpenCodeCliConnector.d.ts.map +1 -0
- package/dist/providers/OpenCodeCliConnector.js +93 -0
- package/dist/providers/OpenCodeCliConnector.js.map +1 -0
- package/dist/providers/OpenCodeModels.d.ts +61 -0
- package/dist/providers/OpenCodeModels.d.ts.map +1 -0
- package/dist/providers/OpenCodeModels.js +53 -0
- package/dist/providers/OpenCodeModels.js.map +1 -0
- package/dist/providers/OpenCodeProvider.d.ts +10 -0
- package/dist/providers/OpenCodeProvider.d.ts.map +1 -0
- package/dist/providers/OpenCodeProvider.js +154 -0
- package/dist/providers/OpenCodeProvider.js.map +1 -0
- package/dist/providers/PiConnector.d.ts +8 -0
- package/dist/providers/PiConnector.d.ts.map +1 -0
- package/dist/providers/PiConnector.js +99 -0
- package/dist/providers/PiConnector.js.map +1 -0
- package/dist/tools/Tool.d.ts +83 -0
- package/dist/tools/Tool.d.ts.map +1 -0
- package/dist/tools/Tool.js +278 -0
- package/dist/tools/Tool.js.map +1 -0
- package/dist/tools/ToolCallingExecutor.d.ts +18 -0
- package/dist/tools/ToolCallingExecutor.d.ts.map +1 -0
- package/dist/tools/ToolCallingExecutor.js +64 -0
- package/dist/tools/ToolCallingExecutor.js.map +1 -0
- package/dist/tools/ToolRegistry.d.ts +45 -0
- package/dist/tools/ToolRegistry.d.ts.map +1 -0
- package/dist/tools/ToolRegistry.js +133 -0
- package/dist/tools/ToolRegistry.js.map +1 -0
- package/package.json +105 -0
- package/src/.gitkeep +1 -0
- package/src/AgentSession.ts +69 -0
- package/src/Capability.ts +240 -0
- package/src/Connector.ts +220 -0
- package/src/ConnectorConfig.ts +146 -0
- package/src/ConnectorRegistry.ts +184 -0
- package/src/ContextManagement.ts +372 -0
- package/src/Conversation.ts +21 -0
- package/src/Errors.ts +87 -0
- package/src/HttpClient.ts +202 -0
- package/src/LlmService.ts +45 -0
- package/src/Models.ts +250 -0
- package/src/ProcessExecutor.ts +157 -0
- package/src/RateLimiter.ts +238 -0
- package/src/Streaming.ts +164 -0
- package/src/StructuredOutput.ts +138 -0
- package/src/TemporaryFiles.ts +51 -0
- package/src/UsageLimits.ts +242 -0
- package/src/eval/Checks.ts +68 -0
- package/src/eval/Eval.ts +112 -0
- package/src/eval/EvalSuite.ts +63 -0
- package/src/eval/Evaluator.ts +31 -0
- package/src/eval/Judge.ts +92 -0
- package/src/observability/MeteredLlmService.ts +156 -0
- package/src/observability/Metrics.ts +195 -0
- package/src/observability/Redaction.ts +55 -0
- package/src/observability/StreamRecorder.ts +32 -0
- package/src/observability/StructuredLogger.ts +62 -0
- package/src/observability/Tracing.ts +141 -0
- package/src/providers/AnthropicModels.ts +96 -0
- package/src/providers/AnthropicProvider.ts +320 -0
- package/src/providers/AntigravityConnector.ts +77 -0
- package/src/providers/ClaudeAgentSession.ts +226 -0
- package/src/providers/ClaudeCliConnector.ts +201 -0
- package/src/providers/CliSupport.ts +100 -0
- package/src/providers/CodexConnector.ts +251 -0
- package/src/providers/ConnectorFactories.ts +142 -0
- package/src/providers/CopilotConnector.ts +47 -0
- package/src/providers/CursorConnector.ts +120 -0
- package/src/providers/GeminiApiProvider.ts +337 -0
- package/src/providers/GeminiCliProvider.ts +793 -0
- package/src/providers/GeminiModels.ts +97 -0
- package/src/providers/GrokCliConnector.ts +117 -0
- package/src/providers/LmStudioModels.ts +117 -0
- package/src/providers/LmStudioProvider.ts +260 -0
- package/src/providers/MockProvider.ts +322 -0
- package/src/providers/OllamaModels.ts +64 -0
- package/src/providers/OllamaProvider.ts +236 -0
- package/src/providers/OpenAIModels.ts +152 -0
- package/src/providers/OpenAIProvider.ts +331 -0
- package/src/providers/OpenCodeCliConnector.ts +135 -0
- package/src/providers/OpenCodeModels.ts +60 -0
- package/src/providers/OpenCodeProvider.ts +278 -0
- package/src/providers/PiConnector.ts +142 -0
- package/src/tools/Tool.ts +379 -0
- package/src/tools/ToolCallingExecutor.ts +106 -0
- 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
|
+
}
|