@lota-sdk/core 0.4.8 → 0.4.10

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 (272) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +96 -22
  3. package/src/ai-gateway/ai-gateway.ts +766 -223
  4. package/src/config/agent-defaults.ts +189 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/background-processing.ts +1 -1
  7. package/src/config/constants.ts +8 -2
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +299 -19
  10. package/src/config/thread-defaults.ts +40 -20
  11. package/src/create-runtime.ts +200 -449
  12. package/src/db/base.service.ts +52 -28
  13. package/src/db/cursor-pagination.ts +71 -30
  14. package/src/db/memory-query-builder.ts +2 -1
  15. package/src/db/memory-store.helpers.ts +4 -7
  16. package/src/db/memory-store.ts +868 -601
  17. package/src/db/memory.ts +396 -280
  18. package/src/db/record-id.ts +32 -10
  19. package/src/db/schema-fingerprint.ts +30 -12
  20. package/src/db/service-normalization.ts +288 -0
  21. package/src/db/service.ts +912 -779
  22. package/src/db/startup.ts +153 -68
  23. package/src/db/transaction-conflict.ts +15 -0
  24. package/src/effect/awaitable-effect.ts +96 -0
  25. package/src/effect/errors.ts +121 -0
  26. package/src/effect/helpers.ts +123 -0
  27. package/src/effect/index.ts +24 -0
  28. package/src/effect/layers.ts +238 -0
  29. package/src/effect/runtime-ref.ts +25 -0
  30. package/src/effect/runtime.ts +46 -0
  31. package/src/effect/services.ts +61 -0
  32. package/src/effect/zod.ts +43 -0
  33. package/src/embeddings/provider.ts +128 -83
  34. package/src/index.ts +48 -1
  35. package/src/openrouter/direct-provider.ts +11 -35
  36. package/src/queues/autonomous-job.queue.ts +117 -73
  37. package/src/queues/context-compaction.queue.ts +50 -17
  38. package/src/queues/delayed-node-promotion.queue.ts +46 -17
  39. package/src/queues/document-processor.queue.ts +52 -77
  40. package/src/queues/memory-consolidation.queue.ts +47 -32
  41. package/src/queues/organization-learning.queue.ts +26 -4
  42. package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
  43. package/src/queues/plan-scheduler.queue.ts +97 -33
  44. package/src/queues/post-chat-memory.queue.ts +56 -26
  45. package/src/queues/queue-factory.ts +227 -59
  46. package/src/queues/standalone-worker.ts +39 -0
  47. package/src/queues/title-generation.queue.ts +45 -11
  48. package/src/redis/connection.ts +182 -113
  49. package/src/redis/index.ts +6 -8
  50. package/src/redis/org-memory-lock.ts +60 -27
  51. package/src/redis/redis-lease-lock.ts +200 -121
  52. package/src/redis/runtime-connection.ts +20 -0
  53. package/src/redis/stream-context.ts +92 -46
  54. package/src/runtime/agent-identity-overrides.ts +2 -2
  55. package/src/runtime/agent-runtime-policy.ts +5 -2
  56. package/src/runtime/agent-stream-helpers.ts +24 -9
  57. package/src/runtime/chat-run-orchestration.ts +102 -19
  58. package/src/runtime/chat-run-registry.ts +36 -2
  59. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  60. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
  61. package/src/runtime/domain-layer.ts +192 -0
  62. package/src/runtime/execution-plan-visibility.ts +2 -2
  63. package/src/runtime/execution-plan.ts +42 -15
  64. package/src/runtime/graph-designer.ts +16 -4
  65. package/src/runtime/helper-model.ts +139 -48
  66. package/src/runtime/index.ts +7 -8
  67. package/src/runtime/indexed-repositories-policy.ts +3 -3
  68. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
  69. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  70. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
  71. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  72. package/src/runtime/memory/memory-scope.ts +53 -0
  73. package/src/runtime/plugin-resolution.ts +124 -25
  74. package/src/runtime/plugin-types.ts +9 -1
  75. package/src/runtime/post-turn-side-effects.ts +177 -130
  76. package/src/runtime/retrieval-adapters.ts +40 -6
  77. package/src/runtime/runtime-accessors.ts +92 -0
  78. package/src/runtime/runtime-config.ts +150 -61
  79. package/src/runtime/runtime-extensions.ts +23 -25
  80. package/src/runtime/runtime-lifecycle.ts +124 -0
  81. package/src/runtime/runtime-services.ts +386 -0
  82. package/src/runtime/runtime-token.ts +47 -0
  83. package/src/runtime/social-chat/social-chat-agent-runner.ts +159 -0
  84. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
  85. package/src/runtime/social-chat/social-chat.ts +630 -0
  86. package/src/runtime/specialist-runner.ts +36 -10
  87. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
  88. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  89. package/src/runtime/thread-chat-helpers.ts +2 -2
  90. package/src/runtime/thread-plan-turn.ts +2 -1
  91. package/src/runtime/thread-turn-context.ts +183 -111
  92. package/src/runtime/turn-lifecycle.ts +93 -27
  93. package/src/services/agent-activity.service.ts +287 -203
  94. package/src/services/agent-executor.service.ts +253 -149
  95. package/src/services/artifact.service.ts +231 -149
  96. package/src/services/attachment.service.ts +171 -115
  97. package/src/services/autonomous-job.service.ts +890 -491
  98. package/src/services/background-work.service.ts +54 -0
  99. package/src/services/chat-run-registry.service.ts +13 -1
  100. package/src/services/context-compaction.service.ts +136 -86
  101. package/src/services/document-chunk.service.ts +151 -88
  102. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  103. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  104. package/src/services/execution-plan/execution-plan-graph.ts +278 -0
  105. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  106. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  107. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  108. package/src/services/feedback-loop.service.ts +132 -76
  109. package/src/services/global-orchestrator.service.ts +101 -168
  110. package/src/services/graph-full-routing.ts +193 -0
  111. package/src/services/index.ts +19 -21
  112. package/src/services/institutional-memory.service.ts +213 -125
  113. package/src/services/learned-skill.service.ts +368 -260
  114. package/src/services/memory/memory-conversation.ts +95 -0
  115. package/src/services/memory/memory-errors.ts +27 -0
  116. package/src/services/memory/memory-org-memory.ts +50 -0
  117. package/src/services/memory/memory-preseeded.ts +86 -0
  118. package/src/services/memory/memory-rerank.ts +297 -0
  119. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
  120. package/src/services/memory/memory.service.ts +674 -0
  121. package/src/services/memory/rerank.service.ts +201 -0
  122. package/src/services/monitoring-window.service.ts +92 -70
  123. package/src/services/mutating-approval.service.ts +62 -53
  124. package/src/services/node-workspace.service.ts +141 -98
  125. package/src/services/notification.service.ts +29 -16
  126. package/src/services/organization-member.service.ts +120 -66
  127. package/src/services/organization.service.ts +153 -77
  128. package/src/services/ownership-dispatcher.service.ts +456 -263
  129. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  130. package/src/services/plan/plan-agent-query.service.ts +322 -0
  131. package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
  132. package/src/services/plan/plan-artifact.service.ts +60 -0
  133. package/src/services/plan/plan-builder.service.ts +76 -0
  134. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  135. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  136. package/src/services/plan/plan-completion-side-effects.ts +169 -0
  137. package/src/services/plan/plan-coordination.service.ts +181 -0
  138. package/src/services/plan/plan-cycle.service.ts +405 -0
  139. package/src/services/plan/plan-deadline.service.ts +533 -0
  140. package/src/services/plan/plan-event-delivery.service.ts +266 -0
  141. package/src/services/plan/plan-executor-context.ts +35 -0
  142. package/src/services/plan/plan-executor-graph.ts +522 -0
  143. package/src/services/plan/plan-executor-helpers.ts +307 -0
  144. package/src/services/plan/plan-executor-persistence.ts +209 -0
  145. package/src/services/plan/plan-executor.service.ts +1737 -0
  146. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  147. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  148. package/src/services/plan/plan-run-serialization.ts +15 -0
  149. package/src/services/plan/plan-run.service.ts +637 -0
  150. package/src/services/plan/plan-scheduler.service.ts +379 -0
  151. package/src/services/plan/plan-template.service.ts +224 -0
  152. package/src/services/plan/plan-transaction-events.ts +36 -0
  153. package/src/services/plan/plan-validator.service.ts +907 -0
  154. package/src/services/plan/plan-workspace.service.ts +131 -0
  155. package/src/services/plugin-executor.service.ts +102 -68
  156. package/src/services/quality-metrics.service.ts +112 -94
  157. package/src/services/queue-job.service.ts +288 -231
  158. package/src/services/recent-activity-title.service.ts +73 -36
  159. package/src/services/recent-activity.service.ts +274 -259
  160. package/src/services/skill-resolver.service.ts +38 -12
  161. package/src/services/social-chat-history.service.ts +190 -122
  162. package/src/services/system-executor.service.ts +96 -61
  163. package/src/services/thread/thread-active-run.ts +203 -0
  164. package/src/services/thread/thread-bootstrap.ts +385 -0
  165. package/src/services/thread/thread-listing.ts +199 -0
  166. package/src/services/thread/thread-memory-block.ts +130 -0
  167. package/src/services/thread/thread-message.service.ts +379 -0
  168. package/src/services/thread/thread-record-store.ts +155 -0
  169. package/src/services/thread/thread-title.service.ts +74 -0
  170. package/src/services/thread/thread-turn-execution.ts +280 -0
  171. package/src/services/thread/thread-turn-message-context.ts +73 -0
  172. package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
  173. package/src/services/thread/thread-turn-streaming.ts +403 -0
  174. package/src/services/thread/thread-turn-tracing.ts +35 -0
  175. package/src/services/thread/thread-turn.ts +376 -0
  176. package/src/services/thread/thread.service.ts +344 -0
  177. package/src/services/user.service.ts +82 -32
  178. package/src/services/write-intent-validator.service.ts +63 -51
  179. package/src/storage/attachment-parser.ts +69 -27
  180. package/src/storage/attachment-storage.service.ts +334 -275
  181. package/src/storage/generated-document-storage.service.ts +66 -34
  182. package/src/system-agents/agent-result.ts +3 -1
  183. package/src/system-agents/context-compaction.agent.ts +3 -3
  184. package/src/system-agents/delegated-agent-factory.ts +159 -90
  185. package/src/system-agents/helper-agent-options.ts +1 -1
  186. package/src/system-agents/memory-reranker.agent.ts +3 -3
  187. package/src/system-agents/memory.agent.ts +3 -3
  188. package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
  189. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
  190. package/src/system-agents/skill-extractor.agent.ts +3 -3
  191. package/src/system-agents/skill-manager.agent.ts +3 -3
  192. package/src/system-agents/thread-router.agent.ts +157 -113
  193. package/src/system-agents/title-generator.agent.ts +3 -3
  194. package/src/tools/execution-plan.tool.ts +241 -171
  195. package/src/tools/fetch-webpage.tool.ts +29 -18
  196. package/src/tools/firecrawl-client.ts +26 -6
  197. package/src/tools/index.ts +1 -0
  198. package/src/tools/memory-block.tool.ts +14 -6
  199. package/src/tools/plan-approval.tool.ts +57 -47
  200. package/src/tools/read-file-parts.tool.ts +44 -33
  201. package/src/tools/remember-memory.tool.ts +65 -45
  202. package/src/tools/search-web.tool.ts +33 -22
  203. package/src/tools/search.tool.ts +41 -29
  204. package/src/tools/team-think.tool.ts +125 -84
  205. package/src/tools/user-questions.tool.ts +4 -3
  206. package/src/tools/web-tool-shared.ts +6 -0
  207. package/src/utils/async.ts +25 -22
  208. package/src/utils/crypto.ts +21 -0
  209. package/src/utils/date-time.ts +40 -1
  210. package/src/utils/errors.ts +111 -20
  211. package/src/utils/hono-error-handler.ts +24 -39
  212. package/src/utils/index.ts +2 -1
  213. package/src/utils/null-proto-record.ts +41 -0
  214. package/src/utils/sse-keepalive.ts +124 -21
  215. package/src/workers/bootstrap.ts +164 -52
  216. package/src/workers/memory-consolidation.worker.ts +325 -237
  217. package/src/workers/organization-learning.worker.ts +50 -16
  218. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  219. package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
  220. package/src/workers/skill-extraction.runner.ts +176 -93
  221. package/src/workers/utils/file-section-chunker.ts +8 -10
  222. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  223. package/src/workers/utils/repomix-file-sections.ts +2 -2
  224. package/src/workers/utils/thread-message-query.ts +97 -38
  225. package/src/workers/worker-utils.ts +74 -31
  226. package/src/config/debug-logger.ts +0 -47
  227. package/src/config/search.ts +0 -3
  228. package/src/redis/connection-accessor.ts +0 -26
  229. package/src/runtime/agent-types.ts +0 -1
  230. package/src/runtime/context-compaction-runtime.ts +0 -87
  231. package/src/runtime/memory-scope.ts +0 -43
  232. package/src/runtime/social-chat-agent-runner.ts +0 -118
  233. package/src/runtime/social-chat.ts +0 -516
  234. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  235. package/src/services/adaptive-playbook.service.ts +0 -152
  236. package/src/services/artifact-provenance.service.ts +0 -172
  237. package/src/services/chat-attachments.service.ts +0 -17
  238. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  239. package/src/services/execution-plan.service.ts +0 -1118
  240. package/src/services/memory.service.ts +0 -914
  241. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  242. package/src/services/plan-agent-query.service.ts +0 -267
  243. package/src/services/plan-artifact.service.ts +0 -50
  244. package/src/services/plan-builder.service.ts +0 -67
  245. package/src/services/plan-checkpoint.service.ts +0 -81
  246. package/src/services/plan-completion-side-effects.ts +0 -80
  247. package/src/services/plan-coordination.service.ts +0 -157
  248. package/src/services/plan-cycle.service.ts +0 -284
  249. package/src/services/plan-deadline.service.ts +0 -430
  250. package/src/services/plan-event-delivery.service.ts +0 -166
  251. package/src/services/plan-executor.service.ts +0 -1950
  252. package/src/services/plan-run.service.ts +0 -515
  253. package/src/services/plan-scheduler.service.ts +0 -240
  254. package/src/services/plan-template.service.ts +0 -177
  255. package/src/services/plan-validator.service.ts +0 -818
  256. package/src/services/plan-workspace.service.ts +0 -83
  257. package/src/services/rerank.service.ts +0 -156
  258. package/src/services/thread-message.service.ts +0 -275
  259. package/src/services/thread-plan-registry.service.ts +0 -22
  260. package/src/services/thread-title.service.ts +0 -39
  261. package/src/services/thread-turn-preparation.service.ts +0 -1147
  262. package/src/services/thread-turn.ts +0 -172
  263. package/src/services/thread.service.ts +0 -869
  264. package/src/utils/env.ts +0 -8
  265. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  266. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  267. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  268. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  269. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  270. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  271. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  272. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -0,0 +1,54 @@
