@lota-sdk/core 0.4.9 → 0.4.11

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 (182) hide show
  1. package/package.json +2 -2
  2. package/src/ai/embedding-cache.ts +3 -1
  3. package/src/ai-gateway/ai-gateway.ts +164 -82
  4. package/src/ai-gateway/index.ts +16 -1
  5. package/src/config/agent-defaults.ts +4 -107
  6. package/src/config/agent-types.ts +1 -1
  7. package/src/config/background-processing.ts +1 -1
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +22 -25
  10. package/src/config/thread-defaults.ts +1 -10
  11. package/src/create-runtime.ts +145 -670
  12. package/src/db/base.service.ts +30 -38
  13. package/src/db/memory-query-builder.ts +2 -1
  14. package/src/db/memory-store.ts +29 -20
  15. package/src/db/memory.ts +188 -195
  16. package/src/db/service-normalization.ts +97 -64
  17. package/src/db/service.ts +496 -384
  18. package/src/db/startup.ts +30 -19
  19. package/src/effect/helpers.ts +30 -5
  20. package/src/effect/index.ts +7 -7
  21. package/src/effect/layers.ts +75 -72
  22. package/src/effect/services.ts +15 -11
  23. package/src/embeddings/provider.ts +65 -71
  24. package/src/index.ts +13 -12
  25. package/src/queues/autonomous-job.queue.ts +177 -143
  26. package/src/queues/context-compaction.queue.ts +41 -39
  27. package/src/queues/delayed-node-promotion.queue.ts +61 -42
  28. package/src/queues/document-processor.queue.ts +5 -3
  29. package/src/queues/index.ts +1 -0
  30. package/src/queues/memory-consolidation.queue.ts +79 -53
  31. package/src/queues/organization-learning.queue.ts +70 -33
  32. package/src/queues/plan-agent-heartbeat.queue.ts +111 -83
  33. package/src/queues/plan-scheduler.queue.ts +101 -97
  34. package/src/queues/post-chat-memory.queue.ts +56 -46
  35. package/src/queues/queue-factory.ts +146 -69
  36. package/src/queues/queues.service.ts +61 -0
  37. package/src/queues/title-generation.queue.ts +44 -44
  38. package/src/redis/connection.ts +181 -164
  39. package/src/redis/org-memory-lock.ts +24 -9
  40. package/src/redis/redis-lease-lock.ts +8 -1
  41. package/src/redis/stream-context.ts +17 -9
  42. package/src/runtime/agent-identity-overrides.ts +7 -3
  43. package/src/runtime/agent-runtime-policy.ts +10 -5
  44. package/src/runtime/agent-stream-helpers.ts +24 -15
  45. package/src/runtime/chat-run-orchestration.ts +1 -1
  46. package/src/runtime/context-compaction/context-compaction-runtime.ts +28 -32
  47. package/src/runtime/context-compaction/context-compaction.ts +131 -85
  48. package/src/runtime/domain-layer.ts +203 -0
  49. package/src/runtime/execution-plan-visibility.ts +5 -2
  50. package/src/runtime/graph-designer.ts +0 -14
  51. package/src/runtime/helper-model.ts +8 -4
  52. package/src/runtime/index.ts +1 -1
  53. package/src/runtime/indexed-repositories-policy.ts +2 -6
  54. package/src/runtime/memory/memory-block.ts +19 -9
  55. package/src/runtime/memory/memory-pipeline.ts +53 -66
  56. package/src/runtime/memory/memory-scope.ts +33 -29
  57. package/src/runtime/plugin-resolution.ts +58 -62
  58. package/src/runtime/post-turn-side-effects.ts +139 -161
  59. package/src/runtime/retrieval-adapters.ts +4 -4
  60. package/src/runtime/runtime-config.ts +3 -9
  61. package/src/runtime/runtime-extensions.ts +0 -43
  62. package/src/runtime/runtime-lifecycle.ts +124 -0
  63. package/src/runtime/runtime-services.ts +455 -0
  64. package/src/runtime/runtime-worker-registry.ts +113 -30
  65. package/src/runtime/social-chat/social-chat-agent-runner.ts +13 -8
  66. package/src/runtime/social-chat/social-chat-history.ts +24 -13
  67. package/src/runtime/social-chat/social-chat.ts +420 -369
  68. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +64 -57
  69. package/src/runtime/team-consultation/team-consultation-prompts.ts +11 -6
  70. package/src/runtime/thread-chat-helpers.ts +18 -9
  71. package/src/runtime/thread-turn-context.ts +28 -74
  72. package/src/runtime/turn-lifecycle.ts +6 -14
  73. package/src/services/agent-activity.service.ts +169 -176
  74. package/src/services/agent-executor.service.ts +207 -196
  75. package/src/services/artifact.service.ts +10 -5
  76. package/src/services/attachment.service.ts +16 -48
  77. package/src/services/autonomous-job.service.ts +81 -87
  78. package/src/services/background-work.service.ts +54 -0
  79. package/src/services/chat-run-registry.service.ts +3 -1
  80. package/src/services/context-compaction.service.ts +8 -10
  81. package/src/services/document-chunk.service.ts +8 -17
  82. package/src/services/execution-plan/execution-plan-graph.ts +122 -109
  83. package/src/services/execution-plan/execution-plan-schedule.ts +1 -15
  84. package/src/services/execution-plan/execution-plan.service.ts +68 -51
  85. package/src/services/feedback-loop.service.ts +1 -1
  86. package/src/services/global-orchestrator.service.ts +49 -15
  87. package/src/services/graph-full-routing.ts +49 -37
  88. package/src/services/index.ts +1 -0
  89. package/src/services/institutional-memory.service.ts +8 -17
  90. package/src/services/learned-skill.service.ts +38 -35
  91. package/src/services/memory/memory-conversation.ts +10 -5
  92. package/src/services/memory/memory-errors.ts +27 -0
  93. package/src/services/memory/memory-org-memory.ts +14 -3
  94. package/src/services/memory/memory-preseeded.ts +10 -4
  95. package/src/services/memory/memory-utils.ts +2 -1
  96. package/src/services/memory/memory.service.ts +37 -52
  97. package/src/services/memory/rerank.service.ts +3 -11
  98. package/src/services/monitoring-window.service.ts +1 -1
  99. package/src/services/mutating-approval.service.ts +1 -1
  100. package/src/services/node-workspace.service.ts +2 -2
  101. package/src/services/notification.service.ts +16 -4
  102. package/src/services/organization-member.service.ts +1 -1
  103. package/src/services/organization.service.ts +34 -51
  104. package/src/services/ownership-dispatcher.service.ts +148 -95
  105. package/src/services/plan/plan-agent-heartbeat.service.ts +30 -16
  106. package/src/services/plan/plan-agent-query.service.ts +13 -9
  107. package/src/services/plan/plan-approval.service.ts +52 -48
  108. package/src/services/plan/plan-artifact.service.ts +2 -2
  109. package/src/services/plan/plan-builder.service.ts +2 -2
  110. package/src/services/plan/plan-checkpoint.service.ts +1 -1
  111. package/src/services/plan/plan-compiler.service.ts +1 -1
  112. package/src/services/plan/plan-completion-side-effects.ts +99 -113
  113. package/src/services/plan/plan-coordination.service.ts +1 -1
  114. package/src/services/plan/plan-cycle.service.ts +171 -202
  115. package/src/services/plan/plan-deadline.service.ts +304 -307
  116. package/src/services/plan/plan-event-delivery.service.ts +84 -72
  117. package/src/services/plan/plan-executor-context.ts +2 -0
  118. package/src/services/plan/plan-executor-graph.ts +375 -353
  119. package/src/services/plan/plan-executor-helpers.ts +60 -75
  120. package/src/services/plan/plan-executor.service.ts +494 -489
  121. package/src/services/plan/plan-run.service.ts +12 -19
  122. package/src/services/plan/plan-scheduler.service.ts +89 -82
  123. package/src/services/plan/plan-template.service.ts +1 -1
  124. package/src/services/plan/plan-transaction-events.ts +8 -5
  125. package/src/services/plan/plan-validator.service.ts +1 -1
  126. package/src/services/plan/plan-workspace.service.ts +17 -11
  127. package/src/services/plugin-executor.service.ts +26 -21
  128. package/src/services/quality-metrics.service.ts +1 -1
  129. package/src/services/queue-job.service.ts +8 -17
  130. package/src/services/recent-activity-title.service.ts +22 -10
  131. package/src/services/recent-activity.service.ts +1 -1
  132. package/src/services/skill-resolver.service.ts +1 -1
  133. package/src/services/social-chat-history.service.ts +37 -20
  134. package/src/services/system-executor.service.ts +25 -20
  135. package/src/services/thread/thread-bootstrap.ts +37 -19
  136. package/src/services/thread/thread-listing.ts +2 -1
  137. package/src/services/thread/thread-memory-block.ts +18 -5
  138. package/src/services/thread/thread-message.service.ts +30 -13
  139. package/src/services/thread/thread-title.service.ts +1 -1
  140. package/src/services/thread/thread-turn-execution.ts +87 -83
  141. package/src/services/thread/thread-turn-preparation.service.ts +65 -40
  142. package/src/services/thread/thread-turn-streaming.ts +32 -36
  143. package/src/services/thread/thread-turn.ts +43 -29
  144. package/src/services/thread/thread.service.ts +32 -8
  145. package/src/services/user.service.ts +1 -1
  146. package/src/services/write-intent-validator.service.ts +1 -1
  147. package/src/storage/attachment-storage.service.ts +7 -4
  148. package/src/storage/generated-document-storage.service.ts +1 -1
  149. package/src/system-agents/context-compaction.agent.ts +1 -1
  150. package/src/system-agents/helper-agent-options.ts +1 -1
  151. package/src/system-agents/memory-reranker.agent.ts +1 -1
  152. package/src/system-agents/memory.agent.ts +1 -1
  153. package/src/system-agents/recent-activity-title-refiner.agent.ts +9 -6
  154. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  155. package/src/system-agents/skill-extractor.agent.ts +1 -1
  156. package/src/system-agents/skill-manager.agent.ts +1 -1
  157. package/src/system-agents/thread-router.agent.ts +23 -20
  158. package/src/system-agents/title-generator.agent.ts +1 -1
  159. package/src/tools/execution-plan.tool.ts +36 -20
  160. package/src/tools/fetch-webpage.tool.ts +30 -22
  161. package/src/tools/firecrawl-client.ts +1 -6
  162. package/src/tools/plan-approval.tool.ts +9 -1
  163. package/src/tools/remember-memory.tool.ts +3 -6
  164. package/src/tools/research-topic.tool.ts +12 -3
  165. package/src/tools/search-web.tool.ts +26 -18
  166. package/src/tools/search.tool.ts +4 -5
  167. package/src/tools/team-think.tool.ts +139 -121
  168. package/src/utils/async.ts +15 -6
  169. package/src/utils/errors.ts +27 -15
  170. package/src/workers/bootstrap.ts +34 -58
  171. package/src/workers/memory-consolidation.worker.ts +4 -1
  172. package/src/workers/organization-learning.worker.ts +16 -3
  173. package/src/workers/regular-chat-memory-digest.helpers.ts +3 -4
  174. package/src/workers/regular-chat-memory-digest.runner.ts +46 -29
  175. package/src/workers/skill-extraction.runner.ts +13 -15
  176. package/src/workers/worker-utils.ts +14 -8
  177. package/src/config/search.ts +0 -3
  178. package/src/effect/awaitable-effect.ts +0 -87
  179. package/src/effect/runtime-ref.ts +0 -25
  180. package/src/effect/runtime.ts +0 -31
  181. package/src/redis/runtime-connection.ts +0 -10
  182. package/src/runtime/agent-types.ts +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lota-sdk/core",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -31,7 +31,7 @@
