@lota-sdk/core 0.1.5

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 (153) hide show
  1. package/infrastructure/schema/00_workstream.surql +55 -0
  2. package/infrastructure/schema/01_memory.surql +47 -0
  3. package/infrastructure/schema/02_execution_plan.surql +62 -0
  4. package/infrastructure/schema/03_learned_skill.surql +32 -0
  5. package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
  6. package/package.json +128 -0
  7. package/src/ai/definitions.ts +308 -0
  8. package/src/bifrost/bifrost.ts +256 -0
  9. package/src/config/agent-defaults.ts +99 -0
  10. package/src/config/constants.ts +33 -0
  11. package/src/config/env-shapes.ts +122 -0
  12. package/src/config/logger.ts +29 -0
  13. package/src/config/model-constants.ts +31 -0
  14. package/src/config/search.ts +17 -0
  15. package/src/config/workstream-defaults.ts +68 -0
  16. package/src/db/base.service.ts +55 -0
  17. package/src/db/cursor-pagination.ts +73 -0
  18. package/src/db/memory-query-builder.ts +207 -0
  19. package/src/db/memory-store.helpers.ts +118 -0
  20. package/src/db/memory-store.rows.ts +29 -0
  21. package/src/db/memory-store.ts +974 -0
  22. package/src/db/memory-types.ts +193 -0
  23. package/src/db/memory.ts +505 -0
  24. package/src/db/record-id.ts +78 -0
  25. package/src/db/service.ts +932 -0
  26. package/src/db/startup.ts +152 -0
  27. package/src/db/tables.ts +20 -0
  28. package/src/document/org-document-chunking.ts +224 -0
  29. package/src/document/parsing.ts +40 -0
  30. package/src/embeddings/provider.ts +76 -0
  31. package/src/index.ts +302 -0
  32. package/src/queues/context-compaction.queue.ts +82 -0
  33. package/src/queues/document-processor.queue.ts +118 -0
  34. package/src/queues/memory-consolidation.queue.ts +65 -0
  35. package/src/queues/post-chat-memory.queue.ts +128 -0
  36. package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
  37. package/src/queues/regular-chat-memory-digest.config.ts +12 -0
  38. package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
  39. package/src/queues/skill-extraction.config.ts +9 -0
  40. package/src/queues/skill-extraction.queue.ts +62 -0
  41. package/src/redis/connection.ts +176 -0
  42. package/src/redis/index.ts +30 -0
  43. package/src/redis/org-memory-lock.ts +43 -0
  44. package/src/redis/redis-lease-lock.ts +158 -0
  45. package/src/runtime/agent-contract.ts +1 -0
  46. package/src/runtime/agent-prompt-context.ts +119 -0
  47. package/src/runtime/agent-runtime-policy.ts +192 -0
  48. package/src/runtime/agent-stream-helpers.ts +117 -0
  49. package/src/runtime/agent-types.ts +22 -0
  50. package/src/runtime/approval-continuation.ts +16 -0
  51. package/src/runtime/chat-attachments.ts +46 -0
  52. package/src/runtime/chat-message.ts +10 -0
  53. package/src/runtime/chat-request-routing.ts +21 -0
  54. package/src/runtime/chat-run-orchestration.ts +25 -0
  55. package/src/runtime/chat-run-registry.ts +20 -0
  56. package/src/runtime/chat-types.ts +18 -0
  57. package/src/runtime/context-compaction-constants.ts +11 -0
  58. package/src/runtime/context-compaction-runtime.ts +86 -0
  59. package/src/runtime/context-compaction.ts +909 -0
  60. package/src/runtime/execution-plan.ts +59 -0
  61. package/src/runtime/helper-model.ts +405 -0
  62. package/src/runtime/indexed-repositories-policy.ts +28 -0
  63. package/src/runtime/instruction-sections.ts +8 -0
  64. package/src/runtime/llm-content.ts +71 -0
  65. package/src/runtime/memory-block.ts +264 -0
  66. package/src/runtime/memory-digest-policy.ts +14 -0
  67. package/src/runtime/memory-format.ts +8 -0
  68. package/src/runtime/memory-pipeline.ts +570 -0
  69. package/src/runtime/memory-prompts-fact.ts +47 -0
  70. package/src/runtime/memory-prompts-parse.ts +3 -0
  71. package/src/runtime/memory-prompts-update.ts +37 -0
  72. package/src/runtime/memory-scope.ts +43 -0
  73. package/src/runtime/plugin-types.ts +10 -0
  74. package/src/runtime/retrieval-adapters.ts +25 -0
  75. package/src/runtime/retrieval-pipeline.ts +3 -0
  76. package/src/runtime/runtime-extensions.ts +154 -0
  77. package/src/runtime/skill-extraction-policy.ts +3 -0
  78. package/src/runtime/team-consultation-orchestrator.ts +245 -0
  79. package/src/runtime/team-consultation-prompts.ts +32 -0
  80. package/src/runtime/title-helpers.ts +12 -0
  81. package/src/runtime/turn-lifecycle.ts +28 -0
  82. package/src/runtime/workstream-chat-helpers.ts +187 -0
  83. package/src/runtime/workstream-routing-policy.ts +301 -0
  84. package/src/runtime/workstream-state.ts +261 -0
  85. package/src/services/attachment.service.ts +159 -0
  86. package/src/services/chat-attachments.service.ts +17 -0
  87. package/src/services/chat-run-registry.service.ts +3 -0
  88. package/src/services/context-compaction-runtime.ts +13 -0
  89. package/src/services/context-compaction.service.ts +115 -0
  90. package/src/services/document-chunk.service.ts +141 -0
  91. package/src/services/execution-plan.service.ts +890 -0
  92. package/src/services/learned-skill.service.ts +328 -0
  93. package/src/services/memory-assessment.service.ts +43 -0
  94. package/src/services/memory.service.ts +807 -0
  95. package/src/services/memory.utils.ts +84 -0
  96. package/src/services/mutating-approval.service.ts +110 -0
  97. package/src/services/recent-activity-title.service.ts +74 -0
  98. package/src/services/recent-activity.service.ts +397 -0
  99. package/src/services/workstream-change-tracker.service.ts +313 -0
  100. package/src/services/workstream-message.service.ts +283 -0
  101. package/src/services/workstream-title.service.ts +58 -0
  102. package/src/services/workstream-turn-preparation.ts +1340 -0
  103. package/src/services/workstream-turn.ts +37 -0
  104. package/src/services/workstream.service.ts +854 -0
  105. package/src/services/workstream.types.ts +118 -0
  106. package/src/storage/attachment-parser.ts +101 -0
  107. package/src/storage/attachment-storage.service.ts +391 -0
  108. package/src/storage/attachments.types.ts +11 -0
  109. package/src/storage/attachments.utils.ts +58 -0
  110. package/src/storage/generated-document-storage.service.ts +55 -0
  111. package/src/system-agents/agent-result.ts +27 -0
  112. package/src/system-agents/context-compacter.agent.ts +46 -0
  113. package/src/system-agents/delegated-agent-factory.ts +177 -0
  114. package/src/system-agents/helper-agent-options.ts +20 -0
  115. package/src/system-agents/memory-reranker.agent.ts +38 -0
  116. package/src/system-agents/memory.agent.ts +58 -0
  117. package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
  118. package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
  119. package/src/system-agents/researcher.agent.ts +34 -0
  120. package/src/system-agents/skill-extractor.agent.ts +88 -0
  121. package/src/system-agents/skill-manager.agent.ts +80 -0
  122. package/src/system-agents/title-generator.agent.ts +42 -0
  123. package/src/system-agents/workstream-tracker.agent.ts +58 -0
  124. package/src/tools/execution-plan.tool.ts +163 -0
  125. package/src/tools/fetch-webpage.tool.ts +132 -0
  126. package/src/tools/firecrawl-client.ts +12 -0
  127. package/src/tools/memory-block.tool.ts +55 -0
  128. package/src/tools/read-file-parts.tool.ts +80 -0
  129. package/src/tools/remember-memory.tool.ts +85 -0
  130. package/src/tools/research-topic.tool.ts +15 -0
  131. package/src/tools/search-tools.ts +55 -0
  132. package/src/tools/search-web.tool.ts +175 -0
  133. package/src/tools/team-think.tool.ts +125 -0
  134. package/src/tools/tool-contract.ts +21 -0
  135. package/src/tools/user-questions.tool.ts +18 -0
  136. package/src/utils/async.ts +50 -0
  137. package/src/utils/date-time.ts +34 -0
  138. package/src/utils/error.ts +10 -0
  139. package/src/utils/errors.ts +28 -0
  140. package/src/utils/hono-error-handler.ts +71 -0
  141. package/src/utils/string.ts +51 -0
  142. package/src/workers/bootstrap.ts +44 -0
  143. package/src/workers/memory-consolidation.worker.ts +318 -0
  144. package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
  145. package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
  146. package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
  147. package/src/workers/skill-extraction.runner.ts +331 -0
  148. package/src/workers/skill-extraction.worker.ts +22 -0
  149. package/src/workers/utils/repo-indexer-chunker.ts +331 -0
  150. package/src/workers/utils/repo-structure-extractor.ts +645 -0
  151. package/src/workers/utils/repomix-process-concurrency.ts +65 -0
  152. package/src/workers/utils/sandbox-error.ts +5 -0
  153. package/src/workers/worker-utils.ts +182 -0