1
+ import { Context, Effect, FiberSet, Layer, Schema } from 'effect'
2
+
3
+ /**
4
+ * Tagged error for the BackgroundWorkService surface. The service itself
5
+ * never returns this — it's reserved for future API extensions.
6
+ */
7
+ export class BackgroundWorkError extends Schema.TaggedErrorClass<BackgroundWorkError>()(
8
+ '@lota-sdk/core/BackgroundWorkError',
9
+ { message: Schema.String, cause: Schema.optional(Schema.Defect) },
10
+ ) {}
11
+
12
+ /**
13
+ * Supervised fire-and-forget fork pool.
14
+ *
15
+ * `run` schedules an effect on a process-wide FiberSet that is interrupted
16
+ * when the runtime scope closes — replaces ad-hoc `Effect.runFork`,
17
+ * `Effect.runForkWith`, and `Effect.forkDetach` call sites that would
18
+ * otherwise leak fibers across shutdown.
19
+ *
20
+ * Both methods accept any `Effect.Effect<A, E>`; failures are logged via
21
+ * `Effect.logError` and never propagate to the caller. `runForget` ignores
22
+ * both the result and the cause — use it for best-effort metrics, cache
23
+ * touches, and other strictly cosmetic side effects.
24
+ */
25
+ export class BackgroundWorkService extends Context.Service<
26
+ BackgroundWorkService,
27
+ {
28
+ readonly run: <A, E>(effect: Effect.Effect<A, E>, label?: string) => Effect.Effect<void>
29
+ readonly runForget: <A, E>(effect: Effect.Effect<A, E>, label?: string) => Effect.Effect<void>
30
+ }
31
+ >()('@lota-sdk/core/BackgroundWorkService') {}
32
+
33
+ export const BackgroundWorkServiceLive = Layer.effect(
34
+ BackgroundWorkService,
35
+ Effect.gen(function* () {
36
+ const set = yield* FiberSet.make<unknown, unknown>()
37
+
38
+ const run = <A, E>(effect: Effect.Effect<A, E>, label?: string): Effect.Effect<void> =>
39
+ Effect.asVoid(
40
+ FiberSet.run(
41
+ set,
42
+ effect.pipe(
43
+ Effect.withLogSpan(label ?? 'background'),
44
+ Effect.catchCause((cause) => Effect.logError(cause)),
45
+ ),
46
+ ),
47
+ )
48
+
49
+ const runForget = <A, E>(effect: Effect.Effect<A, E>, label?: string): Effect.Effect<void> =>
50
+ Effect.asVoid(FiberSet.run(set, effect.pipe(Effect.withLogSpan(label ?? 'background-forget'), Effect.ignore)))
51
+
52
+ return { run, runForget } as const
53
+ }),
54
+ )
@@ -1,3 +1,15 @@
1
+ import { Context, Effect, FiberMap, Layer } from 'effect'
2
+
1
3
  import { ChatRunRegistry } from '../runtime/chat-run-registry'
