@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
@@ -1,3 +1,13 @@
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>()('ChatRunRegistry') {}
6
+
7
+ export const ChatRunRegistryLive = Layer.effect(
8
+ ChatRunRegistryTag,
9
+ Effect.gen(function* () {
10
+ const trackedRuns = yield* FiberMap.make<string>()
11
+ return new ChatRunRegistry(trackedRuns)
12
+ }),
13
+ )
@@ -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
+ >()('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,10 @@
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 { RuntimeConfigServiceTag } from '../effect/services'
6
+ import { ProviderEmbeddings } from '../embeddings/provider'
7
+ import { sha256Hex } from '../utils/crypto'
4
8
  import { CHARS_PER_TOKEN_ESTIMATE } from '../utils/string'
5
9
 
6
10
  type DocumentChunkEmbeddings = {
@@ -8,15 +12,19 @@ type DocumentChunkEmbeddings = {
8
12
  embedQuery(query: string): Promise<number[]>
9
13
  }
10
14
 
11
- function createDocumentChunkEmbeddings(): DocumentChunkEmbeddings {
12
- const embeddings = getDefaultEmbeddings()
15
+ function createDocumentChunkEmbeddings(embeddingModel: string, openRouterApiKey?: string): DocumentChunkEmbeddings {
16
+ const embeddings = new ProviderEmbeddings({ modelId: embeddingModel, openRouterApiKey })
13
17
 
14
18
  return {
15
- embedDocuments: async (documents) => await embeddings.embedDocuments(documents),
16
- embedQuery: async (query) => await embeddings.embedQuery(query),
19
+ embedDocuments: (documents) => embeddings.embedDocuments(documents),
20
+ embedQuery: (query) => embeddings.embedQuery(query),
17
21
  }
18
22
  }
19
23
 
24
+ function estimateDocumentChunkTokenCount(content: string): number {
25
+ return Math.max(1, Math.ceil(content.length / (CHARS_PER_TOKEN_ESTIMATE + 1)))
26
+ }
27
+
20
28
  export interface VersionedDocumentChunkRecordShape {
21
29
  chunkKey: string
22
30
  chunkIndex: number
@@ -28,46 +36,20 @@ export interface VersionedDocumentChunkRecordShape {
28
36
  archivedAt?: string | number | Date | null
29
37
  }
30
38
 
31
- export class DocumentChunkService {
32
- constructor(private readonly embeddings: DocumentChunkEmbeddings = createDocumentChunkEmbeddings()) {}
33
-
39
+ export interface DocumentChunkService {
34
40
  buildChunks(params: {
35
41
  source: string
36
42
  renderMode: 'markdown' | 'text' | 'pdf'
37
43
  text: string
38
44
  sectionPath?: string
39
45
  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
-
46
+ }): ParsedDocumentChunk[]
47
+ hashContent(content: string): string
60
48
  // Uses 4 chars/token (conservative estimate for document content which tends
61
49
  // 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: {
50
+ estimateTokenCount(content: string): number
51
+ embedQuery(query: string): Promise<number[]>
52
+ syncVersionedChunks<TRecord, TPayload>(params: {
71
53
  sourceVersionKey: string
72
54
  chunks: ParsedDocumentChunk[]
73
55
  loadExisting: () => Promise<TRecord[]>
@@ -81,64 +63,154 @@ export class DocumentChunkService {
81
63
  tokenEstimate: number
82
64
  }) => TPayload
83
65
  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
- }
116
-
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)
124
-
125
- if (!hasChanged) {
126
- return
127
- }
66
+ }): Effect.Effect<void, DocumentChunkServiceError>
67
+ }
128
68
 
129
- await params.update(existingRow, payload)
130
- }),
131
- )
69
+ class DocumentChunkServiceError extends Schema.TaggedErrorClass<DocumentChunkServiceError>()(
70
+ 'DocumentChunkServiceError',
71
+ { message: Schema.String, cause: Schema.Defect },
72
+ ) {}
73
+
74
+ function tryDocumentChunkPromise<A>(
75
+ message: string,
76
+ thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
77
+ ): Effect.Effect<A, DocumentChunkServiceError> {
78
+ return Effect.suspend(() => {
79
+ try {
80
+ const value = thunk()
81
+ if (Effect.isEffect(value)) {
82
+ return value.pipe(Effect.mapError((cause) => new DocumentChunkServiceError({ message, cause })))
83
+ }
84
+
85
+ return Effect.tryPromise({
86
+ try: () => Promise.resolve(value),
87
+ catch: (cause) => new DocumentChunkServiceError({ message, cause }),
88
+ })
89
+ } catch (cause) {
90
+ return Effect.fail(new DocumentChunkServiceError({ message, cause }))
91
+ }
92
+ })
93
+ }
132
94
 