@@ -0,0 +1,1340 @@
1
+ import { toTimestamp, withMessageCreatedAt } from '@lota-sdk/shared/runtime/chat-message-metadata'
2
+ import { baseChatMessageSchema } from '@lota-sdk/shared/schemas/chat-api'
3
+ import { messageMetadataSchema, dataPartsSchema } from '@lota-sdk/shared/schemas/chat-message'
4
+ import type { ChatMessage, MessageMetadata } from '@lota-sdk/shared/schemas/chat-message'
5
+ import {
6
+ CONSULT_TEAM_TOOL_NAME,
7
+ CONSULT_SPECIALIST_TOOL_NAME,
8
+ ConsultSpecialistArgsSchema,
9
+ } from '@lota-sdk/shared/schemas/tools'
10
+ import { convertToModelMessages, readUIMessageStream, stepCountIs, tool as createTool, validateUIMessages } from 'ai'
11
+ import type { PrepareStepFunction, StopCondition, ToolSet, UIMessageStreamWriter } from 'ai'
12
+ import type { z } from 'zod'
13
+
14
+ import {
15
+ agentDisplayNames,
16
+ buildAgentTools,
17
+ createAgent,
18
+ getCoreWorkstreamProfile,
19
+ getAgentRuntimeConfig,
20
+ pluginRuntime,
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 { enqueueContextCompaction } from '../queues/context-compaction.queue'
27
+ import { enqueuePostChatMemory } from '../queues/post-chat-memory.queue'
28
+ import { enqueueRecentActivityTitleRefinement } from '../queues/recent-activity-title-refinement.queue'
29
+ import { enqueueRegularChatMemoryDigest } from '../queues/regular-chat-memory-digest.queue'
30
+ import { enqueueSkillExtraction } from '../queues/skill-extraction.queue'
31
+ import { buildAgentPromptContext } from '../runtime/agent-prompt-context'
32
+ import {
33
+ buildSpecialistTaskMessage,
34
+ createAgentMessageMetadata,
35
+ createServerRunAbortController,
36
+ } from '../runtime/agent-stream-helpers'
37
+ import { hasApprovalRespondedParts } from '../runtime/approval-continuation'
38
+ import { buildModelInputMessagesWithUploadMetadata, buildReadableUploadMetadataText } from '../runtime/chat-attachments'
39
+ import { hasMessageContent } from '../runtime/chat-message'
40
+ import { waitForCompactionIfNeeded } from '../runtime/chat-run-orchestration'
41
+ import { mergeStateDelta, parseWorkstreamState } from '../runtime/context-compaction'
42
+ import { CONTEXT_SIZE } from '../runtime/context-compaction-constants'
43
+ import { createExecutionPlanInstructionSectionCache } from '../runtime/execution-plan'
44
+ import { mergeInstructionSections } from '../runtime/instruction-sections'
45
+ import {
46
+ shouldEnqueueOnboardingPostChatMemory,
47
+ shouldEnqueueRegularDigestForWorkstream,
48
+ } from '../runtime/memory-digest-policy'
49
+ import { getRuntimeAdapters, getToolProviders, getTurnHooks } from '../runtime/runtime-extensions'
50
+ import { shouldEnqueueSkillExtraction } from '../runtime/skill-extraction-policy'
51
+ import { finalizeTurnRun } from '../runtime/turn-lifecycle'
52
+ import {
53
+ appendCompactionContextToHistoryMessages,
54
+ buildAgentHistoryMessages,
55
+ buildConversationSummary,
56
+ buildReadableUploadMetadataContext,
57
+ collectToolOutputErrors,
58
+ extractMessageText,
59
+ extractTrackerMessageText,
60
+ toHistoryMessages,
61
+ toOptionalTrimmedString,
62
+ } from '../runtime/workstream-chat-helpers'
63
+ import {
64
+ classifyHighImpactResponse,
65
+ classifyPolicyClasses,
66
+ resolveReasoningProfile,
67
+ } from '../runtime/workstream-routing-policy'
68
+ import { WorkstreamStateSchema } from '../runtime/workstream-state'
69
+ import type { WorkstreamState, WorkstreamStateDelta } from '../runtime/workstream-state'
70
+ import { chatRunRegistry } from '../services/chat-run-registry.service'
71
+ import type { NormalizedWorkstream, WorkstreamRecord } from '../services/workstream.types'
72
+ import { createTeamThinkTool } from '../tools/team-think.tool'
73
+ import { safeEnqueue } from '../utils/async'
74
+ import { toIsoDateTimeString } from '../utils/date-time'
75
+ import { AppError } from '../utils/errors'
76
+ import { attachmentService } from './attachment.service'
77
+ import { listReadableUploadsFromChatMessages } from './chat-attachments.service'
78
+ import { contextCompactionRuntime } from './context-compaction-runtime'
79
+ import { executionPlanService } from './execution-plan.service'
80
+ import { learnedSkillService } from './learned-skill.service'
81
+ import { memoryService } from './memory.service'
82
+ import { recentActivityService } from './recent-activity.service'
83
+ import { updateWorkstreamChangeTracker } from './workstream-change-tracker.service'
84
+ import { workstreamMessageService } from './workstream-message.service'
85
+ import { workstreamService } from './workstream.service'
86
+
87
+ type AgentRuntimeConfig = Record<string, unknown>
88
+ type AgentFactory = Record<string, (...args: unknown[]) => Record<string, (...args: unknown[]) => unknown>>
89
+ type ChatStreamChunk = Parameters<UIMessageStreamWriter<ChatMessage>['write']>[0]
90
+
91
+ interface UIMessageStreamResult {
92
+ toUIMessageStream(options: Record<string, unknown>): ReadableStream<unknown>
93
+ }
94
+
95
+ export function hasUIMessageStream(value: unknown): value is UIMessageStreamResult {
96
+ return (
97
+ typeof value === 'object' &&
98
+ value !== null &&
99
+ typeof (value as { toUIMessageStream?: unknown }).toUIMessageStream === 'function'
100
+ )
101
+ }
102
+
103
+ export function getPluginService(path: string[]): ((...args: unknown[]) => unknown) | undefined {
104
+ let current: unknown = pluginRuntime
105
+ let owner: unknown = undefined
106
+ for (const key of path) {
107
+ if (current === null || current === undefined || typeof current !== 'object') return undefined
108
+ owner = current
109
+ current = (current as Record<string, unknown>)[key]
110
+ }
111
+ if (typeof current !== 'function') {
112
+ return undefined
113
+ }
114
+
115
+ return owner && typeof owner === 'object'
116
+ ? (current as (...args: unknown[]) => unknown).bind(owner)
117
+ : (current as (...args: unknown[]) => unknown)
118
+ }
119
+
120
+ export async function buildIndexedRepositoriesContext(
121
+ workspaceId: string,
122
+ ): Promise<{ provideRepoTool: boolean; defaultSectionsByAgent: Record<string, unknown>; context: string }> {
123
+ const buildContext = getRuntimeAdapters().workstream?.buildIndexedRepositoriesContext
124
+ if (!buildContext) {
125
+ return { provideRepoTool: false, defaultSectionsByAgent: {}, context: '' }
126
+ }
127
+
128
+ const context = await buildContext(workspaceId)
129
+ return {
130
+ provideRepoTool: context.provideRepoTool,
131
+ defaultSectionsByAgent: context.defaultSectionsByAgent,
132
+ context: context.context ?? '',
133
+ }
134
+ }
135
+
136
+ const PRESEEDED_MEMORY_LOOKUP_LIMIT = 3
137
+
138
+ export function parsePersistedWorkstreamState(value: unknown): WorkstreamState | null {
139
+ return parseWorkstreamState(value)
140
+ }
141
+
142
+ export function extractFirstAbsoluteUrl(text: string): URL | null {
143
+ const match = text.match(/https?:\/\/[^\s)]+/i)
144
+ if (!match) return null
145
+
146
+ const normalized = match[0].replace(/[.,;!?]+$/g, '')
147
+ try {
148
+ return new URL(normalized)
149
+ } catch {
150
+ return null
151
+ }
152
+ }
153
+
154
+ export function taskRequestsWebsiteRefresh(task: string, forceRefresh: boolean): boolean {
155
+ return forceRefresh || /\b(refresh|re-run|rerun|run again|extract again|re-extract|overwrite|recrawl)\b/i.test(task)
156
+ }
157
+
158
+ export function buildInspectWebsiteTrackerTitle(input: unknown): string {
159
+ if (!input || typeof input !== 'object') return 'Inspect website intelligence'
160
+
161
+ const task = typeof (input as { task?: unknown }).task === 'string' ? (input as { task: string }).task.trim() : ''
162
+ const forceRefresh = (input as { forceRefresh?: unknown }).forceRefresh === true
163
+ const hostname = extractFirstAbsoluteUrl(task)?.hostname.replace(/^www\./, '') ?? null
164
+
165
+ if (hostname && taskRequestsWebsiteRefresh(task, forceRefresh)) {
166
+ return `Overwrite website-intelligence artifacts for ${hostname}`
167
+ }
168
+ if (hostname) {
169
+ return `Inspect website intelligence for ${hostname}`
170
+ }
171
+ return taskRequestsWebsiteRefresh(task, forceRefresh)
172
+ ? 'Overwrite website-intelligence artifacts'
173
+ : 'Inspect website intelligence'
174
+ }
175
+
176
+ export function emitTransientWorkstreamTrackerState(params: {
177
+ writer?: UIMessageStreamWriter<ChatMessage>
178
+ workstreamRecord: WorkstreamRecord
179
+ existingState: WorkstreamState
180
+ delta: WorkstreamStateDelta
181
+ }): WorkstreamState {
182
+ const nextState = mergeStateDelta(params.existingState, params.delta, () => Date.now())
183
+
184
+ if (params.writer) {
185
+ params.writer.write({
186
+ type: 'data-workstreamTracker',
187
+ data: workstreamService.toPublicWorkstreamDetail({ ...params.workstreamRecord, state: nextState })
188
+ .workstreamState as unknown as Record<string, unknown>,
189
+ transient: true,
190
+ })
191
+ }
192
+
193
+ return nextState
194
+ }
195
+
196
+ export async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
197
+ return await waitForCompactionIfNeeded({
198
+ entityId: recordIdToString(workstreamId, TABLES.WORKSTREAM),
199
+ entityLabel: 'Workstream',
200
+ loadEntity: () => workstreamService.getById(workstreamId),
201
+ isCompacting: (workstream) => workstream.isCompacting === true,
202
+ })
203
+ }
204
+
205
+ export function parseChatMessageCandidate(value: unknown): ChatMessage | undefined {
206
+ const parsed = baseChatMessageSchema.safeParse(value)
207
+ if (!parsed.success) return undefined
208
+ return parsed.data as ChatMessage
209
+ }
210
+
211
+ export function getChatMessageFromToolOutput(output: unknown): ChatMessage | undefined {
212
+ const directCandidate = parseChatMessageCandidate(output)
213
+ if (directCandidate) return directCandidate
214
+
215
+ if (Array.isArray(output)) {
216
+ for (let index = output.length - 1; index >= 0; index -= 1) {
217
+ const candidate = getChatMessageFromToolOutput(output[index])
218
+ if (candidate) return candidate
219
+ }
220
+ }
221
+
222
+ if (output && typeof output === 'object' && 'message' in output) {
223
+ return getChatMessageFromToolOutput((output as { message?: unknown }).message)
224
+ }
225
+
226
+ return undefined
227
+ }
228
+
229
+ export class WorkstreamTurnError extends AppError {
230
+ constructor(
231
+ message: string,
232
+ readonly statusCode: 400 | 409,
233
+ ) {
234
+ super(message, statusCode === 409 ? 'CONFLICT' : 'BAD_REQUEST', statusCode)
235
+ this.name = 'WorkstreamTurnError'
236
+ }
237
+ }
238
+
239
+ export function asRecord(value: unknown): Record<string, unknown> | null {
240
+ return value && typeof value === 'object' ? (value as Record<string, unknown>) : null
241
+ }
242
+
243
+ export function readOptionalString(value: unknown): string | undefined {
244
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined
245
+ }
246
+
247
+ export function readOptionalBoolean(value: unknown): boolean | undefined {
248
+ return typeof value === 'boolean' ? value : undefined
249
+ }
250
+
251
+ export function readInstructionSections(value: unknown): string[] {
252
+ if (!Array.isArray(value)) {
253
+ return []
254
+ }
255
+
256
+ return value
257
+ .filter((section): section is string => typeof section === 'string')
258
+ .map((section) => section.trim())
259
+ .filter((section) => section.length > 0)
260
+ }
261
+
262
+ export function optionalInstructionSection(value: unknown): string[] | undefined {
263
+ const section = readOptionalString(value)
264
+ return section ? [section] : undefined
265
+ }
266
+
267
+ export interface WorkstreamTurnParams {
268
+ workstream: NormalizedWorkstream
269
+ workstreamRef: RecordIdRef
270
+ orgRef: RecordIdRef
271
+ userRef: RecordIdRef
272
+ userName?: string | null
273
+ inputMessage: ChatMessage
274
+ persistInputMessage?: boolean
275
+ abortSignal?: AbortSignal
276
+ }
277
+
278
+ export interface WorkstreamApprovalContinuationParams {
279
+ workstream: NormalizedWorkstream
280
+ workstreamRef: RecordIdRef
281
+ orgRef: RecordIdRef
282
+ userRef: RecordIdRef
283
+ userName?: string | null
284
+ approvalMessages: ChatMessage[]
285
+ abortSignal?: AbortSignal
286
+ }
287
+
288
+ export type WorkstreamRunCoreParams = {
289
+ workstream: NormalizedWorkstream
290
+ workstreamRef: RecordIdRef
291
+ orgRef: RecordIdRef
292
+ userRef: RecordIdRef
293
+ userName?: string | null
294
+ abortSignal?: AbortSignal
295
+ } & (
296
+ | { kind: 'userTurn'; inputMessage: ChatMessage; persistInputMessage?: boolean }
297
+ | { kind: 'approvalContinuation'; approvalMessages: ChatMessage[] }
298
+ )
299
+
300
+ export interface PreparedWorkstreamTurn {
301
+ originalMessages: ChatMessage[]
302
+ run: (writer?: UIMessageStreamWriter<ChatMessage>) => Promise<void>
303
+ }
304
+
305
+ export function upsertChatHistoryMessage(messages: ChatMessage[], nextMessage: ChatMessage): ChatMessage[] {
306
+ const existingIndex = messages.findIndex((message) => message.id === nextMessage.id)
307
+ if (existingIndex === -1) {
308
+ return [...messages, nextMessage]
309
+ }
310
+
311
+ const nextMessages = [...messages]
312
+ nextMessages[existingIndex] = nextMessage
313
+ return nextMessages
314
+ }
315
+
316
+ export function buildRecentActivityChatDeepLink(params: {
317
+ workstream: NormalizedWorkstream
318
+ workstreamId: string
319
+ visibleAgentId: string
320
+ }): { route: '/chat'; search: { chat?: string; agent?: string; tab: 'cos' | 'team' | 'workstreams' } } {
321
+ if (params.workstream.mode === 'direct') {
322
+ return {
323
+ route: '/chat',
324
+ search: { agent: params.visibleAgentId, tab: params.visibleAgentId === 'chief' ? 'cos' : 'team' },
325
+ }
326
+ }
327
+
328
+ return { route: '/chat', search: { chat: params.workstreamId, tab: 'workstreams' } }
329
+ }
330
+
331
+ export function buildRecentActivityChatSystemTitle(params: {
332
+ workstream: NormalizedWorkstream
333
+ visibleAgentId: string
334
+ }): string {
335
+ if (params.workstream.mode === 'direct') {
336
+ return `Conversation with ${agentDisplayNames[params.visibleAgentId]}`
337
+ }
338
+
339
+ return params.workstream.title.trim() || 'Workstream update'
340
+ }
341
+
342
+ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams): Promise<PreparedWorkstreamTurn> {
343
+ const { workstream, workstreamRef, orgRef, userRef, userName } = params
344
+ const runtimeAdapters = getRuntimeAdapters()
345
+ const turnHooks = getTurnHooks()
346
+ const toolProviders = getToolProviders()
347
+ const workspaceProvider = runtimeAdapters.services?.workspaceProvider
348
+ const orgIdString = recordIdToString(orgRef, TABLES.ORGANIZATION)
349
+ const userIdString = recordIdToString(userRef, TABLES.USER)
350
+ const workstreamIdString = recordIdToString(workstreamRef, TABLES.WORKSTREAM)
351
+
352
+ const hydrateMessageFileUrls = (message: ChatMessage): ChatMessage => ({
353
+ ...message,
354
+ parts: attachmentService.hydrateSignedFileUrlsInMessageParts({
355
+ parts: message.parts as Array<Record<string, unknown>>,
356
+ orgId: orgRef,
357
+ userId: userRef,
358
+ }) as ChatMessage['parts'],
359
+ })
360
+
361
+ let inputMessage: ChatMessage | undefined
362
+ const shouldPersistInputMessage = params.kind === 'userTurn' ? params.persistInputMessage !== false : false
363
+ const shouldProcessPostRunSideEffects = params.kind === 'approvalContinuation' || shouldPersistInputMessage
364
+ if (params.kind === 'userTurn') {
365
+ inputMessage = hydrateMessageFileUrls(withMessageCreatedAt(params.inputMessage))
366
+ if (inputMessage.role !== 'user') {
367
+ throw new WorkstreamTurnError('Only user messages can be submitted to the workstream runtime.', 400)
368
+ }
369
+ if (!hasMessageContent(inputMessage.parts)) {
370
+ throw new WorkstreamTurnError('Workstream messages must include text or attachments.', 400)
371
+ }
372
+ if (workstream.mode === 'direct' && !workstream.agentId) {
373
+ throw new WorkstreamTurnError('Direct workstreams require an assigned agent.', 400)
374
+ }
375
+ }
376
+
377
+ const workstreamRecord = await waitForWorkstreamCompactionIfNeeded(workstreamRef)
378
+ if (toOptionalTrimmedString(workstreamRecord.activeRunId)) {
379
+ throw new WorkstreamTurnError('A chat run is already active.', 409)
380
+ }
381
+
382
+ if (params.kind === 'approvalContinuation') {
383
+ const approvedAssistantMessage = [...params.approvalMessages]
384
+ .reverse()
385
+ .find((m) => m.role === 'assistant' && hasApprovalRespondedParts(m))
386
+ if (!approvedAssistantMessage) {
387
+ throw new WorkstreamTurnError('No approval-responded message found.', 400)
388
+ }
389
+ await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [approvedAssistantMessage] })
390
+ }
391
+
392
+ const workspacePromise = workspaceProvider ? workspaceProvider.getWorkspace(orgRef) : Promise.resolve({})
393
+ const initialWorkstreamState = parsePersistedWorkstreamState(workstreamRecord.state)
394
+ const persistedLiveHistoryPromise = workstreamMessageService.listMessagesAfterCursor(
395
+ workstreamRef,
396
+ toOptionalTrimmedString(workstreamRecord.lastCompactedMessageId) ?? undefined,
397
+ )
398
+ const persistedRecentHistoryPromise = workstreamMessageService.listRecentMessages(workstreamRef, 64)
399
+
400
+ let userMessage: ChatMessage | undefined
401
+ if (inputMessage) {
402
+ userMessage = {
403
+ ...inputMessage,
404
+ id: inputMessage.id || Bun.randomUUIDv7(),
405
+ role: 'user',
406
+ parts: inputMessage.parts,
407
+ metadata: { ...inputMessage.metadata, createdAt: toTimestamp(inputMessage.metadata?.createdAt) },
408
+ }
409
+ }
410
+
411
+ const [workspace, persistedLiveHistory, persistedRecentHistory] = await Promise.all([
412
+ workspacePromise,
413
+ persistedLiveHistoryPromise,
414
+ persistedRecentHistoryPromise,
415
+ ])
416
+ const workspaceLifecycleState = workspaceProvider ? await workspaceProvider.getLifecycleState?.(workspace) : undefined
417
+ const workspaceProfileState = workspaceProvider
418
+ ? await workspaceProvider.readProfileProjectionState?.(workspace)
419
+ : undefined
420
+ const [liveHistory, recentHistory] = await Promise.all([
421
+ persistedLiveHistory.length === 0
422
+ ? Promise.resolve([] as ChatMessage[])
423
+ : validateUIMessages<ChatMessage>({
424
+ messages: persistedLiveHistory,
425
+ metadataSchema: messageMetadataSchema,
426
+ dataSchemas: dataPartsSchema,
427
+ }).then((messages) => messages.map(hydrateMessageFileUrls)),
428
+ persistedRecentHistory.length === 0
429
+ ? Promise.resolve([] as ChatMessage[])
430
+ : validateUIMessages<ChatMessage>({
431
+ messages: persistedRecentHistory,
432
+ metadataSchema: messageMetadataSchema,
433
+ dataSchemas: dataPartsSchema,
434
+ }).then((messages) => messages.map(hydrateMessageFileUrls)),
435
+ ])
436
+
437
+ if (userMessage && shouldPersistInputMessage) {
438
+ await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [userMessage] })
439
+ }
440
+
441
+ const originalMessages = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
442
+ const allAssistantMessages: ChatMessage[] = []
443
+ const referenceUserMessage =
444
+ params.kind === 'userTurn' && !shouldPersistInputMessage
445
+ ? [...liveHistory].reverse().find((m) => m.role === 'user')
446
+ : (userMessage ?? [...liveHistory].reverse().find((m) => m.role === 'user'))
447
+ const messageText = referenceUserMessage ? extractMessageText(referenceUserMessage).trim() : ''
448
+ const onboardingActive = workspaceLifecycleState?.bootstrapActive ?? false
449
+ if (workstream.core && !workstream.coreType) {
450
+ throw new WorkstreamTurnError('Core workstreams require a core type.', 400)
451
+ }
452
+ const coreWorkstreamProfile: { config: { agentId: string }; instructions: string; skills?: string[] } | null =
453
+ workstream.core && workstream.coreType
454
+ ? (getCoreWorkstreamProfile(workstream.coreType) as unknown as {
455
+ config: { agentId: string }
456
+ instructions: string
457
+ skills?: string[]
458
+ })
459
+ : null
460
+ const visibleWorkstreamAgentId =
461
+ workstream.mode === 'direct' ? workstream.agentId : (coreWorkstreamProfile?.config.agentId ?? 'chief')
462
+ const coreInstructionSections = coreWorkstreamProfile ? [coreWorkstreamProfile.instructions] : undefined
463
+ const getLinearInstallationByOrgId = getPluginService([
464
+ 'linear',
465
+ 'services',
466
+ 'linearService',
467
+ 'getInstallationByOrgId',
468
+ ])
469
+ const getGithubInstallationForOrganization = getPluginService([
470
+ 'github',
471
+ 'services',
472
+ 'githubService',
473
+ 'getInstallationForOrganization',
474
+ ])
475
+
476
+ const highImpactAssessment = classifyHighImpactResponse({ message: messageText })
477
+ const policyAssessment = classifyPolicyClasses({ message: messageText })
478
+ const reasoningProfile = resolveReasoningProfile({
479
+ message: messageText,
480
+ forceDeep: highImpactAssessment.classes.length > 0 || policyAssessment.classes.length > 0,
481
+ explicitProfile: onboardingActive ? 'standard' : undefined,
482
+ })
483
+
484
+ const [linearInstallation, githubInstallation, indexedRepoContext, recentDomainEvents, promptSummary] =
485
+ await Promise.all([
486
+ getLinearInstallationByOrgId ? (getLinearInstallationByOrgId(orgRef) as Promise<unknown>) : Promise.resolve(null),
487
+ getGithubInstallationForOrganization
488
+ ? (getGithubInstallationForOrganization(orgIdString) as Promise<unknown>)
489
+ : Promise.resolve(null),
490
+ buildIndexedRepositoriesContext(orgIdString),
491
+ workspaceProvider?.listRecentDomainEvents?.(orgRef, 5) ?? Promise.resolve([] as Array<Record<string, unknown>>),
492
+ workspaceProvider?.buildPromptSummary
493
+ ? workspaceProvider.buildPromptSummary(orgRef).catch(() => undefined)
494
+ : Promise.resolve(undefined),
495
+ ])
496
+ let linearInstalled = Boolean(linearInstallation)
497
+ let githubInstalled = Boolean(githubInstallation)
498
+ let promptContext = buildAgentPromptContext({
499
+ workspaceName: workspaceProfileState?.workspaceName ?? readOptionalString((workspace as { name?: unknown }).name),
500
+ summaryBlock: workspaceProfileState?.summaryBlock,
501
+ structuredProfile: workspaceProfileState?.structuredProfile,
502
+ promptSummary,
503
+ userName: userName ?? undefined,
504
+ recentDomainEvents,
505
+ })
506
+ let retrievedKnowledgeSection: string | undefined =
507
+ onboardingActive || !messageText
508
+ ? undefined
509
+ : await workspaceProvider?.buildRetrievedKnowledgeSection?.({
510
+ workspaceId: orgIdString,
511
+ userId: userIdString,
512
+ query: messageText,
513
+ })
514
+ const buildContextResult = asRecord(
515
+ await turnHooks.buildContext?.({
516
+ workstream,
517
+ workstreamRef,
518
+ orgRef,
519
+ userRef,
520
+ userName,
521
+ workspace,
522
+ onboardingActive,
523
+ messageText,
524
+ linearInstalled,
525
+ githubInstalled,
526
+ indexedRepoContext,
527
+ promptContext,
528
+ workspaceLifecycleState,
529
+ workspaceProfileState,
530
+ promptSummary,
531
+ recentDomainEvents,
532
+ retrievedKnowledgeSection,
533
+ }),
534
+ )
535
+ const buildContextPromptDetails = readOptionalString(buildContextResult?.systemWorkspaceDetails)
536
+ if (buildContextPromptDetails) {
537
+ promptContext = { systemWorkspaceDetails: buildContextPromptDetails }
538
+ }
539
+ const buildContextRetrievedKnowledge = readOptionalString(buildContextResult?.retrievedKnowledgeSection)
540
+ if (buildContextRetrievedKnowledge !== undefined) {
541
+ retrievedKnowledgeSection = buildContextRetrievedKnowledge
542
+ }
543
+ const buildContextLinearInstalled = readOptionalBoolean(buildContextResult?.linearInstalled)
544
+ if (buildContextLinearInstalled !== undefined) {
545
+ linearInstalled = buildContextLinearInstalled
546
+ }
547
+ const buildContextGithubInstalled = readOptionalBoolean(buildContextResult?.githubInstalled)
548
+ if (buildContextGithubInstalled !== undefined) {
549
+ githubInstalled = buildContextGithubInstalled
550
+ }
551
+ const hookInstructionSections = readInstructionSections(
552
+ await turnHooks.buildExtraInstructionSections?.({
553
+ workstream,
554
+ workstreamRef,
555
+ orgRef,
556
+ userRef,
557
+ userName,
558
+ workspace,
559
+ onboardingActive,
560
+ messageText,
561
+ linearInstalled,
562
+ githubInstalled,
563
+ indexedRepoContext,
564
+ promptContext,
565
+ workspaceLifecycleState,
566
+ workspaceProfileState,
567
+ promptSummary,
568
+ recentDomainEvents,
569
+ retrievedKnowledgeSection,
570
+ context: buildContextResult,
571
+ }),
572
+ )
573
+
574
+ let memoryBlock = workstreamService.formatMemoryBlockForPrompt(workstreamRecord)
575
+ let workstreamState = initialWorkstreamState
576
+ const executionPlanInstructionSectionCache = createExecutionPlanInstructionSectionCache({
577
+ disabled: onboardingActive,
578
+ loadPlan: async () => await executionPlanService.getActivePlanForWorkstream(workstreamRef),
579
+ })
580
+ const getExecutionPlanInstructionSections = async (): Promise<string[] | undefined> =>
581
+ await executionPlanInstructionSectionCache.getSections()
582
+ const invalidateExecutionPlanInstructionSections = () => {
583
+ executionPlanInstructionSectionCache.invalidate()
584
+ }
585
+ const preSeededMemoriesByAgent = new Map<string, string | undefined>()
586
+ const getPreSeededMemoriesSection = async (agentId: string): Promise<string | undefined> => {
587
+ if (preSeededMemoriesByAgent.has(agentId)) {
588
+ return preSeededMemoriesByAgent.get(agentId)
589
+ }
590
+
591
+ const preSeededMemories = await memoryService.getTopMemories({
592
+ orgId: orgIdString,
593
+ agentName: agentId,
594
+ limit: PRESEEDED_MEMORY_LOOKUP_LIMIT,
595
+ })
596
+ preSeededMemoriesByAgent.set(agentId, preSeededMemories)
597
+ return preSeededMemories
598
+ }
599
+
600
+ const learnedSkillsByAgent = new Map<string, string | undefined>()
601
+ const getLearnedSkillsSection = async (agentId: string): Promise<string | undefined> => {
602
+ if (onboardingActive) return undefined
603
+ if (learnedSkillsByAgent.has(agentId)) return learnedSkillsByAgent.get(agentId)
604
+
605
+ const section = await learnedSkillService
606
+ .retrieveForTurn({ orgId: orgIdString, agentId, query: messageText, limit: 3, minConfidence: 0.6 })
607
+ .catch((error: unknown) => {
608
+ aiLogger.warn`Failed to retrieve learned skills for ${agentId}: ${error}`
609
+ return undefined
610
+ })
611
+ learnedSkillsByAgent.set(agentId, section)
612
+ return section
613
+ }
614
+
615
+ const persistedChatSummary = typeof workstreamRecord.chatSummary === 'string' ? workstreamRecord.chatSummary : ''
616
+ const messagesForContext = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
617
+ let currentMessages = contextCompactionRuntime.prependSummaryMessage(persistedChatSummary, messagesForContext)
618
+ const referenceUserMessageId = referenceUserMessage?.id ?? ''
619
+ const listReadableUploads = (extraMessages: ChatMessage[] = []) =>
620
+ listReadableUploadsFromChatMessages({
621
+ messages: [...currentMessages, ...extraMessages],
622
+ orgId: orgRef,
623
+ userId: userRef,
624
+ })
625
+ const buildRunInputMessages = (extraMessages: ChatMessage[] = []): ChatMessage[] =>
626
+ buildModelInputMessagesWithUploadMetadata({
627
+ messages: [...currentMessages, ...extraMessages],
628
+ latestUserMessageId: referenceUserMessageId,
629
+ uploadMetadataText: buildReadableUploadMetadataText(listReadableUploads(extraMessages)),
630
+ })
631
+
632
+ return {
633
+ originalMessages,
634
+ run: async (writer?: UIMessageStreamWriter<ChatMessage>) => {
635
+ const executeRun = async () => {
636
+ const serverRunId = Bun.randomUUIDv7()
637
+ const runAbort = createServerRunAbortController(params.abortSignal)
638
+ await workstreamService.setActiveRunId(workstreamRef, serverRunId)
639
+ chatRunRegistry.register(serverRunId, runAbort.controller)
640
+
641
+ try {
642
+ const buildAgentMetadataPatch = (agentId: string, agentName: string): NonNullable<MessageMetadata> => ({
643
+ agentId,
644
+ agentName,
645
+ reasoningProfile: reasoningProfile.name,
646
+ highImpactClasses: highImpactAssessment.classes,
647
+ policyClasses: policyAssessment.classes,
648
+ semanticTerminationReason: 'none',
649
+ })
650
+
651
+ const createObserver = (agentId: string) => ({
652
+ run: <T>(fn: () => T | Promise<T>) => Promise.resolve(fn()),
653
+ recordError: (error: unknown) => {
654
+ aiLogger.error`Agent run failed (agent=${agentId}): ${error}`
655
+ },
656
+ recordAbort: (error: unknown) => {
657
+ aiLogger.info`Agent run aborted (agent=${agentId}): ${error instanceof Error ? error.message : String(error)}`
658
+ },
659
+ })
660
+
661
+ const commitAssistantResponse = async (response: ChatMessage, agentId: string, agentName: string) => {
662
+ const committed = withMessageCreatedAt({
663
+ ...response,
664
+ metadata: { ...response.metadata, ...buildAgentMetadataPatch(agentId, agentName) },
665
+ })
666
+
667
+ await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [committed] })
668
+ const currentMessageIndex = currentMessages.findIndex((item) => item.id === committed.id)
669
+ if (currentMessageIndex >= 0) {
670
+ currentMessages[currentMessageIndex] = committed
671
+ } else {
672
+ currentMessages = [...currentMessages, committed]
673
+ }
674
+
675
+ const assistantIndex = allAssistantMessages.findIndex((item) => item.id === committed.id)
676
+ if (assistantIndex >= 0) {
677
+ allAssistantMessages[assistantIndex] = committed
678
+ } else {
679
+ allAssistantMessages.push(committed)
680
+ }
681
+
682
+ return committed
683
+ }
684
+
685
+ const streamAgentResponse = async (streamParams: {
686
+ agentId: string
687
+ mode: 'direct' | 'fixedWorkstreamMode' | 'workstreamMode'
688
+ messages: ChatMessage[]
689
+ tools: ToolSet
690
+ observer: ReturnType<typeof createObserver>
691
+ skills?: string[]
692
+ additionalInstructionSections?: string[]
693
+ writer?: UIMessageStreamWriter<ChatMessage>
694
+ stopWhen?: StopCondition<ToolSet> | Array<StopCondition<ToolSet>>
695
+ prepareStep?: PrepareStepFunction<ToolSet>
696
+ abortSignal?: AbortSignal
697
+ }): Promise<ChatMessage> => {
698
+ const executionPlanInstructionSections = await getExecutionPlanInstructionSections()
699
+ const agentResolution = asRecord(
700
+ await turnHooks.resolveAgent?.({
701
+ agentId: streamParams.agentId,
702
+ mode: streamParams.mode,
703
+ workstream,
704
+ workstreamRef,
705
+ orgRef,
706
+ userRef,
707
+ userName,
708
+ onboardingActive,
709
+ linearInstalled,
710
+ githubInstalled,
711
+ reasoningProfile: reasoningProfile.name,
712
+ skills: streamParams.skills,
713
+ additionalInstructionSections: streamParams.additionalInstructionSections,
714
+ context: buildContextResult,
715
+ }),
716
+ )
717
+ const resolvedAgentId = readOptionalString(agentResolution?.agentId) ?? streamParams.agentId
718
+ const config = getAgentRuntimeConfig({
719
+ agentId: resolvedAgentId,
720
+ workstreamMode: workstream.mode,
721
+ mode: streamParams.mode,
722
+ skills: streamParams.skills,
723
+ onboardingActive,
724
+ linearInstalled,
725
+ reasoningProfile: reasoningProfile.name,
726
+ systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
727
+ preSeededMemoriesSection: await getPreSeededMemoriesSection(resolvedAgentId),
728
+ retrievedKnowledgeSection,
729
+ workstreamMemoryBlock: memoryBlock,
730
+ workstreamStateSection: contextCompactionRuntime.formatWorkstreamStateForPrompt(workstreamState),
731
+ learnedSkillsSection: await getLearnedSkillsSection(resolvedAgentId),
732
+ additionalInstructionSections: mergeInstructionSections(
733
+ executionPlanInstructionSections,
734
+ streamParams.additionalInstructionSections,
735
+ hookInstructionSections,
736
+ readInstructionSections(agentResolution?.additionalInstructionSections),
737
+ optionalInstructionSection(agentResolution?.extraInstructions),
738
+ ),
739
+ context: buildContextResult,
740
+ }) as AgentRuntimeConfig
741
+ const modelMessages = await convertToModelMessages(streamParams.messages, {
742
+ ignoreIncompleteToolCalls: true,
743
+ })
744
+ const agent = (createAgent as unknown as AgentFactory)[config.id as string]({
745
+ mode: streamParams.mode,
746
+ tools: streamParams.tools,
747
+ extraInstructions: config.extraInstructions,
748
+ stopWhen: (agentResolution?.stopWhen as
749
+ | StopCondition<ToolSet>
750
+ | Array<StopCondition<ToolSet>>
751
+ | undefined) ??
752
+ streamParams.stopWhen ?? [stepCountIs(config.maxSteps as number)],
753
+ prepareStep:
754
+ (agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
755
+ })
756
+ const agentAbortSignal = streamParams.abortSignal ?? runAbort.signal
757
+
758
+ let result: unknown
759
+ try {
760
+ result = await streamParams.observer.run(() =>
761
+ agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal }),
762
+ )
763
+ } catch (error) {
764
+ if (agentAbortSignal.aborted) {
765
+ streamParams.observer.recordAbort(error)
766
+ } else {
767
+ streamParams.observer.recordError(error)
768
+ }
769
+ throw error
770
+ }
771
+ if (!hasUIMessageStream(result)) {
772
+ throw new Error(`Agent run for ${resolvedAgentId} did not expose a UI message stream.`)
773
+ }
774
+
775
+ let responseMessage: ChatMessage | null = null
776
+ let resolveFinishedStream!: () => void
777
+ const finishedStream = new Promise<void>((resolve) => {
778
+ resolveFinishedStream = resolve
779
+ })
780
+
781
+ const uiStream = result.toUIMessageStream({
782
+ generateMessageId: () => Bun.randomUUIDv7(),
783
+ originalMessages: streamParams.messages,
784
+ sendReasoning: true,
785
+ sendSources: true,
786
+ messageMetadata: createAgentMessageMetadata({
787
+ agentId: resolvedAgentId,
788
+ agentName: config.displayName as string,
789
+ }),
790
+ onFinish: ({ responseMessage: finishedResponseMessage }: { responseMessage: ChatMessage }) => {
791
+ responseMessage = withMessageCreatedAt(finishedResponseMessage)
792
+ resolveFinishedStream()
793
+ },
794
+ }) as ReadableStream<ChatStreamChunk>
795
+ const reader = uiStream.getReader()
796
+ let liveTrackerState: WorkstreamState = workstreamState ?? parseWorkstreamState(null)
797
+ const liveTrackedToolCalls = new Map<string, string>()
798
+ try {
799
+ for (;;) {
800
+ const { done, value } = await reader.read()
801
+ if (done) break
802
+ if (streamParams.writer) {
803
+ streamParams.writer.write(value)
804
+ }
805
+ if (value.type === 'tool-input-available' && value.toolName === 'inspectWebsite') {
806
+ const title = buildInspectWebsiteTrackerTitle(value.input)
807
+ liveTrackedToolCalls.set(value.toolCallId, title)
808
+ liveTrackerState = emitTransientWorkstreamTrackerState({
809
+ writer: streamParams.writer,
810
+ workstreamRecord,
811
+ existingState: liveTrackerState,
812
+ delta: {
813
+ taskUpdates: [
814
+ {
815
+ title,
816
+ status: 'in-progress',
817
+ owner: streamParams.agentId,
818
+ externalId: value.toolCallId,
819
+ sourceMessageIds: [],
820
+ },
821
+ ],
822
+ },
823
+ })
824
+ }
825
+ if (
826
+ value.type === 'tool-output-available' &&
827
+ value.preliminary !== true &&
828
+ liveTrackedToolCalls.has(value.toolCallId)
829
+ ) {
830
+ liveTrackerState = emitTransientWorkstreamTrackerState({
831
+ writer: streamParams.writer,
832
+ workstreamRecord,
833
+ existingState: liveTrackerState,
834
+ delta: {
835
+ taskUpdates: [
836
+ {
837
+ title: liveTrackedToolCalls.get(value.toolCallId) ?? 'Inspect website intelligence',
838
+ status: 'done',
839
+ owner: streamParams.agentId,
840
+ externalId: value.toolCallId,
841
+ sourceMessageIds: [],
842
+ },
843
+ ],
844
+ },
845
+ })
846
+ liveTrackedToolCalls.delete(value.toolCallId)
847
+ }
848
+ if (
849
+ (value.type === 'tool-output-error' || value.type === 'tool-output-denied') &&
850
+ liveTrackedToolCalls.has(value.toolCallId)
851
+ ) {
852
+ liveTrackerState = emitTransientWorkstreamTrackerState({
853
+ writer: streamParams.writer,
854
+ workstreamRecord,
855
+ existingState: liveTrackerState,
856
+ delta: {
857
+ taskUpdates: [
858
+ {
859
+ title: liveTrackedToolCalls.get(value.toolCallId) ?? 'Inspect website intelligence',
860
+ status: 'blocked',
861
+ owner: streamParams.agentId,
862
+ externalId: value.toolCallId,
863
+ sourceMessageIds: [],
864
+ },
865
+ ],
866
+ },
867
+ })
868
+ liveTrackedToolCalls.delete(value.toolCallId)
869
+ }
870
+ }
871
+ } finally {
872
+ reader.releaseLock()
873
+ }
874
+
875
+ const finalizedResponseMessage = await finishedStream.then(() => responseMessage)
876
+ if (finalizedResponseMessage === null) {
877
+ throw new Error(`Agent run for ${resolvedAgentId} did not produce a response message.`)
878
+ }
879
+
880
+ for (const toolError of collectToolOutputErrors({ responseMessage: finalizedResponseMessage })) {
881
+ aiLogger.warn`Tool execution failed (agent=${resolvedAgentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
882
+ }
883
+
884
+ return finalizedResponseMessage
885
+ }
886
+
887
+ const runVisibleAgent = async (runParams: {
888
+ agentId: string
889
+ mode: 'direct' | 'fixedWorkstreamMode' | 'workstreamMode'
890
+ skills?: string[]
891
+ additionalInstructionSections?: string[]
892
+ extraMessages?: ChatMessage[]
893
+ extraTools?: ToolSet
894
+ }): Promise<ChatMessage> => {
895
+ let runMemoryBlock = memoryBlock
896
+ const tools: ToolSet = {
897
+ ...((await buildAgentTools({
898
+ agentId: runParams.agentId,
899
+ orgId: orgRef,
900
+ userId: userRef,
901
+ userName: userName ?? 'there',
902
+ workstreamId: workstreamRef,
903
+ orgIdString,
904
+ workstreamMode: workstream.mode,
905
+ mode: runParams.mode,
906
+ linearInstalled,
907
+ onboardingActive,
908
+ githubInstalled,
909
+ provideRepoTool: indexedRepoContext.provideRepoTool,
910
+ skills: runParams.skills,
911
+ defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[runParams.agentId],
912
+ memoryBlock: runMemoryBlock,
913
+ onAppendMemoryBlock: (value: string) => {
914
+ runMemoryBlock = value
915
+ },
916
+ availableUploads: listReadableUploads(runParams.extraMessages),
917
+ includeExecutionPlanTools: runParams.mode !== 'fixedWorkstreamMode',
918
+ onExecutionPlanChanged: invalidateExecutionPlanInstructionSections,
919
+ context: buildContextResult,
920
+ })) as ToolSet),
921
+ ...toolProviders,
922
+ ...runParams.extraTools,
923
+ }
924
+ const responseMessage = await streamAgentResponse({
925
+ agentId: runParams.agentId,
926
+ mode: runParams.mode,
927
+ messages: buildRunInputMessages(runParams.extraMessages),
928
+ tools,
929
+ observer: createObserver(runParams.agentId),
930
+ skills: runParams.skills,
931
+ additionalInstructionSections: runParams.additionalInstructionSections,
932
+ writer,
933
+ })
934
+
935
+ memoryBlock = runMemoryBlock
936
+
937
+ return await commitAssistantResponse(
938
+ responseMessage,
939
+ runParams.agentId,
940
+ agentDisplayNames[runParams.agentId] ?? runParams.agentId,
941
+ )
942
+ }
943
+
944
+ const consultSpecialistTool = createTool({
945
+ description: 'Consult one specialist teammate for domain-specific guidance before replying to the user.',
946
+ inputSchema: ConsultSpecialistArgsSchema,
947
+ execute: async function* (
948
+ { agentId, task }: z.infer<typeof ConsultSpecialistArgsSchema>,
949
+ { abortSignal: toolAbortSignal }: { abortSignal?: AbortSignal },
950
+ ) {
951
+ let specialistMemoryBlock = memoryBlock
952
+ const specialistTaskMessage = buildSpecialistTaskMessage({ agentId, task })
953
+ const specialistTools = await buildAgentTools({
954
+ agentId,
955
+ orgId: orgRef,
956
+ userId: userRef,
957
+ userName: userName ?? 'there',
958
+ workstreamId: workstreamRef,
959
+ orgIdString,
960
+ workstreamMode: workstream.mode,
961
+ mode: 'fixedWorkstreamMode',
962
+ linearInstalled,
963
+ onboardingActive,
964
+ githubInstalled,
965
+ provideRepoTool: indexedRepoContext.provideRepoTool,
966
+ defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[agentId],
967
+ memoryBlock: specialistMemoryBlock,
968
+ onAppendMemoryBlock: (value: string) => {
969
+ specialistMemoryBlock = value
970
+ },
971
+ availableUploads: listReadableUploads([specialistTaskMessage]),
972
+ includeExecutionPlanTools: false,
973
+ context: buildContextResult,
974
+ })
975
+
976
+ const specialistExecutionPlanInstructionSections = await getExecutionPlanInstructionSections()
977
+ const specialistConfig = getAgentRuntimeConfig({
978
+ agentId,
979
+ workstreamMode: workstream.mode,
980
+ mode: 'fixedWorkstreamMode',
981
+ onboardingActive,
982
+ linearInstalled,
983
+ reasoningProfile: reasoningProfile.name,
984
+ systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
985
+ preSeededMemoriesSection: await getPreSeededMemoriesSection(agentId),
986
+ retrievedKnowledgeSection,
987
+ workstreamMemoryBlock: specialistMemoryBlock,
988
+ workstreamStateSection: contextCompactionRuntime.formatWorkstreamStateForPrompt(workstreamState),
989
+ learnedSkillsSection: await getLearnedSkillsSection(agentId),
990
+ additionalInstructionSections: mergeInstructionSections(
991
+ specialistExecutionPlanInstructionSections,
992
+ coreInstructionSections,
993
+ hookInstructionSections,
994
+ ),
995
+ context: buildContextResult,
996
+ }) as AgentRuntimeConfig
997
+ const observer = createObserver(agentId)
998
+ const agent = (createAgent as unknown as AgentFactory)[specialistConfig.id as string]({
999
+ mode: 'fixedWorkstreamMode',
1000
+ tools: { ...(specialistTools as ToolSet), ...toolProviders },
1001
+ extraInstructions: specialistConfig.extraInstructions,
1002
+ stopWhen: [stepCountIs(specialistConfig.maxSteps as number)],
1003
+ })
1004
+ const modelMessages = await convertToModelMessages(buildRunInputMessages([specialistTaskMessage]), {
1005
+ ignoreIncompleteToolCalls: true,
1006
+ })
1007
+ const specialistAbortSignal = toolAbortSignal ?? runAbort.signal
1008
+ let result: unknown
1009
+ try {
1010
+ result = await observer.run(() =>
1011
+ agent.stream({ messages: modelMessages, abortSignal: specialistAbortSignal }),
1012
+ )
1013
+ } catch (error) {
1014
+ if (specialistAbortSignal.aborted) {
1015
+ observer.recordAbort(error)
1016
+ } else {
1017
+ observer.recordError(error)
1018
+ }
1019
+ throw error
1020
+ }
1021
+ if (!hasUIMessageStream(result)) {
1022
+ throw new Error(`Specialist ${agentId} did not expose a UI message stream.`)
1023
+ }
1024
+
1025
+ let finalMessage: ChatMessage | null = null
1026
+ for await (const message of readUIMessageStream<ChatMessage>({
1027
+ stream: result.toUIMessageStream({
1028
+ generateMessageId: () => Bun.randomUUIDv7(),
1029
+ sendReasoning: true,
1030
+ sendSources: true,
1031
+ sendStart: false,
1032
+ sendFinish: false,
1033
+ messageMetadata: createAgentMessageMetadata({
1034
+ agentId,
1035
+ agentName: specialistConfig.displayName as string,
1036
+ }),
1037
+ }) as ReadableStream<never>,
1038
+ })) {
1039
+ finalMessage = withMessageCreatedAt(message)
1040
+ yield finalMessage
1041
+ }
1042
+
1043
+ if (!finalMessage) {
1044
+ throw new Error(`Specialist ${agentId} did not produce a response message.`)
1045
+ }
1046
+
1047
+ for (const toolError of collectToolOutputErrors({ responseMessage: finalMessage })) {
1048
+ aiLogger.warn`Tool execution failed (agent=${agentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
1049
+ }
1050
+
1051
+ memoryBlock = specialistMemoryBlock
1052
+ return finalMessage
1053
+ },
1054
+ toModelOutput: ({ output }) => {
1055
+ const result = getChatMessageFromToolOutput(output)
1056
+ const agentName =
1057
+ typeof result?.metadata?.agentName === 'string' && result.metadata.agentName.trim().length > 0
1058
+ ? result.metadata.agentName.trim()
1059
+ : 'Specialist'
1060
+ const summary = result ? extractMessageText(result).trim() : ''
1061
+ return {
1062
+ type: 'text',
1063
+ value: summary ? `${agentName}: ${summary}` : `${agentName} completed the requested task.`,
1064
+ }
1065
+ },
1066
+ })
1067
+
1068
+ const teamThinkTool =
1069
+ workstream.mode === 'group' && !onboardingActive
1070
+ ? createTeamThinkTool({
1071
+ historyMessages: currentMessages,
1072
+ latestUserMessageId: referenceUserMessageId,
1073
+ orgId: orgRef,
1074
+ userId: userRef,
1075
+ workstreamId: workstreamRef,
1076
+ githubInstalled,
1077
+ availableUploads: listReadableUploads(),
1078
+ provideRepoTool: indexedRepoContext.provideRepoTool,
1079
+ defaultRepoSectionsByAgent: indexedRepoContext.defaultSectionsByAgent as never,
1080
+ reasoningProfile: reasoningProfile.name,
1081
+ systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
1082
+ getPreSeededMemoriesSection,
1083
+ retrievedKnowledgeSection,
1084
+ additionalInstructionSections: mergeInstructionSections(
1085
+ coreInstructionSections,
1086
+ hookInstructionSections,
1087
+ ),
1088
+ getAdditionalInstructionSections: getExecutionPlanInstructionSections,
1089
+ context: buildContextResult,
1090
+ toolProviders,
1091
+ abortSignal: runAbort.signal,
1092
+ })
1093
+ : null
1094
+
1095
+ if (workstream.mode === 'direct') {
1096
+ if (!workstream.agentId) {
1097
+ throw new WorkstreamTurnError('Direct workstreams require an assigned agent.', 400)
1098
+ }
1099
+ await runVisibleAgent({ agentId: workstream.agentId, mode: 'direct' })
1100
+ } else if (params.kind === 'userTurn') {
1101
+ await runVisibleAgent({
1102
+ agentId: visibleWorkstreamAgentId ?? 'chief',
1103
+ mode: 'workstreamMode',
1104
+ skills: coreWorkstreamProfile?.skills ? [...coreWorkstreamProfile.skills] : undefined,
1105
+ additionalInstructionSections: mergeInstructionSections(coreInstructionSections, hookInstructionSections),
1106
+ extraTools: {
1107
+ [CONSULT_SPECIALIST_TOOL_NAME]: consultSpecialistTool,
1108
+ ...(teamThinkTool ? { [CONSULT_TEAM_TOOL_NAME]: teamThinkTool } : {}),
1109
+ },
1110
+ })
1111
+ } else {
1112
+ await runVisibleAgent({
1113
+ agentId: visibleWorkstreamAgentId ?? 'chief',
1114
+ mode: 'workstreamMode',
1115
+ skills: coreWorkstreamProfile?.skills ? [...coreWorkstreamProfile.skills] : undefined,
1116
+ additionalInstructionSections: mergeInstructionSections(coreInstructionSections, hookInstructionSections),
1117
+ extraTools: {
1118
+ [CONSULT_SPECIALIST_TOOL_NAME]: consultSpecialistTool,
1119
+ ...(teamThinkTool ? { [CONSULT_TEAM_TOOL_NAME]: teamThinkTool } : {}),
1120
+ },
1121
+ })
1122
+ }
1123
+ } finally {
1124
+ try {
1125
+ const latestWorkstreamRecord = await workstreamService.getById(workstreamRef)
1126
+ const latestPersistedState = WorkstreamStateSchema.safeParse(latestWorkstreamRecord.state).success
1127
+ ? WorkstreamStateSchema.parse(latestWorkstreamRecord.state)
1128
+ : null
1129
+
1130
+ await finalizeTurnRun({
1131
+ serverRunId,
1132
+ getEntity: async () => latestWorkstreamRecord,
1133
+ getUncompactedMessages: (cursor) =>
1134
+ workstreamMessageService.listMessagesAfterCursor(workstreamRef, cursor),
1135
+ assessCompaction: (summaryText, messages) =>
1136
+ contextCompactionRuntime.shouldCompactHistory({
1137
+ summaryText,
1138
+ liveMessages: messages,
1139
+ contextSize: CONTEXT_SIZE,
1140
+ }),
1141
+ enqueueCompaction: () =>
1142
+ enqueueContextCompaction({
1143
+ domain: 'workstream',
1144
+ entityId: workstreamIdString,
1145
+ contextSize: CONTEXT_SIZE,
1146
+ }).then(() => {}),
1147
+ unregisterRun: (runId) => chatRunRegistry.unregister(runId),
1148
+ clearActiveRunId: (runId) => workstreamService.clearActiveRunIdIfMatches(workstreamRef, runId),
1149
+ disposeAbort: () => runAbort.dispose(),
1150
+ })
1151
+
1152
+ let trackerAwareWorkstreamRecord = latestWorkstreamRecord
1153
+ let trackerAwarePersistedState = latestPersistedState
1154
+
1155
+ if (!onboardingActive && allAssistantMessages.length > 0) {
1156
+ const activeExecutionPlan = await executionPlanService.getActivePlanForWorkstream(workstreamRef)
1157
+ const trackerUpdated = await updateWorkstreamChangeTracker({
1158
+ workstreamId: workstreamRef,
1159
+ title: latestWorkstreamRecord.title ?? workstream.title,
1160
+ mode: workstream.mode,
1161
+ ...(workstream.coreType ? { coreType: workstream.coreType } : {}),
1162
+ ...(visibleWorkstreamAgentId ? { visibleAgentId: visibleWorkstreamAgentId } : {}),
1163
+ hasActiveExecutionPlan: activeExecutionPlan !== null,
1164
+ previousSummary:
1165
+ typeof latestWorkstreamRecord.chatSummary === 'string' ? latestWorkstreamRecord.chatSummary : null,
1166
+ existingState: latestPersistedState,
1167
+ userMessageText: referenceUserMessage ? extractMessageText(referenceUserMessage).trim() : null,
1168
+ assistantMessages: allAssistantMessages
1169
+ .map((message) => {
1170
+ const text = extractTrackerMessageText(message).trim()
1171
+ if (!text) return null
1172
+ const label =
1173
+ typeof message.metadata?.agentName === 'string' && message.metadata.agentName.trim().length > 0
1174
+ ? message.metadata.agentName.trim()
1175
+ : typeof message.metadata?.agentId === 'string' && message.metadata.agentId.trim().length > 0
1176
+ ? message.metadata.agentId.trim()
1177
+ : (visibleWorkstreamAgentId ?? 'Assistant')
1178
+ return { label, text }
1179
+ })
1180
+ .filter((message): message is { label: string; text: string } => Boolean(message)),
1181
+ })
1182
+
1183
+ if (trackerUpdated) {
1184
+ const trackedWorkstreamRecord = await workstreamService.getWorkstreamRecord(workstreamRef)
1185
+ trackerAwareWorkstreamRecord = trackedWorkstreamRecord
1186
+ trackerAwarePersistedState = parsePersistedWorkstreamState(trackedWorkstreamRecord.state)
1187
+ if (writer) {
1188
+ writer.write({
1189
+ type: 'data-workstreamTracker',
1190
+ data: workstreamService.toPublicWorkstreamDetail(trackedWorkstreamRecord)
1191
+ .workstreamState as unknown as Record<string, unknown>,
1192
+ transient: true,
1193
+ })
1194
+ }
1195
+ }
1196
+ }
1197
+
1198
+ if (allAssistantMessages.length > 0 && shouldProcessPostRunSideEffects) {
1199
+ const agentMessages = buildAgentHistoryMessages(allAssistantMessages)
1200
+ const historyMessagesForMemory = appendCompactionContextToHistoryMessages(
1201
+ toHistoryMessages(recentHistory),
1202
+ { chatSummary: trackerAwareWorkstreamRecord.chatSummary, persistedState: trackerAwarePersistedState },
1203
+ )
1204
+
1205
+ const userMessageText = referenceUserMessage ? extractMessageText(referenceUserMessage).trim() : ''
1206
+ const readableUploads = listReadableUploads()
1207
+ const attachmentMetadataContext = buildReadableUploadMetadataContext(readableUploads)
1208
+ const hasAttachmentContext = Boolean(attachmentMetadataContext)
1209
+ if (
1210
+ shouldEnqueueOnboardingPostChatMemory({
1211
+ onboardingActive,
1212
+ userMessageText,
1213
+ hasAttachmentContext,
1214
+ agentMessageCount: agentMessages.length,
1215
+ })
1216
+ ) {
1217
+ const memoryUserMessage = userMessageText || 'User uploaded attachment(s).'
1218
+ await safeEnqueue(
1219
+ () =>
1220
+ enqueuePostChatMemory({
1221
+ orgId: orgIdString,
1222
+ workstreamId: workstreamIdString,
1223
+ sourceId: referenceUserMessageId,
1224
+ onboardStatus: readOptionalString((workspace as { onboardStatus?: unknown }).onboardStatus),
1225
+ userMessage: memoryUserMessage,
1226
+ historyMessages: historyMessagesForMemory,
1227
+ agentMessages,
1228
+ memoryBlock: memoryBlock.trim() ? memoryBlock : undefined,
1229
+ attachmentContext: attachmentMetadataContext,
1230
+ }),
1231
+ { operationName: 'post-chat memory extraction enqueue' },
1232
+ )
1233
+ }
1234
+
1235
+ if (params.kind === 'userTurn' && referenceUserMessage) {
1236
+ const conversationSummary = buildConversationSummary({
1237
+ userMessageText,
1238
+ assistantMessages: allAssistantMessages,
1239
+ })
1240
+ if (conversationSummary) {
1241
+ const recentActivityResult = await recentActivityService.recordEvent({
1242
+ orgId: orgRef,
1243
+ userId: userRef,
1244
+ source: 'system',
1245
+ event: {
1246
+ sourceEventId: `chat-turn:${referenceUserMessageId}`,
1247
+ kind: 'chat.turn.completed',
1248
+ targetKind: 'workstream',
1249
+ targetId: workstreamIdString,
1250
+ mergeKey: `workstream:${workstreamIdString}`,
1251
+ title: buildRecentActivityChatSystemTitle({
1252
+ workstream,
1253
+ visibleAgentId: visibleWorkstreamAgentId ?? 'chief',
1254
+ }),
1255
+ sourceLabel: agentDisplayNames[visibleWorkstreamAgentId ?? 'chief'],
1256
+ deepLink: buildRecentActivityChatDeepLink({
1257
+ workstream,
1258
+ workstreamId: workstreamIdString,
1259
+ visibleAgentId: visibleWorkstreamAgentId ?? 'chief',
1260
+ }),
1261
+ metadata: {
1262
+ agentId: visibleWorkstreamAgentId ?? 'chief',
1263
+ agentName: agentDisplayNames[visibleWorkstreamAgentId ?? 'chief'],
1264
+ workstreamId: workstreamIdString,
1265
+ workstreamTitle: trackerAwareWorkstreamRecord.title ?? workstream.title,
1266
+ workstreamMode: workstream.mode,
1267
+ ...(workstream.coreType ? { coreType: workstream.coreType } : {}),
1268
+ userMessageText,
1269
+ assistantSummary: conversationSummary,
1270
+ messageId: referenceUserMessageId,
1271
+ },
1272
+ occurredAt: toIsoDateTimeString(referenceUserMessage.metadata?.createdAt ?? Date.now()),
1273
+ },
1274
+ })
1275
+
1276
+ await safeEnqueue(
1277
+ async () => {
1278
+ const enqueuePostChatOrgAction = getRuntimeAdapters().queues?.enqueuePostChatOrgAction
1279
+ if (!enqueuePostChatOrgAction) {
1280
+ return
1281
+ }
1282
+
1283
+ await enqueuePostChatOrgAction({
1284
+ orgId: orgIdString,
1285
+ workstreamId: workstreamIdString,
1286
+ sourceId: referenceUserMessageId,
1287
+ sourceCreatedAt: referenceUserMessage.metadata?.createdAt ?? Date.now(),
1288
+ conversationSummary,
1289
+ })
1290
+ },
1291
+ { operationName: 'post-chat org action enqueue' },
1292
+ )
1293
+
1294
+ if (recentActivityService.isMeaningfulRefinementCandidate(recentActivityResult.item)) {
1295
+ await safeEnqueue(
1296
+ () => enqueueRecentActivityTitleRefinement({ activityId: recentActivityResult.item.id }),
1297
+ { operationName: 'recent activity title refinement enqueue' },
1298
+ )
1299
+ }
1300
+ }
1301
+ }
1302
+
1303
+ if (shouldEnqueueRegularDigestForWorkstream({ onboardingActive })) {
1304
+ await safeEnqueue(() => enqueueRegularChatMemoryDigest({ orgId: orgIdString }), {
1305
+ operationName: 'regular chat memory digest enqueue',
1306
+ })
1307
+ }
1308
+
1309
+ if (shouldEnqueueSkillExtraction({ onboardingActive })) {
1310
+ await safeEnqueue(() => enqueueSkillExtraction({ orgId: orgIdString }), {
1311
+ operationName: 'skill extraction enqueue',
1312
+ })
1313
+ }
1314
+ }
1315
+
1316
+ if (allAssistantMessages.length > 0) {
1317
+ await turnHooks.afterTurn?.({
1318
+ workstream,
1319
+ workstreamRef,
1320
+ orgRef,
1321
+ userRef,
1322
+ userName,
1323
+ onboardingActive,
1324
+ referenceUserMessage,
1325
+ assistantMessages: allAssistantMessages,
1326
+ latestWorkstreamRecord: trackerAwareWorkstreamRecord,
1327
+ latestPersistedState: trackerAwarePersistedState,
1328
+ context: buildContextResult,
1329
+ })
1330
+ }
1331
+ } catch (postRunError) {
1332
+ aiLogger.error`Workstream post-run cleanup failed: ${postRunError}`
1333
+ }
1334
+ }
1335
+ }
1336
+
1337
+ await executeRun()
1338
+ },
1339
+ }
1340
+ }