@lota-sdk/core 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -0,0 +1,1146 @@
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 Effect.gen(function* () {
574
+ const preSeededMemories = yield* memoryService.getTopMemories({
575
+ orgId: orgIdString,
576
+ agentName: agentId,
577
+ limit: PRESEEDED_MEMORY_LOOKUP_LIMIT,
578
+ })
579
+ preSeededMemoriesByAgent.set(agentId, preSeededMemories)
580
+ return preSeededMemories
581
+ }).pipe(
582
+ Effect.mapError(
583
+ (error) =>
584
+ new ThreadTurnStreamingError({ message: `Failed to load pre-seeded memories for ${agentId}.`, cause: error }),
585
+ ),
586
+ )
587
+ }
588
+
589
+ const learnedSkillsByAgent = new Map<string, string | undefined>()
590
+ const getLearnedSkillsSection = (
591
+ agentId: string,
592
+ queryText = messageText,
593
+ ): Effect.Effect<string | undefined, ThreadTurnStreamingError> => {
594
+ const cacheKey = `${agentId}::${queryText}`
595
+ if (learnedSkillsByAgent.has(cacheKey)) {
596
+ return Effect.succeed(learnedSkillsByAgent.get(cacheKey))
597
+ }
598
+
599
+ return Effect.gen(function* () {
600
+ const section = yield* learnedSkillService
601
+ .retrieveForTurn({ orgId: orgIdString, agentId, query: queryText, limit: 3, minConfidence: 0.6 })
602
+ .pipe(
603
+ Effect.catch((error: unknown) =>
604
+ Effect.sync(() => {
605
+ aiLogger.warn`Failed to retrieve learned skills for ${agentId}: ${error}`
606
+ return undefined
607
+ }),
608
+ ),
609
+ )
610
+ learnedSkillsByAgent.set(cacheKey, section)
611
+ return section
612
+ }).pipe(
613
+ Effect.mapError(
614
+ (error) =>
615
+ new ThreadTurnStreamingError({ message: `Failed to load learned skills for ${agentId}.`, cause: error }),
616
+ ),
617
+ )
618
+ }
619
+
620
+ const persistedCompactionSummary =
621
+ persistedCompactionCursor && typeof threadRecord.compactionSummary === 'string'
622
+ ? threadRecord.compactionSummary
623
+ : ''
624
+ const messagesForContext = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
625
+ const threadTurnMessageContext = createThreadTurnMessageContext({
626
+ contextCompactionRuntime,
627
+ persistedCompactionSummary,
628
+ messagesForContext,
629
+ orgRef: recordIdToString(orgRef, TABLES.ORGANIZATION),
630
+ userRef: recordIdToString(userRef, TABLES.USER),
631
+ latestUserMessageId: referenceUserMessage?.id ?? '',
632
+ listReadableUploadsFromMessages: (uploadParams) =>
633
+ attachmentService.listReadableUploadsFromMessages({
634
+ messages: uploadParams.messages.map((message) => ({ parts: message.parts })),
635
+ orgId: uploadParams.orgId,
636
+ userId: uploadParams.userId,
637
+ }),
638
+ })
639
+ const buildTurnToolParams = (toolParams: {
640
+ agentId: string
641
+ mode: 'direct' | 'fixedThreadMode' | 'threadMode'
642
+ memoryBlock: string
643
+ onAppendMemoryBlock: (value: string) => void
644
+ extraMessages?: ChatMessage[]
645
+ skills?: string[]
646
+ includeExecutionPlanTools: boolean
647
+ }) => ({
648
+ agentId: toolParams.agentId,
649
+ orgId: orgRef,
650
+ userId: userRef,
651
+ userName: userName ?? 'there',
652
+ threadId: threadRef,
653
+ orgIdString,
654
+ threadType: thread.type,
655
+ mode: toolParams.mode,
656
+ linearInstalled,
657
+ onboardingActive,
658
+ githubInstalled,
659
+ provideRepoTool: indexedRepoContext.provideRepoTool,
660
+ skills: toolParams.skills,
661
+ defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[toolParams.agentId],
662
+ memoryBlock: toolParams.memoryBlock,
663
+ onAppendMemoryBlock: toolParams.onAppendMemoryBlock,
664
+ availableUploads: threadTurnMessageContext.listReadableUploads(toolParams.extraMessages),
665
+ includeExecutionPlanTools: toolParams.includeExecutionPlanTools,
666
+ onExecutionPlanChanged: invalidateExecutionPlanInstructionSections,
667
+ context: buildContextResult,
668
+ })
669
+ const agentIdentityOverrides = readRuntimeAgentIdentityOverrides(buildContextResult)
670
+
671
+ const executeRun = (runParams: {
672
+ serverRunId: string
673
+ runAbort: ReturnType<typeof createServerRunAbortController>
674
+ writer?: UIMessageStreamWriter<ChatMessage>
675
+ }): Effect.Effect<PreparedThreadTurnResult | void, DatabaseError | ThreadTurnError | ThreadTurnPreparationError> =>
676
+ Effect.ensuring(
677
+ Effect.gen(function* () {
678
+ const currentRunAbort = runParams.runAbort
679
+ const toAbortReason = (): unknown =>
680
+ currentRunAbort.signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')
681
+ const failIfRunAborted = (): Effect.Effect<void, ThreadTurnPreparationError> =>
682
+ currentRunAbort.signal.aborted
683
+ ? Effect.fail(
684
+ new ThreadTurnPreparationError({ message: 'Thread turn was aborted.', cause: toAbortReason() }),
685
+ )
686
+ : Effect.void
687
+ yield* failIfRunAborted()
688
+ if (params.kind !== 'planTurn') {
689
+ yield* threadService
690
+ .setActiveTurn(threadRef, runParams.serverRunId, params.streamId ?? null)
691
+ .pipe(Effect.withSpan('ThreadTurnPreparation.setActiveTurn'))
692
+ }
693
+
694
+ const agentFactoryConfig = getResolvedAgentFactoryConfig()
695
+
696
+ const streamCtx: StreamAgentResponseContext = {
697
+ turnHooks,
698
+ thread,
699
+ threadRef,
700
+ orgRef,
701
+ userRef,
702
+ userName,
703
+ onboardingActive,
704
+ linearInstalled,
705
+ githubInstalled,
706
+ buildContextResult,
707
+ getExecutionPlanInstructionSections,
708
+ getPreSeededMemoriesSection,
709
+ getLearnedSkillsSection,
710
+ promptContext,
711
+ retrievedKnowledgeSection,
712
+ memoryBlock,
713
+ hookInstructionSections,
714
+ runAbortSignal: currentRunAbort.signal,
715
+ }
716
+
717
+ const { runVisibleAgent } = createThreadTurnVisibleAgentRunner({
718
+ agentFactoryConfig: { buildAgentTools: agentFactoryConfig.buildAgentTools },
719
+ buildTurnToolParams,
720
+ threadTurnMessageContext,
721
+ threadMessageService,
722
+ threadRef,
723
+ writer: runParams.writer,
724
+ streamCtx,
725
+ agentIdentityOverrides,
726
+ getMemoryBlock: () => memoryBlock,
727
+ setMemoryBlock: (value: string) => {
728
+ memoryBlock = value
729
+ },
730
+ onPersistedMessage: (persistedMessage: ChatMessage) => {
731
+ allAssistantMessages = upsertChatHistoryMessage(allAssistantMessages, persistedMessage)
732
+ },
733
+ failIfRunAborted,
734
+ })
735
+
736
+ if (params.kind === 'planTurn') {
737
+ const planTurn = params.planTurn
738
+ const submitPlanTurnNodeResultTool = createTool({
739
+ description: buildPlanTurnSubmitToolDescription(planTurn),
740
+ inputSchema: PlanNodeResultSubmissionSchema,
741
+ execute: (result) =>
742
+ executionPlanService.submitPlanTurnResult({
743
+ threadId: threadRef,
744
+ runId: planTurn.runId,
745
+ nodeId: planTurn.nodeId,
746
+ emittedBy: planTurn.nodeSpec.owner.ref,
747
+ input: result,
748
+ }),
749
+ })
750
+
751
+ yield* effectTryPromise(
752
+ () =>
753
+ runVisibleAgent({
754
+ agentId: planTurn.nodeSpec.owner.ref,
755
+ mode: thread.type === 'default' ? 'direct' : 'threadMode',
756
+ additionalInstructionSections: buildPlanTurnInstructionSections(planTurn),
757
+ extraMessages: [buildPlanTurnPromptMessage(planTurn)],
758
+ includeExecutionPlanTools: false,
759
+ extraTools: { [SUBMIT_PLAN_TURN_RESULT_TOOL_NAME]: submitPlanTurnNodeResultTool },
760
+ filterTools: (tools) => applyPlanTurnToolPolicy(tools, planTurn.nodeSpec),
761
+ metadataPatch: { trigger: 'plan-turn', planRunId: planTurn.runId, planNodeId: planTurn.nodeId },
762
+ }),
763
+ 'Failed to execute plan-turn visible agent.',
764
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.executePlanTurn'))
765
+ } else if (thread.type === 'default') {
766
+ if (!thread.agentId) {
767
+ return yield* new ThreadTurnError({
768
+ message: 'Direct threads require an assigned agent.',
769
+ reason: 'bad-request',
770
+ })
771
+ }
772
+ const directAgentId = thread.agentId
773
+ yield* effectTryPromise(
774
+ () => runVisibleAgent({ agentId: directAgentId, mode: 'direct' }),
775
+ 'Failed to execute direct thread agent.',
776
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.executeDirectThread'))
777
+ } else {
778
+ const wsMembers = (thread as { members?: string[] }).members ?? []
779
+ const members = wsMembers.length > 0 ? wsMembers : [...getAgentRoster()]
780
+ const fallbackAgentId = coreThreadProfile?.config.agentId ?? defaultLeadAgentId
781
+ yield* failIfRunAborted()
782
+ writeMultiAgentEvent(runParams.writer, { phase: 'routing', note: 'Routing this turn to the right agent.' })
783
+
784
+ const recentContext = threadTurnMessageContext.currentMessages
785
+ .slice(-6)
786
+ .map((m) => `${m.role}: ${extractMessageText(m).slice(0, 200)}`)
787
+ .join('\n')
788
+
789
+ const triageResult = yield* effectTryPromise(
790
+ () =>
791
+ triageThreadMessage({
792
+ threadTitle: thread.title,
793
+ members,
794
+ messageText,
795
+ recentContext,
796
+ displayNamesById: agentIdentityOverrides.displayNamesById,
797
+ shortDisplayNamesById: agentIdentityOverrides.shortDisplayNamesById,
798
+ routingAliasesByAgentId: agentIdentityOverrides.routingAliasesByAgentId,
799
+ }),
800
+ 'Failed to triage group thread message.',
801
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.routeGroupThread'))
802
+ yield* failIfRunAborted()
803
+
804
+ const runGroupAgent = (agentId: string, options?: { routingContext?: string }) => {
805
+ const additionalSections = [...(coreInstructionSections ?? []), ...hookInstructionSections]
806
+ if (options?.routingContext) {
807
+ additionalSections.push(`<routing-context>\n${options.routingContext}\n</routing-context>`)
808
+ }
809
+ additionalSections.push(
810
+ '<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>',
811
+ )
812
+
813
+ return runVisibleAgent({
814
+ agentId,
815
+ mode: 'threadMode',
816
+ skills: coreThreadProfile?.skills ? [...coreThreadProfile.skills] : undefined,
817
+ additionalInstructionSections: additionalSections,
818
+ })
819
+ }
820
+
821
+ if (!triageResult) {
822
+ yield* effectTryPromise(
823
+ () => runGroupAgent(fallbackAgentId),
824
+ 'Failed to execute fallback group agent.',
825
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.executeFallbackGroupAgent'))
826
+ yield* failIfRunAborted()
827
+ writeMultiAgentEvent(runParams.writer, { phase: 'complete' })
828
+ } else {
829
+ const respondedAgents: string[] = []
830
+ let lastResponse = yield* effectTryPromise(
831
+ () => runGroupAgent(triageResult.agentId, { routingContext: triageResult.routingContext }),
832
+ 'Failed to execute routed group agent.',
833
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.executeRoutedGroupAgent'))
834
+ respondedAgents.push(triageResult.agentId)
835
+ yield* failIfRunAborted()
836
+
837
+ while (respondedAgents.length < 3) {
838
+ const lastResponseText = extractMessageText(lastResponse).slice(0, 500)
839
+ const checkResult = yield* effectTryPromise(
840
+ () =>
841
+ checkForNextAgent({
842
+ threadTitle: thread.title,
843
+ members,
844
+ messageText,
845
+ respondedAgents,
846
+ lastResponseSummary: lastResponseText,
847
+ displayNamesById: agentIdentityOverrides.displayNamesById,
848
+ shortDisplayNamesById: agentIdentityOverrides.shortDisplayNamesById,
849
+ routingAliasesByAgentId: agentIdentityOverrides.routingAliasesByAgentId,
850
+ }),
851
+ 'Failed to select the next group agent.',
852
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.selectNextGroupAgent'))
853
+ yield* failIfRunAborted()
854
+
855
+ if (checkResult.done || !checkResult.agentId) break
856
+
857
+ writeMultiAgentEvent(runParams.writer, {
858
+ phase: 'waiting-for-agent',
859
+ agentId: checkResult.agentId,
860
+ agentName: resolveRuntimeAgentDisplayName(agentIdentityOverrides, checkResult.agentId),
861
+ note: checkResult.routingContext ?? undefined,
862
+ })
863
+
864
+ const bridgeMessage: ChatMessage = {
865
+ id: Bun.randomUUIDv7(),
866
+ role: 'user',
867
+ parts: [
868
+ {
869
+ type: 'text',
870
+ text: checkResult.routingContext ?? 'Please also provide your perspective on this topic.',
871
+ },
872
+ ],
873
+ metadata: { hidden: true, createdAt: yield* Clock.currentTimeMillis } as MessageMetadata,
874
+ }
875
+ yield* failIfRunAborted()
876
+ yield* threadMessageService
877
+ .upsertMessagesEffect({ threadId: threadRef, messages: [bridgeMessage] })
878
+ .pipe(Effect.withSpan('ThreadTurnPreparation.persistBridgeMessage'))
879
+ threadTurnMessageContext.appendMessages([bridgeMessage])
880
+ yield* failIfRunAborted()
881
+
882
+ lastResponse = yield* effectTryPromise(
883
+ () => runGroupAgent(checkResult.agentId, { routingContext: checkResult.routingContext ?? undefined }),
884
+ 'Failed to execute follow-up group agent.',
885
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.executeFollowUpGroupAgent'))
886
+ respondedAgents.push(checkResult.agentId)
887
+ yield* failIfRunAborted()
888
+ writeMultiAgentEvent(runParams.writer, {
889
+ phase: 'agent-message-persisted',
890
+ agentId: checkResult.agentId,
891
+ agentName: resolveRuntimeAgentDisplayName(agentIdentityOverrides, checkResult.agentId),
892
+ messageId: lastResponse.id,
893
+ })
894
+ }
895
+
896
+ yield* failIfRunAborted()
897
+ writeMultiAgentEvent(runParams.writer, { phase: 'complete' })
898
+ }
899
+ }
900
+ }),
901
+ Effect.gen(function* () {
902
+ const latestThreadRecord = yield* threadService.getById(threadRef)
903
+
904
+ yield* finalizeTurnRunEffect({
905
+ serverRunId: runParams.serverRunId,
906
+ getEntity: () => Effect.succeed(latestThreadRecord),
907
+ getUncompactedMessages: (cursor) =>
908
+ threadMessageService
909
+ .listMessagesAfterCursorEffect(threadRef, cursor)
910
+ .pipe(
911
+ Effect.mapError((error) => new TurnLifecycleError({ message: getErrorMessage(error), cause: error })),
912
+ ),
913
+ assessCompaction: (summaryText, messages) =>
914
+ contextCompactionRuntime.shouldCompactHistory({
915
+ summaryText,
916
+ liveMessages: messages,
917
+ contextSize: CONTEXT_WINDOW_TOKENS,
918
+ }),
919
+ enqueueCompaction: () =>
920
+ enqueueContextCompaction({
921
+ domain: 'thread',
922
+ entityId: threadIdString,
923
+ contextSize: CONTEXT_WINDOW_TOKENS,
924
+ }),
925
+ unregisterRun: () => undefined,
926
+ clearActiveRunId: (runId) =>
927
+ Effect.gen(function* () {
928
+ const activeStreamId = yield* threadService.getActiveStreamId(threadRef)
929
+ yield* threadService.clearActiveTurn(threadRef, { runId, streamId: activeStreamId })
930
+ }).pipe(
931
+ Effect.mapError((error) => new TurnLifecycleError({ message: getErrorMessage(error), cause: error })),
932
+ ),
933
+ disposeAbort: () => runParams.runAbort.dispose(),
934
+ activeStreamId: params.streamId,
935
+ clearActiveStreamId: (streamId) =>
936
+ Effect.gen(function* () {
937
+ const activeRunId = yield* threadService.getActiveRunId(threadRef)
938
+ if (!activeRunId) {
939
+ return
940
+ }
941
+ yield* threadService.clearActiveTurn(threadRef, { runId: activeRunId, streamId })
942
+ }).pipe(
943
+ Effect.mapError((error) => new TurnLifecycleError({ message: getErrorMessage(error), cause: error })),
944
+ ),
945
+ }).pipe(
946
+ Effect.withSpan('ThreadTurnPreparation.finalizeTurnRun'),
947
+ Effect.mapError(
948
+ (error) => new ThreadTurnPreparationError({ message: 'Failed to finalize thread turn run.', cause: error }),
949
+ ),
950
+ )
951
+
952
+ if (allAssistantMessages.length > 0 && shouldProcessPostRunSideEffects) {
953
+ yield* effectTryPromise(
954
+ () =>
955
+ runPostTurnSideEffects({
956
+ thread,
957
+ threadRef,
958
+ orgRef,
959
+ userRef,
960
+ userName,
961
+ orgIdString,
962
+ threadIdString,
963
+ onboardingActive,
964
+ workspace,
965
+ allAssistantMessages,
966
+ referenceUserMessage,
967
+ referenceUserMessageId: referenceUserMessage?.id ?? '',
968
+ loadRecentHistory,
969
+ listReadableUploads: () => threadTurnMessageContext.listReadableUploads(),
970
+ memoryBlock,
971
+ visibleThreadAgentId,
972
+ defaultLeadAgentId,
973
+ latestThreadRecord,
974
+ isUserTurn: params.kind === 'userTurn',
975
+ agentDisplayNamesById: agentIdentityOverrides.displayNamesById,
976
+ }),
977
+ 'Failed to run post-turn side effects.',
978
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.runPostTurnSideEffects'))
979
+ }
980
+
981
+ if (allAssistantMessages.length > 0 && params.kind !== 'planTurn') {
982
+ const afterTurn = turnHooks.afterTurn
983
+ if (afterTurn) {
984
+ yield* effectTryPromise(
985
+ () =>
986
+ afterTurn({
987
+ thread,
988
+ threadRef,
989
+ orgRef,
990
+ userRef,
991
+ userName,
992
+ onboardingActive,
993
+ referenceUserMessage,
994
+ assistantMessages: allAssistantMessages,
995
+ latestThreadRecord,
996
+ context: buildContextResult,
997
+ }),
998
+ 'Failed to run afterTurn hook.',
999
+ ).pipe(Effect.withSpan('ThreadTurnPreparation.afterTurnHook'))
1000
+ }
1001
+ }
1002
+ }).pipe(
1003
+ Effect.catch((postRunError) =>
1004
+ Effect.sync(() => {
1005
+ aiLogger.error`Thread post-run cleanup failed: ${postRunError}`
1006
+ }),
1007
+ ),
1008
+ ),
1009
+ ).pipe(
1010
+ Effect.mapError((error) =>
1011
+ error._tag === 'ServiceError'
1012
+ ? new ThreadTurnPreparationError({ message: error.message, cause: error.cause })
1013
+ : error,
1014
+ ),
1015
+ )
1016
+
1017
+ const toPreparedThreadTurnResult = (): PreparedThreadTurnResult => ({
1018
+ inputMessageId: referenceUserMessage?.id,
1019
+ assistantMessages: [...allAssistantMessages],
1020
+ })
1021
+
1022
+ const run = (writer?: UIMessageStreamWriter<ChatMessage>) => {
1023
+ const serverRunId = Bun.randomUUIDv7()
1024
+
1025
+ const runEffect =
1026
+ params.kind === 'planTurn'
1027
+ ? Effect.gen(function* () {
1028
+ const runAbort = createServerRunAbortController(
1029
+ [params.abortSignal].filter((signal): signal is AbortSignal => Boolean(signal)),
1030
+ )
1031
+ yield* Effect.annotateCurrentSpan('serverRunId', serverRunId)
1032
+ const runResult = yield* executeRun({ serverRunId, runAbort, writer })
1033
+ return runResult ?? toPreparedThreadTurnResult()
1034
+ })
1035
+ : threadService.withActiveRunLease(threadRef, (leaseAbortSignal) =>
1036
+ Effect.gen(function* () {
1037
+ const runAbort = createServerRunAbortController(
1038
+ [params.abortSignal, leaseAbortSignal].filter((signal): signal is AbortSignal => Boolean(signal)),
1039
+ )
1040
+ yield* Effect.annotateCurrentSpan('serverRunId', serverRunId)
1041
+ const runResult = yield* chatRunRegistry.trackRunEffect(
1042
+ serverRunId,
1043
+ runAbort.controller,
1044
+ executeRun({ serverRunId, runAbort, writer }),
1045
+ )
1046
+ return runResult ?? toPreparedThreadTurnResult()
1047
+ }),
1048
+ )
1049
+
1050
+ return runEffect.pipe(Effect.withSpan('ThreadTurnPreparation.executeRun'), Effect.provide(currentContext))
1051
+ }
1052
+
1053
+ return { originalMessages, run }
1054
+ })
1055
+
1056
+ interface ThreadTurnPreparationDeps {
1057
+ attachment: ReturnType<typeof makeAttachmentService>
1058
+ chatRunRegistry: Context.Service.Shape<typeof ChatRunRegistryTag>
1059
+ compactionCoordination: Context.Service.Shape<typeof CompactionCoordinationTag>
1060
+ executionPlan: ReturnType<typeof makeExecutionPlanService>
1061
+ learnedSkill: ReturnType<typeof makeLearnedSkillService>
1062
+ memory: ReturnType<typeof createMemoryService>
1063
+ planRun: ReturnType<typeof makePlanRunService>
1064
+ threadMessage: ReturnType<typeof makeThreadMessageService>
1065
+ thread: ReturnType<typeof makeThreadService>
1066
+ helperModelRuntime: HelperModelRuntime
1067
+ }
1068
+
1069
+ export function makeThreadTurnPreparationService(deps: ThreadTurnPreparationDeps) {
1070
+ const runtimeDeps: ThreadTurnPreparationRuntimeDeps = {
1071
+ attachmentService: deps.attachment,
1072
+ chatRunRegistry: deps.chatRunRegistry,
1073
+ compactionCoordination: deps.compactionCoordination,
1074
+ executionPlanService: deps.executionPlan,
1075
+ learnedSkillService: deps.learnedSkill,
1076
+ memoryService: deps.memory,
1077
+ planRunService: deps.planRun,
1078
+ threadMessageService: deps.threadMessage,
1079
+ threadService: deps.thread,
1080
+ contextCompactionRuntime: createWiredContextCompactionRuntime({
1081
+ helperModelRuntime: deps.helperModelRuntime,
1082
+ now: nowEpochMillis,
1083
+ randomId: () => Bun.randomUUIDv7(),
1084
+ }),
1085
+ }
1086
+ const annotateTurnSpans = <A, E>(params: ThreadRunCoreParams, effect: Effect.Effect<A, E>) =>
1087
+ effect.pipe(
1088
+ Effect.annotateSpans(
1089
+ compactSpanAttributes({
1090
+ ...buildThreadTurnSpanAttributes({
1091
+ threadRef: params.threadRef,
1092
+ orgRef: params.orgRef,
1093
+ userRef: params.userRef,
1094
+ kind: params.kind,
1095
+ streamId: params.streamId,
1096
+ agentId: params.agentIdOverride,
1097
+ threadType: params.thread.type,
1098
+ planRunId: params.kind === 'planTurn' ? params.planTurn.runId : undefined,
1099
+ planNodeId: params.kind === 'planTurn' ? params.planTurn.nodeId : undefined,
1100
+ }),
1101
+ userName: params.userName ?? undefined,
1102
+ }),
1103
+ ),
1104
+ )
1105
+
1106
+ return {
1107
+ prepareThreadRunCoreEffect(params: ThreadRunCoreParams) {
1108
+ return annotateTurnSpans(params, prepareThreadRunCoreEffect(runtimeDeps, params))
1109
+ },
1110
+ prepareThreadRunCore(params: ThreadRunCoreParams) {
1111
+ return annotateTurnSpans(params, prepareThreadRunCoreEffect(runtimeDeps, params))
1112
+ },
1113
+ } as const
1114
+ }
1115
+
1116
+ export class ThreadTurnPreparationServiceTag extends Context.Service<
1117
+ ThreadTurnPreparationServiceTag,
1118
+ ReturnType<typeof makeThreadTurnPreparationService>
1119
+ >()('ThreadTurnPreparationService') {}
1120
+
1121
+ export const ThreadTurnPreparationServiceLive = Layer.effect(
1122
+ ThreadTurnPreparationServiceTag,
1123
+ Effect.gen(function* () {
1124
+ const attachment = yield* AttachmentServiceTag
1125
+ const chatRunRegistry = yield* ChatRunRegistryTag
1126
+ const compactionCoordination = yield* CompactionCoordinationTag
1127
+ const executionPlan = yield* ExecutionPlanServiceTag
1128
+ const learnedSkill = yield* LearnedSkillServiceTag
1129
+ const memory = yield* MemoryServiceTag
1130
+ const planRun = yield* PlanRunServiceTag
1131
+ const threadMessage = yield* ThreadMessageServiceTag
1132
+ const thread = yield* ThreadServiceTag
1133
+ return makeThreadTurnPreparationService({
1134
+ attachment,
1135
+ chatRunRegistry,
1136
+ compactionCoordination,
1137
+ executionPlan,
1138
+ learnedSkill,
1139
+ memory,
1140
+ planRun,
1141
+ threadMessage,
1142
+ thread,
1143
+ helperModelRuntime: yield* HelperModelTag,
1144
+ })
1145
+ }),
1146
+ )