31
31
  "@ai-sdk/openai": "^3.0.53",
32
32
  "@chat-adapter/slack": "^4.26.0",
33
33
  "@chat-adapter/state-ioredis": "^4.26.0",
34
- "@lota-sdk/shared": "0.4.9",
34
+ "@lota-sdk/shared": "0.4.11",
35
35
  "@mendable/firecrawl-js": "^4.18.3",
36
36
  "@surrealdb/node": "^3.0.3",
37
37
  "ai": "^6.0.167",
@@ -107,7 +107,9 @@ export class EmbeddingCache {
107
107
  }
108
108
  }
109
109
 
110
- export class EmbeddingCacheTag extends Context.Service<EmbeddingCacheTag, EmbeddingCache>()('EmbeddingCache') {}
110
+ export class EmbeddingCacheTag extends Context.Service<EmbeddingCacheTag, EmbeddingCache>()(
111
+ '@lota-sdk/core/EmbeddingCache',
112
+ ) {}
111
113
 
112
114
  export const EmbeddingCacheLive = Layer.effect(
113
115
  EmbeddingCacheTag,
@@ -6,7 +6,6 @@ import { Cause, Clock, Context, Duration, Effect, ExecutionPlan, Fiber, Layer, S
6
6
 
7
7
  import { DEFAULT_AI_GATEWAY_URL } from '../config/constants'
8
8
  import { AiGenerationError, ConfigurationError } from '../effect/errors'
9
- import { getLotaSdkRuntime } from '../effect/runtime'
10
9
  import { RuntimeConfigServiceTag } from '../effect/services'
11
10
  import { getDirectOpenRouterProvider, normalizeDirectOpenRouterModelId } from '../openrouter/direct-provider'
12
11
  import { isRecord, readString } from '../utils/string'
@@ -24,16 +23,18 @@ type AiGatewayGeneratedContent = AiGatewayGenerateResult['content'][number]
24
23
  type AiGatewayStreamPart = AiGatewayStreamResult['stream'] extends ReadableStream<infer T> ? T : never
25
24
  type AiGatewayProviderOptions = NonNullable<AiGatewayCallOptions['providerOptions']>
26
25
  type AiGatewayAttemptResult<A> = { source: string; result: A }
26
+ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
27
+ type AiGatewayRunFork = <A, E>(effect: Effect.Effect<A, E, never>) => Fiber.Fiber<A, E | unknown>
27
28
 
28
29
  class AiGatewayGenerateAttempt extends Context.Service<
29
30
  AiGatewayGenerateAttempt,
30
31
  { readonly execute: Effect.Effect<AiGatewayAttemptResult<AiGatewayGenerateResult>, AiGenerationError> }
31
- >()('AiGatewayGenerateAttempt') {}
32
+ >()('@lota-sdk/core/internal/AiGatewayGenerateAttempt') {}
32
33
 
33
34
  class AiGatewayStreamAttempt extends Context.Service<
34
35
  AiGatewayStreamAttempt,
35
36
  { readonly execute: Effect.Effect<AiGatewayAttemptResult<AiGatewayStreamResult>, AiGenerationError> }
36
- >()('AiGatewayStreamAttempt') {}
37
+ >()('@lota-sdk/core/internal/AiGatewayStreamAttempt') {}
37
38
 
38
39
  const EXPECTED_GATEWAY_KEY_PREFIX = 'sk-bf-'
39
40
  const AI_GATEWAY_VIRTUAL_KEY_HEADER = 'x-bf-vk'
@@ -69,6 +70,10 @@ const RETRYABLE_NETWORK_ERROR_PATTERNS = [
69
70
  /timed out/i,
70
71
  ]
71
72
 
73
+ function isAiGenerationError(error: unknown): error is AiGenerationError {
74
+ return isRecord(error) && error._tag === 'AiGenerationError'
75
+ }
76
+
72
77
  function getNumericField(value: Record<string, unknown>, key: string): number | null {
73
78
  const field = value[key]
74
79
  if (typeof field === 'number' && Number.isFinite(field)) return field
@@ -156,7 +161,7 @@ function stringifyProviderField(value: unknown, maxLength: number): string | und
156
161
  }
157
162
 
158
163
  function classifyAiGatewayError(source: string, error: unknown): AiGenerationError {
159
- if (error instanceof AiGenerationError) {
164
+ if (isAiGenerationError(error)) {
160
165
  return error
161
166
  }
162
167
 
@@ -267,12 +272,13 @@ function withAiGatewayResilience<A>(source: string, effect: Effect.Effect<A, AiG
267
272
  function withAiGatewayStreamIdleTimeout(
268
273
  stream: ReadableStream<AiGatewayStreamPart>,
269
274
  source: string,
275
+ runFork: AiGatewayRunFork,
270
276
  onFinalize?: () => void,
271
277
  ): ReadableStream<AiGatewayStreamPart> {
272
278
  let closed = false
273
279
  let reader: ReadableStreamDefaultReader<AiGatewayStreamPart> | null = null
274
- let idleTimeoutFiber: ReturnType<typeof Effect.runFork> | null = null
275
- let bodyPumpFiber: ReturnType<typeof Effect.runFork> | null = null
280
+ let idleTimeoutFiber: Fiber.Fiber<unknown, unknown> | null = null
281
+ let bodyPumpFiber: Fiber.Fiber<unknown, unknown> | null = null
276
282
  let finalized = false
277
283
 
278
284
  const finalize = () => {
@@ -281,9 +287,9 @@ function withAiGatewayStreamIdleTimeout(
281
287
  onFinalize?.()
282
288
  }
283
289
 
284
- const interruptFiber = (fiber: ReturnType<typeof Effect.runFork> | null) => {
290
+ const interruptFiber = (fiber: Fiber.Fiber<unknown, unknown> | null) => {
285
291
  if (!fiber) return
286
- void Effect.runFork(Fiber.interrupt(fiber))
292
+ void runFork(Fiber.interrupt(fiber))
287
293
  }
288
294
 
289
295
  const stopIdleTimeout = () => {
@@ -347,7 +353,7 @@ function withAiGatewayStreamIdleTimeout(
347
353
 
348
354
  const resetIdleTimeout = (controller: ReadableStreamDefaultController<AiGatewayStreamPart>) => {
349
355
  stopIdleTimeout()
350
- idleTimeoutFiber = Effect.runFork(
356
+ idleTimeoutFiber = runFork(
351
357
  Effect.sleep(Duration.millis(AI_GATEWAY_STREAM_IDLE_TIMEOUT_MS)).pipe(
352
358
  Effect.flatMap(() =>
353
359
  Effect.gen(function* () {
@@ -413,7 +419,7 @@ function withAiGatewayStreamIdleTimeout(
413
419
  start(controller) {
414
420
  const streamReader = stream.getReader()
415
421
  reader = streamReader
416
- bodyPumpFiber = Effect.runFork(pumpStreamEffect(streamReader, controller))
422
+ bodyPumpFiber = runFork(pumpStreamEffect(streamReader, controller))
417
423
  },
418
424
  cancel(reason) {
419
425
  closed = true
@@ -465,7 +471,7 @@ function normalizeAiGatewayUrl(value: string): string {
465
471
  export class AiGatewayTag extends Context.Service<
466
472
  AiGatewayTag,
467
473
  { readonly semaphore: Semaphore.Semaphore; readonly provider: ReturnType<typeof createOpenAI> }
468
- >()('AiGateway') {}
474
+ >()('@lota-sdk/core/AiGateway') {}
469
475
 
470
476
  export const AiGatewayLive = Layer.effect(
471
477
  AiGatewayTag,
@@ -487,27 +493,32 @@ export const AiGatewayLive = Layer.effect(
487
493
  }),
488
494
  )
489
495
 
490
- function resolveFromRuntime<I, T>(tag: Context.Key<I, T>): T {
491
- return getLotaSdkRuntime().runSync(Effect.service(tag))
492
- }
493
-
494
- function getAiGateway(): AiGatewayTag['Service'] {
495
- return resolveFromRuntime(AiGatewayTag)
496
- }
496
+ type AiGatewayRuntimeConfig = Context.Service.Shape<typeof RuntimeConfigServiceTag>
497
497
 
498
- function withAiGatewayConcurrency<A>(effect: Effect.Effect<A, AiGenerationError>): Effect.Effect<A, AiGenerationError> {
499
- return getAiGateway().semaphore.withPermit(effect)
498
+ function withAiGatewayConcurrency<A>(
499
+ effect: Effect.Effect<A, AiGenerationError>,
500
+ ): Effect.Effect<A, AiGenerationError, AiGatewayTag> {
501
+ return Effect.gen(function* () {
502
+ const gateway = yield* AiGatewayTag
503
+ return yield* gateway.semaphore.withPermit(effect)
504
+ })
500
505
  }
501
506
 
502
507
  function withAiGatewayStreamConcurrency(
503
508
  effect: Effect.Effect<AiGatewayAttemptResult<AiGatewayStreamResult>, AiGenerationError>,
504
- ): Effect.Effect<AiGatewayAttemptResult<AiGatewayStreamResult>, AiGenerationError> {
509
+ runFork: AiGatewayRunFork,
510
+ ): Effect.Effect<AiGatewayAttemptResult<AiGatewayStreamResult>, AiGenerationError, AiGatewayTag> {
505
511
  return Effect.uninterruptibleMask((restore) =>
506
512
  Effect.gen(function* () {
507
- const { semaphore } = getAiGateway()
513
+ const { semaphore } = yield* AiGatewayTag
508
514
  const currentContext = yield* Effect.context<never>()
509
515
  yield* semaphore.take(1)
510
516
 
517
+ // NOTE: manual release intentional — permit outlives Effect scope for the
518
+ // stream lifetime. The stream consumer drains asynchronously after this
519
+ // Effect resolves; the permit is released by either the idle-timeout
520
+ // finalize callback or the error path below. The `released` guard makes
521
+ // the release idempotent across those paths.
511
522
  let released = false
512
523
  const release = () => {
513
524
  if (released) return
@@ -517,13 +528,14 @@ function withAiGatewayStreamConcurrency(
517
528
 
518
529
  const attempt = yield* restore(effect).pipe(
519
530
  Effect.catchTag('AiGenerationError', (error) => Effect.sync(release).pipe(Effect.andThen(Effect.fail(error)))),
531
+ Effect.onInterrupt(() => Effect.sync(release)),
520
532
  )
521
533
 
522
534
  return {
523
535
  ...attempt,
524
536
  result: {
525
537
  ...attempt.result,
526
- stream: withAiGatewayStreamIdleTimeout(attempt.result.stream, attempt.source, release),
538
+ stream: withAiGatewayStreamIdleTimeout(attempt.result.stream, attempt.source, runFork, release),
527
539
  },
528
540
  }
529
541
  }),
@@ -610,18 +622,20 @@ function isOpenRouterModel(modelId: string): boolean {
610
622
  return modelId.trim().toLowerCase().startsWith('openrouter/')
611
623
  }
612
624
 
613
- function hasDirectOpenRouterFallback(modelId: string): boolean {
614
- const config = resolveFromRuntime(RuntimeConfigServiceTag)
625
+ function hasDirectOpenRouterFallback(config: AiGatewayRuntimeConfig, modelId: string): boolean {
615
626
  return isOpenRouterModel(modelId) && Boolean(config.aiGateway.openRouterApiKey?.trim())
616
627
  }
617
628
 
618
- function getDirectOpenRouterChatModel(modelId: string): AiGatewayLanguageModel {
619
- const config = resolveFromRuntime(RuntimeConfigServiceTag)
629
+ function getDirectOpenRouterChatModel(config: AiGatewayRuntimeConfig, modelId: string): AiGatewayLanguageModel {
620
630
  return getDirectOpenRouterProvider(config.aiGateway.openRouterApiKey).chat(normalizeDirectOpenRouterModelId(modelId))
621
631
  }
622
632
 
623
- function shouldFallbackToDirectOpenRouter(modelId: string, error: AiGenerationError): boolean {
624
- return hasDirectOpenRouterFallback(modelId) && isRetryableAiGatewayError(error)
633
+ function shouldFallbackToDirectOpenRouter(
634
+ config: AiGatewayRuntimeConfig,
635
+ modelId: string,
636
+ error: AiGenerationError,
637
+ ): boolean {
638
+ return hasDirectOpenRouterFallback(config, modelId) && isRetryableAiGatewayError(error)
625
639
  }
626
640
 
627
641
  function attemptAiGatewayGenerate(
@@ -653,22 +667,25 @@ function attemptAiGatewayStream(
653
667
  }
654
668
 
655
669
  function attemptDirectOpenRouterGenerate(
670
+ config: AiGatewayRuntimeConfig,
656
671
  modelId: string,
657
672
  params: AiGatewayCallOptions,
658
673
  ): Effect.Effect<AiGatewayAttemptResult<AiGatewayGenerateResult>, AiGenerationError> {
659
- const model = getDirectOpenRouterChatModel(modelId)
674
+ const model = getDirectOpenRouterChatModel(config, modelId)
660
675
  return attemptAiGatewayGenerate('openrouter.generate', () => model.doGenerate(params))
661
676
  }
662
677
 
663
678
  function attemptDirectOpenRouterStream(
679
+ config: AiGatewayRuntimeConfig,
664
680
  modelId: string,
665
681
  params: AiGatewayCallOptions,
666
682
  ): Effect.Effect<AiGatewayAttemptResult<AiGatewayStreamResult>, AiGenerationError> {
667
- const model = getDirectOpenRouterChatModel(modelId)
683
+ const model = getDirectOpenRouterChatModel(config, modelId)
668
684
  return attemptAiGatewayStream('openrouter.stream', () => model.doStream(params))
669
685
  }
670
686
 
671
687
  function executeGenerateAttemptPlan(
688
+ config: AiGatewayRuntimeConfig,
672
689
  modelId: string,
673
690
  params: AiGatewayCallOptions,
674
691
  doGenerate: () => PromiseLike<AiGatewayGenerateResult>,
@@ -681,7 +698,7 @@ function executeGenerateAttemptPlan(
681
698
  return yield* attempt.execute
682
699
  })
683
700
 
684
- if (!hasDirectOpenRouterFallback(modelId)) {
701
+ if (!hasDirectOpenRouterFallback(config, modelId)) {
685
702
  return effect.pipe(
686
703
  Effect.provide(primary),
687
704
  Effect.withSpan('AiGateway.executeGeneratePlan'),
@@ -695,9 +712,9 @@ function executeGenerateAttemptPlan(
695
712
  { provide: primary },
696
713
  {
697
714
  provide: Layer.succeed(AiGatewayGenerateAttempt, {
698
- execute: attemptDirectOpenRouterGenerate(modelId, params),
715
+ execute: attemptDirectOpenRouterGenerate(config, modelId, params),
699
716
  }),
700
- while: (error: AiGenerationError) => shouldFallbackToDirectOpenRouter(modelId, error),
717
+ while: (error: AiGenerationError) => shouldFallbackToDirectOpenRouter(config, modelId, error),
701
718
  },
702
719
  ),
703
720
  ),
@@ -707,6 +724,7 @@ function executeGenerateAttemptPlan(
707
724
  }
708
725
 
709
726
  function executeStreamAttemptPlan(
727
+ config: AiGatewayRuntimeConfig,
710
728
  modelId: string,
711
729
  params: AiGatewayCallOptions,
712
730
  doStream: () => PromiseLike<AiGatewayStreamResult>,
@@ -719,7 +737,7 @@ function executeStreamAttemptPlan(
719
737
  return yield* attempt.execute
720
738
  })
721
739
 
722
- if (!hasDirectOpenRouterFallback(modelId)) {
740
+ if (!hasDirectOpenRouterFallback(config, modelId)) {
723
741
  return effect.pipe(
724
742
  Effect.provide(primary),
725
743
  Effect.withSpan('AiGateway.executeStreamPlan'),
@@ -732,8 +750,10 @@ function executeStreamAttemptPlan(
732
750
  ExecutionPlan.make(
733
751
  { provide: primary },
734
752
  {
735
- provide: Layer.succeed(AiGatewayStreamAttempt, { execute: attemptDirectOpenRouterStream(modelId, params) }),
736
- while: (error: AiGenerationError) => shouldFallbackToDirectOpenRouter(modelId, error),
753
+ provide: Layer.succeed(AiGatewayStreamAttempt, {
754
+ execute: attemptDirectOpenRouterStream(config, modelId, params),
755
+ }),
756
+ while: (error: AiGenerationError) => shouldFallbackToDirectOpenRouter(config, modelId, error),
737
757
  },
738
758
  ),
739
759
  ),
@@ -821,7 +841,56 @@ function addAiGatewayReasoningRawChunks(
821
841
  return { ...params, includeRawChunks: true }
822
842
  }
823
843
 
824
- function createAiGatewayLanguageModelMiddleware(modelId: string): LanguageModelMiddleware {
844
+ function resolveProviderModel(
845
+ provider: ReturnType<typeof createOpenAI>,
846
+ modelId: string,
847
+ providerId: string,
848
+ ): AiGatewayLanguageModel {
849
+ return providerId === OPENAI_CHAT_PROVIDER_ID ? provider.chat(modelId) : provider(modelId)
850
+ }
851
+
852
+ // Module-level Promise slot that `createLotaRuntime` populates during boot.
853
+ // This is a legitimate per-process singleton (mirrors the worker bootstrap
854
+ // pattern in `workers/bootstrap.ts`): the AI gateway middleware is dispatched
855
+ // by AI SDK callers that live outside Effect context, so the middleware needs
856
+ // a way to run gateway Effects without capturing a `ManagedRuntime` through
857
+ // every `aiGatewayModel(modelId)` call site.
858
+ //
859
+ // Only `createLotaRuntime` writes to the slot; resetting on disconnect is a
860
+ // Phase 3b concern — for now it stays alive for the process lifetime.
861
+ let aiGatewayRuntimeReady: Promise<{
862
+ gateway: Context.Service.Shape<typeof AiGatewayTag>
863
+ runtimeConfig: Context.Service.Shape<typeof RuntimeConfigServiceTag>
864
+ runPromise: <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A>
865
+ runFork: AiGatewayRunFork
866
+ }> | null = null
867
+
868
+ export function bindAiGatewayRuntime(params: {
869
+ gateway: Context.Service.Shape<typeof AiGatewayTag>
870
+ runtimeConfig: Context.Service.Shape<typeof RuntimeConfigServiceTag>
871
+ runPromise: <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A>
872
+ runFork: AiGatewayRunFork
873
+ }): void {
874
+ aiGatewayRuntimeReady = Promise.resolve(params)
875
+ }
876
+
877
+ export function clearAiGatewayRuntime(): void {
878
+ aiGatewayRuntimeReady = null
879
+ }
880
+
881
+ async function getAiGatewayRuntime(): Promise<{
882
+ gateway: Context.Service.Shape<typeof AiGatewayTag>
883
+ runtimeConfig: Context.Service.Shape<typeof RuntimeConfigServiceTag>
884
+ runPromise: <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A>
885
+ runFork: AiGatewayRunFork
886
+ }> {
887
+ if (!aiGatewayRuntimeReady) {
888
+ throw new Error('AI gateway runtime has not been initialized. Call createLotaRuntime() first.')
889
+ }
890
+ return aiGatewayRuntimeReady
891
+ }
892
+
893
+ function createAiGatewayLanguageModelMiddleware(modelId: string, providerId: string): LanguageModelMiddleware {
825
894
  return {
826
895
  specificationVersion: 'v3',
827
896
  transformParams: ({ params, type }) =>
@@ -830,10 +899,12 @@ function createAiGatewayLanguageModelMiddleware(modelId: string): LanguageModelM
830
899
  addAiGatewayReasoningRawChunks(normalizeAiGatewayChatProviderOptions(params, modelId), type),
831
900
  ),
832
901
  ),
833
- wrapGenerate: ({ doGenerate, params }) =>
834
- Effect.runPromise(
902
+ wrapGenerate: async ({ params }) => {
903
+ const { gateway, runtimeConfig, runPromise } = await getAiGatewayRuntime()
904
+ const model = resolveProviderModel(gateway.provider, modelId, providerId)
905
+ return runPromise(
835
906
  withAiGatewayConcurrency(
836
- executeGenerateAttemptPlan(modelId, params, doGenerate).pipe(
907
+ executeGenerateAttemptPlan(runtimeConfig, modelId, params, () => model.doGenerate(params)).pipe(
837
908
  Effect.map(({ result }) => ({
838
909
  ...result,
839
910
  content: injectAiGatewayChatReasoningContent(
@@ -842,12 +913,15 @@ function createAiGatewayLanguageModelMiddleware(modelId: string): LanguageModelM
842
913
  ),
843
914
  })),
844
915
  ),
845
- ),
846
- ),
847
- wrapStream: ({ doStream, params }) =>
848
- Effect.runPromise(
916
+ ).pipe(Effect.provideService(AiGatewayTag, gateway)),
917
+ )
918
+ },
919
+ wrapStream: async ({ params }) => {
920
+ const { gateway, runtimeConfig, runPromise, runFork } = await getAiGatewayRuntime()
921
+ const model = resolveProviderModel(gateway.provider, modelId, providerId)
922
+ return runPromise(
849
923
  withAiGatewayStreamConcurrency(
850
- executeStreamAttemptPlan(modelId, params, doStream).pipe(
924
+ executeStreamAttemptPlan(runtimeConfig, modelId, params, () => model.doStream(params)).pipe(
851
925
  Effect.map((attempt) => ({
852
926
  ...attempt,
853
927
  result: isReasoningEnabled(params)
@@ -855,8 +929,12 @@ function createAiGatewayLanguageModelMiddleware(modelId: string): LanguageModelM
855
929
  : attempt.result,
856
930
  })),
857
931
  ),
858
- ).pipe(Effect.map(({ result }) => result)),
859
- ),
932
+ runFork,
933
+ )
934
+ .pipe(Effect.map(({ result }) => result))
935
+ .pipe(Effect.provideService(AiGatewayTag, gateway)),
936
+ )
937
+ },
860
938
  }
861
939
  }
862
940
 
@@ -893,36 +971,42 @@ function withAiGatewayDevTools<TModel extends AiGatewayLanguageModel>(model: TMo
893
971
  return wrapLanguageModel({ model, middleware: devToolsMiddleware() }) as TModel
894
972
  }
895
973
 
896
- function createLazyAiGatewayLanguageModel(params: {
897
- modelId: string
898
- providerId: string
899
- resolve: () => AiGatewayLanguageModel
900
- }): AiGatewayLanguageModel {
974
+ function createAiGatewayLanguageModelPlaceholder(modelId: string, providerId: string): AiGatewayLanguageModel {
975
+ const unreachable = (method: string) =>
976
+ Promise.reject(
977
+ new Error(
978
+ `[ai-gateway] AiGateway language model ${modelId}.${method} was invoked without the gateway middleware; ` +
979
+ 'this call path should be fully handled by createAiGatewayLanguageModelMiddleware.',
980
+ ),
981
+ )
982
+
901
983
  return {
902
984
  specificationVersion: 'v3',
903
- provider: params.providerId,
904
- modelId: params.modelId,
985
+ provider: providerId,
986
+ modelId,
905
987
  supportedUrls: {},
906
- doGenerate: (options) => params.resolve().doGenerate(options),
907
- doStream: (options) => params.resolve().doStream(options),
988
+ doGenerate: () => unreachable('doGenerate'),
989
+ doStream: () => unreachable('doStream'),
908
990
  }
909
991
  }
910
992
 
911
- function createLazyAiGatewayEmbeddingModel(modelId: string): AiGatewayEmbeddingModel {
993
+ function createAiGatewayEmbeddingModelPlaceholder(modelId: string): AiGatewayEmbeddingModel {
912
994
  return {
913
995
  specificationVersion: 'v3',
914
996
  provider: OPENAI_EMBEDDING_PROVIDER_ID,
915
997
  modelId,
916
998
  maxEmbeddingsPerCall: OPENAI_EMBEDDING_MAX_PER_CALL,
917
999
  supportsParallelCalls: true,
918
- doEmbed: (options) => getAiGatewayProvider().embeddingModel(modelId).doEmbed(options),
1000
+ doEmbed: () =>
1001
+ Promise.reject(
1002
+ new Error(
1003
+ `[ai-gateway] AiGateway embedding model ${modelId}.doEmbed was invoked without the gateway middleware; ` +
1004
+ 'this call path should be fully handled by aiGatewayEmbeddingModel middleware.',
1005
+ ),
1006
+ ),
919
1007
  }
920
1008
  }
921
1009
 
922
- export function getAiGatewayProvider() {
923
- return getAiGateway().provider
924
- }
925
-
926
1010
  export function aiGatewayModel(modelId: string) {
927
1011
  if (isOpenRouterModel(modelId)) {
928
1012
  return aiGatewayChatModel(modelId)
@@ -930,12 +1014,8 @@ export function aiGatewayModel(modelId: string) {
930
1014
 
931
1015
  return withAiGatewayDevTools(
932
1016
  wrapLanguageModel({
933
- model: createLazyAiGatewayLanguageModel({
934
- modelId,
935
- providerId: OPENAI_RESPONSES_PROVIDER_ID,
936
- resolve: () => getAiGatewayProvider()(modelId),
937
- }),
938
- middleware: createAiGatewayLanguageModelMiddleware(modelId),
1017
+ model: createAiGatewayLanguageModelPlaceholder(modelId, OPENAI_RESPONSES_PROVIDER_ID),
1018
+ middleware: createAiGatewayLanguageModelMiddleware(modelId, OPENAI_RESPONSES_PROVIDER_ID),
939
1019
  }),
940
1020
  )
941
1021
  }
@@ -947,30 +1027,32 @@ export function aiGatewayOpenRouterResponseHealingModel(modelId: string) {
947
1027
  export function aiGatewayChatModel(modelId: string) {
948
1028
  return withAiGatewayDevTools(
949
1029
  wrapLanguageModel({
950
- model: createLazyAiGatewayLanguageModel({
951
- modelId,
952
- providerId: OPENAI_CHAT_PROVIDER_ID,
953
- resolve: () => getAiGatewayProvider().chat(modelId),
954
- }),
955
- middleware: createAiGatewayLanguageModelMiddleware(modelId),
1030
+ model: createAiGatewayLanguageModelPlaceholder(modelId, OPENAI_CHAT_PROVIDER_ID),
1031
+ middleware: createAiGatewayLanguageModelMiddleware(modelId, OPENAI_CHAT_PROVIDER_ID),
956
1032
  }),
957
1033
  )
958
1034
  }
959
1035
 
960
1036
  export function aiGatewayEmbeddingModel(modelId: string) {
961
1037
  return wrapEmbeddingModel({
962
- model: createLazyAiGatewayEmbeddingModel(modelId),
1038
+ model: createAiGatewayEmbeddingModelPlaceholder(modelId),
963
1039
  middleware: {
964
1040
  specificationVersion: 'v3',
965
- wrapEmbed: ({ doEmbed }) =>
966
- Effect.runPromise(
1041
+ wrapEmbed: async ({ params }) => {
1042
+ const { gateway, runPromise } = await getAiGatewayRuntime()
1043
+ const embeddingModel = gateway.provider.embeddingModel(modelId)
1044
+ return runPromise(
967
1045
  withAiGatewayConcurrency(
968
1046
  withAiGatewayResilience(
969
1047
  'ai-gateway.embed',
970
- Effect.tryPromise({ try: doEmbed, catch: (cause) => classifyAiGatewayError('ai-gateway.embed', cause) }),
971
- ),
972
- ).pipe(Effect.withSpan('AiGateway.embed'), Effect.annotateSpans({ modelId })),
973
- ),
1048
+ Effect.tryPromise({
1049
+ try: () => embeddingModel.doEmbed(params),
1050
+ catch: (cause) => classifyAiGatewayError('ai-gateway.embed', cause),
1051
+ }),
1052
+ ).pipe(Effect.withSpan('AiGateway.embed'), Effect.annotateSpans({ modelId })),
1053
+ ).pipe(Effect.provideService(AiGatewayTag, gateway)),
1054
+ )
1055
+ },
974
1056
  },
975
1057
  })
976
1058
  }
@@ -1,2 +1,17 @@
1
- export * from './ai-gateway'
1
+ export {
2
+ AiGatewayLive,
3
+ AiGatewayTag,
4
+ DEFAULT_AI_GATEWAY_URL,
5
+ aiGatewayChatModel,
6
+ aiGatewayEmbeddingModel,
7
+ aiGatewayModel,
8
+ aiGatewayOpenRouterResponseHealingModel,
9
+ bindAiGatewayRuntime,
10
+ extractAiGatewayChatReasoningDeltaText,
11
+ extractAiGatewayChatReasoningText,
12
+ injectAiGatewayChatReasoningContent,
13
+ injectAiGatewayChatReasoningStream,
14
+ normalizeAiGatewayChatProviderOptions,
15
+ normalizeAiGatewayUrl,
16
+ } from './ai-gateway'
2
17
  export * from './cache-headers'
@@ -1,9 +1,6 @@
1
1
  import type { ToolSet } from 'ai'
2
- import { Effect } from 'effect'
3
2
 
4
3
  import { ConfigurationError } from '../effect/errors'
5
- import { getCurrentRuntime } from '../effect/runtime-ref'
6
- import { AgentConfigServiceTag, AgentFactoryServiceTag } from '../effect/services'
7
4
  import type {
8
5
  AgentFactory,
9
6
  AgentRuntimeConfigParams,
@@ -131,111 +128,11 @@ export interface CoreThreadProfile {
131
128
  instructions: string
132
129
  }
133
130
 
134
- function resolveAgentConfigFromRuntime(): ResolvedAgentConfig {
135
- return getCurrentRuntime().runSync(Effect.service(AgentConfigServiceTag))
131
+ export function isAgentName(agentConfig: ResolvedAgentConfig, value: unknown): value is string {
132
+ return typeof value === 'string' && agentConfig.rosterSet.has(value)
136
133
  }
137
134
 
138
- function resolveAgentFactoryConfigFromRuntime(): ResolvedAgentFactoryConfig {
139
- return getCurrentRuntime().runSync(Effect.service(AgentFactoryServiceTag))
140
- }
141
-
142
- export function getResolvedAgentConfig(): ResolvedAgentConfig {
143
- return resolveAgentConfigFromRuntime()
144
- }
145
-
146
- export function getResolvedAgentFactoryConfig(): ResolvedAgentFactoryConfig {
147
- return resolveAgentFactoryConfigFromRuntime()
148
- }
149
-
150
- export function isAgentName(value: unknown): value is string {
151
- return typeof value === 'string' && resolveAgentConfigFromRuntime().rosterSet.has(value)
152
- }
153
-
154
- export function getAgentRoster(): readonly string[] {
155
- return resolveAgentConfigFromRuntime().roster
156
- }
157
-
158
- export function getAgentDisplayNames(): Record<string, string> {
159
- return resolveAgentConfigFromRuntime().displayNames
160
- }
161
-
162
- export function getAgentShortDisplayNames(): Record<string, string> {
163
- return resolveAgentConfigFromRuntime().shortDisplayNames
164
- }
165
-
166
- export function getAgentDescriptions(): Record<string, string> {
167
- return resolveAgentConfigFromRuntime().descriptions
168
- }
169
-
170
- export function getLeadAgentId(): string {
171
- return resolveAgentConfigFromRuntime().leadAgentId
172
- }
173
-
174
- export function getLeadAgentDisplayName(): string {
175
- const resolved = resolveAgentConfigFromRuntime()
176
- return resolved.displayNames[resolved.leadAgentId] ?? resolved.leadAgentId
177
- }
178
-
179
- export function getRouterModelId(): string | undefined {
180
- return resolveAgentConfigFromRuntime().routerModelId
181
- }
182
-
183
- export function getTeamConsultParticipants(): readonly string[] {
184
- return resolveAgentConfigFromRuntime().teamConsultParticipants
185
- }
186
-
187
- export function getCoreThreadProfile(coreType: string): CoreThreadProfile {
188
- return resolveAgentConfigFromRuntime().getCoreThreadProfile(coreType)
189
- }
190
-
191
- export function resolveAgentNameAlias(value: unknown): string | undefined {
135
+ export function resolveAgentNameAlias(agentConfig: ResolvedAgentConfig, value: unknown): string | undefined {
192
136
  if (typeof value !== 'string') return undefined
193
- return resolveAgentConfigFromRuntime().aliasMap.get(normalizeAgentLookupKey(value))
194
- }
195
-
196
- export function getCreateAgentRegistry(): AgentFactory {
197
- return resolveAgentFactoryConfigFromRuntime().createAgent
198
- }
199
-
200
- export function buildAgentTools(...args: Parameters<AgentToolBuilder>): ReturnType<AgentToolBuilder> {
201
- return resolveAgentFactoryConfigFromRuntime().buildAgentTools(...args)
202
- }
203
-
204
- export function getAgentRuntimeConfig(
205
- ...args: Parameters<AgentRuntimeConfigProvider>
206
- ): ReturnType<AgentRuntimeConfigProvider> {
207
- return resolveAgentFactoryConfigFromRuntime().getAgentRuntimeConfig(...args)
208
- }
209
-
210
- export function getPluginRuntime(): Record<string, unknown> | undefined {
211
- return resolveAgentFactoryConfigFromRuntime().pluginRuntime
212
- }
213
-
214
- const AGENT_MENTION_REGEX = /(^|[^\w])@([a-z][a-z0-9_-]*)\b/gi
215
-
216
- export interface AgentMentionMatch {
217
- agent: string
218
- mention: string
219
- index: number
220
- length: number
221
- }
222
-
223
- export function extractAgentMentions(
224
- message: string,
225
- agentConfig: ResolvedAgentConfig = resolveAgentConfigFromRuntime(),
226
- ): AgentMentionMatch[] {
227
- const matches: AgentMentionMatch[] = []
228
- if (!message.trim()) return matches
229
-
230
- const regex = new RegExp(AGENT_MENTION_REGEX)
231
- for (const rawMatch of message.matchAll(regex)) {
232
- const prefix = rawMatch[1]
233
- const rawAgent = rawMatch[2].toLowerCase()
234
- if (!agentConfig.rosterSet.has(rawAgent)) continue
235
-
236
- const index = rawMatch.index + prefix.length
237
- matches.push({ agent: rawAgent, mention: `@${rawAgent}`, index, length: rawAgent.length + 1 })
238
- }
239
-
240
- return matches
137
+ return agentConfig.aliasMap.get(normalizeAgentLookupKey(value))
241
138
  }
@@ -1,8 +1,8 @@
1
+ import type { ChatMode, CreateRoutedAgentOptions } from '@lota-sdk/shared'
1
2
  import type { ToolLoopAgent, ToolSet } from 'ai'
2
3
 
3
4
  import type { RecordIdRef } from '../db/record-id'
4
5
  import type { AgentRuntimeConfig, AgentRuntimeRuleOptions } from '../runtime/agent-runtime-policy'
5
- import type { ChatMode, CreateRoutedAgentOptions } from '../runtime/agent-types'
6
6
 
7
7
  export interface AgentToolBuilderParams {
8
8
  agentId: string