2
4
 
3
- export const chatRunRegistry = new ChatRunRegistry()
5
+ export class ChatRunRegistryTag extends Context.Service<ChatRunRegistryTag, ChatRunRegistry>()(
6
+ '@lota-sdk/core/ChatRunRegistry',
7
+ ) {}
8
+
9
+ export const ChatRunRegistryLive = Layer.effect(
10
+ ChatRunRegistryTag,
11
+ Effect.gen(function* () {
12
+ const trackedRuns = yield* FiberMap.make<string>()
13
+ return new ChatRunRegistry(trackedRuns)
14
+ }),
15
+ )
@@ -1,16 +1,26 @@
1
1
  import type { ChatMessage } from '@lota-sdk/shared'
2
+ import { Context, Effect, Layer } from 'effect'
2
3
 
3
4
  import { chatLogger } from '../config/logger'
4
5
  import type { RecordIdRef } from '../db/record-id'
5
6
  import { recordIdToString } from '../db/record-id'
6
- import { databaseService } from '../db/service'
7
+ import type { SurrealDBService } from '../db/service'
7
8
  import { TABLES } from '../db/tables'
8
- import { getRedisConnection } from '../redis/connection-accessor'
9
- import { withRedisLeaseLock } from '../redis/redis-lease-lock'
10
- import { CONTEXT_WINDOW_TOKENS, THREAD_RAW_TAIL_MESSAGES } from '../runtime/context-compaction-constants'
11
- import { contextCompactionRuntime, compactMemoryBlockSummary } from './context-compaction-runtime.singleton'
12
- import { threadMessageService } from './thread-message.service'
13
- import { ThreadSchema } from './thread.types'
9
+ import { BadRequestError } from '../effect/errors'
10
+ import { DatabaseServiceTag, RedisServiceTag } from '../effect/services'
11
+ import type { RedisConnectionManager } from '../redis/connection'
12
+ import { withLeaseLock } from '../redis/redis-lease-lock'
13
+ import {
14
+ CONTEXT_WINDOW_TOKENS,
15
+ THREAD_RAW_TAIL_MESSAGES,
16
+ } from '../runtime/context-compaction/context-compaction-constants'
17
+ import { createWiredContextCompactionRuntime } from '../runtime/context-compaction/context-compaction-runtime'
18
+ import type { HelperModelRuntime } from '../runtime/helper-model'
19
+ import { HelperModelTag } from '../runtime/helper-model'
20
+ import { nowEpochMillis } from '../utils/date-time'
21
+ import type { makeThreadMessageService } from './thread/thread-message.service'
22
+ import { ThreadMessageServiceTag } from './thread/thread-message.service'
23
+ import { ThreadSchema } from './thread/thread.types'
14
24
 
