@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,1148 @@
1
+ import {
2
+ THREAD,
3
+ dataPartsSchemas,
4
+ messageMetadataSchema,
5
+ PlanNodeResultSubmissionSchema,
6
+ SUBMIT_PLAN_TURN_RESULT_TOOL_NAME,
7
+ toTimestamp,
8
+ withMessageCreatedAt,
9
+ } from '@lota-sdk/shared'
10
+ import type { ChatMessage, MessageMetadata } from '@lota-sdk/shared'
11
+ import { tool as createTool, validateUIMessages } from 'ai'
12
+ import type { UIMessageStreamWriter } from 'ai'
13
+ import { Clock, Context, Schema, Effect, Layer } from 'effect'
14
+
15
+ import type { CoreThreadProfile } from '../../config/agent-defaults'
16
+ import {
17
+ getAgentRoster,
18
+ getLeadAgentId,
19
+ getCoreThreadProfile,
20
+ getResolvedAgentFactoryConfig,
21
+ } from '../../config/agent-defaults'
22
+ import { aiLogger } from '../../config/logger'
23
+ import type { RecordIdRef } from '../../db/record-id'
24
+ import { recordIdToString } from '../../db/record-id'
25
+ import { TABLES } from '../../db/tables'
26
+ import type { DatabaseError, ServiceError } from '../../effect/errors'
27
+ import { ThreadTurnError } from '../../effect/errors'
28
+ import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
29
+ import { enqueueContextCompaction } from '../../queues/context-compaction.queue'
30
+ import { enqueueThreadTitleGeneration } from '../../queues/title-generation.queue'
31
+ import {
32
+ readRuntimeAgentIdentityOverrides,
33
+ resolveRuntimeAgentDisplayName,
34
+ } from '../../runtime/agent-identity-overrides'
35
+ import { createServerRunAbortController } from '../../runtime/agent-stream-helpers'
36
+ import { hasApprovalRespondedParts } from '../../runtime/approval-continuation'
37
+ import { hasMessageContent } from '../../runtime/chat-message'
38
+ import { CompactionCoordinationTag } from '../../runtime/chat-run-orchestration'
39
+ import { CONTEXT_WINDOW_TOKENS } from '../../runtime/context-compaction/context-compaction-constants'
40
+ import { createWiredContextCompactionRuntime } from '../../runtime/context-compaction/context-compaction-runtime'
41
+ import { createExecutionPlanInstructionSectionCache, toExecutionPlanCacheError } from '../../runtime/execution-plan'
42
+ import type { HelperModelRuntime } from '../../runtime/helper-model'
43
+ import { HelperModelTag } from '../../runtime/helper-model'
44
+ import { runPostTurnSideEffects } from '../../runtime/post-turn-side-effects'
45
+ import { getRuntimeAdapters, getTurnHooks } from '../../runtime/runtime-extensions'
46
+ import { extractMessageText, toOptionalTrimmedString } from '../../runtime/thread-chat-helpers'
47
+ import {
48
+ buildPlanTurnInstructionSections,
49
+ buildPlanTurnPromptMessage,
50
+ buildPlanTurnSubmitToolDescription,
51
+ } from '../../runtime/thread-plan-turn'
52
+ import type { ThreadPlanTurnContext } from '../../runtime/thread-plan-turn'
53
+ import { assembleThreadTurnContext } from '../../runtime/thread-turn-context'
54
+ import { finalizeTurnRunEffect, TurnLifecycleError } from '../../runtime/turn-lifecycle'
55
+ import { triageThreadMessage, checkForNextAgent } from '../../system-agents/thread-router.agent'
56
+ import { safeEnqueue } from '../../utils/async'
57
+ import { nowEpochMillis } from '../../utils/date-time'
58
+ import { getErrorMessage } from '../../utils/errors'
59
+ import type { makeAttachmentService } from '../attachment.service'
60
+ import { AttachmentServiceTag } from '../attachment.service'
61
+ import { ChatRunRegistryTag } from '../chat-run-registry.service'
62
+ import type { makeExecutionPlanService } from '../execution-plan/execution-plan.service'
63
+ import { ExecutionPlanServiceTag } from '../execution-plan/execution-plan.service'
64
+ import type { makeLearnedSkillService } from '../learned-skill.service'
65
+ import { LearnedSkillServiceTag } from '../learned-skill.service'
66
+ import type { createMemoryService } from '../memory/memory.service'
67
+ import { MemoryServiceTag } from '../memory/memory.service'
68
+ import type { makePlanRunService } from '../plan/plan-run.service'
69
+ import { PlanRunServiceTag } from '../plan/plan-run.service'
70
+ import type { makeThreadMessageService } from './thread-message.service'
71
+ import { ThreadMessageServiceTag } from './thread-message.service'
72
+ import {
73
+ applyPlanTurnToolPolicy,
74
+ createThreadTurnVisibleAgentRunner,
75
+ writeMultiAgentEvent,
76
+ } from './thread-turn-execution'
77
+ import { createThreadTurnMessageContext, upsertChatHistoryMessage } from './thread-turn-message-context'
78
+ import { ThreadTurnStreamingError } from './thread-turn-streaming'
79
+ import type { StreamAgentResponseContext } from './thread-turn-streaming'
80
+ import { buildThreadTurnSpanAttributes, compactSpanAttributes } from './thread-turn-tracing'
81
+ import { ThreadServiceTag } from './thread.service'
82
+ import type { makeThreadService } from './thread.service'
83
+ import type { NormalizedThread } from './thread.types'
84
+
85
+ interface ThreadTurnPreparationServiceContext {
86
+ attachmentService: ReturnType<typeof makeAttachmentService>
87
+ chatRunRegistry: Context.Service.Shape<typeof ChatRunRegistryTag>
88
+ compactionCoordination: Context.Service.Shape<typeof CompactionCoordinationTag>
89
+ executionPlanService: ReturnType<typeof makeExecutionPlanService>
90
+ learnedSkillService: ReturnType<typeof makeLearnedSkillService>
91
+ memoryService: ReturnType<typeof createMemoryService>
92
+ planRunService: ReturnType<typeof makePlanRunService>
93
+ threadMessageService: ReturnType<typeof makeThreadMessageService>
94
+ threadService: ReturnType<typeof makeThreadService>
95
+ contextCompactionRuntime: ReturnType<typeof createWiredContextCompactionRuntime>
96
+ }
97
+
98
+ type ThreadTurnPreparationRuntimeDeps = ThreadTurnPreparationServiceContext
99
+
100
+ const PRESEEDED_MEMORY_LOOKUP_LIMIT = 3
101
+
102
+ function waitForThreadCompactionIfNeeded(deps: ThreadTurnPreparationRuntimeDeps, threadId: RecordIdRef) {
103
+ return deps.compactionCoordination.waitIfNeeded({
104
+ entityId: recordIdToString(threadId, TABLES.THREAD),
105
+ entityLabel: 'Thread',
106
+ loadEntity: () => deps.threadService.getById(threadId),
107
+ isCompacting: (thread) => thread.isCompacting === true,
108
+ })
109
+ }
110
+
111
+ export class ThreadTurnPreparationError extends Schema.TaggedErrorClass<ThreadTurnPreparationError>()(
112
+ 'ThreadTurnPreparationError',
113
+ { message: Schema.String, cause: Schema.optional(Schema.Defect) },
114
+ ) {}
115
+
116
+ const effectTryPromise = makeEffectTryPromiseWithMessage(
117
+ (message, cause) => new ThreadTurnPreparationError({ message, cause }),
118
+ )
119
+
120
+ export interface ThreadTurnParams {
121
+ thread: NormalizedThread
122
+ threadRef: RecordIdRef
123
+ orgRef: RecordIdRef
124
+ userRef: RecordIdRef
125
+ userName?: string | null
126
+ agentIdOverride?: string
127
+ inputMessage: ChatMessage
128
+ skipInputMessagePersistence?: boolean
129
+ abortSignal?: AbortSignal
130
+ streamId?: string
131
+ }
132
+
133
+ export interface ThreadApprovalContinuationParams {
134
+ thread: NormalizedThread
135
+ threadRef: RecordIdRef
136
+ orgRef: RecordIdRef
137
+ userRef: RecordIdRef
138
+ userName?: string | null
139
+ approvalMessages: ChatMessage[]
140
+ abortSignal?: AbortSignal
141
+ streamId?: string
142
+ }
143
+
144
+ export interface ThreadPlanTurnParams {
145
+ thread: NormalizedThread
146
+ threadRef: RecordIdRef
147
+ orgRef: RecordIdRef
148
+ userRef: RecordIdRef
149
+ userName?: string | null
150
+ planTurn: ThreadPlanTurnContext
151
+ abortSignal?: AbortSignal
152
+ streamId?: string
153
+ }
154
+
155
+ type ThreadRunCoreParams = {
156
+ thread: NormalizedThread
157
+ threadRef: RecordIdRef
158
+ orgRef: RecordIdRef
159
+ userRef: RecordIdRef
160
+ userName?: string | null
161
+ agentIdOverride?: string
162
+ abortSignal?: AbortSignal
163
+ streamId?: string
164
+ } & (
165
+ | { kind: 'userTurn'; inputMessage: ChatMessage; skipInputMessagePersistence?: boolean }
166
+ | { kind: 'approvalContinuation'; approvalMessages: ChatMessage[] }
167
+ | { kind: 'nativeToolApprovalTurn'; approvalMessages: ChatMessage[] }
168
+ | { kind: 'planTurn'; planTurn: ThreadPlanTurnContext }
169
+ )
170
+
171
+ export interface PreparedThreadTurnResult {
172
+ inputMessageId?: string
173
+ assistantMessages: ChatMessage[]
174
+ }
175
+
176
+ type RecentHistoryLoadError = ServiceError | ThreadTurnPreparationError
177
+
178
+ function hydrateThreadMessageFileUrls(
179
+ attachmentService: ReturnType<typeof makeAttachmentService>,
180
+ orgRef: RecordIdRef,
181
+ userRef: RecordIdRef,
182
+ message: ChatMessage,
183
+ ): ChatMessage {
184
+ return {
185
+ ...message,
186
+ parts: attachmentService.hydrateSignedFileUrlsInMessageParts({
187
+ parts: message.parts,
188
+ orgId: orgRef,
189
+ userId: userRef,
190
+ }),
191
+ }
192
+ }
193
+
194
+ function validateAndHydrateThreadMessagesEffect(params: {
195
+ attachmentService: ReturnType<typeof makeAttachmentService>
196
+ orgRef: RecordIdRef
197
+ userRef: RecordIdRef
198
+ messages: ChatMessage[]
199
+ errorMessage: string
200
+ }): Effect.Effect<ChatMessage[], ThreadTurnPreparationError> {
201
+ const { attachmentService, orgRef, userRef, messages, errorMessage } = params
202
+
203
+ return effectTryPromise(
204
+ () =>
205
+ validateUIMessages<ChatMessage>({
206
+ messages,
207
+ metadataSchema: messageMetadataSchema,
208
+ dataSchemas: dataPartsSchemas,
209
+ }),
210
+ errorMessage,
211
+ ).pipe(
212
+ Effect.map((validated) =>
213
+ validated.map((message) => hydrateThreadMessageFileUrls(attachmentService, orgRef, userRef, message)),
214
+ ),
215
+ )
216
+ }
217
+
218
+ function validateUserTurnInputMessageEffect(params: {
219
+ thread: NormalizedThread
220
+ inputMessage: ChatMessage
221
+ attachmentService: ReturnType<typeof makeAttachmentService>
222
+ orgRef: RecordIdRef
223
+ userRef: RecordIdRef
224
+ }): Effect.Effect<ChatMessage, ThreadTurnError> {
225
+ const { thread, inputMessage, attachmentService, orgRef, userRef } = params
226
+
227
+ return Effect.gen(function* () {
228
+ const createdAt = yield* Clock.currentTimeMillis
229
+ const hydratedInputMessage = hydrateThreadMessageFileUrls(
230
+ attachmentService,
231
+ orgRef,
232
+ userRef,
233
+ withMessageCreatedAt(inputMessage, createdAt),
234
+ )
235
+
236
+ if (hydratedInputMessage.role !== 'user') {
237
+ return yield* new ThreadTurnError({
238
+ message: 'Only user messages can be submitted to the thread runtime.',
239
+ reason: 'bad-request',
240
+ })
241
+ }
242
+ if (!hasMessageContent(hydratedInputMessage.parts)) {
243
+ return yield* new ThreadTurnError({
244
+ message: 'Thread messages must include text or attachments.',
245
+ reason: 'bad-request',
246
+ })
247
+ }
248
+ if (thread.type === 'default' && !thread.agentId) {
249
+ return yield* new ThreadTurnError({
250
+ message: 'Default threads require an assigned agent.',
251
+ reason: 'bad-request',
252
+ })
253
+ }
254
+
255
+ return hydratedInputMessage
256
+ })
257
+ }
258
+
259
+ function createRecentHistoryLoaderEffect(params: {
260
+ runEffect: (effect: Effect.Effect<ChatMessage[], RecentHistoryLoadError>) => Promise<ChatMessage[]>
261
+ threadMessageService: ReturnType<typeof makeThreadMessageService>
262
+ threadRef: RecordIdRef
263
+ attachmentService: ReturnType<typeof makeAttachmentService>
264
+ orgRef: RecordIdRef
265
+ userRef: RecordIdRef
266
+ }) {
267
+ const { runEffect, threadMessageService, threadRef, attachmentService, orgRef, userRef } = params
268
+
269
+ return Effect.gen(function* () {
270
+ const cachedRecentHistory = yield* Effect.cached(
271
+ Effect.gen(function* () {
272
+ const persistedRecentHistory = yield* threadMessageService.listRecentMessagesEffect(threadRef, 64)
273
+ if (persistedRecentHistory.length === 0) {
274
+ return [] as ChatMessage[]
275
+ }
276
+
277
+ return yield* validateAndHydrateThreadMessagesEffect({
278
+ attachmentService,
279
+ orgRef,
280
+ userRef,
281
+ messages: persistedRecentHistory,
282
+ errorMessage: 'Failed to validate recent thread history.',
283
+ })
284
+ }),
285
+ )
286
+
287
+ return () => runEffect(cachedRecentHistory)
288
+ })
289
+ }
290
+
291
+ function loadPersistedLiveHistoryEffect(params: {
292
+ threadMessageService: ReturnType<typeof makeThreadMessageService>
293
+ threadRef: RecordIdRef
294
+ persistedCompactionCursor?: string
295
+ attachmentService: ReturnType<typeof makeAttachmentService>
296
+ orgRef: RecordIdRef
297
+ userRef: RecordIdRef
298
+ }): Effect.Effect<ChatMessage[], ThreadTurnPreparationError> {
299
+ const { threadMessageService, threadRef, persistedCompactionCursor, attachmentService, orgRef, userRef } = params
300
+
301
+ return Effect.gen(function* () {
302
+ const persistedLiveHistory = yield* threadMessageService
303
+ .listMessagesAfterCursorEffect(threadRef, persistedCompactionCursor)
304
+ .pipe(
305
+ Effect.withSpan('ThreadTurnPreparation.loadPersistedHistory'),
306
+ Effect.mapError(
307
+ (error) => new ThreadTurnPreparationError({ message: 'Failed to load thread history.', cause: error }),
308
+ ),
309
+ )
310
+
311
+ if (persistedLiveHistory.length === 0) {
312
+ return [] as ChatMessage[]
313
+ }
314
+
315
+ return yield* validateAndHydrateThreadMessagesEffect({
316
+ attachmentService,
317
+ orgRef,
318
+ userRef,
319
+ messages: persistedLiveHistory,
320
+ errorMessage: 'Failed to validate thread history.',
321
+ }).pipe(Effect.withSpan('ThreadTurnPreparation.validateAndHydrateHistory'))
322
+ })
323
+ }
324
+
325
+ function deriveTurnMessageInputs(params: {
326
+ params: ThreadRunCoreParams
327
+ inputMessage?: ChatMessage
328
+ liveHistory: ChatMessage[]
329
+ shouldPersistInputMessage: boolean
330
+ }): {
331
+ userMessage?: ChatMessage
332
+ originalMessages: ChatMessage[]
333
+ referenceUserMessage?: ChatMessage
334
+ messageText: string
335
+ } {
336
+ const { params: turnParams, inputMessage, liveHistory } = params
337
+
338
+ const userMessage: ChatMessage | undefined = inputMessage
339
+ ? {
340
+ ...inputMessage,
341
+ id: inputMessage.id,
342
+ role: 'user',
343
+ parts: inputMessage.parts,
344
+ metadata: { ...inputMessage.metadata, createdAt: toTimestamp(inputMessage.metadata?.createdAt) },
345
+ }
346
+ : undefined
347
+
348
+ const originalMessages = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
349
+ const referenceUserMessage =
350
+ turnParams.kind === 'planTurn'
351
+ ? undefined
352
+ : (userMessage ?? [...liveHistory].reverse().find((message) => message.role === 'user'))
353
+
354
+ const messageText =
355
+ turnParams.kind === 'planTurn'
356
+ ? `${turnParams.planTurn.nodeSpec.label}\n${turnParams.planTurn.nodeSpec.objective}\n${turnParams.planTurn.nodeSpec.instructions}`
357
+ : referenceUserMessage
358
+ ? extractMessageText(referenceUserMessage).trim()
359
+ : ''
360
+
361
+ return { userMessage, originalMessages, referenceUserMessage, messageText }
362
+ }
363
+
364
+ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThreadRunCore')(function* (
365
+ deps: ThreadTurnPreparationRuntimeDeps,
366
+ params: ThreadRunCoreParams,
367
+ ) {
368
+ const runtimeAdapters = getRuntimeAdapters()
369
+ const turnHooks = getTurnHooks()
370
+ const workspaceProvider = runtimeAdapters.workspaceProvider
371
+ const workspacePromise =
372
+ params.kind !== 'approvalContinuation' && workspaceProvider
373
+ ? workspaceProvider.getWorkspace(params.orgRef)
374
+ : Promise.resolve<Record<string, unknown>>({})
375
+ const attachmentService = deps.attachmentService
376
+ const executionPlanService = deps.executionPlanService
377
+ const learnedSkillService = deps.learnedSkillService
378
+ const memoryService = deps.memoryService
379
+ const planRunService = deps.planRunService
380
+ const threadMessageService = deps.threadMessageService
381
+ const threadService = deps.threadService
382
+ const contextCompactionRuntime = deps.contextCompactionRuntime
383
+ const chatRunRegistry = deps.chatRunRegistry
384
+ const { thread, threadRef, orgRef, userRef, userName } = params
385
+ const orgIdString = recordIdToString(orgRef, TABLES.ORGANIZATION)
386
+ const userIdString = recordIdToString(userRef, TABLES.USER)
387
+ const threadIdString = recordIdToString(threadRef, TABLES.THREAD)
388
+
389
+ const shouldPersistInputMessage = params.kind === 'userTurn' ? params.skipInputMessagePersistence !== true : false
390
+ const shouldProcessPostRunSideEffects =
391
+ params.kind !== 'planTurn' &&
392
+ (params.kind === 'approvalContinuation' || params.kind === 'nativeToolApprovalTurn' || shouldPersistInputMessage)
393
+ const inputMessage =
394
+ params.kind === 'userTurn'
395
+ ? yield* validateUserTurnInputMessageEffect({
396
+ thread,
397
+ inputMessage: params.inputMessage,
398
+ attachmentService,
399
+ orgRef,
400
+ userRef,
401
+ })
402
+ : undefined
403
+ const currentContext = yield* Effect.context()
404
+ const runPromiseWithCurrentContext = Effect.runPromiseWith(currentContext)
405
+
406
+ // Start workspace fetch early unless approval handling will short-circuit the turn.
407
+ const threadRecord = yield* waitForThreadCompactionIfNeeded(deps, threadRef).pipe(
408
+ Effect.mapError(
409
+ (error) => new ThreadTurnPreparationError({ message: 'Failed to wait for thread compaction.', cause: error }),
410
+ ),
411
+ Effect.withSpan('ThreadTurnPreparation.waitForCompactionGate'),
412
+ )
413
+ // Plan turns run without the chat lease — they must not block or be blocked by user messages.
414
+ if (params.kind !== 'planTurn') {
415
+ const persistedActiveRunId = toOptionalTrimmedString(threadRecord.activeRunId)
416
+ if (persistedActiveRunId && !(yield* threadService.hasActiveRunLease(threadRef))) {
417
+ const clearedStaleRun = yield* threadService.clearStaleActiveRunIfMissingFromRegistry(threadRef)
418
+ if (!clearedStaleRun) {
419
+ return yield* new ThreadTurnError({ message: 'A chat run is already active.', reason: 'conflict' })
420
+ }
421
+ }
422
+ }
423
+
424
+ if (params.kind === 'approvalContinuation' || params.kind === 'nativeToolApprovalTurn') {
425
+ const approvedAssistantMessage = [...params.approvalMessages]
426
+ .reverse()
427
+ .find((m) => m.role === 'assistant' && hasApprovalRespondedParts(m))
428
+ if (!approvedAssistantMessage) {
429
+ return yield* new ThreadTurnError({ message: 'No approval-responded message found.', reason: 'bad-request' })
430
+ }
431
+ yield* threadMessageService
432
+ .upsertMessagesEffect({ threadId: threadRef, messages: [approvedAssistantMessage] })
433
+ .pipe(Effect.withSpan('ThreadTurnPreparation.persistApprovalMessage'))
434
+ }
435
+
436
+ const persistedCompactionCursor = toOptionalTrimmedString(threadRecord.lastCompactedMessageId) ?? undefined
437
+ const loadRecentHistory = yield* createRecentHistoryLoaderEffect({
438
+ runEffect: (effect) => runPromiseWithCurrentContext(effect),
439
+ threadMessageService,
440
+ threadRef,
441
+ attachmentService,
442
+ orgRef,
443
+ userRef,
444
+ })
445
+ const liveHistory = yield* loadPersistedLiveHistoryEffect({
446
+ threadMessageService,
447
+ threadRef,
448
+ persistedCompactionCursor,
449
+ attachmentService,
450
+ orgRef,
451
+ userRef,
452
+ })
453
+ const { userMessage, originalMessages, referenceUserMessage, messageText } = deriveTurnMessageInputs({
454
+ params,
455
+ inputMessage,
456
+ liveHistory,
457
+ shouldPersistInputMessage,
458
+ })
459
+
460
+ if (userMessage && shouldPersistInputMessage) {
461
+ yield* threadMessageService
462
+ .upsertMessagesEffect({ threadId: threadRef, messages: [userMessage] })
463
+ .pipe(Effect.withSpan('ThreadTurnPreparation.persistUserMessage'))
464
+ }
465
+
466
+ let allAssistantMessages: ChatMessage[] = []
467
+
468
+ const respondedBy = recordIdToString(userRef, TABLES.USER)
469
+ if (params.kind === 'approvalContinuation') {
470
+ yield* executionPlanService
471
+ .applyApprovalResponseFromMessages({
472
+ threadId: threadRef,
473
+ approvalMessages: params.approvalMessages,
474
+ respondedBy,
475
+ })
476
+ .pipe(Effect.withSpan('ThreadTurnPreparation.applyApprovalContinuation'))
477
+
478
+ return {
479
+ originalMessages,
480
+ run: () => Effect.succeed({ inputMessageId: referenceUserMessage?.id, assistantMessages: [] }),
481
+ }
482
+ }
483
+
484
+ if (
485
+ params.kind === 'userTurn' &&
486
+ thread.type === 'group' &&
487
+ threadRecord.nameGenerated !== true &&
488
+ threadRecord.title === THREAD.DEFAULT_TITLE &&
489
+ messageText.length > 0
490
+ ) {
491
+ void safeEnqueue(() => enqueueThreadTitleGeneration({ threadId: threadIdString, sourceText: messageText }), {
492
+ operationName: 'thread-title-generation',
493
+ })
494
+ }
495
+
496
+ if (thread.type === 'thread' && !thread.threadType) {
497
+ return yield* new ThreadTurnError({ message: 'Core threads require a thread type.', reason: 'bad-request' })
498
+ }
499
+ const coreThreadProfile: CoreThreadProfile | null =
500
+ thread.type === 'thread' && thread.threadType ? getCoreThreadProfile(thread.threadType) : null
501
+ const defaultLeadAgentId = getLeadAgentId()
502
+ const visibleThreadAgentId =
503
+ params.agentIdOverride ??
504
+ (thread.type === 'default' ? thread.agentId : (coreThreadProfile?.config.agentId ?? defaultLeadAgentId))
505
+ const coreInstructionSections = coreThreadProfile ? [coreThreadProfile.instructions] : undefined
506
+ const assembledContext = yield* effectTryPromise(
507
+ () =>
508
+ assembleThreadTurnContext({
509
+ thread,
510
+ threadRef,
511
+ orgRef,
512
+ userRef,
513
+ userName,
514
+ orgIdString,
515
+ userIdString,
516
+ messageText,
517
+ workspacePromise,
518
+ workspaceProvider,
519
+ turnHooks,
520
+ }),
521
+ 'Failed to assemble thread turn context.',
522
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.assembleThreadTurnContext'))
523
+ const {
524
+ workspace,
525
+ onboardingActive,
526
+ linearInstalled,
527
+ githubInstalled,
528
+ indexedRepoContext,
529
+ promptContext,
530
+ retrievedKnowledgeSection,
531
+ buildContextResult,
532
+ hookInstructionSections,
533
+ } = assembledContext
534
+
535
+ let memoryBlock = threadService.formatMemoryBlockForPrompt(threadRecord)
536
+ const executionPlanInstructionSectionCache = createExecutionPlanInstructionSectionCache({
537
+ disabled: false,
538
+ loadPlansEffect: () =>
539
+ Effect.gen(function* () {
540
+ const runs = yield* planRunService.getActiveRunRecords(threadRef)
541
+ return yield* Effect.forEach(runs, (run) => planRunService.toSerializablePlan(run, { slim: true }))
542
+ }).pipe(Effect.mapError(toExecutionPlanCacheError)),
543
+ })
544
+ const getExecutionPlanInstructionSections = (): Effect.Effect<string[] | undefined, ThreadTurnStreamingError> =>
545
+ executionPlanInstructionSectionCache
546
+ .getSectionsEffect()
547
+ .pipe(
548
+ Effect.mapError(
549
+ (error) =>
550
+ new ThreadTurnStreamingError({ message: 'Failed to load execution plan instructions.', cause: error }),
551
+ ),
552
+ )
553
+ const invalidateExecutionPlanInstructionSections = () => {
554
+ executionPlanInstructionSectionCache.invalidate()
555
+ }
556
+ if (userMessage) {
557
+ const appliedHumanInput = yield* executionPlanService
558
+ .applyHumanInputFromUserMessage({ threadId: threadRef, message: userMessage, respondedBy })
559
+ .pipe(Effect.withSpan('ThreadTurnPreparation.applyExecutionPlanHumanInput'))
560
+ if (appliedHumanInput) {
561
+ invalidateExecutionPlanInstructionSections()
562
+ }
563
+ }
564
+
565
+ const preSeededMemoriesByAgent = new Map<string, string | undefined>()
566
+ const getPreSeededMemoriesSection = (
567
+ agentId: string,
568
+ ): Effect.Effect<string | undefined, ThreadTurnStreamingError> => {
569
+ if (preSeededMemoriesByAgent.has(agentId)) {
570
+ return Effect.succeed(preSeededMemoriesByAgent.get(agentId))
571
+ }
572
+
573
+ return memoryService
574
+ .getTopMemories({ orgId: orgIdString, agentName: agentId, limit: PRESEEDED_MEMORY_LOOKUP_LIMIT })
575
+ .pipe(
576
+ Effect.tap((preSeededMemories) =>
577
+ Effect.sync(() => {
578
+ preSeededMemoriesByAgent.set(agentId, preSeededMemories)
579
+ }),
580
+ ),
581
+ Effect.mapError(
582
+ (error) =>
583
+ new ThreadTurnStreamingError({
584
+ message: `Failed to load pre-seeded memories for ${agentId}.`,
585
+ cause: error,
586
+ }),
587
+ ),
588
+ )
589
+ }
590
+
591
+ const learnedSkillsByAgent = new Map<string, string | undefined>()
592
+ const getLearnedSkillsSection = (
593
+ agentId: string,
594
+ queryText = messageText,
595
+ ): Effect.Effect<string | undefined, ThreadTurnStreamingError> => {
596
+ const cacheKey = `${agentId}::${queryText}`
597
+ if (learnedSkillsByAgent.has(cacheKey)) {
598
+ return Effect.succeed(learnedSkillsByAgent.get(cacheKey))
599
+ }
600
+
601
+ return Effect.gen(function* () {
602
+ const section = yield* learnedSkillService
603
+ .retrieveForTurn({ orgId: orgIdString, agentId, query: queryText, limit: 3, minConfidence: 0.6 })
604
+ .pipe(
605
+ Effect.catch((error: unknown) =>
606
+ Effect.sync(() => {
607
+ aiLogger.warn`Failed to retrieve learned skills for ${agentId}: ${error}`
608
+ return undefined
609
+ }),
610
+ ),
611
+ )
612
+ learnedSkillsByAgent.set(cacheKey, section)
613
+ return section
614
+ }).pipe(
615
+ Effect.mapError(
616
+ (error) =>
617
+ new ThreadTurnStreamingError({ message: `Failed to load learned skills for ${agentId}.`, cause: error }),
618
+ ),
619
+ )
620
+ }
621
+
622
+ const persistedCompactionSummary =
623
+ persistedCompactionCursor && typeof threadRecord.compactionSummary === 'string'
624
+ ? threadRecord.compactionSummary
625
+ : ''
626
+ const messagesForContext = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
627
+ const threadTurnMessageContext = createThreadTurnMessageContext({
628
+ contextCompactionRuntime,
629
+ persistedCompactionSummary,
630
+ messagesForContext,
631
+ orgRef: recordIdToString(orgRef, TABLES.ORGANIZATION),
632
+ userRef: recordIdToString(userRef, TABLES.USER),
633
+ latestUserMessageId: referenceUserMessage?.id ?? '',
634
+ listReadableUploadsFromMessages: (uploadParams) =>
635
+ attachmentService.listReadableUploadsFromMessages({
636
+ messages: uploadParams.messages.map((message) => ({ parts: message.parts })),
637
+ orgId: uploadParams.orgId,
638
+ userId: uploadParams.userId,
639
+ }),
640
+ })
641
+ const buildTurnToolParams = (toolParams: {
642
+ agentId: string
643
+ mode: 'direct' | 'fixedThreadMode' | 'threadMode'
644
+ memoryBlock: string
645
+ onAppendMemoryBlock: (value: string) => void
646
+ extraMessages?: ChatMessage[]
647
+ skills?: string[]
648
+ includeExecutionPlanTools: boolean
649
+ }) => ({
650
+ agentId: toolParams.agentId,
651
+ orgId: orgRef,
652
+ userId: userRef,
653
+ userName: userName ?? 'there',
654
+ threadId: threadRef,
655
+ orgIdString,
656
+ threadType: thread.type,
657
+ mode: toolParams.mode,
658
+ linearInstalled,
659
+ onboardingActive,
660
+ githubInstalled,
661
+ provideRepoTool: indexedRepoContext.provideRepoTool,
662
+ skills: toolParams.skills,
663
+ defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[toolParams.agentId],
664
+ memoryBlock: toolParams.memoryBlock,
665
+ onAppendMemoryBlock: toolParams.onAppendMemoryBlock,
666
+ availableUploads: threadTurnMessageContext.listReadableUploads(toolParams.extraMessages),
667
+ includeExecutionPlanTools: toolParams.includeExecutionPlanTools,
668
+ onExecutionPlanChanged: invalidateExecutionPlanInstructionSections,
669
+ context: buildContextResult,
670
+ })
671
+ const agentIdentityOverrides = readRuntimeAgentIdentityOverrides(buildContextResult)
672
+
673
+ const executeRun = (runParams: {
674
+ serverRunId: string
675
+ runAbort: ReturnType<typeof createServerRunAbortController>
676
+ writer?: UIMessageStreamWriter<ChatMessage>
677
+ }): Effect.Effect<PreparedThreadTurnResult | void, DatabaseError | ThreadTurnError | ThreadTurnPreparationError> =>
678
+ Effect.ensuring(
679
+ Effect.gen(function* () {
680
+ const currentRunAbort = runParams.runAbort
681
+ const toAbortReason = (): unknown =>
682
+ currentRunAbort.signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')
683
+ const failIfRunAborted = (): Effect.Effect<void, ThreadTurnPreparationError> =>
684
+ currentRunAbort.signal.aborted
685
+ ? Effect.fail(
686
+ new ThreadTurnPreparationError({ message: 'Thread turn was aborted.', cause: toAbortReason() }),
687
+ )
688
+ : Effect.void
689
+ yield* failIfRunAborted()
690
+ if (params.kind !== 'planTurn') {
691
+ yield* threadService
692
+ .setActiveTurn(threadRef, runParams.serverRunId, params.streamId ?? null)
693
+ .pipe(Effect.withSpan('ThreadTurnPreparation.setActiveTurn'))
694
+ }
695
+
696
+ const agentFactoryConfig = getResolvedAgentFactoryConfig()
697
+
698
+ const streamCtx: StreamAgentResponseContext = {
699
+ turnHooks,
700
+ thread,
701
+ threadRef,
702
+ orgRef,
703
+ userRef,
704
+ userName,
705
+ onboardingActive,
706
+ linearInstalled,
707
+ githubInstalled,
708
+ buildContextResult,
709
+ getExecutionPlanInstructionSections,
710
+ getPreSeededMemoriesSection,
711
+ getLearnedSkillsSection,
712
+ promptContext,
713
+ retrievedKnowledgeSection,
714
+ memoryBlock,
715
+ hookInstructionSections,
716
+ runAbortSignal: currentRunAbort.signal,
717
+ }
718
+
719
+ const { runVisibleAgent } = createThreadTurnVisibleAgentRunner({
720
+ agentFactoryConfig: { buildAgentTools: agentFactoryConfig.buildAgentTools },
721
+ buildTurnToolParams,
722
+ threadTurnMessageContext,
723
+ threadMessageService,
724
+ threadRef,
725
+ writer: runParams.writer,
726
+ streamCtx,
727
+ agentIdentityOverrides,
728
+ getMemoryBlock: () => memoryBlock,
729
+ setMemoryBlock: (value: string) => {
730
+ memoryBlock = value
731
+ },
732
+ onPersistedMessage: (persistedMessage: ChatMessage) => {
733
+ allAssistantMessages = upsertChatHistoryMessage(allAssistantMessages, persistedMessage)
734
+ },
735
+ failIfRunAborted,
736
+ })
737
+
738
+ if (params.kind === 'planTurn') {
739
+ const planTurn = params.planTurn
740
+ const submitPlanTurnNodeResultTool = createTool({
741
+ description: buildPlanTurnSubmitToolDescription(planTurn),
742
+ inputSchema: PlanNodeResultSubmissionSchema,
743
+ execute: (result) =>
744
+ executionPlanService.submitPlanTurnResult({
745
+ threadId: threadRef,
746
+ runId: planTurn.runId,
747
+ nodeId: planTurn.nodeId,
748
+ emittedBy: planTurn.nodeSpec.owner.ref,
749
+ input: result,
750
+ }),
751
+ })
752
+
753
+ yield* effectTryPromise(
754
+ () =>
755
+ runVisibleAgent({
756
+ agentId: planTurn.nodeSpec.owner.ref,
757
+ mode: thread.type === 'default' ? 'direct' : 'threadMode',
758
+ additionalInstructionSections: buildPlanTurnInstructionSections(planTurn),
759
+ extraMessages: [buildPlanTurnPromptMessage(planTurn)],
760
+ includeExecutionPlanTools: false,
761
+ extraTools: { [SUBMIT_PLAN_TURN_RESULT_TOOL_NAME]: submitPlanTurnNodeResultTool },
762
+ filterTools: (tools) => applyPlanTurnToolPolicy(tools, planTurn.nodeSpec),
763
+ metadataPatch: { trigger: 'plan-turn', planRunId: planTurn.runId, planNodeId: planTurn.nodeId },
764
+ }),
765
+ 'Failed to execute plan-turn visible agent.',
766
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.executePlanTurn'))
767
+ } else if (thread.type === 'default') {
768
+ if (!thread.agentId) {
769
+ return yield* new ThreadTurnError({
770
+ message: 'Direct threads require an assigned agent.',
771
+ reason: 'bad-request',
772
+ })
773
+ }
774
+ const directAgentId = thread.agentId
775
+ yield* effectTryPromise(
776
+ () => runVisibleAgent({ agentId: directAgentId, mode: 'direct' }),
777
+ 'Failed to execute direct thread agent.',
778
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.executeDirectThread'))
779
+ } else {
780
+ const wsMembers = (thread as { members?: string[] }).members ?? []
781
+ const members = wsMembers.length > 0 ? wsMembers : [...getAgentRoster()]
782
+ const fallbackAgentId = coreThreadProfile?.config.agentId ?? defaultLeadAgentId
783
+ yield* failIfRunAborted()
784
+ writeMultiAgentEvent(runParams.writer, { phase: 'routing', note: 'Routing this turn to the right agent.' })
785
+
786
+ const recentContext = threadTurnMessageContext.currentMessages
787
+ .slice(-6)
788
+ .map((m) => `${m.role}: ${extractMessageText(m).slice(0, 200)}`)
789
+ .join('\n')
790
+
791
+ const triageResult = yield* effectTryPromise(
792
+ () =>
793
+ triageThreadMessage({
794
+ threadTitle: thread.title,
795
+ members,
796
+ messageText,
797
+ recentContext,
798
+ displayNamesById: agentIdentityOverrides.displayNamesById,
799
+ shortDisplayNamesById: agentIdentityOverrides.shortDisplayNamesById,
800
+ routingAliasesByAgentId: agentIdentityOverrides.routingAliasesByAgentId,
801
+ }),
802
+ 'Failed to triage group thread message.',
803
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.routeGroupThread'))
804
+ yield* failIfRunAborted()
805
+
806
+ const runGroupAgent = (agentId: string, options?: { routingContext?: string }) => {
807
+ const additionalSections = [...(coreInstructionSections ?? []), ...hookInstructionSections]
808
+ if (options?.routingContext) {
809
+ additionalSections.push(`<routing-context>\n${options.routingContext}\n</routing-context>`)
810
+ }
811
+ additionalSections.push(
812
+ '<multi-agent-protocol>\nYou are responding as part of a multi-agent thread. Focus on your domain expertise. Be direct and concise — another agent may follow up on different aspects.\n</multi-agent-protocol>',
813
+ )
814
+
815
+ return runVisibleAgent({
816
+ agentId,
817
+ mode: 'threadMode',
818
+ skills: coreThreadProfile?.skills ? [...coreThreadProfile.skills] : undefined,
819
+ additionalInstructionSections: additionalSections,
820
+ })
821
+ }
822
+
823
+ if (!triageResult) {
824
+ yield* effectTryPromise(
825
+ () => runGroupAgent(fallbackAgentId),
826
+ 'Failed to execute fallback group agent.',
827
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.executeFallbackGroupAgent'))
828
+ yield* failIfRunAborted()
829
+ writeMultiAgentEvent(runParams.writer, { phase: 'complete' })
830
+ } else {
831
+ const respondedAgents: string[] = []
832
+ let lastResponse = yield* effectTryPromise(
833
+ () => runGroupAgent(triageResult.agentId, { routingContext: triageResult.routingContext }),
834
+ 'Failed to execute routed group agent.',
835
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.executeRoutedGroupAgent'))
836
+ respondedAgents.push(triageResult.agentId)
837
+ yield* failIfRunAborted()
838
+
839
+ while (respondedAgents.length < 3) {
840
+ const lastResponseText = extractMessageText(lastResponse).slice(0, 500)
841
+ const checkResult = yield* effectTryPromise(
842
+ () =>
843
+ checkForNextAgent({
844
+ threadTitle: thread.title,
845
+ members,
846
+ messageText,
847
+ respondedAgents,
848
+ lastResponseSummary: lastResponseText,
849
+ displayNamesById: agentIdentityOverrides.displayNamesById,
850
+ shortDisplayNamesById: agentIdentityOverrides.shortDisplayNamesById,
851
+ routingAliasesByAgentId: agentIdentityOverrides.routingAliasesByAgentId,
852
+ }),
853
+ 'Failed to select the next group agent.',
854
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.selectNextGroupAgent'))
855
+ yield* failIfRunAborted()
856
+
857
+ if (checkResult.done || !checkResult.agentId) break
858
+
859
+ writeMultiAgentEvent(runParams.writer, {
860
+ phase: 'waiting-for-agent',
861
+ agentId: checkResult.agentId,
862
+ agentName: resolveRuntimeAgentDisplayName(agentIdentityOverrides, checkResult.agentId),
863
+ note: checkResult.routingContext ?? undefined,
864
+ })
865
+
866
+ const bridgeMessage: ChatMessage = {
867
+ id: Bun.randomUUIDv7(),
868
+ role: 'user',
869
+ parts: [
870
+ {
871
+ type: 'text',
872
+ text: checkResult.routingContext ?? 'Please also provide your perspective on this topic.',
873
+ },
874
+ ],
875
+ metadata: { hidden: true, createdAt: yield* Clock.currentTimeMillis } as MessageMetadata,
876
+ }
877
+ yield* failIfRunAborted()
878
+ yield* threadMessageService
879
+ .upsertMessagesEffect({ threadId: threadRef, messages: [bridgeMessage] })
880
+ .pipe(Effect.withSpan('ThreadTurnPreparation.persistBridgeMessage'))
881
+ threadTurnMessageContext.appendMessages([bridgeMessage])
882
+ yield* failIfRunAborted()
883
+
884
+ lastResponse = yield* effectTryPromise(
885
+ () => runGroupAgent(checkResult.agentId, { routingContext: checkResult.routingContext ?? undefined }),
886
+ 'Failed to execute follow-up group agent.',
887
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.executeFollowUpGroupAgent'))
888
+ respondedAgents.push(checkResult.agentId)
889
+ yield* failIfRunAborted()
890
+ writeMultiAgentEvent(runParams.writer, {
891
+ phase: 'agent-message-persisted',
892
+ agentId: checkResult.agentId,
893
+ agentName: resolveRuntimeAgentDisplayName(agentIdentityOverrides, checkResult.agentId),
894
+ messageId: lastResponse.id,
895
+ })
896
+ }
897
+
898
+ yield* failIfRunAborted()
899
+ writeMultiAgentEvent(runParams.writer, { phase: 'complete' })
900
+ }
901
+ }
902
+ }),
903
+ Effect.gen(function* () {
904
+ const latestThreadRecord = yield* threadService.getById(threadRef)
905
+
906
+ yield* finalizeTurnRunEffect({
907
+ serverRunId: runParams.serverRunId,
908
+ getEntity: () => Effect.succeed(latestThreadRecord),
909
+ getUncompactedMessages: (cursor) =>
910
+ threadMessageService
911
+ .listMessagesAfterCursorEffect(threadRef, cursor)
912
+ .pipe(
913
+ Effect.mapError((error) => new TurnLifecycleError({ message: getErrorMessage(error), cause: error })),
914
+ ),
915
+ assessCompaction: (summaryText, messages) =>
916
+ contextCompactionRuntime.shouldCompactHistory({
917
+ summaryText,
918
+ liveMessages: messages,
919
+ contextSize: CONTEXT_WINDOW_TOKENS,
920
+ }),
921
+ enqueueCompaction: () =>
922
+ enqueueContextCompaction({
923
+ domain: 'thread',
924
+ entityId: threadIdString,
925
+ contextSize: CONTEXT_WINDOW_TOKENS,
926
+ }),
927
+ unregisterRun: () => undefined,
928
+ clearActiveRunId: (runId) =>
929
+ Effect.gen(function* () {
930
+ const activeStreamId = yield* threadService.getActiveStreamId(threadRef)
931
+ yield* threadService.clearActiveTurn(threadRef, { runId, streamId: activeStreamId })
932
+ }).pipe(
933
+ Effect.mapError((error) => new TurnLifecycleError({ message: getErrorMessage(error), cause: error })),
934
+ ),
935
+ disposeAbort: () => runParams.runAbort.dispose(),
936
+ activeStreamId: params.streamId,
937
+ clearActiveStreamId: (streamId) =>
938
+ Effect.gen(function* () {
939
+ const activeRunId = yield* threadService.getActiveRunId(threadRef)
940
+ if (!activeRunId) {
941
+ return
942
+ }
943
+ yield* threadService.clearActiveTurn(threadRef, { runId: activeRunId, streamId })
944
+ }).pipe(
945
+ Effect.mapError((error) => new TurnLifecycleError({ message: getErrorMessage(error), cause: error })),
946
+ ),
947
+ }).pipe(
948
+ Effect.withSpan('ThreadTurnPreparation.finalizeTurnRun'),
949
+ Effect.mapError(
950
+ (error) => new ThreadTurnPreparationError({ message: 'Failed to finalize thread turn run.', cause: error }),
951
+ ),
952
+ )
953
+
954
+ if (allAssistantMessages.length > 0 && shouldProcessPostRunSideEffects) {
955
+ yield* effectTryPromise(
956
+ () =>
957
+ runPostTurnSideEffects({
958
+ thread,
959
+ threadRef,
960
+ orgRef,
961
+ userRef,
962
+ userName,
963
+ orgIdString,
964
+ threadIdString,
965
+ onboardingActive,
966
+ workspace,
967
+ allAssistantMessages,
968
+ referenceUserMessage,
969
+ referenceUserMessageId: referenceUserMessage?.id ?? '',
970
+ loadRecentHistory,
971
+ listReadableUploads: () => threadTurnMessageContext.listReadableUploads(),
972
+ memoryBlock,
973
+ visibleThreadAgentId,
974
+ defaultLeadAgentId,
975
+ latestThreadRecord,
976
+ isUserTurn: params.kind === 'userTurn',
977
+ agentDisplayNamesById: agentIdentityOverrides.displayNamesById,
978
+ }),
979
+ 'Failed to run post-turn side effects.',
980
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.runPostTurnSideEffects'))
981
+ }
982
+
983
+ if (allAssistantMessages.length > 0 && params.kind !== 'planTurn') {
984
+ const afterTurn = turnHooks.afterTurn
985
+ if (afterTurn) {
986
+ yield* effectTryPromise(
987
+ () =>
988
+ afterTurn({
989
+ thread,
990
+ threadRef,
991
+ orgRef,
992
+ userRef,
993
+ userName,
994
+ onboardingActive,
995
+ referenceUserMessage,
996
+ assistantMessages: allAssistantMessages,
997
+ latestThreadRecord,
998
+ context: buildContextResult,
999
+ }),
1000
+ 'Failed to run afterTurn hook.',
1001
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.afterTurnHook'))
1002
+ }
1003
+ }
1004
+ }).pipe(
1005
+ Effect.catch((postRunError) =>
1006
+ Effect.sync(() => {
1007
+ aiLogger.error`Thread post-run cleanup failed: ${postRunError}`
1008
+ }),
1009
+ ),
1010
+ ),
1011
+ ).pipe(
1012
+ Effect.mapError((error) =>
1013
+ error._tag === 'ServiceError'
1014
+ ? new ThreadTurnPreparationError({ message: error.message, cause: error.cause })
1015
+ : error,
1016
+ ),
1017
+ )
1018
+
1019
+ const toPreparedThreadTurnResult = (): PreparedThreadTurnResult => ({
1020
+ inputMessageId: referenceUserMessage?.id,
1021
+ assistantMessages: [...allAssistantMessages],
1022
+ })
1023
+
1024
+ const run = (writer?: UIMessageStreamWriter<ChatMessage>) => {
1025
+ const serverRunId = Bun.randomUUIDv7()
1026
+
1027
+ const runEffect =
1028
+ params.kind === 'planTurn'
1029
+ ? Effect.gen(function* () {
1030
+ const runAbort = createServerRunAbortController(
1031
+ [params.abortSignal].filter((signal): signal is AbortSignal => Boolean(signal)),
1032
+ )
1033
+ yield* Effect.annotateCurrentSpan('serverRunId', serverRunId)
1034
+ const runResult = yield* executeRun({ serverRunId, runAbort, writer })
1035
+ return runResult ?? toPreparedThreadTurnResult()
1036
+ })
1037
+ : threadService.withActiveRunLease(threadRef, (leaseAbortSignal) =>
1038
+ Effect.gen(function* () {
1039
+ const runAbort = createServerRunAbortController(
1040
+ [params.abortSignal, leaseAbortSignal].filter((signal): signal is AbortSignal => Boolean(signal)),
1041
+ )
1042
+ yield* Effect.annotateCurrentSpan('serverRunId', serverRunId)
1043
+ const runResult = yield* chatRunRegistry.trackRunEffect(
1044
+ serverRunId,
1045
+ runAbort.controller,
1046
+ executeRun({ serverRunId, runAbort, writer }),
1047
+ )
1048
+ return runResult ?? toPreparedThreadTurnResult()
1049
+ }),
1050
+ )
1051
+
1052
+ return runEffect.pipe(Effect.withSpan('ThreadTurnPreparation.executeRun'), Effect.provide(currentContext))
1053
+ }
1054
+
1055
+ return { originalMessages, run }
1056
+ })
1057
+
1058
+ interface ThreadTurnPreparationDeps {
1059
+ attachment: ReturnType<typeof makeAttachmentService>
1060
+ chatRunRegistry: Context.Service.Shape<typeof ChatRunRegistryTag>
1061
+ compactionCoordination: Context.Service.Shape<typeof CompactionCoordinationTag>
1062
+ executionPlan: ReturnType<typeof makeExecutionPlanService>
1063
+ learnedSkill: ReturnType<typeof makeLearnedSkillService>
1064
+ memory: ReturnType<typeof createMemoryService>
1065
+ planRun: ReturnType<typeof makePlanRunService>
1066
+ threadMessage: ReturnType<typeof makeThreadMessageService>
1067
+ thread: ReturnType<typeof makeThreadService>
1068
+ helperModelRuntime: HelperModelRuntime
1069
+ }
1070
+
1071
+ export function makeThreadTurnPreparationService(deps: ThreadTurnPreparationDeps) {
1072
+ const runtimeDeps: ThreadTurnPreparationRuntimeDeps = {
1073
+ attachmentService: deps.attachment,
1074
+ chatRunRegistry: deps.chatRunRegistry,
1075
+ compactionCoordination: deps.compactionCoordination,
1076
+ executionPlanService: deps.executionPlan,
1077
+ learnedSkillService: deps.learnedSkill,
1078
+ memoryService: deps.memory,
1079
+ planRunService: deps.planRun,
1080
+ threadMessageService: deps.threadMessage,
1081
+ threadService: deps.thread,
1082
+ contextCompactionRuntime: createWiredContextCompactionRuntime({
1083
+ helperModelRuntime: deps.helperModelRuntime,
1084
+ now: nowEpochMillis,
1085
+ randomId: () => Bun.randomUUIDv7(),
1086
+ }),
1087
+ }
1088
+ const annotateTurnSpans = <A, E, R>(params: ThreadRunCoreParams, effect: Effect.Effect<A, E, R>) =>
1089
+ effect.pipe(
1090
+ Effect.annotateSpans(
1091
+ compactSpanAttributes({
1092
+ ...buildThreadTurnSpanAttributes({
1093
+ threadRef: params.threadRef,
1094
+ orgRef: params.orgRef,
1095
+ userRef: params.userRef,
1096
+ kind: params.kind,
1097
+ streamId: params.streamId,
1098
+ agentId: params.agentIdOverride,
1099
+ threadType: params.thread.type,
1100
+ planRunId: params.kind === 'planTurn' ? params.planTurn.runId : undefined,
1101
+ planNodeId: params.kind === 'planTurn' ? params.planTurn.nodeId : undefined,
1102
+ }),
1103
+ userName: params.userName ?? undefined,
1104
+ }),
1105
+ ),
1106
+ )
1107
+
1108
+ return {
1109
+ prepareThreadRunCoreEffect(params: ThreadRunCoreParams) {
1110
+ return annotateTurnSpans(params, prepareThreadRunCoreEffect(runtimeDeps, params))
1111
+ },
1112
+ prepareThreadRunCore(params: ThreadRunCoreParams) {
1113
+ return annotateTurnSpans(params, prepareThreadRunCoreEffect(runtimeDeps, params))
1114
+ },
1115
+ } as const
1116
+ }
1117
+
1118
+ export class ThreadTurnPreparationServiceTag extends Context.Service<
1119
+ ThreadTurnPreparationServiceTag,
1120
+ ReturnType<typeof makeThreadTurnPreparationService>
1121
+ >()('@lota-sdk/core/ThreadTurnPreparationService') {}
1122
+
1123
+ export const ThreadTurnPreparationServiceLive = Layer.effect(
1124
+ ThreadTurnPreparationServiceTag,
1125
+ Effect.gen(function* () {
1126
+ const attachment = yield* AttachmentServiceTag
1127
+ const chatRunRegistry = yield* ChatRunRegistryTag
1128
+ const compactionCoordination = yield* CompactionCoordinationTag
1129
+ const executionPlan = yield* ExecutionPlanServiceTag
1130
+ const learnedSkill = yield* LearnedSkillServiceTag
1131
+ const memory = yield* MemoryServiceTag
1132
+ const planRun = yield* PlanRunServiceTag
1133
+ const threadMessage = yield* ThreadMessageServiceTag
1134
+ const thread = yield* ThreadServiceTag
1135
+ return makeThreadTurnPreparationService({
1136
+ attachment,
1137
+ chatRunRegistry,
1138
+ compactionCoordination,
1139
+ executionPlan,
1140
+ learnedSkill,
1141
+ memory,
1142
+ planRun,
1143
+ threadMessage,
1144
+ thread,
1145
+ helperModelRuntime: yield* HelperModelTag,
1146
+ })
1147
+ }),
1148
+ )