133
- const removedCurrentVersionRows = existingRows.filter((row) => {
134
- const current = params.selectShape(row)
135
- return current.sourceVersionKey === params.sourceVersionKey && !seenChunkKeys.has(current.chunkKey)
136
- })
95
+ export function makeDocumentChunkService(embeddings: DocumentChunkEmbeddings): DocumentChunkService {
96
+ return {
97
+ buildChunks(params) {
98
+ if (
99
+ params.source === 'indexedOutcome' ||
100
+ params.source === 'websiteIntelligence' ||
101
+ params.renderMode === 'markdown'
102
+ ) {
103
+ return chunkMarkdownDocument({ text: params.text, baseSectionPath: params.sectionPath })
104
+ }
105
+
106
+ if (params.renderMode === 'pdf' && params.pages && params.pages.length > 0) {
107
+ return chunkPagedDocument({ pages: params.pages })
108
+ }
109
+
110
+ return chunkPlainTextDocument({ text: params.text, sectionPath: params.sectionPath })
111
+ },
112
+
113
+ hashContent(content) {
114
+ return sha256Hex(content)
115
+ },
116
+
117
+ estimateTokenCount(content) {
118
+ return estimateDocumentChunkTokenCount(content)
119
+ },
120
+
121
+ embedQuery(query) {
122
+ return embeddings.embedQuery(query)
123
+ },
124
+
125
+ syncVersionedChunks(params) {
126
+ return Effect.gen(function* () {
127
+ const [existingRows, embeddedChunks] = yield* Effect.all([
128
+ tryDocumentChunkPromise('Failed to load existing document chunks.', () => params.loadExisting()),
129
+ tryDocumentChunkPromise('Failed to embed document chunks.', () =>
130
+ embeddings.embedDocuments(params.chunks.map((chunk) => chunk.content)),
131
+ ),
132
+ ])
133
+
134
+ const existingByChunkKey = new Map(
135
+ existingRows
136
+ .filter((row) => params.selectShape(row).sourceVersionKey === params.sourceVersionKey)
137
+ .map((row) => [params.selectShape(row).chunkKey, row]),
138
+ )
139
+ const seenChunkKeys = new Set<string>()
140
+ const staleVersionRows = existingRows.filter(
141
+ (row) => params.selectShape(row).sourceVersionKey !== params.sourceVersionKey,
142
+ )
143
+
144
+ yield* tryDocumentChunkPromise('Failed to archive stale document chunks.', () =>
145
+ params.archive(staleVersionRows),
146
+ )
147
+
148
+ yield* Effect.forEach(
149
+ params.chunks,
150
+ (chunk, index) =>
151
+ Effect.gen(function* () {
152
+ const contentHash = sha256Hex(chunk.content)
153
+ const existingRow = existingByChunkKey.get(chunk.chunkKey)
154
+ const payload = params.buildPayload({
155
+ chunk,
156
+ embedding: embeddedChunks[index] ?? [],
157
+ contentHash,
158
+ tokenEstimate: estimateDocumentChunkTokenCount(chunk.content),
159
+ })
160
+
161
+ seenChunkKeys.add(chunk.chunkKey)
162
+
163
+ if (!existingRow) {
164
+ yield* tryDocumentChunkPromise(`Failed to create document chunk ${chunk.chunkKey}.`, () =>
165
+ params.create(payload),
166
+ )
167
+ return
168
+ }
169
+
170
+ const current = params.selectShape(existingRow)
171
+ const hasChanged =
172
+ current.contentHash !== contentHash ||
173
+ current.chunkIndex !== chunk.chunkIndex ||
174
+ (current.sectionPath ?? null) !== (chunk.sectionPath ?? null) ||
175
+ (current.pageStart ?? null) !== (chunk.pageStart ?? null) ||
176
+ (current.pageEnd ?? null) !== (chunk.pageEnd ?? null)
177
+
178
+ if (!hasChanged) {
179
+ return
180
+ }
181
+
182
+ yield* tryDocumentChunkPromise(`Failed to update document chunk ${chunk.chunkKey}.`, () =>
183
+ params.update(existingRow, payload),
184
+ )
185
+ }),
186
+ { concurrency: 'unbounded', discard: true },
187
+ )
188
+
189
+ const removedCurrentVersionRows = existingRows.filter((row) => {
190
+ const current = params.selectShape(row)
191
+ return current.sourceVersionKey === params.sourceVersionKey && !seenChunkKeys.has(current.chunkKey)
192
+ })
137
193
 
138
- await params.archive(removedCurrentVersionRows)
194
+ yield* tryDocumentChunkPromise('Failed to archive removed current-version chunks.', () =>
195
+ params.archive(removedCurrentVersionRows),
196
+ )
197
+ })
198
+ },
139
199
  }
140
200
  }
141
201
 
142
- export const documentChunkService = new DocumentChunkService()
202
+ export class DocumentChunkServiceTag extends Context.Service<DocumentChunkServiceTag, DocumentChunkService>()(
203
+ 'DocumentChunkService',
204
+ ) {}
205
+
206
+ export const DocumentChunkServiceLive = Layer.effect(
207
+ DocumentChunkServiceTag,
208
+ Effect.gen(function* () {
209
+ const runtimeConfig = yield* RuntimeConfigServiceTag
210
+ return makeDocumentChunkService(
211
+ createDocumentChunkEmbeddings(runtimeConfig.aiGateway.embeddingModel, runtimeConfig.aiGateway.openRouterApiKey),
212
+ )
213
+ }),
214
+ )
143
215
 
144
216
  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
+ }