15
25
  interface PersistedCompactionMetrics {
16
26
  domain: 'thread'
@@ -24,87 +34,127 @@ interface PersistedCompactionMetrics {
24
34
  estimatedTokens: number
25
35
  }
26
36
 
27
- class ContextCompactionService {
28
- createSummaryMessage(summaryText: string) {
29
- return contextCompactionRuntime.createSummaryMessage(summaryText)
30
- }
31
-
32
- estimateThreshold(contextSize = CONTEXT_WINDOW_TOKENS): number {
33
- return contextCompactionRuntime.estimateThreshold(contextSize)
34
- }
35
-
36
- shouldCompactHistory(params: { summaryText: string; liveMessages: ChatMessage[]; contextSize?: number }) {
37
- return contextCompactionRuntime.shouldCompactHistory(params)
38
- }
39
-
40
- async compactThreadHistory(params: { threadId: RecordIdRef; contextSize?: number }): Promise<{ compacted: boolean }> {
41
- const entityId = recordIdToString(params.threadId, TABLES.THREAD)
42
-
43
- return withRedisLeaseLock(
44
- {
45
- redis: getRedisConnection(),
46
- lockKey: `compaction:lock:${entityId}`,
47
- lockTtlMs: 120_000,
48
- maxWaitMs: 30_000,
49
- label: 'context-compaction',
50
- },
51
- async () => {
52
- const thread = await databaseService.findOne(TABLES.THREAD, { id: params.threadId }, ThreadSchema)
53
- if (!thread) {
54
- throw new Error(`Thread not found for compaction: ${entityId}`)
55
- }
56
-
57
- const liveMessages = await threadMessageService.listMessagesAfterCursor(
58
- params.threadId,
59
- typeof thread.lastCompactedMessageId === 'string' ? thread.lastCompactedMessageId : undefined,
60
- )
61
-
62
- const result = await contextCompactionRuntime.compactHistory({
63
- summaryText: typeof thread.compactionSummary === 'string' ? thread.compactionSummary : '',
64
- liveMessages,
65
- tailMessageCount: THREAD_RAW_TAIL_MESSAGES,
66
- contextSize: params.contextSize,
67
- })
68
-
69
- if (!result.compacted || !result.lastCompactedMessageId) {
70
- return { compacted: false }
71
- }
72
-
73
- if (result.compactedMessages.length > 0) {
74
- await threadMessageService.upsertMessages({ threadId: params.threadId, messages: result.compactedMessages })
75
- }
76
-
77
- await databaseService.update(
78
- TABLES.THREAD,
79
- params.threadId,
80
- { compactionSummary: result.summaryText, lastCompactedMessageId: result.lastCompactedMessageId },
81
- ThreadSchema,
82
- )
83
-
84
- this.logCompactionMetrics({
85
- domain: 'thread',
86
- entityId,
87
- inputChars: result.inputChars,
88
- outputChars: result.outputChars,
89
- savedChars: Math.max(0, result.inputChars - result.outputChars),
90
- summaryLength: result.summaryText.length,
91
- compactedMessageCount: result.compactedMessageCount,
92
- remainingMessageCount: result.remainingMessageCount,
93
- estimatedTokens: result.estimatedTokens,
94
- })
95
-
96
- return { compacted: true }
97
- },
98
- )
99
- }
37
+ function logCompactionMetrics(metrics: PersistedCompactionMetrics): void {
38
+ chatLogger.info`Persisted chat compaction applied metrics=${JSON.stringify(metrics)}`
39
+ }
100
40
 
101
- async compactMemoryBlock(params: { previousSummary: string; newEntriesText: string }): Promise<string> {
102
- return compactMemoryBlockSummary(params)
103
- }
41
+ interface ContextCompactionDeps {
42
+ db: SurrealDBService
43
+ redis: RedisConnectionManager
44
+ threadMessageService: ReturnType<typeof makeThreadMessageService>
45
+ helperModelRuntime: HelperModelRuntime
46
+ }
104
47
 
105
- private logCompactionMetrics(metrics: PersistedCompactionMetrics): void {
106
- chatLogger.info`Persisted chat compaction applied metrics=${JSON.stringify(metrics)}`
48
+ export function makeContextCompactionService(deps: ContextCompactionDeps) {
49
+ const { db, redis, threadMessageService, helperModelRuntime } = deps
50
+ const contextCompactionRuntime = createWiredContextCompactionRuntime({
51
+ helperModelRuntime,
52
+ now: nowEpochMillis,
53
+ randomId: () => Bun.randomUUIDv7(),
54
+ })
55
+
56
+ const compactThreadHistoryEffect = (params: { threadId: RecordIdRef; contextSize?: number }) =>
57
+ Effect.gen(function* () {
58
+ const entityId = recordIdToString(params.threadId, TABLES.THREAD)
59
+
60
+ return yield* withLeaseLock(
61
+ {
62
+ redis: redis.getConnection(),
63
+ lockKey: `compaction:lock:${entityId}`,
64
+ lockTtlMs: 120_000,
65
+ maxWaitMs: 30_000,
66
+ label: 'context-compaction',
67
+ },
68
+ () =>
69
+ Effect.gen(function* () {
70
+ const thread = yield* db.findOne(TABLES.THREAD, { id: params.threadId }, ThreadSchema)
71
+ if (!thread) {
72
+ return yield* new BadRequestError({ message: `Thread not found for compaction: ${entityId}` })
73
+ }
74
+
75
+ const liveMessages = yield* threadMessageService.listMessagesAfterCursorEffect(
76
+ params.threadId,
77
+ typeof thread.lastCompactedMessageId === 'string' ? thread.lastCompactedMessageId : undefined,
78
+ )
79
+
80
+ const result = yield* Effect.tryPromise(() =>
81
+ contextCompactionRuntime.compactHistory({
82
+ summaryText: typeof thread.compactionSummary === 'string' ? thread.compactionSummary : '',
83
+ liveMessages,
84
+ tailMessageCount: THREAD_RAW_TAIL_MESSAGES,
85
+ contextSize: params.contextSize,
86
+ }),
87
+ )
88
+
89
+ if (!result.compacted || !result.lastCompactedMessageId) {
90
+ return { compacted: false }
91
+ }
92
+
93
+ if (result.compactedMessages.length > 0) {
94
+ yield* threadMessageService.upsertMessagesEffect({
95
+ threadId: params.threadId,
96
+ messages: result.compactedMessages,
97
+ })
98
+ }
99
+
100
+ yield* db.update(
101
+ TABLES.THREAD,
102
+ params.threadId,
103
+ { compactionSummary: result.summaryText, lastCompactedMessageId: result.lastCompactedMessageId },
104
+ ThreadSchema,
105
+ )
106
+
107
+ logCompactionMetrics({
108
+ domain: 'thread',
109
+ entityId,
110
+ inputChars: result.inputChars,
111
+ outputChars: result.outputChars,
112
+ savedChars: Math.max(0, result.inputChars - result.outputChars),
113
+ summaryLength: result.summaryText.length,
114
+ compactedMessageCount: result.compactedMessageCount,
115
+ remainingMessageCount: result.remainingMessageCount,
116
+ estimatedTokens: result.estimatedTokens,
117
+ })
118
+
119
+ return { compacted: true }
120
+ }),
121
+ )
122
+ })
123
+
124
+ const compactMemoryBlockEffect = (params: { previousSummary: string; newEntriesText: string }) =>
125
+ Effect.tryPromise(() => contextCompactionRuntime.compactMemoryBlockSummary(params))
126
+
127
+ return {
128
+ createSummaryMessage(summaryText: string) {
129
+ return contextCompactionRuntime.createSummaryMessage(summaryText)
130
+ },
131
+
132
+ estimateThreshold(contextSize = CONTEXT_WINDOW_TOKENS): number {
133
+ return contextCompactionRuntime.estimateThreshold(contextSize)
134
+ },
135
+
136
+ shouldCompactHistory(params: { summaryText: string; liveMessages: ChatMessage[]; contextSize?: number }) {
137
+ return contextCompactionRuntime.shouldCompactHistory(params)
138
+ },
139
+
140
+ compactThreadHistory: compactThreadHistoryEffect,
141
+
142
+ compactMemoryBlock: compactMemoryBlockEffect,
107
143
  }
108
144
  }
109
145
 
110
- export const contextCompactionService = new ContextCompactionService()
146
+ export class ContextCompactionServiceTag extends Context.Service<
147
+ ContextCompactionServiceTag,
148
+ ReturnType<typeof makeContextCompactionService>
149
+ >()('@lota-sdk/core/ContextCompactionService') {}
150
+
151
+ export const ContextCompactionServiceLive = Layer.effect(
152
+ ContextCompactionServiceTag,
153
+ Effect.gen(function* () {
154
+ const db = yield* DatabaseServiceTag
155
+ const redis = yield* RedisServiceTag
156
+ const threadMessageService = yield* ThreadMessageServiceTag
157
+ const helperModelRuntime = yield* HelperModelTag
158
+ return makeContextCompactionService({ db, redis, threadMessageService, helperModelRuntime })
159
+ }),
160
+ )
@@ -1,6 +1,11 @@
1
+ import { Context, Schema, Effect, Layer } from 'effect'
2
+
1
3
  import { chunkMarkdownDocument, chunkPagedDocument, chunkPlainTextDocument } from '../document/org-document-chunking'
2
4
  import type { ParsedDocumentChunk } from '../document/org-document-chunking'
3
- import { getDefaultEmbeddings } from '../embeddings/provider'
5
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
6
+ import { RuntimeConfigServiceTag } from '../effect/services'
7
+ import { ProviderEmbeddings } from '../embeddings/provider'
8
+ import { sha256Hex } from '../utils/crypto'
4
9
  import { CHARS_PER_TOKEN_ESTIMATE } from '../utils/string'
5
10
 
6
11
  type DocumentChunkEmbeddings = {
@@ -8,15 +13,19 @@ type DocumentChunkEmbeddings = {
8
13
  embedQuery(query: string): Promise<number[]>
9
14
  }
10
15
 
11
- function createDocumentChunkEmbeddings(): DocumentChunkEmbeddings {
12
- const embeddings = getDefaultEmbeddings()
16
+ function createDocumentChunkEmbeddings(embeddingModel: string, openRouterApiKey?: string): DocumentChunkEmbeddings {
17
+ const embeddings = new ProviderEmbeddings({ modelId: embeddingModel, openRouterApiKey })
13
18
 
14
19
  return {
15
- embedDocuments: async (documents) => await embeddings.embedDocuments(documents),
16
- embedQuery: async (query) => await embeddings.embedQuery(query),
20
+ embedDocuments: (documents) => embeddings.embedDocuments(documents),
21
+ embedQuery: (query) => embeddings.embedQuery(query),
17
22
  }
18
23
  }
19
24
 
25
+ function estimateDocumentChunkTokenCount(content: string): number {
26
+ return Math.max(1, Math.ceil(content.length / (CHARS_PER_TOKEN_ESTIMATE + 1)))
27
+ }
28
+
20
29
  export interface VersionedDocumentChunkRecordShape {
21
30
  chunkKey: string
22
31
  chunkIndex: number
@@ -28,46 +37,20 @@ export interface VersionedDocumentChunkRecordShape {
28
37
  archivedAt?: string | number | Date | null
29
38
  }
30
39
 
31
- export class DocumentChunkService {
32
- constructor(private readonly embeddings: DocumentChunkEmbeddings = createDocumentChunkEmbeddings()) {}
33
-
40
+ export interface DocumentChunkService {
34
41
  buildChunks(params: {
35
42
  source: string
36
43
  renderMode: 'markdown' | 'text' | 'pdf'
37
44
  text: string
38
45
  sectionPath?: string
39
46
  pages?: Array<{ pageNumber: number; text: string }>
40
- }): ParsedDocumentChunk[] {
41
- if (
42
- params.source === 'indexedOutcome' ||
43
- params.source === 'websiteIntelligence' ||
44
- params.renderMode === 'markdown'
45
- ) {
46
- return chunkMarkdownDocument({ text: params.text, baseSectionPath: params.sectionPath })
47
- }
48
-
49
- if (params.renderMode === 'pdf' && params.pages && params.pages.length > 0) {
50
- return chunkPagedDocument({ pages: params.pages })
51
- }
52
-
53
- return chunkPlainTextDocument({ text: params.text, sectionPath: params.sectionPath })
54
- }
55
-
56
- hashContent(content: string): string {
57
- return new Bun.CryptoHasher('sha256').update(content).digest('hex')
58
- }
59
-
47
+ }): ParsedDocumentChunk[]
48
+ hashContent(content: string): string
60
49
  // Uses 4 chars/token (conservative estimate for document content which tends
61
50
  // to have longer words than conversational text where 3 chars/token is used).
62
- estimateTokenCount(content: string): number {
63
- return Math.max(1, Math.ceil(content.length / (CHARS_PER_TOKEN_ESTIMATE + 1)))
64
- }
65
-
66
- async embedQuery(query: string): Promise<number[]> {
67
- return this.embeddings.embedQuery(query)
68
- }
69
-
70
- async syncVersionedChunks<TRecord, TPayload>(params: {
51
+ estimateTokenCount(content: string): number
52
+ embedQuery(query: string): Promise<number[]>
53
+ syncVersionedChunks<TRecord, TPayload>(params: {
71
54
  sourceVersionKey: string
72
55
  chunks: ParsedDocumentChunk[]
73
56
  loadExisting: () => Promise<TRecord[]>
@@ -81,64 +64,144 @@ export class DocumentChunkService {
81
64
  tokenEstimate: number
82
65
  }) => TPayload
83
66
  selectShape: (row: TRecord) => VersionedDocumentChunkRecordShape
84
- }): Promise<void> {
85
- const existingRows = await params.loadExisting()
86
- const existingByChunkKey = new Map(
87
- existingRows
88
- .filter((row) => params.selectShape(row).sourceVersionKey === params.sourceVersionKey)
89
- .map((row) => [params.selectShape(row).chunkKey, row]),
90
- )
91
- const embeddings = await this.embeddings.embedDocuments(params.chunks.map((chunk) => chunk.content))
92
- const seenChunkKeys = new Set<string>()
93
- const staleVersionRows = existingRows.filter(
94
- (row) => params.selectShape(row).sourceVersionKey !== params.sourceVersionKey,
95
- )
96
-
97
- await params.archive(staleVersionRows)
98
-
99
- await Promise.all(
100
- params.chunks.map(async (chunk, index) => {
101
- const contentHash = this.hashContent(chunk.content)
102
- const existingRow = existingByChunkKey.get(chunk.chunkKey)
103
- const payload = params.buildPayload({
104
- chunk,
105
- embedding: embeddings[index] ?? [],
106
- contentHash,
107
- tokenEstimate: this.estimateTokenCount(chunk.content),
108
- })
109
-
110
- seenChunkKeys.add(chunk.chunkKey)
111
-
112
- if (!existingRow) {
113
- await params.create(payload)
114
- return
115
- }
67
+ }): Effect.Effect<void, DocumentChunkServiceError>
68
+ }
116
69
 
117
- const current = params.selectShape(existingRow)
118
- const hasChanged =
119
- current.contentHash !== contentHash ||
120
- current.chunkIndex !== chunk.chunkIndex ||
121
- (current.sectionPath ?? null) !== (chunk.sectionPath ?? null) ||
122
- (current.pageStart ?? null) !== (chunk.pageStart ?? null) ||
123
- (current.pageEnd ?? null) !== (chunk.pageEnd ?? null)
70
+ class DocumentChunkServiceError extends Schema.TaggedErrorClass<DocumentChunkServiceError>()(
71
+ 'DocumentChunkServiceError',
72
+ { message: Schema.String, cause: Schema.Defect },
73
+ ) {}
124
74
 
125
- if (!hasChanged) {
126
- return
127
- }
75
+ const effectTryDocumentChunkPromise = makeEffectTryPromiseWithMessage(
76
+ (message, cause) => new DocumentChunkServiceError({ message, cause }),
77
+ )
128
78
 
129
- await params.update(existingRow, payload)
130
- }),
131
- )
79
+ function tryDocumentChunkPromise<A>(
80
+ message: string,
81
+ evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
82
+ ): Effect.Effect<A, DocumentChunkServiceError> {
83
+ return effectTryDocumentChunkPromise(evaluate, message)
84
+ }
132
85
 
133
- const removedCurrentVersionRows = existingRows.filter((row) => {
134
- const current = params.selectShape(row)
135
- return current.sourceVersionKey === params.sourceVersionKey && !seenChunkKeys.has(current.chunkKey)
136
- })
86
+ export function makeDocumentChunkService(embeddings: DocumentChunkEmbeddings): DocumentChunkService {
87
+ return {
88
+ buildChunks(params) {
89
+ if (
90
+ params.source === 'indexedOutcome' ||
91
+ params.source === 'websiteIntelligence' ||
92
+ params.renderMode === 'markdown'
93
+ ) {
94
+ return chunkMarkdownDocument({ text: params.text, baseSectionPath: params.sectionPath })
95
+ }
96
+
97
+ if (params.renderMode === 'pdf' && params.pages && params.pages.length > 0) {
98
+ return chunkPagedDocument({ pages: params.pages })
99
+ }
100
+
101
+ return chunkPlainTextDocument({ text: params.text, sectionPath: params.sectionPath })
102
+ },
103
+
104
+ hashContent(content) {
105
+ return sha256Hex(content)
106
+ },
107
+
108
+ estimateTokenCount(content) {
109
+ return estimateDocumentChunkTokenCount(content)
110
+ },
111
+
112
+ embedQuery(query) {
113
+ return embeddings.embedQuery(query)
114
+ },
115
+
116
+ syncVersionedChunks(params) {
117
+ return Effect.gen(function* () {
118
+ const [existingRows, embeddedChunks] = yield* Effect.all([
119
+ tryDocumentChunkPromise('Failed to load existing document chunks.', () => params.loadExisting()),
120
+ tryDocumentChunkPromise('Failed to embed document chunks.', () =>
121
+ embeddings.embedDocuments(params.chunks.map((chunk) => chunk.content)),
122
+ ),
123
+ ])
124
+
125
+ const existingByChunkKey = new Map(
126
+ existingRows
127
+ .filter((row) => params.selectShape(row).sourceVersionKey === params.sourceVersionKey)
128
+ .map((row) => [params.selectShape(row).chunkKey, row]),
129
+ )
130
+ const seenChunkKeys = new Set<string>()
131
+ const staleVersionRows = existingRows.filter(
132
+ (row) => params.selectShape(row).sourceVersionKey !== params.sourceVersionKey,
133
+ )
134
+
135
+ yield* tryDocumentChunkPromise('Failed to archive stale document chunks.', () =>
136
+ params.archive(staleVersionRows),
137
+ )
138
+
139
+ yield* Effect.forEach(
140
+ params.chunks,
141
+ (chunk, index) =>
142
+ Effect.gen(function* () {
143
+ const contentHash = sha256Hex(chunk.content)
144
+ const existingRow = existingByChunkKey.get(chunk.chunkKey)
145
+ const payload = params.buildPayload({
146
+ chunk,
147
+ embedding: embeddedChunks[index] ?? [],
148
+ contentHash,
149
+ tokenEstimate: estimateDocumentChunkTokenCount(chunk.content),
150
+ })
151
+
152
+ seenChunkKeys.add(chunk.chunkKey)
153
+
154
+ if (!existingRow) {
155
+ yield* tryDocumentChunkPromise(`Failed to create document chunk ${chunk.chunkKey}.`, () =>
156
+ params.create(payload),
157
+ )
158
+ return
159
+ }
160
+
161
+ const current = params.selectShape(existingRow)
162
+ const hasChanged =
163
+ current.contentHash !== contentHash ||
164
+ current.chunkIndex !== chunk.chunkIndex ||
165
+ (current.sectionPath ?? null) !== (chunk.sectionPath ?? null) ||
166
+ (current.pageStart ?? null) !== (chunk.pageStart ?? null) ||
167
+ (current.pageEnd ?? null) !== (chunk.pageEnd ?? null)
168
+
169
+ if (!hasChanged) {
170
+ return
171
+ }
172
+
173
+ yield* tryDocumentChunkPromise(`Failed to update document chunk ${chunk.chunkKey}.`, () =>
174
+ params.update(existingRow, payload),
175
+ )
176
+ }),
177
+ { concurrency: 'unbounded', discard: true },
178
+ )
179
+
180
+ const removedCurrentVersionRows = existingRows.filter((row) => {
181
+ const current = params.selectShape(row)
182
+ return current.sourceVersionKey === params.sourceVersionKey && !seenChunkKeys.has(current.chunkKey)
183
+ })
137
184
 
138
- await params.archive(removedCurrentVersionRows)
185
+ yield* tryDocumentChunkPromise('Failed to archive removed current-version chunks.', () =>
186
+ params.archive(removedCurrentVersionRows),
187
+ )
188
+ })
189
+ },
139
190
  }
140
191
  }
141
192
 
142
- export const documentChunkService = new DocumentChunkService()
193
+ export class DocumentChunkServiceTag extends Context.Service<DocumentChunkServiceTag, DocumentChunkService>()(
194
+ '@lota-sdk/core/DocumentChunkService',
195
+ ) {}
196
+
197
+ export const DocumentChunkServiceLive = Layer.effect(
198
+ DocumentChunkServiceTag,
199
+ Effect.gen(function* () {
200
+ const runtimeConfig = yield* RuntimeConfigServiceTag
201
+ return makeDocumentChunkService(
202
+ createDocumentChunkEmbeddings(runtimeConfig.aiGateway.embeddingModel, runtimeConfig.aiGateway.openRouterApiKey),
203
+ )
204
+ }),
205
+ )
143
206
 
144
207
  export type { ParsedDocumentChunk }
@@ -0,0 +1,26 @@
1
+ import type { ChatMessage } from '@lota-sdk/shared'
2
+
3
+ import { readApprovalContinuationResponse } from '../../runtime/approval-continuation'
4
+ import type { HumanNodeResponsePayload } from '../plan/plan-executor-helpers'
5
+
6
+ export function buildApprovalResponseFromMessages(
7
+ messages: ChatMessage[],
8
+ ): { approvalId: string; response: HumanNodeResponsePayload } | null {
9
+ for (const message of [...messages].reverse()) {
10
+ if (message.role !== 'assistant') continue
11
+ const approvalResponse = readApprovalContinuationResponse(message)
12
+ if (!approvalResponse) continue
13
+
14
+ const response: HumanNodeResponsePayload = {
15
+ approved: approvalResponse.approved,
16
+ requiredEdits: [...approvalResponse.requiredEdits],
17
+ }
18
+ if (approvalResponse.comments) {
19
+ response.comments = approvalResponse.comments
20
+ }
21
+
22
+ return { approvalId: approvalResponse.approvalId, response }
23
+ }
24
+
25
+ return null
26
+ }
@@ -0,0 +1,29 @@
1
+ import type { RecordIdInput } from '../../db/record-id'
2
+ import { recordIdToString } from '../../db/record-id'
3
+ import { TABLES } from '../../db/tables'
4
+
5
+ export function aggregateBlockingIssues(issues: Array<{ code: string; message: string }>): string {
6
+ return issues.map((issue) => `${issue.code}: ${issue.message}`).join(' | ')
7
+ }
8
+
9
+ export function hasCrossThreadSourceContext(
10
+ sourceThreadId: RecordIdInput | undefined,
11
+ threadId: RecordIdInput,
12
+ ): boolean {
13
+ if (!sourceThreadId) {
14
+ return false
15
+ }
16
+
17
+ return recordIdToString(sourceThreadId, TABLES.THREAD) !== recordIdToString(threadId, TABLES.THREAD)
18
+ }
19
+
20
+ export function isPlanVisibleInThreadContext(
21
+ threadId: RecordIdInput,
22
+ params: { threadId: RecordIdInput; sourceThreadId?: RecordIdInput },
23
+ ): boolean {
24
+ const currentThreadId = recordIdToString(threadId, TABLES.THREAD)
25
+ return (
26
+ currentThreadId === recordIdToString(params.threadId, TABLES.THREAD) ||
27
+ (params.sourceThreadId !== undefined && currentThreadId === recordIdToString(params.sourceThreadId, TABLES.THREAD))
28
+ )
29
+ }