@lota-sdk/core 0.4.8 → 0.4.9

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 (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/thread-defaults.ts +33 -21
  9. package/src/create-runtime.ts +725 -387
  10. package/src/db/base.service.ts +52 -28
  11. package/src/db/cursor-pagination.ts +71 -30
  12. package/src/db/memory-store.helpers.ts +4 -7
  13. package/src/db/memory-store.ts +856 -598
  14. package/src/db/memory.ts +398 -275
  15. package/src/db/record-id.ts +32 -10
  16. package/src/db/schema-fingerprint.ts +30 -12
  17. package/src/db/service-normalization.ts +255 -0
  18. package/src/db/service.ts +726 -761
  19. package/src/db/startup.ts +140 -66
  20. package/src/db/transaction-conflict.ts +15 -0
  21. package/src/effect/awaitable-effect.ts +87 -0
  22. package/src/effect/errors.ts +121 -0
  23. package/src/effect/helpers.ts +98 -0
  24. package/src/effect/index.ts +22 -0
  25. package/src/effect/layers.ts +228 -0
  26. package/src/effect/runtime-ref.ts +25 -0
  27. package/src/effect/runtime.ts +31 -0
  28. package/src/effect/services.ts +57 -0
  29. package/src/effect/zod.ts +43 -0
  30. package/src/embeddings/provider.ts +122 -76
  31. package/src/index.ts +46 -1
  32. package/src/openrouter/direct-provider.ts +11 -35
  33. package/src/queues/autonomous-job.queue.ts +130 -74
  34. package/src/queues/context-compaction.queue.ts +60 -15
  35. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  36. package/src/queues/document-processor.queue.ts +52 -77
  37. package/src/queues/memory-consolidation.queue.ts +47 -32
  38. package/src/queues/organization-learning.queue.ts +13 -4
  39. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  40. package/src/queues/plan-scheduler.queue.ts +107 -31
  41. package/src/queues/post-chat-memory.queue.ts +66 -24
  42. package/src/queues/queue-factory.ts +142 -52
  43. package/src/queues/standalone-worker.ts +39 -0
  44. package/src/queues/title-generation.queue.ts +54 -9
  45. package/src/redis/connection.ts +84 -32
  46. package/src/redis/index.ts +6 -8
  47. package/src/redis/org-memory-lock.ts +60 -27
  48. package/src/redis/redis-lease-lock.ts +200 -121
  49. package/src/redis/runtime-connection.ts +10 -0
  50. package/src/redis/stream-context.ts +84 -46
  51. package/src/runtime/agent-identity-overrides.ts +2 -2
  52. package/src/runtime/agent-runtime-policy.ts +4 -1
  53. package/src/runtime/agent-stream-helpers.ts +20 -9
  54. package/src/runtime/chat-run-orchestration.ts +102 -19
  55. package/src/runtime/chat-run-registry.ts +36 -2
  56. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  57. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  58. package/src/runtime/execution-plan-visibility.ts +2 -2
  59. package/src/runtime/execution-plan.ts +42 -15
  60. package/src/runtime/graph-designer.ts +11 -7
  61. package/src/runtime/helper-model.ts +135 -48
  62. package/src/runtime/index.ts +7 -7
  63. package/src/runtime/indexed-repositories-policy.ts +3 -3
  64. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  65. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  66. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  67. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  68. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  69. package/src/runtime/plugin-resolution.ts +144 -24
  70. package/src/runtime/plugin-types.ts +9 -1
  71. package/src/runtime/post-turn-side-effects.ts +197 -130
  72. package/src/runtime/retrieval-adapters.ts +38 -4
  73. package/src/runtime/runtime-config.ts +150 -61
  74. package/src/runtime/runtime-extensions.ts +21 -34
  75. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  76. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  77. package/src/runtime/social-chat/social-chat.ts +594 -0
  78. package/src/runtime/specialist-runner.ts +36 -10
  79. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  80. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  81. package/src/runtime/thread-chat-helpers.ts +2 -2
  82. package/src/runtime/thread-plan-turn.ts +2 -1
  83. package/src/runtime/thread-turn-context.ts +172 -94
  84. package/src/runtime/turn-lifecycle.ts +93 -27
  85. package/src/services/agent-activity.service.ts +287 -203
  86. package/src/services/agent-executor.service.ts +329 -217
  87. package/src/services/artifact.service.ts +225 -148
  88. package/src/services/attachment.service.ts +137 -115
  89. package/src/services/autonomous-job.service.ts +888 -491
  90. package/src/services/chat-run-registry.service.ts +11 -1
  91. package/src/services/context-compaction.service.ts +136 -86
  92. package/src/services/document-chunk.service.ts +162 -90
  93. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  94. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  95. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  96. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  97. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  98. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  99. package/src/services/feedback-loop.service.ts +132 -76
  100. package/src/services/global-orchestrator.service.ts +80 -170
  101. package/src/services/graph-full-routing.ts +182 -0
  102. package/src/services/index.ts +18 -21
  103. package/src/services/institutional-memory.service.ts +220 -123
  104. package/src/services/learned-skill.service.ts +364 -259
  105. package/src/services/memory/memory-conversation.ts +95 -0
  106. package/src/services/memory/memory-org-memory.ts +39 -0
  107. package/src/services/memory/memory-preseeded.ts +80 -0
  108. package/src/services/memory/memory-rerank.ts +297 -0
  109. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  110. package/src/services/memory/memory.service.ts +692 -0
  111. package/src/services/memory/rerank.service.ts +209 -0
  112. package/src/services/monitoring-window.service.ts +92 -70
  113. package/src/services/mutating-approval.service.ts +62 -53
  114. package/src/services/node-workspace.service.ts +141 -98
  115. package/src/services/notification.service.ts +17 -16
  116. package/src/services/organization-member.service.ts +120 -66
  117. package/src/services/organization.service.ts +144 -51
  118. package/src/services/ownership-dispatcher.service.ts +415 -264
  119. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  120. package/src/services/plan/plan-agent-query.service.ts +322 -0
  121. package/src/services/plan/plan-approval.service.ts +102 -0
  122. package/src/services/plan/plan-artifact.service.ts +60 -0
  123. package/src/services/plan/plan-builder.service.ts +76 -0
  124. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  125. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  126. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  127. package/src/services/plan/plan-coordination.service.ts +181 -0
  128. package/src/services/plan/plan-cycle.service.ts +398 -0
  129. package/src/services/plan/plan-deadline.service.ts +547 -0
  130. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  131. package/src/services/plan/plan-executor-context.ts +35 -0
  132. package/src/services/plan/plan-executor-graph.ts +475 -0
  133. package/src/services/plan/plan-executor-helpers.ts +322 -0
  134. package/src/services/plan/plan-executor-persistence.ts +209 -0
  135. package/src/services/plan/plan-executor.service.ts +1654 -0
  136. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  137. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  138. package/src/services/plan/plan-run-serialization.ts +15 -0
  139. package/src/services/plan/plan-run.service.ts +644 -0
  140. package/src/services/plan/plan-scheduler.service.ts +385 -0
  141. package/src/services/plan/plan-template.service.ts +224 -0
  142. package/src/services/plan/plan-transaction-events.ts +33 -0
  143. package/src/services/plan/plan-validator.service.ts +907 -0
  144. package/src/services/plan/plan-workspace.service.ts +125 -0
  145. package/src/services/plugin-executor.service.ts +97 -68
  146. package/src/services/quality-metrics.service.ts +112 -94
  147. package/src/services/queue-job.service.ts +296 -230
  148. package/src/services/recent-activity-title.service.ts +65 -36
  149. package/src/services/recent-activity.service.ts +274 -259
  150. package/src/services/skill-resolver.service.ts +38 -12
  151. package/src/services/social-chat-history.service.ts +176 -125
  152. package/src/services/system-executor.service.ts +91 -61
  153. package/src/services/thread/thread-active-run.ts +203 -0
  154. package/src/services/thread/thread-bootstrap.ts +369 -0
  155. package/src/services/thread/thread-listing.ts +198 -0
  156. package/src/services/thread/thread-memory-block.ts +117 -0
  157. package/src/services/thread/thread-message.service.ts +363 -0
  158. package/src/services/thread/thread-record-store.ts +155 -0
  159. package/src/services/thread/thread-title.service.ts +74 -0
  160. package/src/services/thread/thread-turn-execution.ts +280 -0
  161. package/src/services/thread/thread-turn-message-context.ts +73 -0
  162. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  163. package/src/services/thread/thread-turn-streaming.ts +402 -0
  164. package/src/services/thread/thread-turn-tracing.ts +35 -0
  165. package/src/services/thread/thread-turn.ts +343 -0
  166. package/src/services/thread/thread.service.ts +335 -0
  167. package/src/services/user.service.ts +82 -32
  168. package/src/services/write-intent-validator.service.ts +63 -51
  169. package/src/storage/attachment-parser.ts +69 -27
  170. package/src/storage/attachment-storage.service.ts +331 -275
  171. package/src/storage/generated-document-storage.service.ts +66 -34
  172. package/src/system-agents/agent-result.ts +3 -1
  173. package/src/system-agents/context-compaction.agent.ts +2 -2
  174. package/src/system-agents/delegated-agent-factory.ts +159 -90
  175. package/src/system-agents/memory-reranker.agent.ts +2 -2
  176. package/src/system-agents/memory.agent.ts +2 -2
  177. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  178. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  179. package/src/system-agents/skill-extractor.agent.ts +2 -2
  180. package/src/system-agents/skill-manager.agent.ts +2 -2
  181. package/src/system-agents/thread-router.agent.ts +157 -113
  182. package/src/system-agents/title-generator.agent.ts +2 -2
  183. package/src/tools/execution-plan.tool.ts +220 -161
  184. package/src/tools/fetch-webpage.tool.ts +21 -17
  185. package/src/tools/firecrawl-client.ts +16 -6
  186. package/src/tools/index.ts +1 -0
  187. package/src/tools/memory-block.tool.ts +14 -6
  188. package/src/tools/plan-approval.tool.ts +49 -47
  189. package/src/tools/read-file-parts.tool.ts +44 -33
  190. package/src/tools/remember-memory.tool.ts +65 -45
  191. package/src/tools/search-web.tool.ts +26 -22
  192. package/src/tools/search.tool.ts +41 -29
  193. package/src/tools/team-think.tool.ts +124 -83
  194. package/src/tools/user-questions.tool.ts +4 -3
  195. package/src/tools/web-tool-shared.ts +6 -0
  196. package/src/utils/async.ts +17 -23
  197. package/src/utils/crypto.ts +21 -0
  198. package/src/utils/date-time.ts +40 -1
  199. package/src/utils/errors.ts +95 -16
  200. package/src/utils/hono-error-handler.ts +24 -39
  201. package/src/utils/index.ts +2 -1
  202. package/src/utils/null-proto-record.ts +41 -0
  203. package/src/utils/sse-keepalive.ts +124 -21
  204. package/src/workers/bootstrap.ts +186 -51
  205. package/src/workers/memory-consolidation.worker.ts +325 -237
  206. package/src/workers/organization-learning.worker.ts +50 -16
  207. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  208. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  209. package/src/workers/skill-extraction.runner.ts +176 -93
  210. package/src/workers/utils/file-section-chunker.ts +8 -10
  211. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  212. package/src/workers/utils/repomix-file-sections.ts +2 -2
  213. package/src/workers/utils/thread-message-query.ts +97 -38
  214. package/src/workers/worker-utils.ts +56 -31
  215. package/src/config/debug-logger.ts +0 -47
  216. package/src/redis/connection-accessor.ts +0 -26
  217. package/src/runtime/context-compaction-runtime.ts +0 -87
  218. package/src/runtime/social-chat-agent-runner.ts +0 -118
  219. package/src/runtime/social-chat.ts +0 -516
  220. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  221. package/src/services/adaptive-playbook.service.ts +0 -152
  222. package/src/services/artifact-provenance.service.ts +0 -172
  223. package/src/services/chat-attachments.service.ts +0 -17
  224. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  225. package/src/services/execution-plan.service.ts +0 -1118
  226. package/src/services/memory.service.ts +0 -914
  227. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  228. package/src/services/plan-agent-query.service.ts +0 -267
  229. package/src/services/plan-approval.service.ts +0 -83
  230. package/src/services/plan-artifact.service.ts +0 -50
  231. package/src/services/plan-builder.service.ts +0 -67
  232. package/src/services/plan-checkpoint.service.ts +0 -81
  233. package/src/services/plan-completion-side-effects.ts +0 -80
  234. package/src/services/plan-coordination.service.ts +0 -157
  235. package/src/services/plan-cycle.service.ts +0 -284
  236. package/src/services/plan-deadline.service.ts +0 -430
  237. package/src/services/plan-event-delivery.service.ts +0 -166
  238. package/src/services/plan-executor.service.ts +0 -1950
  239. package/src/services/plan-run.service.ts +0 -515
  240. package/src/services/plan-scheduler.service.ts +0 -240
  241. package/src/services/plan-template.service.ts +0 -177
  242. package/src/services/plan-validator.service.ts +0 -818
  243. package/src/services/plan-workspace.service.ts +0 -83
  244. package/src/services/rerank.service.ts +0 -156
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -0,0 +1,228 @@
1
+ import type { ToolSet } from 'ai'
2
+ import { Effect, Layer, Logger, References } from 'effect'
3
+
4
+ import type { CoreThreadProfile } from '../config/agent-defaults'
5
+ import { resolveAgentConfig, resolveAgentFactoryConfig } from '../config/agent-defaults'
6
+ import type { AgentFactory, AgentRuntimeConfigProvider, AgentToolBuilder } from '../config/agent-types'
7
+ import { configureLotaLogger, getLotaLoggers, toEffectLogLevel } from '../config/logger'
8
+ import type { LotaLogLevel } from '../config/logger'
9
+ import { resolveThreadConfig } from '../config/thread-defaults'
10
+ import type { LotaThreadConfig } from '../config/thread-defaults'
11
+ import { LOTA_SDK_DATABASE_NAME } from '../db/sdk-database'
12
+ import type { SurrealDatabaseConfig } from '../db/service'
13
+ import { SurrealDBService } from '../db/service'
14
+ import type { CreateRedisConnectionManagerOptions } from '../redis/connection'
15
+ import { createRedisConnectionManager } from '../redis/connection'
16
+ import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
17
+ import type { LotaRuntimeAdapters, LotaRuntimeTurnHooks } from '../runtime/runtime-extensions'
18
+ import type { LotaRuntimeWorkerExtensions } from '../runtime/runtime-worker-registry'
19
+ import { QueueJobServiceLive } from '../services/queue-job.service'
20
+ import { getErrorMessage } from '../utils/errors'
21
+ import { DatabaseError, RedisError } from './errors'
22
+ import { effectTryServicePromise } from './helpers'
23
+ import {
24
+ AppLoggerTag,
25
+ AgentConfigServiceTag,
26
+ AgentFactoryServiceTag,
27
+ DatabaseServiceTag,
28
+ RedisServiceTag,
29
+ RuntimeAdaptersServiceTag,
30
+ RuntimeConfigServiceTag,
31
+ RuntimeWorkerExtensionsServiceTag,
32
+ ThreadConfigServiceTag,
33
+ ToolProvidersServiceTag,
34
+ TurnHooksServiceTag,
35
+ } from './services'
36
+
37
+ const EMPTY_TOOLS = {} as ToolSet
38
+ const EMPTY_WORKERS: LotaRuntimeWorkerExtensions = {}
39
+
40
+ export function DatabaseLive(config: SurrealDatabaseConfig) {
41
+ return Layer.effect(
42
+ DatabaseServiceTag,
43
+ Effect.acquireRelease(
44
+ Effect.try({
45
+ try: () => new SurrealDBService(config),
46
+ catch: (error) =>
47
+ new DatabaseError({ message: `Failed to create database service: ${getErrorMessage(error)}`, cause: error }),
48
+ }),
49
+ (databaseService) =>
50
+ Effect.ignore(
51
+ databaseService
52
+ .disconnect()
53
+ .pipe(
54
+ Effect.tapError((error) =>
55
+ Effect.logWarning(`Failed to clean up database service: ${getErrorMessage(error)}`),
56
+ ),
57
+ ),
58
+ ),
59
+ ),
60
+ )
61
+ }
62
+
63
+ export function RedisLive(config: CreateRedisConnectionManagerOptions) {
64
+ return Layer.effect(
65
+ RedisServiceTag,
66
+ Effect.acquireRelease(
67
+ Effect.try({
68
+ try: () => createRedisConnectionManager(config),
69
+ catch: (error) =>
70
+ new RedisError({
71
+ message: `Failed to create Redis connection manager: ${getErrorMessage(error)}`,
72
+ cause: error,
73
+ }),
74
+ }),
75
+ (redisManager) =>
76
+ Effect.ignore(
77
+ effectTryServicePromise(
78
+ () => redisManager.closeConnection(),
79
+ 'Failed to close Redis connection manager',
80
+ ).pipe(
81
+ Effect.tapError((error) =>
82
+ Effect.logWarning(`Failed to clean up Redis connection manager: ${getErrorMessage(error)}`),
83
+ ),
84
+ ),
85
+ ),
86
+ ),
87
+ )
88
+ }
89
+
90
+ export function AppLoggerLive(level: LotaLogLevel = 'info') {
91
+ return Layer.mergeAll(
92
+ Layer.effectDiscard(Effect.sync(() => configureLotaLogger(level))),
93
+ Layer.succeed(AppLoggerTag, getLotaLoggers()),
94
+ Logger.layer([Logger.consolePretty()]),
95
+ Layer.succeed(References.MinimumLogLevel, toEffectLogLevel(level)),
96
+ )
97
+ }
98
+
99
+ export function RuntimeConfigLive(config: ResolvedLotaRuntimeConfig) {
100
+ return Layer.succeed(RuntimeConfigServiceTag, config)
101
+ }
102
+
103
+ export function AgentConfigLive(config: {
104
+ roster: readonly string[]
105
+ leadAgentId: string
106
+ displayNames: Record<string, string>
107
+ shortDisplayNames?: Record<string, string>
108
+ descriptions?: Record<string, string>
109
+ routerModelId?: string
110
+ teamConsultParticipants: readonly string[]
111
+ getCoreThreadProfile?: (coreType: string) => CoreThreadProfile
112
+ }) {
113
+ return Layer.succeed(AgentConfigServiceTag, resolveAgentConfig(config))
114
+ }
115
+
116
+ export function AgentFactoryLive(config: {
117
+ createAgent?: AgentFactory
118
+ buildAgentTools?: AgentToolBuilder
119
+ getAgentRuntimeConfig?: AgentRuntimeConfigProvider
120
+ pluginRuntime?: Record<string, unknown>
121
+ }) {
122
+ return Layer.succeed(AgentFactoryServiceTag, resolveAgentFactoryConfig(config))
123
+ }
124
+
125
+ export function ThreadConfigLive(params: { agentRoster: readonly string[]; config?: LotaThreadConfig }) {
126
+ return Layer.succeed(ThreadConfigServiceTag, resolveThreadConfig(params))
127
+ }
128
+
129
+ export function RuntimeExtensionsLive(params: {
130
+ adapters?: LotaRuntimeAdapters
131
+ turnHooks?: LotaRuntimeTurnHooks
132
+ toolProviders?: ToolSet
133
+ extraWorkers?: LotaRuntimeWorkerExtensions
134
+ }) {
135
+ return Layer.mergeAll(
136
+ Layer.succeed(RuntimeAdaptersServiceTag, params.adapters ?? {}),
137
+ Layer.succeed(TurnHooksServiceTag, params.turnHooks ?? {}),
138
+ Layer.succeed(ToolProvidersServiceTag, params.toolProviders ?? EMPTY_TOOLS),
139
+ Layer.succeed(RuntimeWorkerExtensionsServiceTag, params.extraWorkers ?? EMPTY_WORKERS),
140
+ )
141
+ }
142
+
143
+ // ── Shared Infrastructure Layer Builders ─────────────────────────────
144
+
145
+ /**
146
+ * Builds the full infrastructure layer used by `createLotaRuntime`.
147
+ * Contains all config, database, redis, agent, thread, and extension layers.
148
+ */
149
+ export function buildInfrastructureLayer(
150
+ runtimeConfig: ResolvedLotaRuntimeConfig,
151
+ options: { resolvedAgentDisplayNames: Record<string, string>; observabilityLayer: Layer.Layer<never> },
152
+ ) {
153
+ return Layer.mergeAll(
154
+ AppLoggerLive(runtimeConfig.logging.level),
155
+ RuntimeConfigLive(runtimeConfig),
156
+ AgentConfigLive({
157
+ roster: runtimeConfig.agents.roster,
158
+ leadAgentId: runtimeConfig.agents.leadAgentId,
159
+ displayNames: options.resolvedAgentDisplayNames,
160
+ shortDisplayNames: runtimeConfig.agents.shortDisplayNames,
161
+ descriptions: runtimeConfig.agents.descriptions,
162
+ routerModelId: runtimeConfig.agents.routerModelId,
163
+ teamConsultParticipants: runtimeConfig.agents.teamConsultParticipants,
164
+ getCoreThreadProfile: runtimeConfig.agents.getCoreThreadProfile,
165
+ }),
166
+ AgentFactoryLive({
167
+ createAgent: runtimeConfig.agents.createAgent,
168
+ buildAgentTools: runtimeConfig.agents.buildAgentTools,
169
+ getAgentRuntimeConfig: runtimeConfig.agents.getAgentRuntimeConfig,
170
+ pluginRuntime: runtimeConfig.pluginRuntime,
171
+ }),
172
+ ThreadConfigLive({ agentRoster: runtimeConfig.agents.roster, config: runtimeConfig.threads }),
173
+ RuntimeExtensionsLive({
174
+ adapters: runtimeConfig.runtimeAdapters,
175
+ turnHooks: runtimeConfig.turnHooks,
176
+ toolProviders: runtimeConfig.toolProviders ?? {},
177
+ extraWorkers: runtimeConfig.extraWorkers,
178
+ }),
179
+ DatabaseLive({
180
+ url: runtimeConfig.database.url,
181
+ namespace: runtimeConfig.database.namespace,
182
+ database: LOTA_SDK_DATABASE_NAME,
183
+ username: runtimeConfig.database.username,
184
+ password: runtimeConfig.database.password,
185
+ }),
186
+ RedisLive({ url: runtimeConfig.redis.url }),
187
+ options.observabilityLayer,
188
+ )
189
+ }
190
+
191
+ /**
192
+ * Builds the minimal infrastructure layer used by sandboxed workers.
193
+ * Omits observability and uses empty runtime extensions.
194
+ */
195
+ export function buildWorkerInfrastructureLayer(runtimeConfig: ResolvedLotaRuntimeConfig) {
196
+ const infrastructureLayer = Layer.mergeAll(
197
+ AppLoggerLive(runtimeConfig.logging.level),
198
+ RuntimeConfigLive(runtimeConfig),
199
+ AgentConfigLive({
200
+ roster: runtimeConfig.agents.roster,
201
+ leadAgentId: runtimeConfig.agents.leadAgentId,
202
+ displayNames: runtimeConfig.agents.displayNames,
203
+ shortDisplayNames: runtimeConfig.agents.shortDisplayNames,
204
+ descriptions: runtimeConfig.agents.descriptions,
205
+ routerModelId: runtimeConfig.agents.routerModelId,
206
+ teamConsultParticipants: runtimeConfig.agents.teamConsultParticipants,
207
+ getCoreThreadProfile: runtimeConfig.agents.getCoreThreadProfile,
208
+ }),
209
+ AgentFactoryLive({
210
+ createAgent: runtimeConfig.agents.createAgent,
211
+ buildAgentTools: runtimeConfig.agents.buildAgentTools,
212
+ getAgentRuntimeConfig: runtimeConfig.agents.getAgentRuntimeConfig,
213
+ pluginRuntime: runtimeConfig.pluginRuntime,
214
+ }),
215
+ ThreadConfigLive({ agentRoster: runtimeConfig.agents.roster, config: runtimeConfig.threads }),
216
+ RuntimeExtensionsLive({}),
217
+ DatabaseLive({
218
+ url: runtimeConfig.database.url,
219
+ namespace: runtimeConfig.database.namespace,
220
+ database: LOTA_SDK_DATABASE_NAME,
221
+ username: runtimeConfig.database.username,
222
+ password: runtimeConfig.database.password,
223
+ }),
224
+ RedisLive({ url: runtimeConfig.redis.url }),
225
+ )
226
+
227
+ return QueueJobServiceLive.pipe(Layer.provideMerge(infrastructureLayer))
228
+ }
@@ -0,0 +1,25 @@
1
+ import type { ManagedRuntime } from 'effect'
2
+
3
+ // eslint-disable-next-line typescript-eslint/no-explicit-any -- ManagedRuntime is contravariant in R; `any` is the only valid wildcard
4
+ type SdkManagedRuntime = ManagedRuntime.ManagedRuntime<any, any>
5
+
6
+ let currentRuntime: SdkManagedRuntime | null = null
7
+
8
+ export function setCurrentRuntime<R, E>(runtime: ManagedRuntime.ManagedRuntime<R, E>): void {
9
+ currentRuntime = runtime as SdkManagedRuntime
10
+ }
11
+
12
+ export function clearCurrentRuntime(): void {
13
+ currentRuntime = null
14
+ }
15
+
16
+ export function getCurrentRuntime(): SdkManagedRuntime {
17
+ if (!currentRuntime) {
18
+ throw new Error('Lota Effect runtime is not configured. Call createLotaRuntime() first.')
19
+ }
20
+ return currentRuntime
21
+ }
22
+
23
+ export function getOptionalCurrentRuntime(): SdkManagedRuntime | null {
24
+ return currentRuntime
25
+ }
@@ -0,0 +1,31 @@
1
+ import type { Effect, ManagedRuntime } from 'effect'
2
+
3
+ import { clearCurrentRuntime, getCurrentRuntime, getOptionalCurrentRuntime, setCurrentRuntime } from './runtime-ref'
4
+
5
+ export function setLotaSdkRuntime<R, E>(runtime: ManagedRuntime.ManagedRuntime<R, E>): void {
6
+ setCurrentRuntime(runtime)
7
+ }
8
+
9
+ export function clearLotaSdkRuntime(): void {
10
+ clearCurrentRuntime()
11
+ }
12
+
13
+ export function getOptionalLotaSdkRuntime() {
14
+ return getOptionalCurrentRuntime()
15
+ }
16
+
17
+ /** Returns the current managed runtime or throws. */
18
+ export function getLotaSdkRuntime() {
19
+ return getCurrentRuntime()
20
+ }
21
+
22
+ /**
23
+ * Run an effect as a Promise using the current managed runtime's context.
24
+ * The runtime must be initialized via `createLotaRuntime()` before any call.
25
+ */
26
+ export function runPromise<A, E>(
27
+ effect: Effect.Effect<A, E, never>,
28
+ options?: { readonly signal?: AbortSignal },
29
+ ): Promise<A> {
30
+ return getCurrentRuntime().runPromise(effect, options)
31
+ }
@@ -0,0 +1,57 @@
1
+ import type { ToolSet } from 'ai'
2
+ import { Context } from 'effect'
3
+
4
+ import type { ResolvedAgentConfig } from '../config/agent-defaults'
5
+ import type { AgentFactory, AgentRuntimeConfigProvider, AgentToolBuilder } from '../config/agent-types'
6
+ import type { LotaLoggerSet } from '../config/logger'
7
+ import type { ResolvedThreadBootstrapConfig } from '../config/thread-defaults'
8
+ import type { SurrealDBService } from '../db/service'
9
+ import type { RedisConnectionManager } from '../redis/connection'
10
+ import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
11
+ import type { LotaRuntimeAdapters, LotaRuntimeTurnHooks } from '../runtime/runtime-extensions'
12
+ import type { LotaRuntimeWorkerExtensions } from '../runtime/runtime-worker-registry'
13
+
14
+ export class DatabaseServiceTag extends Context.Service<DatabaseServiceTag, SurrealDBService>()('DatabaseService') {}
15
+
16
+ export class RedisServiceTag extends Context.Service<RedisServiceTag, RedisConnectionManager>()('RedisService') {}
17
+
18
+ export class AppLoggerTag extends Context.Service<AppLoggerTag, LotaLoggerSet>()('AppLogger') {}
19
+
20
+ export class RuntimeConfigServiceTag extends Context.Service<RuntimeConfigServiceTag, ResolvedLotaRuntimeConfig>()(
21
+ 'RuntimeConfigService',
22
+ ) {}
23
+
24
+ export class AgentConfigServiceTag extends Context.Service<AgentConfigServiceTag, ResolvedAgentConfig>()(
25
+ 'AgentConfigService',
26
+ ) {}
27
+
28
+ export class AgentFactoryServiceTag extends Context.Service<
29
+ AgentFactoryServiceTag,
30
+ {
31
+ readonly createAgent: AgentFactory
32
+ readonly buildAgentTools: AgentToolBuilder
33
+ readonly getAgentRuntimeConfig: AgentRuntimeConfigProvider
34
+ readonly pluginRuntime?: Record<string, unknown>
35
+ }
36
+ >()('AgentFactoryService') {}
37
+
38
+ export class ThreadConfigServiceTag extends Context.Service<ThreadConfigServiceTag, ResolvedThreadBootstrapConfig>()(
39
+ 'ThreadConfigService',
40
+ ) {}
41
+
42
+ export class RuntimeAdaptersServiceTag extends Context.Service<RuntimeAdaptersServiceTag, LotaRuntimeAdapters>()(
43
+ 'RuntimeAdaptersService',
44
+ ) {}
45
+
46
+ export class TurnHooksServiceTag extends Context.Service<TurnHooksServiceTag, LotaRuntimeTurnHooks>()(
47
+ 'TurnHooksService',
48
+ ) {}
49
+
50
+ export class ToolProvidersServiceTag extends Context.Service<ToolProvidersServiceTag, ToolSet>()(
51
+ 'ToolProvidersService',
52
+ ) {}
53
+
54
+ export class RuntimeWorkerExtensionsServiceTag extends Context.Service<
55
+ RuntimeWorkerExtensionsServiceTag,
56
+ LotaRuntimeWorkerExtensions
57
+ >()('RuntimeWorkerExtensionsService') {}
@@ -0,0 +1,43 @@
1
+ import { Effect } from 'effect'
2
+ import { ZodError } from 'zod'
3
+ import type { z } from 'zod'
4
+
5
+ import { ValidationError } from './errors'
6
+ import type { ValidationIssue } from './errors'
7
+
8
+ type ZodIssueLike = { readonly path: ReadonlyArray<PropertyKey>; readonly message: string }
9
+
10
+ function toValidationIssuePath(path: ReadonlyArray<PropertyKey>): string {
11
+ return path.map((segment) => String(segment)).join('.') || 'root'
12
+ }
13
+
14
+ function getZodIssues(error: Pick<ZodError, 'issues'> | ReadonlyArray<ZodIssueLike>): ReadonlyArray<ZodIssueLike> {
15
+ return 'issues' in error ? error.issues : error
16
+ }
17
+
18
+ export function toValidationIssues(
19
+ error: Pick<ZodError, 'issues'> | ReadonlyArray<ZodIssueLike>,
20
+ ): ReadonlyArray<ValidationIssue> {
21
+ return getZodIssues(error).map((issue) => ({ path: toValidationIssuePath(issue.path), message: issue.message }))
22
+ }
23
+
24
+ export function summarizeZodIssues(issues: ReadonlyArray<ZodIssueLike>, maxIssues = 5): string {
25
+ return issues
26
+ .slice(0, maxIssues)
27
+ .map((issue) => `${toValidationIssuePath(issue.path)}: ${issue.message}`)
28
+ .join('; ')
29
+ }
30
+
31
+ export function toValidationError(error: unknown, message?: string): ValidationError {
32
+ return new ValidationError({
33
+ message: message ?? (error instanceof ZodError ? 'Schema validation failed' : 'Validation failed'),
34
+ issues: error instanceof ZodError ? toValidationIssues(error) : undefined,
35
+ })
36
+ }
37
+
38
+ export function zodParse<TSchema extends z.ZodTypeAny>(
39
+ schema: TSchema,
40
+ value: unknown,
41
+ ): Effect.Effect<z.infer<TSchema>, ValidationError> {
42
+ return Effect.try({ try: () => schema.parse(value), catch: toValidationError })
43
+ }
@@ -1,12 +1,9 @@
1
1
  import { embed, embedMany } from 'ai'
2
+ import { Schema, Effect } from 'effect'
2
3
 
3
- import { getEmbeddingCache } from '../ai/embedding-cache'
4
- import {
5
- getDirectOpenRouterProvider,
6
- normalizeDirectOpenRouterModelId,
7
- resetDirectOpenRouterProviderForTests,
8
- } from '../openrouter/direct-provider'
9
- import { getRuntimeConfig } from '../runtime/runtime-config'
4
+ import { ConfigurationError } from '../effect/errors'
5
+ import { getOptionalCurrentRuntime } from '../effect/runtime-ref'
6
+ import { getDirectOpenRouterProvider, normalizeDirectOpenRouterModelId } from '../openrouter/direct-provider'
10
7
 
11
8
  const SUPPORTED_EMBEDDING_PREFIXES = ['openai/', 'openrouter/'] as const
12
9
 
@@ -19,85 +16,129 @@ type ProviderEmbeddingsOptions = {
19
16
  embedFn?: typeof embed
20
17
  embedManyFn?: typeof embedMany
21
18
  getCache?: () => SharedEmbeddingCache | null
22
- modelId?: string
19
+ modelId: string
20
+ openRouterApiKey?: string
23
21
  }
24
22
 
25
- function resolveEmbeddingModel(modelId: string) {
23
+ function resolveEmbeddingModel(modelId: string, openRouterApiKey?: string) {
26
24
  const normalized = modelId.trim()
27
25
  if (!normalized) {
28
- throw new Error('[embeddings-provider] Model id is required.')
26
+ throw new ConfigurationError({ message: '[embeddings-provider] Model id is required.', key: 'embeddingModelId' })
29
27
  }
30
28
 
31
29
  if (!SUPPORTED_EMBEDDING_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
32
- throw new Error(
33
- `[embeddings-provider] Unsupported model id "${modelId}". Use one of: ${SUPPORTED_EMBEDDING_PREFIXES.join(', ')}*.`,
34
- )
30
+ throw new ConfigurationError({
31
+ message: `[embeddings-provider] Unsupported model id "${modelId}". Use one of: ${SUPPORTED_EMBEDDING_PREFIXES.join(', ')}*.`,
32
+ key: 'embeddingModelId',
33
+ })
35
34
  }
36
35
 
37
- return getDirectOpenRouterProvider().embeddingModel(normalizeDirectOpenRouterModelId(normalized))
36
+ return getDirectOpenRouterProvider(openRouterApiKey).embeddingModel(normalizeDirectOpenRouterModelId(normalized))
38
37
  }
39
38
 
40
39
  function normalizeEmbedding(embedding: readonly number[]): number[] {
41
40
  return embedding.map((value) => Number(value))
42
41
  }
43
42
 
43
+ class EmbeddingProviderError extends Schema.TaggedErrorClass<EmbeddingProviderError>()('EmbeddingProviderError', {
44
+ message: Schema.String,
45
+ cause: Schema.Defect,
46
+ }) {}
47
+
48
+ function tryEmbeddingPromise<A>(
49
+ message: string,
50
+ thunk: () => PromiseLike<A>,
51
+ ): Effect.Effect<A, EmbeddingProviderError> {
52
+ return Effect.tryPromise({
53
+ try: () => Promise.resolve(thunk()),
54
+ catch: (cause) => new EmbeddingProviderError({ message, cause }),
55
+ })
56
+ }
57
+
44
58
  export class ProviderEmbeddings {
45
59
  private readonly embedFn: typeof embed
46
60
  private readonly embedManyFn: typeof embedMany
47
61
  private readonly getCache: () => SharedEmbeddingCache | null
48
- private readonly configuredModelId?: string
49
- private resolvedModelId: string | null = null
62
+ private readonly resolvedModelId: string
50
63
  private _model: ReturnType<typeof resolveEmbeddingModel> | null = null
64
+ /** In-flight dedup: concurrent embedQuery calls for the same text share one API round-trip. */
65
+ private readonly inflightEmbeddings = new Map<string, Promise<number[]>>()
66
+
67
+ private readonly openRouterApiKey: string | undefined
51
68
 
52
- constructor(options: ProviderEmbeddingsOptions = {}) {
69
+ constructor(options: ProviderEmbeddingsOptions) {
53
70
  this.embedFn = options.embedFn ?? embed
54
71
  this.embedManyFn = options.embedManyFn ?? embedMany
55
- this.getCache = options.getCache ?? getEmbeddingCache
56
- this.configuredModelId = options.modelId
72
+ this.getCache = options.getCache ?? (() => null)
73
+ this.resolvedModelId = options.modelId
74
+ this.openRouterApiKey = options.openRouterApiKey
57
75
  }
58
76
 
59
77
  private getModelId(): string {
60
- if (!this.resolvedModelId) {
61
- this.resolvedModelId = this.configuredModelId ?? getRuntimeConfig().aiGateway.embeddingModel
62
- }
63
-
64
78
  return this.resolvedModelId
65
79
  }
66
80
 
67
81
  private getModel() {
68
82
  if (!this._model) {
69
- this._model = resolveEmbeddingModel(this.getModelId())
83
+ this._model = resolveEmbeddingModel(this.getModelId(), this.openRouterApiKey)
70
84
  }
71
85
  return this._model
72
86
  }
73
87
 
74
- private async loadCachedEmbedding(text: string): Promise<number[] | null> {
88
+ private loadCachedEmbedding(text: string): Promise<number[] | null> {
75
89
  const redisCache = this.getCache()
76
- if (!redisCache) return null
90
+ if (!redisCache) return Promise.resolve(null)
77
91
 
78
92
  return redisCache.get(this.getModelId(), text)
79
93
  }
80
94
 
81
- async embedQuery(text: string): Promise<number[]> {
95
+ private runEffect<A>(effect: Effect.Effect<A, EmbeddingProviderError>): Promise<A> {
96
+ const runtime = getOptionalCurrentRuntime()
97
+ return runtime ? runtime.runPromise(effect) : Effect.runPromise(effect)
98
+ }
99
+
100
+ embedQuery(text: string): Promise<number[]> {
82
101
  const input = text.trim()
83
- if (!input) return []
102
+ if (!input) return Promise.resolve([])
84
103
 
85
- const cached = await this.loadCachedEmbedding(input)
86
- if (cached) return cached
104
+ const dedupKey = `${this.getModelId()}:${input}`
105
+ const pending = this.inflightEmbeddings.get(dedupKey)
106
+ if (pending) return pending
87
107
 
88
- const result = await this.embedFn({ model: this.getModel(), value: input, maxRetries: 2 })
89
- const embedding = normalizeEmbedding(result.embedding)
108
+ const promise = this.runEffect(this.executeEmbedQueryEffect(input))
109
+ this.inflightEmbeddings.set(dedupKey, promise)
110
+ void promise.finally(() => this.inflightEmbeddings.delete(dedupKey))
90
111
 
91
- const redisCache = this.getCache()
92
- if (redisCache) {
93
- void redisCache.set(this.getModelId(), input, embedding)
94
- }
112
+ return promise
113
+ }
114
+
115
+ private executeEmbedQueryEffect(input: string): Effect.Effect<number[], EmbeddingProviderError> {
116
+ const self = this
117
+
118
+ return Effect.gen(function* () {
119
+ const cached = yield* tryEmbeddingPromise('Failed to load cached query embedding.', () =>
120
+ self.loadCachedEmbedding(input),
121
+ )
122
+ if (cached) {
123
+ return cached
124
+ }
125
+
126
+ const result = yield* tryEmbeddingPromise('Failed to generate query embedding.', () =>
127
+ self.embedFn({ model: self.getModel(), value: input, maxRetries: 2 }),
128
+ )
129
+ const embedding = normalizeEmbedding(result.embedding)
130
+
131
+ const redisCache = self.getCache()
132
+ if (redisCache) {
133
+ void redisCache.set(self.getModelId(), input, embedding)
134
+ }
95
135
 
96
- return embedding
136
+ return embedding
137
+ }).pipe(Effect.withSpan('ProviderEmbeddings.executeEmbedQuery'))
97
138
  }
98
139
 
99
- async embedDocuments(values: string[]): Promise<number[][]> {
100
- if (values.length === 0) return []
140
+ embedDocuments(values: string[]): Promise<number[][]> {
141
+ if (values.length === 0) return Promise.resolve([])
101
142
 
102
143
  const normalized = values.map((value) => value.trim())
103
144
  const nonEmptyEntries = normalized
@@ -105,57 +146,62 @@ export class ProviderEmbeddings {
105
146
  .filter((entry) => entry.value.length > 0)
106
147
 
107
148
  if (nonEmptyEntries.length === 0) {
108
- return normalized.map(() => [])
149
+ return Promise.resolve(normalized.map(() => []))
109
150
  }
110
151
 
111
152
  const uniqueTexts = [...new Set(nonEmptyEntries.map((entry) => entry.value))]
112
- const embeddingsByText = new Map<string, number[]>()
113
- let missingTexts = [...uniqueTexts]
114
-
115
- const redisCache = this.getCache()
116
- if (redisCache && missingTexts.length > 0) {
117
- const redisResults = await Promise.all(
118
- missingTexts.map(async (text) => ({ text, embedding: await redisCache.get(this.getModelId(), text) })),
119
- )
153
+ return this.runEffect(this.embedDocumentsEffect(normalized, uniqueTexts))
154
+ }
120
155
 
121
- missingTexts = []
122
- for (const result of redisResults) {
123
- if (!result.embedding) {
124
- missingTexts.push(result.text)
125
- continue
156
+ private embedDocumentsEffect(
157
+ normalized: string[],
158
+ uniqueTexts: string[],
159
+ ): Effect.Effect<number[][], EmbeddingProviderError> {
160
+ const self = this
161
+
162
+ return Effect.gen(function* () {
163
+ const embeddingsByText = new Map<string, number[]>()
164
+ let missingTexts = [...uniqueTexts]
165
+ const redisCache = self.getCache()
166
+ const redisResults =
167
+ redisCache && missingTexts.length > 0
168
+ ? yield* Effect.all(
169
+ missingTexts.map((text) =>
170
+ tryEmbeddingPromise('Failed to load cached document embedding.', () =>
171
+ redisCache.get(self.getModelId(), text),
172
+ ).pipe(Effect.map((embedding) => ({ text, embedding }))),
173
+ ),
174
+ )
175
+ : ([] as Array<{ text: string; embedding: number[] | null }>)
176
+
177
+ if (redisCache && missingTexts.length > 0) {
178
+ missingTexts = []
179
+ for (const result of redisResults) {
180
+ if (!result.embedding) {
181
+ missingTexts.push(result.text)
182
+ continue
183
+ }
184
+
185
+ embeddingsByText.set(result.text, result.embedding)
126
186
  }
127
-
128
- embeddingsByText.set(result.text, result.embedding)
129
187
  }
130
- }
131
188
 
132
- if (missingTexts.length > 0) {
133
- const result = await this.embedManyFn({ model: this.getModel(), values: missingTexts, maxRetries: 2 })
189
+ if (missingTexts.length === 0) {
190
+ return normalized.map((text) => (text ? (embeddingsByText.get(text) ?? []) : []))
191
+ }
134
192
 
193
+ const result = yield* tryEmbeddingPromise('Failed to generate document embeddings.', () =>
194
+ self.embedManyFn({ model: self.getModel(), values: missingTexts, maxRetries: 2 }),
195
+ )
135
196
  missingTexts.forEach((text, index) => {
136
197
  const embedding = normalizeEmbedding(result.embeddings[index] ?? [])
137
198
  embeddingsByText.set(text, embedding)
138
199
  if (redisCache) {
139
- void redisCache.set(this.getModelId(), text, embedding)
200
+ void redisCache.set(self.getModelId(), text, embedding)
140
201
  }
141
202
  })
142
- }
143
-
144
- return normalized.map((text) => (text ? (embeddingsByText.get(text) ?? []) : []))
145
- }
146
- }
147
203
 
148
- let defaultEmbeddings: ProviderEmbeddings | null = null
149
-
150
- export function getDefaultEmbeddings(): ProviderEmbeddings {
151
- if (!defaultEmbeddings) {
152
- defaultEmbeddings = new ProviderEmbeddings()
204
+ return normalized.map((text) => (text ? (embeddingsByText.get(text) ?? []) : []))
205
+ }).pipe(Effect.withSpan('ProviderEmbeddings.embedDocuments'))
153
206
  }
154
-
155
- return defaultEmbeddings
156
- }
157
-
158
- export function resetDefaultEmbeddingsForTests(): void {
159
- defaultEmbeddings = null
160
- resetDirectOpenRouterProviderForTests()
161
207
  }