@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
@@ -1,1147 +0,0 @@
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, PlanNodeSpecRecord } from '@lota-sdk/shared'
11
- import { convertToModelMessages, stepCountIs, tool as createTool, validateUIMessages } from 'ai'
12
- import type { PrepareStepFunction, StopCondition, ToolLoopAgent, ToolSet, UIMessageStreamWriter } from 'ai'
13
-
14
- import type { CoreThreadProfile } from '../config/agent-defaults'
15
- import {
16
- agentRoster,
17
- buildAgentTools,
18
- createAgent,
19
- getLeadAgentId,
20
- getCoreThreadProfile,
21
- getAgentRuntimeConfig,
22
- } from '../config/agent-defaults'
23
- import { lotaDebugLogger } from '../config/debug-logger'
24
- import { aiLogger } from '../config/logger'
25
- import type { RecordIdRef } from '../db/record-id'
26
- import { recordIdToString } from '../db/record-id'
27
- import { TABLES } from '../db/tables'
28
- import { enqueueContextCompaction } from '../queues/context-compaction.queue'
29
- import { enqueueThreadTitleGeneration } from '../queues/title-generation.queue'
30
- import { readRuntimeAgentIdentityOverrides, resolveRuntimeAgentDisplayName } from '../runtime/agent-identity-overrides'
31
- import { OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES } from '../runtime/agent-runtime-policy'
32
- import { createAgentMessageMetadata, createServerRunAbortController } from '../runtime/agent-stream-helpers'
33
- import { hasApprovalRespondedParts } from '../runtime/approval-continuation'
34
- import { buildModelInputMessagesWithUploadMetadata, buildReadableUploadMetadataText } from '../runtime/chat-attachments'
35
- import { hasMessageContent } from '../runtime/chat-message'
36
- import { waitForCompactionIfNeeded } from '../runtime/chat-run-orchestration'
37
- import { CONTEXT_WINDOW_TOKENS } from '../runtime/context-compaction-constants'
38
- import { createExecutionPlanInstructionSectionCache } from '../runtime/execution-plan'
39
- import { mergeInstructionSections } from '../runtime/instruction-sections'
40
- import { runPostTurnSideEffects } from '../runtime/post-turn-side-effects'
41
- import { getRuntimeAdapters, getToolProviders, getTurnHooks } from '../runtime/runtime-extensions'
42
- import {
43
- asRecord,
44
- collectCompletedConsultTeamMessages,
45
- collectToolOutputErrors,
46
- extractMessageText,
47
- readInstructionSections,
48
- readOptionalString,
49
- toOptionalTrimmedString,
50
- } from '../runtime/thread-chat-helpers'
51
- import {
52
- buildPlanTurnInstructionSections,
53
- buildPlanTurnPromptMessage,
54
- buildPlanTurnSubmitToolDescription,
55
- } from '../runtime/thread-plan-turn'
56
- import type { ThreadPlanTurnContext } from '../runtime/thread-plan-turn'
57
- import { assembleThreadTurnContext } from '../runtime/thread-turn-context'
58
- import { finalizeTurnRun } from '../runtime/turn-lifecycle'
59
- import { chatRunRegistry } from '../services/chat-run-registry.service'
60
- import type { NormalizedThread, ThreadRecord } from '../services/thread.types'
61
- import { triageThreadMessage, checkForNextAgent } from '../system-agents/thread-router.agent'
62
- import { safeEnqueue } from '../utils/async'
63
- import { AppError } from '../utils/errors'
64
- import { attachmentService } from './attachment.service'
65
- import { listReadableUploadsFromChatMessages } from './chat-attachments.service'
66
- import { contextCompactionRuntime } from './context-compaction-runtime.singleton'
67
- import { executionPlanService } from './execution-plan.service'
68
- import { learnedSkillService } from './learned-skill.service'
69
- import { memoryService } from './memory.service'
70
- import { planRunService } from './plan-run.service'
71
- import { threadMessageService } from './thread-message.service'
72
- import { ActiveThreadRunConflictError, threadService } from './thread.service'
73
-
74
- type ChatStreamChunk = Parameters<UIMessageStreamWriter<ChatMessage>['write']>[0]
75
-
76
- interface UIMessageStreamResult {
77
- toUIMessageStream(options: Record<string, unknown>): ReadableStream<unknown>
78
- }
79
-
80
- function hasUIMessageStream(value: unknown): value is UIMessageStreamResult {
81
- return (
82
- typeof value === 'object' &&
83
- value !== null &&
84
- typeof (value as { toUIMessageStream?: unknown }).toUIMessageStream === 'function'
85
- )
86
- }
87
-
88
- const PRESEEDED_MEMORY_LOOKUP_LIMIT = 3
89
-
90
- async function waitForThreadCompactionIfNeeded(threadId: RecordIdRef): Promise<ThreadRecord> {
91
- return waitForCompactionIfNeeded({
92
- entityId: recordIdToString(threadId, TABLES.THREAD),
93
- entityLabel: 'Thread',
94
- loadEntity: () => threadService.getById(threadId),
95
- isCompacting: (thread) => thread.isCompacting === true,
96
- })
97
- }
98
-
99
- class ThreadTurnError extends AppError {
100
- constructor(
101
- message: string,
102
- readonly statusCode: 400 | 409,
103
- ) {
104
- super(message, statusCode === 409 ? 'CONFLICT' : 'BAD_REQUEST', statusCode)
105
- this.name = 'ThreadTurnError'
106
- }
107
- }
108
-
109
- function optionalInstructionSection(value: unknown): string[] | undefined {
110
- const section = readOptionalString(value)
111
- return section ? [section] : undefined
112
- }
113
-
114
- function writeMultiAgentEvent(
115
- writer: UIMessageStreamWriter<ChatMessage> | undefined,
116
- event: {
117
- phase: 'routing' | 'waiting-for-agent' | 'agent-message-persisted' | 'complete'
118
- agentId?: string
119
- agentName?: string
120
- messageId?: string
121
- note?: string
122
- },
123
- ): void {
124
- if (!writer) return
125
-
126
- const chunk: ChatStreamChunk = {
127
- type: 'data-multi-agent-event',
128
- id: `multi-agent-${Bun.randomUUIDv7()}`,
129
- data: event,
130
- transient: true,
131
- }
132
- writer.write(chunk)
133
- }
134
-
135
- function applyPlanTurnToolPolicy(tools: ToolSet, nodeSpec: PlanNodeSpecRecord): ToolSet {
136
- const blockedToolNames = new Set([...OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES, ...nodeSpec.toolPolicy.deny])
137
- const allowList = nodeSpec.toolPolicy.allow.length > 0 ? new Set(nodeSpec.toolPolicy.allow) : null
138
-
139
- return Object.fromEntries(
140
- Object.entries(tools).filter(
141
- ([toolName]) =>
142
- !blockedToolNames.has(toolName) &&
143
- (toolName === SUBMIT_PLAN_TURN_RESULT_TOOL_NAME || allowList === null || allowList.has(toolName)),
144
- ),
145
- )
146
- }
147
-
148
- export interface ThreadTurnParams {
149
- thread: NormalizedThread
150
- threadRef: RecordIdRef
151
- orgRef: RecordIdRef
152
- userRef: RecordIdRef
153
- userName?: string | null
154
- agentIdOverride?: string
155
- inputMessage: ChatMessage
156
- skipInputMessagePersistence?: boolean
157
- abortSignal?: AbortSignal
158
- streamId?: string
159
- }
160
-
161
- export interface ThreadApprovalContinuationParams {
162
- thread: NormalizedThread
163
- threadRef: RecordIdRef
164
- orgRef: RecordIdRef
165
- userRef: RecordIdRef
166
- userName?: string | null
167
- approvalMessages: ChatMessage[]
168
- abortSignal?: AbortSignal
169
- streamId?: string
170
- }
171
-
172
- export interface ThreadPlanTurnParams {
173
- thread: NormalizedThread
174
- threadRef: RecordIdRef
175
- orgRef: RecordIdRef
176
- userRef: RecordIdRef
177
- userName?: string | null
178
- planTurn: ThreadPlanTurnContext
179
- abortSignal?: AbortSignal
180
- streamId?: string
181
- }
182
-
183
- type ThreadRunCoreParams = {
184
- thread: NormalizedThread
185
- threadRef: RecordIdRef
186
- orgRef: RecordIdRef
187
- userRef: RecordIdRef
188
- userName?: string | null
189
- agentIdOverride?: string
190
- abortSignal?: AbortSignal
191
- streamId?: string
192
- } & (
193
- | { kind: 'userTurn'; inputMessage: ChatMessage; skipInputMessagePersistence?: boolean }
194
- | { kind: 'approvalContinuation'; approvalMessages: ChatMessage[] }
195
- | { kind: 'nativeToolApprovalTurn'; approvalMessages: ChatMessage[] }
196
- | { kind: 'planTurn'; planTurn: ThreadPlanTurnContext }
197
- )
198
-
199
- interface PreparedThreadTurn {
200
- originalMessages: ChatMessage[]
201
- run: (writer?: UIMessageStreamWriter<ChatMessage>) => Promise<PreparedThreadTurnResult>
202
- }
203
-
204
- export interface PreparedThreadTurnResult {
205
- inputMessageId?: string
206
- assistantMessages: ChatMessage[]
207
- }
208
-
209
- function upsertChatHistoryMessage(messages: ChatMessage[], nextMessage: ChatMessage): ChatMessage[] {
210
- const existingIndex = messages.findIndex((message) => message.id === nextMessage.id)
211
- if (existingIndex === -1) {
212
- return [...messages, nextMessage]
213
- }
214
-
215
- const nextMessages = [...messages]
216
- nextMessages[existingIndex] = nextMessage
217
- return nextMessages
218
- }
219
-
220
- interface StreamAgentResponseContext {
221
- turnHooks: ReturnType<typeof getTurnHooks>
222
- thread: NormalizedThread
223
- threadRef: RecordIdRef
224
- orgRef: RecordIdRef
225
- userRef: RecordIdRef
226
- userName?: string | null
227
- onboardingActive: boolean
228
- linearInstalled: boolean
229
- githubInstalled: boolean
230
- buildContextResult: Record<string, unknown> | null
231
- getExecutionPlanInstructionSections: () => Promise<string[] | undefined>
232
- getPreSeededMemoriesSection: (agentId: string) => Promise<string | undefined>
233
- getLearnedSkillsSection: (agentId: string, queryText?: string) => Promise<string | undefined>
234
- promptContext: { systemWorkspaceDetails?: string }
235
- retrievedKnowledgeSection: string | undefined
236
- memoryBlock: string
237
- hookInstructionSections: string[]
238
- runAbortSignal: AbortSignal
239
- }
240
-
241
- interface StreamAgentResponseParams {
242
- agentId: string
243
- mode: 'direct' | 'fixedThreadMode' | 'threadMode'
244
- messages: ChatMessage[]
245
- tools: ToolSet
246
- observer: {
247
- run: <T>(fn: () => T | Promise<T>) => Promise<T>
248
- recordError: (error: unknown) => void
249
- recordAbort: (error: unknown) => void
250
- }
251
- skills?: string[]
252
- additionalInstructionSections?: string[]
253
- includeExecutionPlanTools?: boolean
254
- writer?: UIMessageStreamWriter<ChatMessage>
255
- stopWhen?: StopCondition<ToolSet> | Array<StopCondition<ToolSet>>
256
- prepareStep?: PrepareStepFunction<ToolSet>
257
- abortSignal?: AbortSignal
258
- }
259
-
260
- async function streamAgentResponse(
261
- ctx: StreamAgentResponseContext,
262
- streamParams: StreamAgentResponseParams,
263
- ): Promise<ChatMessage> {
264
- const agentTimer = lotaDebugLogger.timer(`agent:${streamParams.agentId}`)
265
- const agentIdentityOverrides = readRuntimeAgentIdentityOverrides(ctx.buildContextResult)
266
- // Skip full plan state during plan turns — the plan-turn sections already have the active node contract
267
- const executionPlanInstructionSections =
268
- streamParams.includeExecutionPlanTools === false ? undefined : await ctx.getExecutionPlanInstructionSections()
269
- agentTimer.step('get-execution-plan')
270
- const agentResolution = asRecord(
271
- await ctx.turnHooks.resolveAgent?.({
272
- agentId: streamParams.agentId,
273
- mode: streamParams.mode,
274
- thread: ctx.thread,
275
- threadRef: ctx.threadRef,
276
- orgRef: ctx.orgRef,
277
- userRef: ctx.userRef,
278
- userName: ctx.userName,
279
- onboardingActive: ctx.onboardingActive,
280
- linearInstalled: ctx.linearInstalled,
281
- githubInstalled: ctx.githubInstalled,
282
- skills: streamParams.skills,
283
- additionalInstructionSections: streamParams.additionalInstructionSections,
284
- context: ctx.buildContextResult,
285
- }),
286
- )
287
- agentTimer.step('hook:resolveAgent')
288
- const resolvedAgentId = readOptionalString(agentResolution?.agentId) ?? streamParams.agentId
289
- const latestUserMessage = [...streamParams.messages].reverse().find((message) => message.role === 'user')
290
- const latestUserMessageText = latestUserMessage ? extractMessageText(latestUserMessage).trim() : undefined
291
- const [preSeededMemoriesSection, learnedSkillsSection] = await Promise.all([
292
- ctx.getPreSeededMemoriesSection(resolvedAgentId),
293
- ctx.getLearnedSkillsSection(resolvedAgentId, latestUserMessageText),
294
- ])
295
- agentTimer.step('parallel-fetch(memories+skills)')
296
- const toolNames = new Set(Object.keys(streamParams.tools))
297
- const hasRetrievalTools = [
298
- 'memorySearch',
299
- 'conversationSearch',
300
- 'queryKnowledge',
301
- 'researchTopic',
302
- 'fetchWebpage',
303
- 'inspectWebsite',
304
- ].some((toolName) => toolNames.has(toolName))
305
- const hasDomainRoutingSkills =
306
- (streamParams.skills ?? []).some((skill) => skill.startsWith('cpo-') || skill.startsWith('mentor-')) ||
307
- resolvedAgentId === 'cpo' ||
308
- resolvedAgentId === 'mentor'
309
- const config = getAgentRuntimeConfig({
310
- agentId: resolvedAgentId,
311
- threadType: ctx.thread.type,
312
- mode: streamParams.mode,
313
- skills: streamParams.skills,
314
- onboardingActive: ctx.onboardingActive,
315
- linearInstalled: ctx.linearInstalled,
316
- systemWorkspaceDetails: ctx.promptContext.systemWorkspaceDetails,
317
- preSeededMemoriesSection,
318
- retrievedKnowledgeSection: ctx.retrievedKnowledgeSection,
319
- threadMemoryBlock: ctx.memoryBlock,
320
- learnedSkillsSection,
321
- userMessageText: latestUserMessageText,
322
- ruleOptions: { includeMemr3Rule: hasRetrievalTools, includeDomainReasoningFallbackRule: hasDomainRoutingSkills },
323
- additionalInstructionSections: mergeInstructionSections(
324
- executionPlanInstructionSections,
325
- streamParams.additionalInstructionSections,
326
- ctx.hookInstructionSections,
327
- readInstructionSections(agentResolution?.additionalInstructionSections),
328
- optionalInstructionSection(agentResolution?.extraInstructions),
329
- ),
330
- context: ctx.buildContextResult,
331
- }) as Record<string, unknown>
332
- agentTimer.step('build-agent-config')
333
- const modelMessages = await convertToModelMessages(streamParams.messages, { ignoreIncompleteToolCalls: true })
334
- agentTimer.step('convert-model-messages')
335
- const agent = createAgent[config.id as string]({
336
- mode: streamParams.mode,
337
- tools: streamParams.tools,
338
- extraInstructions: config.extraInstructions,
339
- maxRetries: 3,
340
- stopWhen: (agentResolution?.stopWhen as StopCondition<ToolSet> | Array<StopCondition<ToolSet>> | undefined) ??
341
- streamParams.stopWhen ?? [stepCountIs(config.maxSteps as number)],
342
- prepareStep: (agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
343
- }) as ToolLoopAgent<never, ToolSet>
344
- const agentAbortSignal = streamParams.abortSignal ?? ctx.runAbortSignal
345
- agentTimer.step('agent-construction')
346
-
347
- let result: unknown
348
- try {
349
- result = await streamParams.observer.run(() =>
350
- agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal }),
351
- )
352
- agentTimer.step('agent.stream()-resolved')
353
- } catch (error) {
354
- if (agentAbortSignal.aborted) {
355
- streamParams.observer.recordAbort(error)
356
- throw error
357
- }
358
-
359
- streamParams.observer.recordError(error)
360
- throw error
361
- }
362
- if (!hasUIMessageStream(result)) {
363
- throw new Error(`Agent run for ${resolvedAgentId} did not expose a UI message stream.`)
364
- }
365
-
366
- let responseMessage: ChatMessage | null = null
367
- let resolveFinishedStream!: () => void
368
- const finishedStream = new Promise<void>((resolve) => {
369
- resolveFinishedStream = resolve
370
- })
371
-
372
- const uiStream = result.toUIMessageStream({
373
- generateMessageId: () => Bun.randomUUIDv7(),
374
- originalMessages: streamParams.messages,
375
- sendReasoning: true,
376
- sendSources: true,
377
- messageMetadata: createAgentMessageMetadata({
378
- agentId: resolvedAgentId,
379
- agentName: resolveRuntimeAgentDisplayName(agentIdentityOverrides, resolvedAgentId),
380
- }),
381
- onFinish: ({ responseMessage: finishedResponseMessage }: { responseMessage: ChatMessage }) => {
382
- responseMessage = withMessageCreatedAt(finishedResponseMessage, Date.now())
383
- resolveFinishedStream()
384
- },
385
- }) as ReadableStream<ChatStreamChunk>
386
- const reader = uiStream.getReader()
387
- let firstChunkLogged = false
388
- try {
389
- for (;;) {
390
- const { done, value } = await reader.read()
391
- if (done) break
392
- if (!firstChunkLogged) {
393
- agentTimer.step('first-stream-chunk')
394
- firstChunkLogged = true
395
- }
396
- if (streamParams.writer) {
397
- streamParams.writer.write(value)
398
- }
399
- }
400
- } finally {
401
- reader.releaseLock()
402
- }
403
- agentTimer.step('stream-complete')
404
-
405
- await finishedStream
406
- // responseMessage is set inside the stream callback — linter cannot track cross-callback assignment
407
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
408
- if (responseMessage === null) {
409
- throw new Error(`Agent run for ${resolvedAgentId} did not produce a response message.`)
410
- }
411
-
412
- for (const toolError of collectToolOutputErrors({ responseMessage: responseMessage })) {
413
- aiLogger.error`Tool execution failed (agent=${resolvedAgentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
414
- }
415
-
416
- return responseMessage
417
- }
418
-
419
- export async function prepareThreadRunCore(params: ThreadRunCoreParams): Promise<PreparedThreadTurn> {
420
- const { thread, threadRef, orgRef, userRef, userName } = params
421
- const runtimeAdapters = getRuntimeAdapters()
422
- const turnHooks = getTurnHooks()
423
- const toolProviders = getToolProviders()
424
- const workspaceProvider = runtimeAdapters.workspaceProvider
425
- const orgIdString = recordIdToString(orgRef, TABLES.ORGANIZATION)
426
- const userIdString = recordIdToString(userRef, TABLES.USER)
427
- const threadIdString = recordIdToString(threadRef, TABLES.THREAD)
428
-
429
- const hydrateMessageFileUrls = (message: ChatMessage): ChatMessage => ({
430
- ...message,
431
- parts: attachmentService.hydrateSignedFileUrlsInMessageParts({
432
- parts: message.parts as Array<Record<string, unknown>>,
433
- orgId: orgRef,
434
- userId: userRef,
435
- }) as ChatMessage['parts'],
436
- })
437
-
438
- let inputMessage: ChatMessage | undefined
439
- const shouldPersistInputMessage = params.kind === 'userTurn' ? params.skipInputMessagePersistence !== true : false
440
- const shouldProcessPostRunSideEffects =
441
- params.kind !== 'planTurn' &&
442
- (params.kind === 'approvalContinuation' || params.kind === 'nativeToolApprovalTurn' || shouldPersistInputMessage)
443
- if (params.kind === 'userTurn') {
444
- inputMessage = hydrateMessageFileUrls(withMessageCreatedAt(params.inputMessage, Date.now()))
445
- if (inputMessage.role !== 'user') {
446
- throw new ThreadTurnError('Only user messages can be submitted to the thread runtime.', 400)
447
- }
448
- if (!hasMessageContent(inputMessage.parts)) {
449
- throw new ThreadTurnError('Thread messages must include text or attachments.', 400)
450
- }
451
- if (thread.type === 'default' && !thread.agentId) {
452
- throw new ThreadTurnError('Default threads require an assigned agent.', 400)
453
- }
454
- }
455
-
456
- const timer = lotaDebugLogger.timer('prepare')
457
-
458
- // Start workspace fetch early unless approval handling will short-circuit the turn.
459
- const workspacePromise =
460
- params.kind !== 'approvalContinuation' && workspaceProvider
461
- ? workspaceProvider.getWorkspace(orgRef)
462
- : Promise.resolve({})
463
-
464
- const threadRecord = await waitForThreadCompactionIfNeeded(threadRef)
465
- timer.step('compaction-gate')
466
- // Plan turns run without the chat lease — they must not block or be blocked by user messages.
467
- if (params.kind !== 'planTurn') {
468
- if ((await threadService.hasActiveRunLease(threadRef)) || toOptionalTrimmedString(threadRecord.activeRunId)) {
469
- const clearedStaleRun = await threadService.clearStaleActiveRunIfMissingFromRegistry(threadRef)
470
- if (!clearedStaleRun) {
471
- throw new ThreadTurnError('A chat run is already active.', 409)
472
- }
473
- }
474
- }
475
-
476
- if (params.kind === 'approvalContinuation' || params.kind === 'nativeToolApprovalTurn') {
477
- const approvedAssistantMessage = [...params.approvalMessages]
478
- .reverse()
479
- .find((m) => m.role === 'assistant' && hasApprovalRespondedParts(m))
480
- if (!approvedAssistantMessage) {
481
- throw new ThreadTurnError('No approval-responded message found.', 400)
482
- }
483
- await threadMessageService.upsertMessages({ threadId: threadRef, messages: [approvedAssistantMessage] })
484
- timer.step('persist-approval-message')
485
- }
486
-
487
- const persistedCompactionCursor = toOptionalTrimmedString(threadRecord.lastCompactedMessageId) ?? undefined
488
- const persistedLiveHistoryPromise = threadMessageService.listMessagesAfterCursor(threadRef, persistedCompactionCursor)
489
- let recentHistoryPromise: Promise<ChatMessage[]> | null = null
490
- const loadRecentHistory = async (): Promise<ChatMessage[]> => {
491
- if (!recentHistoryPromise) {
492
- recentHistoryPromise = threadMessageService
493
- .listRecentMessages(threadRef, 64)
494
- .then(async (persistedRecentHistory) => {
495
- if (persistedRecentHistory.length === 0) {
496
- return [] as ChatMessage[]
497
- }
498
-
499
- const messages = await validateUIMessages<ChatMessage>({
500
- messages: persistedRecentHistory,
501
- metadataSchema: messageMetadataSchema,
502
- dataSchemas: dataPartsSchemas,
503
- })
504
- return messages.map(hydrateMessageFileUrls)
505
- })
506
- }
507
-
508
- return await recentHistoryPromise
509
- }
510
-
511
- let userMessage: ChatMessage | undefined
512
- if (inputMessage) {
513
- userMessage = {
514
- ...inputMessage,
515
- id: inputMessage.id,
516
- role: 'user',
517
- parts: inputMessage.parts,
518
- metadata: { ...inputMessage.metadata, createdAt: toTimestamp(inputMessage.metadata?.createdAt) },
519
- }
520
- }
521
-
522
- const persistedLiveHistory = await persistedLiveHistoryPromise
523
- timer.step('fetch-history')
524
- const liveHistory = await (persistedLiveHistory.length === 0
525
- ? Promise.resolve([] as ChatMessage[])
526
- : validateUIMessages<ChatMessage>({
527
- messages: persistedLiveHistory,
528
- metadataSchema: messageMetadataSchema,
529
- dataSchemas: dataPartsSchemas,
530
- }).then((messages) => messages.map(hydrateMessageFileUrls)))
531
- timer.step('validate+hydrate-history')
532
-
533
- if (userMessage && shouldPersistInputMessage) {
534
- await threadMessageService.upsertMessages({ threadId: threadRef, messages: [userMessage] })
535
- timer.step('persist-user-message')
536
- }
537
-
538
- const originalMessages = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
539
- let allAssistantMessages: ChatMessage[] = []
540
- const referenceUserMessage =
541
- params.kind === 'planTurn'
542
- ? undefined
543
- : params.kind === 'userTurn' && !shouldPersistInputMessage
544
- ? [...liveHistory].reverse().find((m) => m.role === 'user')
545
- : (userMessage ?? [...liveHistory].reverse().find((m) => m.role === 'user'))
546
- const messageText =
547
- params.kind === 'planTurn'
548
- ? `${params.planTurn.nodeSpec.label}\n${params.planTurn.nodeSpec.objective}\n${params.planTurn.nodeSpec.instructions}`
549
- : referenceUserMessage
550
- ? extractMessageText(referenceUserMessage).trim()
551
- : ''
552
-
553
- const respondedBy = recordIdToString(userRef, TABLES.USER)
554
- if (params.kind === 'approvalContinuation') {
555
- await executionPlanService.applyApprovalResponseFromMessages({
556
- threadId: threadRef,
557
- approvalMessages: params.approvalMessages,
558
- respondedBy,
559
- })
560
- timer.step('approval-continuation')
561
-
562
- return { originalMessages, run: async () => ({ inputMessageId: referenceUserMessage?.id, assistantMessages: [] }) }
563
- }
564
-
565
- if (
566
- params.kind === 'userTurn' &&
567
- thread.type === 'group' &&
568
- threadRecord.nameGenerated !== true &&
569
- threadRecord.title === THREAD.DEFAULT_TITLE &&
570
- messageText.length > 0
571
- ) {
572
- void safeEnqueue(() => enqueueThreadTitleGeneration({ threadId: threadIdString, sourceText: messageText }), {
573
- operationName: 'thread-title-generation',
574
- })
575
- }
576
-
577
- if (thread.type === 'thread' && !thread.threadType) {
578
- throw new ThreadTurnError('Core threads require a thread type.', 400)
579
- }
580
- const coreThreadProfile: CoreThreadProfile | null =
581
- thread.type === 'thread' && thread.threadType ? getCoreThreadProfile(thread.threadType) : null
582
- const defaultLeadAgentId = getLeadAgentId()
583
- const visibleThreadAgentId =
584
- params.agentIdOverride ??
585
- (thread.type === 'default' ? thread.agentId : (coreThreadProfile?.config.agentId ?? defaultLeadAgentId))
586
- const coreInstructionSections = coreThreadProfile ? [coreThreadProfile.instructions] : undefined
587
- const assembledContext = await assembleThreadTurnContext({
588
- thread,
589
- threadRef,
590
- orgRef,
591
- userRef,
592
- userName,
593
- orgIdString,
594
- userIdString,
595
- messageText,
596
- workspacePromise,
597
- workspaceProvider,
598
- turnHooks,
599
- onStep: (step) => timer.step(step),
600
- })
601
- const {
602
- workspace,
603
- onboardingActive,
604
- linearInstalled,
605
- githubInstalled,
606
- indexedRepoContext,
607
- promptContext,
608
- retrievedKnowledgeSection,
609
- buildContextResult,
610
- hookInstructionSections,
611
- } = assembledContext
612
-
613
- let memoryBlock = threadService.formatMemoryBlockForPrompt(threadRecord)
614
- const executionPlanInstructionSectionCache = createExecutionPlanInstructionSectionCache({
615
- disabled: false,
616
- loadPlans: async () => {
617
- const runs = await planRunService.getActiveRunRecords(threadRef)
618
- return Promise.all(runs.map((run) => planRunService.toSerializablePlan(run, { slim: true })))
619
- },
620
- })
621
- const getExecutionPlanInstructionSections = async (): Promise<string[] | undefined> =>
622
- await executionPlanInstructionSectionCache.getSections()
623
- const invalidateExecutionPlanInstructionSections = () => {
624
- executionPlanInstructionSectionCache.invalidate()
625
- }
626
- if (userMessage) {
627
- const appliedHumanInput = await executionPlanService.applyHumanInputFromUserMessage({
628
- threadId: threadRef,
629
- message: userMessage,
630
- respondedBy,
631
- })
632
- if (appliedHumanInput) {
633
- invalidateExecutionPlanInstructionSections()
634
- }
635
- }
636
- timer.step('execution-plan-input')
637
-
638
- const preSeededMemoriesByAgent = new Map<string, string | undefined>()
639
- const getPreSeededMemoriesSection = async (agentId: string): Promise<string | undefined> => {
640
- if (preSeededMemoriesByAgent.has(agentId)) {
641
- return preSeededMemoriesByAgent.get(agentId)
642
- }
643
-
644
- const preSeededMemories = await memoryService.getTopMemories({
645
- orgId: orgIdString,
646
- agentName: agentId,
647
- limit: PRESEEDED_MEMORY_LOOKUP_LIMIT,
648
- })
649
- preSeededMemoriesByAgent.set(agentId, preSeededMemories)
650
- return preSeededMemories
651
- }
652
-
653
- const learnedSkillsByAgent = new Map<string, string | undefined>()
654
- const getLearnedSkillsSection = async (agentId: string, queryText = messageText): Promise<string | undefined> => {
655
- const cacheKey = `${agentId}::${queryText}`
656
- if (learnedSkillsByAgent.has(cacheKey)) return learnedSkillsByAgent.get(cacheKey)
657
-
658
- const section = await learnedSkillService
659
- .retrieveForTurn({ orgId: orgIdString, agentId, query: queryText, limit: 3, minConfidence: 0.6 })
660
- .catch((error) => {
661
- aiLogger.warn`Failed to retrieve learned skills for ${agentId}: ${error}`
662
- return undefined
663
- })
664
- learnedSkillsByAgent.set(cacheKey, section)
665
- return section
666
- }
667
-
668
- const persistedCompactionSummary =
669
- persistedCompactionCursor && typeof threadRecord.compactionSummary === 'string'
670
- ? threadRecord.compactionSummary
671
- : ''
672
- const messagesForContext = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
673
- let currentMessages = contextCompactionRuntime.prependSummaryMessage(persistedCompactionSummary, messagesForContext)
674
- const referenceUserMessageId = referenceUserMessage?.id ?? ''
675
- const listReadableUploads = (extraMessages: ChatMessage[] = []) =>
676
- listReadableUploadsFromChatMessages({
677
- messages: [...currentMessages, ...extraMessages],
678
- orgId: orgRef,
679
- userId: userRef,
680
- })
681
- const buildRunInputMessages = (extraMessages: ChatMessage[] = []): ChatMessage[] =>
682
- buildModelInputMessagesWithUploadMetadata({
683
- messages: [...currentMessages, ...extraMessages],
684
- latestUserMessageId: referenceUserMessageId,
685
- uploadMetadataText: buildReadableUploadMetadataText(listReadableUploads(extraMessages)),
686
- })
687
- const buildTurnToolParams = (toolParams: {
688
- agentId: string
689
- mode: 'direct' | 'fixedThreadMode' | 'threadMode'
690
- memoryBlock: string
691
- onAppendMemoryBlock: (value: string) => void
692
- extraMessages?: ChatMessage[]
693
- skills?: string[]
694
- includeExecutionPlanTools: boolean
695
- }) => ({
696
- agentId: toolParams.agentId,
697
- orgId: orgRef,
698
- userId: userRef,
699
- userName: userName ?? 'there',
700
- threadId: threadRef,
701
- orgIdString,
702
- threadType: thread.type,
703
- mode: toolParams.mode,
704
- linearInstalled,
705
- onboardingActive,
706
- githubInstalled,
707
- provideRepoTool: indexedRepoContext.provideRepoTool,
708
- skills: toolParams.skills,
709
- defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[toolParams.agentId],
710
- memoryBlock: toolParams.memoryBlock,
711
- onAppendMemoryBlock: toolParams.onAppendMemoryBlock,
712
- availableUploads: listReadableUploads(toolParams.extraMessages),
713
- includeExecutionPlanTools: toolParams.includeExecutionPlanTools,
714
- onExecutionPlanChanged: invalidateExecutionPlanInstructionSections,
715
- context: buildContextResult,
716
- })
717
-
718
- timer.step('preparation-complete')
719
-
720
- return {
721
- originalMessages,
722
- run: async (writer?: UIMessageStreamWriter<ChatMessage>) => {
723
- const executeRun = async (leaseAbortSignal?: AbortSignal): Promise<PreparedThreadTurnResult | void> => {
724
- const agentIdentityOverrides = readRuntimeAgentIdentityOverrides(buildContextResult)
725
- const runTimer = lotaDebugLogger.timer('run')
726
- const serverRunId = Bun.randomUUIDv7()
727
- const runAbortSignals = leaseAbortSignal ? [params.abortSignal, leaseAbortSignal] : [params.abortSignal]
728
- const runAbort = createServerRunAbortController(
729
- runAbortSignals.filter((signal): signal is AbortSignal => Boolean(signal)),
730
- )
731
- if (runAbort.signal.aborted) {
732
- throw runAbort.signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')
733
- }
734
- // Plan turns run without the chat lease — don't claim the active run slot.
735
- if (params.kind !== 'planTurn') {
736
- await threadService.setActiveTurn(threadRef, serverRunId, params.streamId ?? null)
737
- chatRunRegistry.register(serverRunId, runAbort.controller)
738
- }
739
- runTimer.step('set-active-run+stream')
740
-
741
- try {
742
- const throwIfRunAborted = () => {
743
- if (!runAbort.signal.aborted) return
744
- throw runAbort.signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')
745
- }
746
-
747
- const buildAgentMetadataPatch = (agentId: string, agentName: string): NonNullable<MessageMetadata> => ({
748
- agentId,
749
- agentName,
750
- semanticTerminationReason: 'none',
751
- })
752
-
753
- const createObserver = (agentId: string) => ({
754
- run: <T>(fn: () => T | Promise<T>) => Promise.resolve(fn()),
755
- recordError: (error: unknown) => {
756
- aiLogger.error`Agent run failed (agent=${agentId}): ${error}`
757
- },
758
- recordAbort: (error: unknown) => {
759
- aiLogger.info`Agent run aborted (agent=${agentId}): ${error instanceof Error ? error.message : String(error)}`
760
- },
761
- })
762
-
763
- const commitAssistantResponse = async (
764
- response: ChatMessage,
765
- agentId: string,
766
- agentName: string,
767
- metadataPatch?: NonNullable<MessageMetadata>,
768
- ) => {
769
- throwIfRunAborted()
770
-
771
- const toCommittedAssistantMessage = (
772
- message: ChatMessage,
773
- resolvedAgentId: string,
774
- resolvedAgentName: string,
775
- patch?: NonNullable<MessageMetadata>,
776
- ) =>
777
- withMessageCreatedAt(
778
- {
779
- ...message,
780
- metadata: {
781
- ...message.metadata,
782
- ...buildAgentMetadataPatch(resolvedAgentId, resolvedAgentName),
783
- ...patch,
784
- },
785
- },
786
- toTimestamp(message.metadata?.createdAt) ?? Date.now(),
787
- )
788
-
789
- const committedConsultMessages = collectCompletedConsultTeamMessages({ responseMessage: response }).flatMap(
790
- (consultMessage) => {
791
- const consultAgentId = readOptionalString(consultMessage.metadata?.agentId)
792
- const consultAgentName = readOptionalString(consultMessage.metadata?.agentName)
793
- if (!consultAgentId || !consultAgentName) {
794
- return []
795
- }
796
-
797
- return [toCommittedAssistantMessage(consultMessage, consultAgentId, consultAgentName)]
798
- },
799
- )
800
-
801
- const committed = toCommittedAssistantMessage(response, agentId, agentName, metadataPatch)
802
- const messagesToPersist = [...committedConsultMessages, committed]
803
-
804
- await threadMessageService.upsertMessages({ threadId: threadRef, messages: messagesToPersist })
805
- for (const persistedMessage of messagesToPersist) {
806
- currentMessages = upsertChatHistoryMessage(currentMessages, persistedMessage)
807
- allAssistantMessages = upsertChatHistoryMessage(allAssistantMessages, persistedMessage)
808
- }
809
- throwIfRunAborted()
810
-
811
- return committed
812
- }
813
-
814
- const streamCtx: StreamAgentResponseContext = {
815
- turnHooks,
816
- thread,
817
- threadRef,
818
- orgRef,
819
- userRef,
820
- userName,
821
- onboardingActive,
822
- linearInstalled,
823
- githubInstalled,
824
- buildContextResult,
825
- getExecutionPlanInstructionSections,
826
- getPreSeededMemoriesSection,
827
- getLearnedSkillsSection,
828
- promptContext,
829
- retrievedKnowledgeSection,
830
- memoryBlock,
831
- hookInstructionSections,
832
- runAbortSignal: runAbort.signal,
833
- }
834
-
835
- const runVisibleAgent = async (runParams: {
836
- agentId: string
837
- mode: 'direct' | 'fixedThreadMode' | 'threadMode'
838
- skills?: string[]
839
- additionalInstructionSections?: string[]
840
- extraMessages?: ChatMessage[]
841
- extraTools?: ToolSet
842
- filterTools?: (tools: ToolSet) => ToolSet
843
- includeExecutionPlanTools?: boolean
844
- metadataPatch?: NonNullable<MessageMetadata>
845
- }): Promise<ChatMessage> => {
846
- const visibleTimer = lotaDebugLogger.timer(`visible:${runParams.agentId}`)
847
- let runMemoryBlock = memoryBlock
848
- const includeExecutionPlanTools =
849
- runParams.includeExecutionPlanTools ?? runParams.mode !== 'fixedThreadMode'
850
- const rawTools: ToolSet = {
851
- ...((await buildAgentTools(
852
- buildTurnToolParams({
853
- agentId: runParams.agentId,
854
- mode: runParams.mode,
855
- skills: runParams.skills,
856
- memoryBlock: runMemoryBlock,
857
- onAppendMemoryBlock: (value: string) => {
858
- runMemoryBlock = value
859
- },
860
- extraMessages: runParams.extraMessages,
861
- includeExecutionPlanTools,
862
- }),
863
- )) as ToolSet),
864
- ...toolProviders,
865
- ...runParams.extraTools,
866
- }
867
- const tools = runParams.filterTools ? runParams.filterTools(rawTools) : rawTools
868
- visibleTimer.step('build-agent-tools')
869
- streamCtx.memoryBlock = memoryBlock
870
- throwIfRunAborted()
871
- const responseMessage = await streamAgentResponse(streamCtx, {
872
- agentId: runParams.agentId,
873
- mode: runParams.mode,
874
- messages: buildRunInputMessages(runParams.extraMessages),
875
- tools,
876
- observer: createObserver(runParams.agentId),
877
- skills: runParams.skills,
878
- additionalInstructionSections: runParams.additionalInstructionSections,
879
- includeExecutionPlanTools,
880
- writer,
881
- })
882
-
883
- visibleTimer.step('stream-agent-response')
884
- memoryBlock = runMemoryBlock
885
-
886
- return commitAssistantResponse(
887
- responseMessage,
888
- runParams.agentId,
889
- resolveRuntimeAgentDisplayName(agentIdentityOverrides, runParams.agentId),
890
- runParams.metadataPatch,
891
- )
892
- }
893
-
894
- if (params.kind === 'planTurn') {
895
- const planTurn = params.planTurn
896
- const submitPlanTurnNodeResultTool = createTool({
897
- description: buildPlanTurnSubmitToolDescription(planTurn),
898
- inputSchema: PlanNodeResultSubmissionSchema,
899
- execute: async (result) =>
900
- await executionPlanService.submitPlanTurnResult({
901
- threadId: threadRef,
902
- runId: planTurn.runId,
903
- nodeId: planTurn.nodeId,
904
- emittedBy: planTurn.nodeSpec.owner.ref,
905
- input: result,
906
- }),
907
- })
908
-
909
- await runVisibleAgent({
910
- agentId: planTurn.nodeSpec.owner.ref,
911
- mode: thread.type === 'default' ? 'direct' : 'threadMode',
912
- additionalInstructionSections: buildPlanTurnInstructionSections(planTurn),
913
- extraMessages: [buildPlanTurnPromptMessage(planTurn)],
914
- includeExecutionPlanTools: false,
915
- extraTools: { [SUBMIT_PLAN_TURN_RESULT_TOOL_NAME]: submitPlanTurnNodeResultTool },
916
- filterTools: (tools) => applyPlanTurnToolPolicy(tools, planTurn.nodeSpec),
917
- metadataPatch: { trigger: 'plan-turn', planRunId: planTurn.runId, planNodeId: planTurn.nodeId },
918
- })
919
- } else {
920
- if (thread.type === 'default') {
921
- if (!thread.agentId) {
922
- throw new ThreadTurnError('Direct threads require an assigned agent.', 400)
923
- }
924
- await runVisibleAgent({ agentId: thread.agentId, mode: 'direct' })
925
- } else {
926
- // Multi-agent orchestration for group threads
927
- const wsMembers = (thread as { members?: string[] }).members ?? []
928
- const members = wsMembers.length > 0 ? wsMembers : [...agentRoster]
929
- const fallbackAgentId = coreThreadProfile?.config.agentId ?? defaultLeadAgentId
930
- throwIfRunAborted()
931
- writeMultiAgentEvent(writer, { phase: 'routing', note: 'Routing this turn to the right agent.' })
932
-
933
- const recentContext = currentMessages
934
- .slice(-6)
935
- .map((m) => `${m.role}: ${extractMessageText(m).slice(0, 200)}`)
936
- .join('\n')
937
-
938
- const triageResult = await triageThreadMessage({
939
- threadTitle: thread.title,
940
- members,
941
- messageText,
942
- recentContext,
943
- displayNamesById: agentIdentityOverrides.displayNamesById,
944
- shortDisplayNamesById: agentIdentityOverrides.shortDisplayNamesById,
945
- routingAliasesByAgentId: agentIdentityOverrides.routingAliasesByAgentId,
946
- })
947
- throwIfRunAborted()
948
-
949
- const runGroupAgent = async (agentId: string, options?: { routingContext?: string }) => {
950
- const additionalSections = [...(coreInstructionSections ?? []), ...hookInstructionSections]
951
- if (options?.routingContext) {
952
- additionalSections.push(`<routing-context>\n${options.routingContext}\n</routing-context>`)
953
- }
954
- // Multi-agent member protocol: be direct, focus on domain
955
- additionalSections.push(
956
- '<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>',
957
- )
958
-
959
- return await runVisibleAgent({
960
- agentId,
961
- mode: 'threadMode',
962
- skills: coreThreadProfile?.skills ? [...coreThreadProfile.skills] : undefined,
963
- additionalInstructionSections: additionalSections,
964
- })
965
- }
966
-
967
- if (!triageResult) {
968
- // No specialist match — fallback to owner (core) or chief (non-core), single visible turn.
969
- await runGroupAgent(fallbackAgentId)
970
- throwIfRunAborted()
971
- writeMultiAgentEvent(writer, { phase: 'complete' })
972
- } else {
973
- const respondedAgents: string[] = []
974
- let lastResponse = await runGroupAgent(triageResult.agentId, {
975
- routingContext: triageResult.routingContext,
976
- })
977
- respondedAgents.push(triageResult.agentId)
978
- throwIfRunAborted()
979
-
980
- // Follow-up specialists stream visibly in order so the user can
981
- // watch each specialist reply instead of waiting for a persisted refresh.
982
- while (respondedAgents.length < 3) {
983
- const lastResponseText = extractMessageText(lastResponse).slice(0, 500)
984
- const checkResult = await checkForNextAgent({
985
- threadTitle: thread.title,
986
- members,
987
- messageText,
988
- respondedAgents,
989
- lastResponseSummary: lastResponseText,
990
- displayNamesById: agentIdentityOverrides.displayNamesById,
991
- shortDisplayNamesById: agentIdentityOverrides.shortDisplayNamesById,
992
- routingAliasesByAgentId: agentIdentityOverrides.routingAliasesByAgentId,
993
- })
994
- throwIfRunAborted()
995
-
996
- if (checkResult.done || !checkResult.agentId) break
997
-
998
- writeMultiAgentEvent(writer, {
999
- phase: 'waiting-for-agent',
1000
- agentId: checkResult.agentId,
1001
- agentName: resolveRuntimeAgentDisplayName(agentIdentityOverrides, checkResult.agentId),
1002
- note: checkResult.routingContext ?? undefined,
1003
- })
1004
-
1005
- // Insert hidden bridge message between agent turns
1006
- const bridgeMessage: ChatMessage = {
1007
- id: Bun.randomUUIDv7(),
1008
- role: 'user',
1009
- parts: [
1010
- {
1011
- type: 'text',
1012
- text: checkResult.routingContext ?? 'Please also provide your perspective on this topic.',
1013
- },
1014
- ],
1015
- metadata: { hidden: true, createdAt: Date.now() } as MessageMetadata,
1016
- }
1017
- throwIfRunAborted()
1018
- await threadMessageService.upsertMessages({ threadId: threadRef, messages: [bridgeMessage] })
1019
- currentMessages = upsertChatHistoryMessage(currentMessages, bridgeMessage)
1020
- throwIfRunAborted()
1021
-
1022
- lastResponse = await runGroupAgent(checkResult.agentId, {
1023
- routingContext: checkResult.routingContext ?? undefined,
1024
- })
1025
- respondedAgents.push(checkResult.agentId)
1026
- throwIfRunAborted()
1027
- writeMultiAgentEvent(writer, {
1028
- phase: 'agent-message-persisted',
1029
- agentId: checkResult.agentId,
1030
- agentName: resolveRuntimeAgentDisplayName(agentIdentityOverrides, checkResult.agentId),
1031
- messageId: lastResponse.id,
1032
- })
1033
- }
1034
-
1035
- throwIfRunAborted()
1036
- writeMultiAgentEvent(writer, { phase: 'complete' })
1037
- }
1038
- }
1039
- }
1040
- } finally {
1041
- try {
1042
- const latestThreadRecord = await threadService.getById(threadRef)
1043
-
1044
- await finalizeTurnRun({
1045
- serverRunId,
1046
- getEntity: async () => latestThreadRecord,
1047
- getUncompactedMessages: (cursor) => threadMessageService.listMessagesAfterCursor(threadRef, cursor),
1048
- assessCompaction: (summaryText, messages) =>
1049
- contextCompactionRuntime.shouldCompactHistory({
1050
- summaryText,
1051
- liveMessages: messages,
1052
- contextSize: CONTEXT_WINDOW_TOKENS,
1053
- }),
1054
- enqueueCompaction: async () => {
1055
- await enqueueContextCompaction({
1056
- domain: 'thread',
1057
- entityId: threadIdString,
1058
- contextSize: CONTEXT_WINDOW_TOKENS,
1059
- })
1060
- },
1061
- unregisterRun: (runId) => chatRunRegistry.unregister(runId),
1062
- clearActiveRunId: async (runId) => {
1063
- const activeStreamId = await threadService.getActiveStreamId(threadRef)
1064
- await threadService.clearActiveTurn(threadRef, { runId, streamId: activeStreamId })
1065
- },
1066
- disposeAbort: () => runAbort.dispose(),
1067
- activeStreamId: params.streamId,
1068
- clearActiveStreamId: async (streamId) => {
1069
- const activeRunId = await threadService.getActiveRunId(threadRef)
1070
- if (!activeRunId) return
1071
- await threadService.clearActiveTurn(threadRef, { runId: activeRunId, streamId })
1072
- },
1073
- })
1074
-
1075
- if (allAssistantMessages.length > 0 && shouldProcessPostRunSideEffects) {
1076
- await runPostTurnSideEffects({
1077
- thread,
1078
- threadRef,
1079
- orgRef,
1080
- userRef,
1081
- userName,
1082
- orgIdString,
1083
- threadIdString,
1084
- onboardingActive,
1085
- workspace,
1086
- allAssistantMessages,
1087
- referenceUserMessage,
1088
- referenceUserMessageId,
1089
- loadRecentHistory,
1090
- listReadableUploads: () => listReadableUploads(),
1091
- memoryBlock,
1092
- visibleThreadAgentId,
1093
- defaultLeadAgentId,
1094
- latestThreadRecord,
1095
- isUserTurn: params.kind === 'userTurn',
1096
- agentDisplayNamesById: agentIdentityOverrides.displayNamesById,
1097
- })
1098
- }
1099
-
1100
- if (allAssistantMessages.length > 0 && params.kind !== 'planTurn') {
1101
- await turnHooks.afterTurn?.({
1102
- thread,
1103
- threadRef,
1104
- orgRef,
1105
- userRef,
1106
- userName,
1107
- onboardingActive,
1108
- referenceUserMessage,
1109
- assistantMessages: allAssistantMessages,
1110
- latestThreadRecord,
1111
- context: buildContextResult,
1112
- })
1113
- }
1114
- } catch (postRunError) {
1115
- aiLogger.error`Thread post-run cleanup failed: ${postRunError}`
1116
- }
1117
- }
1118
- }
1119
-
1120
- // Plan turns run without the chat lease so they never block user messages.
1121
- // The heartbeat lock already prevents duplicate plan turns on the same node.
1122
- if (params.kind === 'planTurn') {
1123
- const runResult = await executeRun()
1124
- if (runResult) {
1125
- return runResult
1126
- }
1127
- return { inputMessageId: referenceUserMessage?.id, assistantMessages: [...allAssistantMessages] }
1128
- }
1129
-
1130
- try {
1131
- return await threadService.withActiveRunLease(threadRef, async (leaseAbortSignal) => {
1132
- const runResult = await executeRun(leaseAbortSignal)
1133
- if (runResult) {
1134
- return runResult
1135
- }
1136
-
1137
- return { inputMessageId: referenceUserMessage?.id, assistantMessages: [...allAssistantMessages] }
1138
- })
1139
- } catch (error) {
1140
- if (error instanceof ActiveThreadRunConflictError) {
1141
- throw new ThreadTurnError(error.message, 409)
1142
- }
1143
- throw error
1144
- }
1145
- },
1146
- }
1147
- }