@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
@@ -1,8 +1,8 @@
1
1
  import type { ChatMessage } from '@lota-sdk/shared'
2
2
  import type { LanguageModelUsage, UIMessageStreamOptions } from 'ai'
3
- import { Duration, Effect, Fiber } from 'effect'
3
+ import { Duration, Effect, Exit, Scope } from 'effect'
4
4
 
5
- import { getAgentDisplayNames, getLeadAgentDisplayName } from '../config/agent-defaults'
5
+ import type { ResolvedAgentConfig } from '../config/agent-defaults'
6
6
  import { nowEpochMillis } from '../utils/date-time'
7
7
  import { readRecord as _readRecord } from '../utils/string'
8
8
 
@@ -106,17 +106,20 @@ export function createTimedAbortSignal(parentSignal: AbortSignal, timeoutMs: num
106
106
  const abortFromParent = () => {
107
107
  controller.abort((parentSignal as AbortSignal & { reason?: unknown }).reason)
108
108
  }
109
- const timeoutFiber = Effect.runFork(
110
- Effect.sleep(Duration.millis(timeoutMs)).pipe(
111
- Effect.tap(() =>
112
- Effect.sync(() => {
113
- if (disposed || controller.signal.aborted) return
114
- didTimeout = true
115
- controller.abort(new Error(`Timed out after ${timeoutMs}ms`))
116
- }),
117
- ),
109
+
110
+ // Own a scope so the timeout fiber is cleaned up via Scope.close() on dispose,
111
+ // matching Effect v4 best practices (forkScoped + scope-bound finalizers).
112
+ const scope = Scope.makeUnsafe()
113
+ const timeoutEffect = Effect.sleep(Duration.millis(timeoutMs)).pipe(
114
+ Effect.andThen(
115
+ Effect.sync(() => {
116
+ if (disposed || controller.signal.aborted) return
117
+ didTimeout = true
118
+ controller.abort(new Error(`Timed out after ${timeoutMs}ms`))
119
+ }),
118
120
  ),
119
121
  )
122
+ Effect.runFork(Scope.provide(Effect.forkScoped(timeoutEffect), scope))
120
123
 
121
124
  if (parentSignal.aborted) {
122
125
  abortFromParent()
@@ -128,16 +131,22 @@ export function createTimedAbortSignal(parentSignal: AbortSignal, timeoutMs: num
128
131
  signal: controller.signal,
129
132
  didTimeout: () => didTimeout,
130
133
  dispose: () => {
134
+ if (disposed) return
131
135
  disposed = true
132
136
  parentSignal.removeEventListener('abort', abortFromParent)
133
- void Effect.runFork(Fiber.interrupt(timeoutFiber))
137
+ void Effect.runPromise(Scope.close(scope, Exit.void).pipe(Effect.catchCause(() => Effect.void)))
134
138
  },
135
139
  }
136
140
  }
137
141
 
138
- export function buildSpecialistTaskMessage(params: { agentId: string; task: string }): ChatMessage {
139
- const displayName = getAgentDisplayNames()[params.agentId] ?? params.agentId
140
- const leadAgentDisplayName = getLeadAgentDisplayName()
142
+ export function buildSpecialistTaskMessage(params: {
143
+ agentConfig: ResolvedAgentConfig
144
+ agentId: string
145
+ task: string
146
+ }): ChatMessage {
147
+ const displayName = params.agentConfig.displayNames[params.agentId] ?? params.agentId
148
+ const leadAgentDisplayName =
149
+ params.agentConfig.displayNames[params.agentConfig.leadAgentId] ?? params.agentConfig.leadAgentId
141
150
  return {
142
151
  id: Bun.randomUUIDv7(),
143
152
  role: 'user',
@@ -37,7 +37,7 @@ interface CompactionCoordination {
37
37
  }
38
38
 
39
39
  export class CompactionCoordinationTag extends Context.Service<CompactionCoordinationTag, CompactionCoordination>()(
40
- 'CompactionCoordination',
40
+ '@lota-sdk/core/CompactionCoordination',
41
41
  ) {}
42
42
 
43
43
  export const CompactionCoordinationLive = Layer.effect(
@@ -48,33 +48,31 @@ function tryContextCompactionPromise<A>(
48
48
  })
49
49
  }
50
50
 
51
- function runContextCompacter(helperModelRuntime: HelperModelRuntime, params: ContextCompactionRunnerParams) {
52
- return Effect.runPromise(
53
- tryContextCompactionPromise('Failed to compact runtime context.', () =>
54
- helperModelRuntime.generateHelperStructured({
55
- tag: 'context-compaction',
56
- createAgent: createContextCompactionAgent,
57
- messages: [
58
- {
59
- role: 'user',
60
- content: buildContextCompactionPrompt({
61
- previousSummary: params.previousSummary,
62
- transcript: params.transcript,
63
- }),
64
- },
65
- ],
66
- schema: ContextCompactionOutputSchema,
67
- maxOutputTokens: 8_000,
68
- }),
69
- ).pipe(Effect.map(parseCompactionOutput)),
70
- )
51
+ function runContextCompacterEffect(helperModelRuntime: HelperModelRuntime, params: ContextCompactionRunnerParams) {
52
+ return tryContextCompactionPromise('Failed to compact runtime context.', () =>
53
+ helperModelRuntime.generateHelperStructured({
54
+ tag: 'context-compaction',
55
+ createAgent: createContextCompactionAgent,
56
+ messages: [
57
+ {
58
+ role: 'user',
59
+ content: buildContextCompactionPrompt({
60
+ previousSummary: params.previousSummary,
61
+ transcript: params.transcript,
62
+ }),
63
+ },
64
+ ],
65
+ schema: ContextCompactionOutputSchema,
66
+ maxOutputTokens: 8_000,
67
+ }),
68
+ ).pipe(Effect.flatMap(parseCompactionOutput))
71
69
  }
72
70
 
73
71
  export function createWiredContextCompactionRuntime(deps: CreateContextCompactionRuntimeDeps) {
74
72
  const { helperModelRuntime } = deps
75
73
 
76
74
  const runtime = createContextCompactionRuntime({
77
- runCompacter: (params) => runContextCompacter(helperModelRuntime, params),
75
+ runCompacter: (params) => runContextCompacterEffect(helperModelRuntime, params),
78
76
  now: deps.now,
79
77
  randomId: deps.randomId,
80
78
  thresholdRatio: CONTEXT_COMPACTION_THRESHOLD_RATIO,
@@ -86,20 +84,18 @@ export function createWiredContextCompactionRuntime(deps: CreateContextCompactio
86
84
  includedToolPrefixes: CONTEXT_COMPACTION_INCLUDED_TOOL_PREFIXES,
87
85
  })
88
86
 
89
- function compactMemoryBlockSummary(params: { previousSummary: string; newEntriesText: string }): Promise<string> {
87
+ function compactMemoryBlockSummary(params: { previousSummary: string; newEntriesText: string }) {
90
88
  const previousSummary = params.previousSummary.trim()
91
89
  const newEntriesText = params.newEntriesText.trim()
92
- if (!previousSummary && !newEntriesText) return Promise.resolve('')
90
+ if (!previousSummary && !newEntriesText) return Effect.succeed('')
93
91
 
94
- return Effect.runPromise(
95
- tryContextCompactionPromise('Failed to compact memory block summary.', () =>
96
- helperModelRuntime.generateHelperText({
97
- tag: 'memory-block-compaction',
98
- createAgent: createContextCompactionAgent,
99
- messages: [{ role: 'user', content: buildMemoryBlockCompactionPrompt({ previousSummary, newEntriesText }) }],
100
- maxOutputTokens: CONTEXT_COMPACTION_MAX_OUTPUT_TOKENS,
101
- }),
102
- ),
92
+ return tryContextCompactionPromise('Failed to compact memory block summary.', () =>
93
+ helperModelRuntime.generateHelperText({
94
+ tag: 'memory-block-compaction',
95
+ createAgent: createContextCompactionAgent,
96
+ messages: [{ role: 'user', content: buildMemoryBlockCompactionPrompt({ previousSummary, newEntriesText }) }],
97
+ maxOutputTokens: CONTEXT_COMPACTION_MAX_OUTPUT_TOKENS,
98
+ }),
103
99
  )
104
100
  }
105
101
 
@@ -1,7 +1,8 @@
1
1
  import type { ChatMessage } from '@lota-sdk/shared'
2
- import { Schema, Effect } from 'effect'
2
+ import { Duration, Effect, Schedule, Schema } from 'effect'
3
3
  import { z } from 'zod'
4
4
 
5
+ import { iterateEffect } from '../../effect/helpers'
5
6
  import { nowEpochMillis } from '../../utils/date-time'
6
7
  import {
7
8
  CHARS_PER_TOKEN_ESTIMATE,
@@ -64,6 +65,13 @@ class CompactionError extends Schema.TaggedErrorClass<CompactionError>()('Compac
64
65
  cause: Schema.optional(Schema.Defect),
65
66
  }) {}
66
67
 
68
+ class CompactionParseError extends Schema.TaggedErrorClass<CompactionParseError>()('CompactionParseError', {
69
+ message: Schema.String,
70
+ cause: Schema.optional(Schema.Defect),
71
+ }) {}
72
+
73
+ const COMPACTION_RUNNER_RETRY_OPTIONS = { times: 2, schedule: Schedule.exponential(Duration.millis(500), 2) } as const
74
+
67
75
  export interface ContextCompactionPromptParams {
68
76
  previousSummary: string
69
77
  transcript: string
@@ -80,7 +88,9 @@ export interface CompactionOutput {
80
88
  summary: string
81
89
  }
82
90
 
83
- export type ContextCompactionRunner = (params: ContextCompactionRunnerParams) => Promise<CompactionOutput>
91
+ export type ContextCompactionRunner = (
92
+ params: ContextCompactionRunnerParams,
93
+ ) => Effect.Effect<CompactionOutput, unknown>
84
94
 
85
95
  export interface CreateContextCompactionRuntimeOptions {
86
96
  runCompacter: ContextCompactionRunner
@@ -104,7 +114,7 @@ export interface ContextCompactionRuntime {
104
114
  liveMessages: ChatMessage[]
105
115
  contextSize?: number
106
116
  }) => CompactionAssessment
107
- compactHistory: (params: CompactHistoryParams) => Promise<CompactHistoryResult>
117
+ compactHistory: (params: CompactHistoryParams) => Effect.Effect<CompactHistoryResult, CompactionError>
108
118
  }
109
119
 
110
120
  function estimateTokens(text: string): number {
@@ -325,22 +335,32 @@ export function createContextCompactionRuntime(
325
335
  return { estimatedTokens, threshold, shouldCompact: estimatedTokens >= threshold }
326
336
  }
327
337
 
328
- const compactContextMessagesEffect = (params: { previousSummary: string; newMessages: ContextMessage[] }) =>
329
- Effect.gen(function* () {
330
- const chunks = splitByCharBudget(params.newMessages, compactionChunkMaxChars)
331
- let summary = normalizeSummary(params.previousSummary)
332
-
333
- for (const chunk of chunks) {
334
- const transcript = toCompactionTranscript(chunk)
335
- const output = yield* Effect.tryPromise({
336
- try: () => options.runCompacter({ previousSummary: summary, chunk, transcript }),
337
- catch: (error: unknown) => new CompactionError({ message: String(error), cause: error }),
338
- })
339
- summary = normalizeSummary(output.summary)
340
- }
341
-
342
- return { summary }
343
- })
338
+ const compactContextMessagesEffect = Effect.fn('ContextCompaction.compactContextMessages')(function* (params: {
339
+ previousSummary: string
340
+ newMessages: ContextMessage[]
341
+ }) {
342
+ const chunks = splitByCharBudget(params.newMessages, compactionChunkMaxChars)
343
+ const initialSummary = normalizeSummary(params.previousSummary)
344
+
345
+ const finalSummary = yield* iterateEffect<{ summary: string; index: number }, CompactionError, never>(
346
+ { summary: initialSummary, index: 0 },
347
+ {
348
+ while: (state) => state.index < chunks.length,
349
+ body: (state) =>
350
+ Effect.gen(function* () {
351
+ const chunk = chunks[state.index]
352
+ const transcript = toCompactionTranscript(chunk)
353
+ const output = yield* options.runCompacter({ previousSummary: state.summary, chunk, transcript }).pipe(
354
+ Effect.mapError((error) => new CompactionError({ message: String(error), cause: error })),
355
+ Effect.retry(COMPACTION_RUNNER_RETRY_OPTIONS),
356
+ )
357
+ return { summary: normalizeSummary(output.summary), index: state.index + 1 }
358
+ }),
359
+ },
360
+ )
361
+
362
+ return { summary: finalSummary.summary }
363
+ })
344
364
 
345
365
  const rollupSummaryIfOversizedEffect = (summary: string) =>
346
366
  Effect.gen(function* () {
@@ -356,81 +376,104 @@ export function createContextCompactionRuntime(
356
376
  return normalizeSummary(output.summary)
357
377
  })
358
378
 
379
+ type CompactionLoopState = {
380
+ summaryText: string
381
+ remainingMessages: ChatMessage[]
382
+ compactedMessages: ChatMessage[]
383
+ lastCompactedMessageId: string | undefined
384
+ estimatedTokens: number
385
+ done: boolean
386
+ }
387
+
359
388
  const compactHistoryEffect = (params: CompactHistoryParams) =>
360
389
  Effect.gen(function* () {
361
- let summaryText = normalizeSummary(params.summaryText)
362
- let remainingMessages = [...params.liveMessages]
363
- let compactedMessages: ChatMessage[] = []
364
- let lastCompactedMessageId: string | undefined
365
- const summaryPayload = buildSyntheticSummaryPayload(summaryText)
366
- const initialPayload = stringifyUnknown([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages]) ?? ''
390
+ const initialSummaryText = normalizeSummary(params.summaryText)
391
+ const initialRemaining = [...params.liveMessages]
392
+ const summaryPayload = buildSyntheticSummaryPayload(initialSummaryText)
393
+ const initialPayload = stringifyUnknown([...(summaryPayload ? [summaryPayload] : []), ...initialRemaining]) ?? ''
367
394
  const inputChars = initialPayload.length
368
395
 
369
- const buildEarlyExitResult = (estimatedTokens: number): CompactHistoryResult => {
370
- const exitSummaryPayload = buildSyntheticSummaryPayload(summaryText)
396
+ const buildExitResult = (state: CompactionLoopState): CompactHistoryResult => {
397
+ const exitSummaryPayload = buildSyntheticSummaryPayload(state.summaryText)
371
398
  const outputPayload =
372
- stringifyUnknown([...(exitSummaryPayload ? [exitSummaryPayload] : []), ...remainingMessages]) ?? ''
399
+ stringifyUnknown([...(exitSummaryPayload ? [exitSummaryPayload] : []), ...state.remainingMessages]) ?? ''
373
400
  return {
374
- compacted: compactedMessages.length > 0,
375
- summaryText,
376
- ...(lastCompactedMessageId ? { lastCompactedMessageId } : {}),
377
- compactedMessages,
378
- compactedMessageCount: compactedMessages.length,
379
- remainingMessageCount: remainingMessages.length,
380
- estimatedTokens,
401
+ compacted: state.compactedMessages.length > 0,
402
+ summaryText: state.summaryText,
403
+ ...(state.lastCompactedMessageId ? { lastCompactedMessageId: state.lastCompactedMessageId } : {}),
404
+ compactedMessages: state.compactedMessages,
405
+ compactedMessageCount: state.compactedMessages.length,
406
+ remainingMessageCount: state.remainingMessages.length,
407
+ estimatedTokens: state.estimatedTokens,
381
408
  inputChars,
382
409
  outputChars: outputPayload.length,
383
410
  }
384
411
  }
385
412
 
386
- for (;;) {
387
- const assessment = shouldCompactHistory({
388
- summaryText,
389
- liveMessages: remainingMessages,
390
- contextSize: params.contextSize,
391
- })
392
-
393
- if (!assessment.shouldCompact) {
394
- return buildEarlyExitResult(assessment.estimatedTokens)
395
- }
396
-
397
- const boundary = Math.max(0, remainingMessages.length - params.tailMessageCount)
398
- if (boundary <= 0) {
399
- return buildEarlyExitResult(assessment.estimatedTokens)
400
- }
401
-
402
- const candidatePrefix = remainingMessages.slice(0, boundary)
403
- const messagesToCompact = candidatePrefix.filter((message) => !readIsCompacted(message))
404
- const contextMessages = messagesToCompact
405
- .map(toContextMessageFromChatMessage)
406
- .filter((message) => compactWhitespace(message.text).length > 0)
407
- const sourceText = toCompactionTranscript(contextMessages)
408
-
409
- if (!compactWhitespace(sourceText)) {
410
- return buildEarlyExitResult(assessment.estimatedTokens)
411
- }
412
-
413
- let nextSummary = normalizeSummary(
414
- (yield* compactContextMessagesEffect({ previousSummary: summaryText, newMessages: contextMessages })).summary,
415
- )
416
- nextSummary = yield* rollupSummaryIfOversizedEffect(nextSummary)
417
-
418
- if (nextSummary.length >= sourceText.length) {
419
- return yield* new CompactionError({ message: 'Compaction summary is not shorter than compacted source' })
420
- }
413
+ const initialState: CompactionLoopState = {
414
+ summaryText: initialSummaryText,
415
+ remainingMessages: initialRemaining,
416
+ compactedMessages: [],
417
+ lastCompactedMessageId: undefined,
418
+ estimatedTokens: 0,
419
+ done: false,
420
+ }
421
421
 
422
- summaryText = nextSummary
423
- compactedMessages = [
424
- ...compactedMessages,
425
- ...candidatePrefix.map((message) => markMessageCompacted(message, now)),
426
- ]
427
- lastCompactedMessageId = candidatePrefix.at(-1)?.id ?? lastCompactedMessageId
428
- remainingMessages = remainingMessages.slice(boundary)
422
+ const finalState = yield* iterateEffect<CompactionLoopState, CompactionError, never>(initialState, {
423
+ while: (state) => !state.done,
424
+ body: (state) =>
425
+ Effect.gen(function* () {
426
+ const assessment = shouldCompactHistory({
427
+ summaryText: state.summaryText,
428
+ liveMessages: state.remainingMessages,
429
+ contextSize: params.contextSize,
430
+ })
431
+
432
+ if (!assessment.shouldCompact) {
433
+ return { ...state, estimatedTokens: assessment.estimatedTokens, done: true }
434
+ }
435
+
436
+ const boundary = Math.max(0, state.remainingMessages.length - params.tailMessageCount)
437
+ if (boundary <= 0) {
438
+ return { ...state, estimatedTokens: assessment.estimatedTokens, done: true }
439
+ }
440
+
441
+ const candidatePrefix = state.remainingMessages.slice(0, boundary)
442
+ const messagesToCompact = candidatePrefix.filter((message) => !readIsCompacted(message))
443
+ const contextMessages = messagesToCompact
444
+ .map(toContextMessageFromChatMessage)
445
+ .filter((message) => compactWhitespace(message.text).length > 0)
446
+ const sourceText = toCompactionTranscript(contextMessages)
447
+
448
+ if (!compactWhitespace(sourceText)) {
449
+ return { ...state, estimatedTokens: assessment.estimatedTokens, done: true }
450
+ }
451
+
452
+ const compacted = yield* compactContextMessagesEffect({
453
+ previousSummary: state.summaryText,
454
+ newMessages: contextMessages,
455
+ })
456
+ const rolledSummary = yield* rollupSummaryIfOversizedEffect(normalizeSummary(compacted.summary))
457
+
458
+ if (rolledSummary.length >= sourceText.length) {
459
+ return yield* new CompactionError({ message: 'Compaction summary is not shorter than compacted source' })
460
+ }
461
+
462
+ return {
463
+ summaryText: rolledSummary,
464
+ remainingMessages: state.remainingMessages.slice(boundary),
465
+ compactedMessages: [
466
+ ...state.compactedMessages,
467
+ ...candidatePrefix.map((message) => markMessageCompacted(message, now)),
468
+ ],
469
+ lastCompactedMessageId: candidatePrefix.at(-1)?.id ?? state.lastCompactedMessageId,
470
+ estimatedTokens: assessment.estimatedTokens,
471
+ done: false,
472
+ }
473
+ }),
474
+ })
429
475
 
430
- if (remainingMessages.length <= params.tailMessageCount) {
431
- continue
432
- }
433
- }
476
+ return buildExitResult(finalState)
434
477
  })
435
478
 
436
479
  return {
@@ -438,11 +481,14 @@ export function createContextCompactionRuntime(
438
481
  prependSummaryMessage,
439
482
  estimateThreshold,
440
483
  shouldCompactHistory,
441
- compactHistory: (params) => Effect.runPromise(compactHistoryEffect(params)),
484
+ compactHistory: compactHistoryEffect,
442
485
  }
443
486
  }
444
487
 
445
- export function parseCompactionOutput(value: unknown): CompactionOutput {
446
- const parsed = ContextCompactionOutputSchema.parse(value)
447
- return { summary: parsed.summary }
488
+ export function parseCompactionOutput(value: unknown): Effect.Effect<CompactionOutput, CompactionParseError> {
489
+ const parsed = ContextCompactionOutputSchema.safeParse(value)
490
+ if (!parsed.success) {
491
+ return Effect.fail(new CompactionParseError({ message: 'Failed to parse compaction output', cause: parsed.error }))
492
+ }
493
+ return Effect.succeed({ summary: parsed.data.summary })
448
494
  }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Builds the domain-service Layer tree for `createLotaRuntime`.
3
+ *
4
+ * The services form a 9-tier dependency graph on top of the infrastructure
5
+ * layer (config, logging, database, redis, agents, threads, extensions).
6
+ * Each tier is provided with the accumulated context of earlier tiers so
7
+ * every service resolves cleanly when the ManagedRuntime eagerly loads them.
8
+ */
9
+
10
+ import type { Layer as LayerType } from 'effect'
11
+ import { Layer } from 'effect'
12
+
13
+ import { AiGatewayLive } from '../ai-gateway/ai-gateway'
14
+ import { EmbeddingCacheLive } from '../ai/embedding-cache'
15
+ import type { buildInfrastructureLayer } from '../effect/layers'
16
+ import { LotaQueuesLive } from '../queues/queues.service'
17
+ import { SharedThreadStreamSubscriberLive } from '../redis/stream-context'
18
+ import { AgentActivityServiceLive } from '../services/agent-activity.service'
19
+ import { AgentExecutorServiceLive } from '../services/agent-executor.service'
20
+ import { ArtifactServiceLive } from '../services/artifact.service'
21
+ import { AttachmentServiceLive } from '../services/attachment.service'
22
+ import { AutonomousJobServiceLive } from '../services/autonomous-job.service'
23
+ import { BackgroundWorkServiceLive } from '../services/background-work.service'
24
+ import { ChatRunRegistryLive } from '../services/chat-run-registry.service'
25
+ import { ContextCompactionServiceLive } from '../services/context-compaction.service'
26
+ import { DocumentChunkServiceLive } from '../services/document-chunk.service'
27
+ import { ExecutionPlanServiceLive } from '../services/execution-plan/execution-plan.service'
28
+ import { FeedbackLoopServiceLive } from '../services/feedback-loop.service'
29
+ import { GlobalOrchestratorServiceLive } from '../services/global-orchestrator.service'
30
+ import { InstitutionalMemoryServiceLive } from '../services/institutional-memory.service'
31
+ import { LearnedSkillServiceLive } from '../services/learned-skill.service'
32
+ import { MemoryServiceLive } from '../services/memory/memory.service'
33
+ import { RerankServiceLive } from '../services/memory/rerank.service'
34
+ import { MonitoringWindowServiceLive } from '../services/monitoring-window.service'
35
+ import { MutatingApprovalServiceLive } from '../services/mutating-approval.service'
36
+ import { NodeWorkspaceServiceLive } from '../services/node-workspace.service'
37
+ import { NotificationServiceLive } from '../services/notification.service'
38
+ import { OrganizationMemberServiceLive } from '../services/organization-member.service'
39
+ import { OrganizationServiceLive } from '../services/organization.service'
40
+ import { OwnershipDispatcherServiceLive } from '../services/ownership-dispatcher.service'
41
+ import { PlanAgentHeartbeatServiceLive } from '../services/plan/plan-agent-heartbeat.service'
42
+ import { PlanAgentQueryServiceLive } from '../services/plan/plan-agent-query.service'
43
+ import { PlanApprovalServiceLive } from '../services/plan/plan-approval.service'
44
+ import { PlanArtifactServiceLive } from '../services/plan/plan-artifact.service'
45
+ import { PlanBuilderServiceLive } from '../services/plan/plan-builder.service'
46
+ import { PlanCheckpointServiceLive } from '../services/plan/plan-checkpoint.service'
47
+ import { PlanCompilerServiceLive } from '../services/plan/plan-compiler.service'
48
+ import { PlanCoordinationServiceLive } from '../services/plan/plan-coordination.service'
49
+ import { PlanCycleServiceLive } from '../services/plan/plan-cycle.service'
50
+ import { PlanDeadlineServiceLive } from '../services/plan/plan-deadline.service'
51
+ import { PlanEventDeliveryServiceLive } from '../services/plan/plan-event-delivery.service'
52
+ import { PlanExecutorServiceLive } from '../services/plan/plan-executor.service'
53
+ import { PlanRunServiceLive } from '../services/plan/plan-run.service'
54
+ import { PlanSchedulerServiceLive } from '../services/plan/plan-scheduler.service'
55
+ import { PlanTemplateServiceLive } from '../services/plan/plan-template.service'
56
+ import { PlanValidatorServiceLive } from '../services/plan/plan-validator.service'
57
+ import { PlanWorkspaceServiceLive } from '../services/plan/plan-workspace.service'
58
+ import { PluginExecutorServiceLive } from '../services/plugin-executor.service'
59
+ import { QualityMetricsServiceLive } from '../services/quality-metrics.service'
60
+ import { QueueJobServiceLive } from '../services/queue-job.service'
61
+ import { RecentActivityTitleServiceLive } from '../services/recent-activity-title.service'
62
+ import { RecentActivityServiceLive } from '../services/recent-activity.service'
63
+ import { SkillResolverServiceLive } from '../services/skill-resolver.service'
64
+ import { SocialChatHistoryServiceLive } from '../services/social-chat-history.service'
65
+ import { SystemExecutorServiceLive } from '../services/system-executor.service'
66
+ import { ThreadMessageServiceLive } from '../services/thread/thread-message.service'
67
+ import { ThreadTitleServiceLive } from '../services/thread/thread-title.service'
68
+ import { ThreadTurnServiceLive } from '../services/thread/thread-turn'
69
+ import { ThreadTurnPreparationServiceLive } from '../services/thread/thread-turn-preparation.service'
70
+ import { ThreadServiceLive } from '../services/thread/thread.service'
71
+ import { UserServiceLive } from '../services/user.service'
72
+ import { WriteIntentValidatorServiceLive } from '../services/write-intent-validator.service'
73
+ import { AttachmentStorageServiceLive } from '../storage/attachment-storage.service'
74
+ import { GeneratedDocumentStorageServiceLive } from '../storage/generated-document-storage.service'
75
+ import { FirecrawlLive } from '../tools/firecrawl-client'
76
+ import { CompactionCoordinationLive } from './chat-run-orchestration'
77
+ import { HelperModelLive } from './helper-model'
78
+
79
+ function provide<A, E, R extends RCtx, RCtx, E2>(
80
+ layer: LayerType.Layer<A, E, R>,
81
+ ctx: LayerType.Layer<RCtx, E2, never>,
82
+ ): LayerType.Layer<A, E | E2, never> {
83
+ return Layer.provideMerge(ctx)(layer)
84
+ }
85
+
86
+ type InfrastructureLayer = ReturnType<typeof buildInfrastructureLayer>
87
+
88
+ /**
89
+ * Compose the domain service layer tree on top of the supplied infrastructure
90
+ * layer. The returned layer has no remaining requirements and can be fed
91
+ * straight into `ManagedRuntime.make`.
92
+ */
93
+ export function buildDomainServiceLayer(infrastructureLayer: InfrastructureLayer) {
94
+ const tier0 = provide(
95
+ Layer.mergeAll(
96
+ BackgroundWorkServiceLive,
97
+ ChatRunRegistryLive,
98
+ CompactionCoordinationLive,
99
+ DocumentChunkServiceLive,
100
+ NodeWorkspaceServiceLive,
101
+ NotificationServiceLive,
102
+ PlanArtifactServiceLive,
103
+ PlanBuilderServiceLive,
104
+ WriteIntentValidatorServiceLive,
105
+ ),
106
+ infrastructureLayer,
107
+ )
108
+ const ctx0 = Layer.mergeAll(
109
+ infrastructureLayer,
110
+ provide(Layer.mergeAll(AiGatewayLive, EmbeddingCacheLive, FirecrawlLive, HelperModelLive), infrastructureLayer),
111
+ tier0,
112
+ )
113
+
114
+ const tier1 = provide(
115
+ Layer.mergeAll(
116
+ SharedThreadStreamSubscriberLive,
117
+ AgentExecutorServiceLive,
118
+ AttachmentStorageServiceLive,
119
+ GeneratedDocumentStorageServiceLive,
120
+ LearnedSkillServiceLive,
121
+ OrganizationServiceLive,
122
+ OrganizationMemberServiceLive,
123
+ PlanApprovalServiceLive,
124
+ PlanRunServiceLive,
125
+ PlanWorkspaceServiceLive,
126
+ PluginExecutorServiceLive,
127
+ QualityMetricsServiceLive,
128
+ QueueJobServiceLive,
129
+ RecentActivityServiceLive,
130
+ RerankServiceLive,
131
+ SocialChatHistoryServiceLive,
132
+ SystemExecutorServiceLive,
133
+ ThreadMessageServiceLive,
134
+ UserServiceLive,
135
+ ),
136
+ ctx0,
137
+ )
138
+ const ctx1Base = Layer.mergeAll(ctx0, tier1)
139
+
140
+ // LotaQueuesLive depends only on Redis + QueueJobService from tier1/infra.
141
+ // It's threaded in before the tiers that need queue enqueue access so the
142
+ // service layers that enqueue jobs (AutonomousJobService, plan scheduler,
143
+ // heartbeat, etc.) can resolve LotaQueuesServiceTag cleanly.
144
+ const queuesTier = provide(LotaQueuesLive, ctx1Base)
145
+ const ctx1 = Layer.mergeAll(ctx1Base, queuesTier)
146
+ // PlanSchedulerServiceLive previously sat in tier1; it now depends on the
147
+ // queue layer, so it moves to the merged ctx1 context with queues available.
148
+ const planSchedulerTier = provide(PlanSchedulerServiceLive, ctx1)
149
+ const ctx1WithScheduler = Layer.mergeAll(ctx1, planSchedulerTier)
150
+
151
+ const tier2 = provide(
152
+ Layer.mergeAll(
153
+ ArtifactServiceLive,
154
+ AttachmentServiceLive,
155
+ ContextCompactionServiceLive,
156
+ FeedbackLoopServiceLive,
157
+ InstitutionalMemoryServiceLive,
158
+ MemoryServiceLive,
159
+ MonitoringWindowServiceLive,
160
+ MutatingApprovalServiceLive,
161
+ PlanAgentQueryServiceLive,
162
+ PlanCheckpointServiceLive,
163
+ PlanCoordinationServiceLive,
164
+ PlanEventDeliveryServiceLive,
165
+ SkillResolverServiceLive,
166
+ RecentActivityTitleServiceLive,
167
+ ),
168
+ ctx1WithScheduler,
169
+ )
170
+ const ctx2 = Layer.mergeAll(ctx1WithScheduler, tier2)
171
+
172
+ const tier3 = provide(Layer.mergeAll(PlanValidatorServiceLive, ThreadServiceLive), ctx2)
173
+ const ctx3 = Layer.mergeAll(ctx2, tier3)
174
+
175
+ const tier4 = provide(Layer.mergeAll(PlanCompilerServiceLive, ThreadTitleServiceLive), ctx3)
176
+ const ctx4 = Layer.mergeAll(ctx3, tier4)
177
+
178
+ const tier5 = provide(PlanExecutorServiceLive, ctx4)
179
+ const ctx5 = Layer.mergeAll(ctx4, tier5)
180
+
181
+ const tier6 = provide(
182
+ Layer.mergeAll(OwnershipDispatcherServiceLive, PlanAgentHeartbeatServiceLive, PlanDeadlineServiceLive),
183
+ ctx5,
184
+ )
185
+ const ctx6 = Layer.mergeAll(ctx5, tier6)
186
+
187
+ const tier7 = provide(Layer.mergeAll(ExecutionPlanServiceLive, GlobalOrchestratorServiceLive), ctx6)
188
+ const ctx7 = Layer.mergeAll(ctx6, tier7)
189
+
190
+ const tier8 = provide(
191
+ Layer.mergeAll(
192
+ PlanTemplateServiceLive,
193
+ AutonomousJobServiceLive,
194
+ AgentActivityServiceLive,
195
+ ThreadTurnPreparationServiceLive,
196
+ ),
197
+ ctx7,
198
+ )
199
+ const ctx8 = Layer.mergeAll(ctx7, tier8)
200
+
201
+ const tier9 = provide(Layer.mergeAll(PlanCycleServiceLive, ThreadTurnServiceLive), ctx8)
202
+ return Layer.mergeAll(ctx8, tier9)
203
+ }