@lota-sdk/core 0.1.14 → 0.1.16

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 (174) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +9 -8
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/embedding-cache.ts +7 -6
  11. package/src/ai/index.ts +0 -1
  12. package/src/bifrost/bifrost.ts +14 -14
  13. package/src/config/agent-defaults.ts +32 -22
  14. package/src/config/agent-types.ts +11 -0
  15. package/src/config/constants.ts +2 -14
  16. package/src/config/debug-logger.ts +5 -1
  17. package/src/config/index.ts +3 -0
  18. package/src/config/logger.ts +7 -9
  19. package/src/config/model-constants.ts +16 -34
  20. package/src/config/search.ts +1 -15
  21. package/src/create-runtime.ts +453 -0
  22. package/src/db/cursor-pagination.ts +3 -6
  23. package/src/db/index.ts +2 -0
  24. package/src/db/memory-store.rows.ts +7 -7
  25. package/src/db/memory-store.ts +24 -24
  26. package/src/db/memory.ts +18 -16
  27. package/src/db/schema-fingerprint.ts +1 -0
  28. package/src/db/service.ts +193 -122
  29. package/src/db/startup.ts +9 -13
  30. package/src/db/surreal-mutation.ts +43 -0
  31. package/src/db/tables.ts +7 -0
  32. package/src/db/workstream-message-row.ts +15 -0
  33. package/src/embeddings/provider.ts +1 -1
  34. package/src/index.ts +1 -1
  35. package/src/queues/context-compaction.queue.ts +17 -52
  36. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  37. package/src/queues/document-processor.queue.ts +7 -7
  38. package/src/queues/index.ts +3 -0
  39. package/src/queues/memory-consolidation.queue.ts +18 -54
  40. package/src/queues/plan-scheduler.queue.ts +97 -0
  41. package/src/queues/post-chat-memory.queue.ts +15 -60
  42. package/src/queues/queue-factory.ts +100 -0
  43. package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
  44. package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
  45. package/src/queues/skill-extraction.queue.ts +15 -50
  46. package/src/queues/workstream-title-generation.queue.ts +15 -51
  47. package/src/redis/connection.ts +12 -3
  48. package/src/redis/index.ts +2 -1
  49. package/src/redis/org-memory-lock.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +41 -8
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +106 -21
  53. package/src/runtime/agent-stream-helpers.ts +2 -1
  54. package/src/runtime/approval-continuation.ts +12 -6
  55. package/src/runtime/context-compaction-constants.ts +1 -1
  56. package/src/runtime/context-compaction-runtime.ts +7 -5
  57. package/src/runtime/context-compaction.ts +40 -97
  58. package/src/runtime/execution-plan.ts +23 -19
  59. package/src/runtime/graph-designer.ts +15 -0
  60. package/src/runtime/helper-model.ts +10 -196
  61. package/src/runtime/index.ts +14 -1
  62. package/src/runtime/llm-content.ts +1 -1
  63. package/src/runtime/memory-block.ts +11 -12
  64. package/src/runtime/memory-pipeline.ts +26 -10
  65. package/src/runtime/plugin-resolution.ts +35 -0
  66. package/src/runtime/plugin-types.ts +73 -1
  67. package/src/runtime/retrieval-adapters.ts +1 -1
  68. package/src/runtime/runtime-config.ts +25 -12
  69. package/src/runtime/runtime-extensions.ts +91 -15
  70. package/src/runtime/runtime-worker-registry.ts +6 -0
  71. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  72. package/src/runtime/team-consultation-prompts.ts +11 -2
  73. package/src/runtime/title-helpers.ts +11 -4
  74. package/src/runtime/workstream-chat-helpers.ts +6 -7
  75. package/src/runtime/workstream-routing-policy.ts +0 -30
  76. package/src/runtime/workstream-state.ts +17 -7
  77. package/src/services/adaptive-playbook.service.ts +152 -0
  78. package/src/services/agent-executor.service.ts +293 -0
  79. package/src/services/artifact-provenance.service.ts +172 -0
  80. package/src/services/attachment.service.ts +7 -12
  81. package/src/services/context-compaction.service.ts +75 -58
  82. package/src/services/context-enrichment.service.ts +33 -0
  83. package/src/services/coordination-registry.service.ts +117 -0
  84. package/src/services/document-chunk.service.ts +38 -33
  85. package/src/services/domain-agent-executor.service.ts +71 -0
  86. package/src/services/execution-plan.service.ts +271 -50
  87. package/src/services/feedback-loop.service.ts +96 -0
  88. package/src/services/global-orchestrator.service.ts +148 -0
  89. package/src/services/index.ts +26 -0
  90. package/src/services/institutional-memory.service.ts +145 -0
  91. package/src/services/learned-skill.service.ts +30 -15
  92. package/src/services/memory-assessment.service.ts +3 -2
  93. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
  94. package/src/services/memory.service.ts +55 -69
  95. package/src/services/monitoring-window.service.ts +86 -0
  96. package/src/services/mutating-approval.service.ts +1 -1
  97. package/src/services/node-workspace.service.ts +155 -0
  98. package/src/services/notification.service.ts +39 -0
  99. package/src/services/organization-member.service.ts +12 -5
  100. package/src/services/organization.service.ts +5 -5
  101. package/src/services/ownership-dispatcher.service.ts +403 -0
  102. package/src/services/plan-approval.service.ts +1 -1
  103. package/src/services/plan-artifact.service.ts +1 -0
  104. package/src/services/plan-builder.service.ts +1 -0
  105. package/src/services/plan-checkpoint.service.ts +30 -2
  106. package/src/services/plan-compiler.service.ts +5 -0
  107. package/src/services/plan-coordination.service.ts +152 -0
  108. package/src/services/plan-cycle.service.ts +284 -0
  109. package/src/services/plan-deadline.service.ts +287 -0
  110. package/src/services/plan-executor.service.ts +386 -58
  111. package/src/services/plan-helpers.ts +15 -0
  112. package/src/services/plan-run.service.ts +41 -7
  113. package/src/services/plan-scheduler.service.ts +240 -0
  114. package/src/services/plan-template.service.ts +117 -0
  115. package/src/services/plan-validator.service.ts +87 -20
  116. package/src/services/plan-workspace.service.ts +83 -0
  117. package/src/services/playbook-registry.service.ts +67 -0
  118. package/src/services/plugin-executor.service.ts +103 -0
  119. package/src/services/quality-metrics.service.ts +132 -0
  120. package/src/services/recent-activity-title.service.ts +3 -10
  121. package/src/services/recent-activity.service.ts +33 -43
  122. package/src/services/skill-resolver.service.ts +19 -0
  123. package/src/services/system-executor.service.ts +105 -0
  124. package/src/services/workstream-message.service.ts +29 -41
  125. package/src/services/workstream-plan-registry.service.ts +22 -0
  126. package/src/services/workstream-title.service.ts +3 -9
  127. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
  128. package/src/services/workstream-turn.ts +2 -2
  129. package/src/services/workstream.service.ts +55 -65
  130. package/src/services/workstream.types.ts +10 -19
  131. package/src/services/write-intent-validator.service.ts +81 -0
  132. package/src/storage/attachment-parser.ts +1 -1
  133. package/src/storage/attachment-storage.service.ts +4 -4
  134. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
  135. package/src/storage/generated-document-storage.service.ts +3 -2
  136. package/src/storage/index.ts +2 -2
  137. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  138. package/src/system-agents/delegated-agent-factory.ts +5 -2
  139. package/src/system-agents/index.ts +8 -0
  140. package/src/system-agents/memory-reranker.agent.ts +1 -1
  141. package/src/system-agents/memory.agent.ts +1 -1
  142. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  143. package/src/tools/execution-plan.tool.ts +17 -19
  144. package/src/tools/fetch-webpage.tool.ts +20 -18
  145. package/src/tools/index.ts +2 -3
  146. package/src/tools/read-file-parts.tool.ts +1 -1
  147. package/src/tools/search-web.tool.ts +18 -15
  148. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  149. package/src/tools/team-think.tool.ts +14 -8
  150. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  151. package/src/utils/async.ts +3 -2
  152. package/src/utils/date-time.ts +4 -32
  153. package/src/utils/env.ts +8 -0
  154. package/src/utils/errors.ts +47 -0
  155. package/src/utils/hono-error-handler.ts +1 -2
  156. package/src/utils/index.ts +19 -2
  157. package/src/utils/string.ts +128 -1
  158. package/src/workers/bootstrap.ts +2 -2
  159. package/src/workers/index.ts +1 -0
  160. package/src/workers/memory-consolidation.worker.ts +12 -12
  161. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  162. package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
  163. package/src/workers/skill-extraction.runner.ts +8 -102
  164. package/src/workers/utils/file-section-chunker.ts +6 -3
  165. package/src/workers/utils/repomix-file-sections.ts +2 -2
  166. package/src/workers/utils/sandbox-error.ts +11 -2
  167. package/src/workers/utils/workstream-message-query.ts +97 -0
  168. package/src/workers/worker-utils.ts +6 -2
  169. package/src/runtime/retrieval-pipeline.ts +0 -3
  170. package/src/runtime.ts +0 -387
  171. package/src/tools/log-hello-world.tool.ts +0 -17
  172. package/src/utils/error.ts +0 -10
  173. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  174. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -1,18 +1,20 @@
1
1
  import {
2
+ WORKSTREAM,
2
3
  baseChatMessageSchema,
3
4
  CONSULT_SPECIALIST_TOOL_NAME,
4
5
  CONSULT_TEAM_TOOL_NAME,
5
6
  ConsultSpecialistArgsSchema,
6
- dataPartsSchema,
7
+ dataPartsSchemas,
7
8
  messageMetadataSchema,
8
9
  toTimestamp,
9
10
  withMessageCreatedAt,
10
11
  } from '@lota-sdk/shared'
11
12
  import type { ChatMessage, MessageMetadata } from '@lota-sdk/shared'
12
13
  import { convertToModelMessages, readUIMessageStream, stepCountIs, tool as createTool, validateUIMessages } from 'ai'
13
- import type { PrepareStepFunction, StopCondition, ToolSet, UIMessageStreamWriter } from 'ai'
14
+ import type { PrepareStepFunction, StopCondition, ToolLoopAgent, ToolSet, UIMessageStreamWriter } from 'ai'
14
15
  import type { z } from 'zod'
15
16
 
17
+ import type { CoreWorkstreamProfile } from '../config/agent-defaults'
16
18
  import {
17
19
  agentDisplayNames,
18
20
  buildAgentTools,
@@ -20,7 +22,6 @@ import {
20
22
  getLeadAgentId,
21
23
  getCoreWorkstreamProfile,
22
24
  getAgentRuntimeConfig,
23
- pluginRuntime,
24
25
  } from '../config/agent-defaults'
25
26
  import { lotaDebugLogger } from '../config/debug-logger'
26
27
  import { aiLogger } from '../config/logger'
@@ -45,7 +46,7 @@ import { buildModelInputMessagesWithUploadMetadata, buildReadableUploadMetadataT
45
46
  import { hasMessageContent } from '../runtime/chat-message'
46
47
  import { waitForCompactionIfNeeded } from '../runtime/chat-run-orchestration'
47
48
  import { parseWorkstreamState } from '../runtime/context-compaction'
48
- import { CONTEXT_SIZE } from '../runtime/context-compaction-constants'
49
+ import { CONTEXT_WINDOW_TOKENS } from '../runtime/context-compaction-constants'
49
50
  import { createExecutionPlanInstructionSectionCache } from '../runtime/execution-plan'
50
51
  import { mergeInstructionSections } from '../runtime/instruction-sections'
51
52
  import {
@@ -54,6 +55,7 @@ import {
54
55
  shouldEnqueueOnboardingPostChatMemory,
55
56
  shouldEnqueueRegularDigestForWorkstream,
56
57
  } from '../runtime/memory-digest-policy'
58
+ import { buildIndexedRepositoriesContext, getPluginService } from '../runtime/plugin-resolution'
57
59
  import { getRuntimeAdapters, getToolProviders, getTurnHooks } from '../runtime/runtime-extensions'
58
60
  import { shouldEnqueueSkillExtraction } from '../runtime/skill-extraction-policy'
59
61
  import { finalizeTurnRun } from '../runtime/turn-lifecycle'
@@ -81,16 +83,15 @@ import { toIsoDateTimeString } from '../utils/date-time'
81
83
  import { AppError } from '../utils/errors'
82
84
  import { attachmentService } from './attachment.service'
83
85
  import { listReadableUploadsFromChatMessages } from './chat-attachments.service'
84
- import { contextCompactionRuntime } from './context-compaction-runtime'
86
+ import { contextCompactionRuntime } from './context-compaction-runtime.singleton'
85
87
  import { executionPlanService } from './execution-plan.service'
86
88
  import { learnedSkillService } from './learned-skill.service'
87
89
  import { memoryService } from './memory.service'
90
+ import { planRunService } from './plan-run.service'
88
91
  import { recentActivityService } from './recent-activity.service'
89
92
  import { workstreamMessageService } from './workstream-message.service'
90
93
  import { workstreamService } from './workstream.service'
91
94
 
92
- type AgentRuntimeConfig = Record<string, unknown>
93
- type AgentFactory = Record<string, (...args: unknown[]) => Record<string, (...args: unknown[]) => unknown>>
94
95
  type ChatStreamChunk = Parameters<UIMessageStreamWriter<ChatMessage>['write']>[0]
95
96
 
96
97
  interface UIMessageStreamResult {
@@ -105,45 +106,8 @@ function hasUIMessageStream(value: unknown): value is UIMessageStreamResult {
105
106
  )
106
107
  }
107
108
 
108
- function getPluginService(path: string[]): ((...args: unknown[]) => unknown) | undefined {
109
- let current: unknown = pluginRuntime
110
- let owner: unknown = undefined
111
- for (const key of path) {
112
- if (current === null || current === undefined || typeof current !== 'object') return undefined
113
- owner = current
114
- current = (current as Record<string, unknown>)[key]
115
- }
116
- if (typeof current !== 'function') {
117
- return undefined
118
- }
119
-
120
- return owner && typeof owner === 'object'
121
- ? (current as (...args: unknown[]) => unknown).bind(owner)
122
- : (current as (...args: unknown[]) => unknown)
123
- }
124
-
125
- async function buildIndexedRepositoriesContext(
126
- workspaceId: string,
127
- ): Promise<{ provideRepoTool: boolean; defaultSectionsByAgent: Record<string, unknown>; context: string }> {
128
- const buildContext = getRuntimeAdapters().workstream?.buildIndexedRepositoriesContext
129
- if (!buildContext) {
130
- return { provideRepoTool: false, defaultSectionsByAgent: {}, context: '' }
131
- }
132
-
133
- const context = await buildContext(workspaceId)
134
- return {
135
- provideRepoTool: context.provideRepoTool,
136
- defaultSectionsByAgent: context.defaultSectionsByAgent,
137
- context: context.context ?? '',
138
- }
139
- }
140
-
141
109
  const PRESEEDED_MEMORY_LOOKUP_LIMIT = 3
142
110
 
143
- function parsePersistedWorkstreamState(value: unknown): WorkstreamState | null {
144
- return parseWorkstreamState(value)
145
- }
146
-
147
111
  function stripExecutionPlanFieldsFromWorkstreamState(
148
112
  state: WorkstreamState | null | undefined,
149
113
  hasExecutionPlan: boolean,
@@ -154,7 +118,7 @@ function stripExecutionPlanFieldsFromWorkstreamState(
154
118
  }
155
119
 
156
120
  async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
157
- return await waitForCompactionIfNeeded({
121
+ return waitForCompactionIfNeeded({
158
122
  entityId: recordIdToString(workstreamId, TABLES.WORKSTREAM),
159
123
  entityLabel: 'Workstream',
160
124
  loadEntity: () => workstreamService.getById(workstreamId),
@@ -231,7 +195,7 @@ export interface WorkstreamTurnParams {
231
195
  userRef: RecordIdRef
232
196
  userName?: string | null
233
197
  inputMessage: ChatMessage
234
- persistInputMessage?: boolean
198
+ skipInputMessagePersistence?: boolean
235
199
  abortSignal?: AbortSignal
236
200
  streamId?: string
237
201
  }
@@ -256,7 +220,7 @@ type WorkstreamRunCoreParams = {
256
220
  abortSignal?: AbortSignal
257
221
  streamId?: string
258
222
  } & (
259
- | { kind: 'userTurn'; inputMessage: ChatMessage; persistInputMessage?: boolean }
223
+ | { kind: 'userTurn'; inputMessage: ChatMessage; skipInputMessagePersistence?: boolean }
260
224
  | { kind: 'approvalContinuation'; approvalMessages: ChatMessage[] }
261
225
  | { kind: 'nativeToolApprovalTurn'; approvalMessages: ChatMessage[] }
262
226
  )
@@ -300,6 +264,338 @@ function buildRecentActivityChatSystemTitle(params: {
300
264
  return params.workstream.title.trim() || 'Workstream update'
301
265
  }
302
266
 
267
+ interface StreamAgentResponseContext {
268
+ turnHooks: ReturnType<typeof getTurnHooks>
269
+ workstream: NormalizedWorkstream
270
+ workstreamRef: RecordIdRef
271
+ orgRef: RecordIdRef
272
+ userRef: RecordIdRef
273
+ userName?: string | null
274
+ onboardingActive: boolean
275
+ linearInstalled: boolean
276
+ githubInstalled: boolean
277
+ reasoningProfileName: string
278
+ buildContextResult: Record<string, unknown> | null
279
+ getExecutionPlanInstructionSections: () => Promise<string[] | undefined>
280
+ getPreSeededMemoriesSection: (agentId: string) => Promise<string | undefined>
281
+ getWorkstreamStateSection: () => Promise<string | undefined>
282
+ getLearnedSkillsSection: (agentId: string) => Promise<string | undefined>
283
+ promptContext: { systemWorkspaceDetails?: string }
284
+ retrievedKnowledgeSection: string | undefined
285
+ memoryBlock: string
286
+ hookInstructionSections: string[]
287
+ runAbortSignal: AbortSignal
288
+ }
289
+
290
+ interface StreamAgentResponseParams {
291
+ agentId: string
292
+ mode: 'direct' | 'fixedWorkstreamMode' | 'workstreamMode'
293
+ messages: ChatMessage[]
294
+ tools: ToolSet
295
+ observer: {
296
+ run: <T>(fn: () => T | Promise<T>) => Promise<T>
297
+ recordError: (error: unknown) => void
298
+ recordAbort: (error: unknown) => void
299
+ }
300
+ skills?: string[]
301
+ additionalInstructionSections?: string[]
302
+ writer?: UIMessageStreamWriter<ChatMessage>
303
+ stopWhen?: StopCondition<ToolSet> | Array<StopCondition<ToolSet>>
304
+ prepareStep?: PrepareStepFunction<ToolSet>
305
+ abortSignal?: AbortSignal
306
+ }
307
+
308
+ async function streamAgentResponse(
309
+ ctx: StreamAgentResponseContext,
310
+ streamParams: StreamAgentResponseParams,
311
+ ): Promise<ChatMessage> {
312
+ const agentTimer = lotaDebugLogger.timer(`agent:${streamParams.agentId}`)
313
+ const executionPlanInstructionSections = await ctx.getExecutionPlanInstructionSections()
314
+ agentTimer.step('get-execution-plan')
315
+ const agentResolution = asRecord(
316
+ await ctx.turnHooks.resolveAgent?.({
317
+ agentId: streamParams.agentId,
318
+ mode: streamParams.mode,
319
+ workstream: ctx.workstream,
320
+ workstreamRef: ctx.workstreamRef,
321
+ orgRef: ctx.orgRef,
322
+ userRef: ctx.userRef,
323
+ userName: ctx.userName,
324
+ onboardingActive: ctx.onboardingActive,
325
+ linearInstalled: ctx.linearInstalled,
326
+ githubInstalled: ctx.githubInstalled,
327
+ reasoningProfile: ctx.reasoningProfileName,
328
+ skills: streamParams.skills,
329
+ additionalInstructionSections: streamParams.additionalInstructionSections,
330
+ context: ctx.buildContextResult,
331
+ }),
332
+ )
333
+ agentTimer.step('hook:resolveAgent')
334
+ const resolvedAgentId = readOptionalString(agentResolution?.agentId) ?? streamParams.agentId
335
+ const [preSeededMemoriesSection, workstreamStateSection, learnedSkillsSection] = await Promise.all([
336
+ ctx.getPreSeededMemoriesSection(resolvedAgentId),
337
+ ctx.getWorkstreamStateSection(),
338
+ ctx.getLearnedSkillsSection(resolvedAgentId),
339
+ ])
340
+ agentTimer.step('parallel-fetch(memories+state+skills)')
341
+ const config = getAgentRuntimeConfig({
342
+ agentId: resolvedAgentId,
343
+ workstreamMode: ctx.workstream.mode,
344
+ mode: streamParams.mode,
345
+ skills: streamParams.skills,
346
+ onboardingActive: ctx.onboardingActive,
347
+ linearInstalled: ctx.linearInstalled,
348
+ reasoningProfile: ctx.reasoningProfileName,
349
+ systemWorkspaceDetails: ctx.promptContext.systemWorkspaceDetails,
350
+ preSeededMemoriesSection,
351
+ retrievedKnowledgeSection: ctx.retrievedKnowledgeSection,
352
+ workstreamMemoryBlock: ctx.memoryBlock,
353
+ workstreamStateSection,
354
+ learnedSkillsSection,
355
+ additionalInstructionSections: mergeInstructionSections(
356
+ executionPlanInstructionSections,
357
+ streamParams.additionalInstructionSections,
358
+ ctx.hookInstructionSections,
359
+ readInstructionSections(agentResolution?.additionalInstructionSections),
360
+ optionalInstructionSection(agentResolution?.extraInstructions),
361
+ ),
362
+ context: ctx.buildContextResult,
363
+ }) as Record<string, unknown>
364
+ agentTimer.step('build-agent-config')
365
+ const modelMessages = await convertToModelMessages(streamParams.messages, { ignoreIncompleteToolCalls: true })
366
+ agentTimer.step('convert-model-messages')
367
+ const agent = createAgent[config.id as string]({
368
+ mode: streamParams.mode,
369
+ tools: streamParams.tools,
370
+ extraInstructions: config.extraInstructions,
371
+ stopWhen: (agentResolution?.stopWhen as StopCondition<ToolSet> | Array<StopCondition<ToolSet>> | undefined) ??
372
+ streamParams.stopWhen ?? [stepCountIs(config.maxSteps as number)],
373
+ prepareStep: (agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
374
+ }) as ToolLoopAgent<never, ToolSet>
375
+ const agentAbortSignal = streamParams.abortSignal ?? ctx.runAbortSignal
376
+ agentTimer.step('agent-construction')
377
+
378
+ let result: unknown
379
+ try {
380
+ result = await streamParams.observer.run(() =>
381
+ agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal }),
382
+ )
383
+ agentTimer.step('agent.stream()-resolved')
384
+ } catch (error) {
385
+ if (agentAbortSignal.aborted) {
386
+ streamParams.observer.recordAbort(error)
387
+ } else {
388
+ streamParams.observer.recordError(error)
389
+ }
390
+ throw error
391
+ }
392
+ if (!hasUIMessageStream(result)) {
393
+ throw new Error(`Agent run for ${resolvedAgentId} did not expose a UI message stream.`)
394
+ }
395
+
396
+ let responseMessage: ChatMessage | null = null
397
+ let resolveFinishedStream!: () => void
398
+ const finishedStream = new Promise<void>((resolve) => {
399
+ resolveFinishedStream = resolve
400
+ })
401
+
402
+ const uiStream = result.toUIMessageStream({
403
+ generateMessageId: () => Bun.randomUUIDv7(),
404
+ originalMessages: streamParams.messages,
405
+ sendReasoning: true,
406
+ sendSources: true,
407
+ messageMetadata: createAgentMessageMetadata({ agentId: resolvedAgentId, agentName: config.displayName as string }),
408
+ onFinish: ({ responseMessage: finishedResponseMessage }: { responseMessage: ChatMessage }) => {
409
+ responseMessage = withMessageCreatedAt(finishedResponseMessage, Date.now())
410
+ resolveFinishedStream()
411
+ },
412
+ }) as ReadableStream<ChatStreamChunk>
413
+ const reader = uiStream.getReader()
414
+ let firstChunkLogged = false
415
+ try {
416
+ for (;;) {
417
+ const { done, value } = await reader.read()
418
+ if (done) break
419
+ if (!firstChunkLogged) {
420
+ agentTimer.step('first-stream-chunk')
421
+ firstChunkLogged = true
422
+ }
423
+ if (streamParams.writer) {
424
+ streamParams.writer.write(value)
425
+ }
426
+ }
427
+ } finally {
428
+ reader.releaseLock()
429
+ }
430
+ agentTimer.step('stream-complete')
431
+
432
+ await finishedStream
433
+ // responseMessage is set inside the stream callback — linter cannot track cross-callback assignment
434
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
435
+ if (responseMessage === null) {
436
+ throw new Error(`Agent run for ${resolvedAgentId} did not produce a response message.`)
437
+ }
438
+
439
+ for (const toolError of collectToolOutputErrors({ responseMessage: responseMessage })) {
440
+ aiLogger.error`Tool execution failed (agent=${resolvedAgentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
441
+ }
442
+
443
+ return responseMessage
444
+ }
445
+
446
+ interface PostTurnSideEffectsParams {
447
+ workstream: NormalizedWorkstream
448
+ workstreamRef: RecordIdRef
449
+ orgRef: RecordIdRef
450
+ userRef: RecordIdRef
451
+ userName?: string | null
452
+ orgIdString: string
453
+ workstreamIdString: string
454
+ onboardingActive: boolean
455
+ workspace: unknown
456
+ allAssistantMessages: ChatMessage[]
457
+ referenceUserMessage: ChatMessage | undefined
458
+ referenceUserMessageId: string
459
+ recentHistory: ChatMessage[]
460
+ listReadableUploads: () => ReturnType<typeof listReadableUploadsFromChatMessages>
461
+ memoryBlock: string
462
+ visibleWorkstreamAgentId: string | null | undefined
463
+ defaultLeadAgentId: string
464
+ latestWorkstreamRecord: WorkstreamRecord
465
+ latestPersistedState: WorkstreamState | null
466
+ turnHooks: ReturnType<typeof getTurnHooks>
467
+ buildContextResult: Record<string, unknown> | null
468
+ isUserTurn: boolean
469
+ }
470
+
471
+ async function runPostTurnSideEffects(params: PostTurnSideEffectsParams): Promise<void> {
472
+ const turnCount = await workstreamService.incrementTurnCount(params.workstreamRef)
473
+ const agentMessages = buildAgentHistoryMessages(params.allAssistantMessages)
474
+ const historyMessagesForMemory = appendPersistedWorkstreamContextToHistoryMessages(
475
+ toHistoryMessages(params.recentHistory),
476
+ { compactionSummary: params.latestWorkstreamRecord.compactionSummary, persistedState: params.latestPersistedState },
477
+ )
478
+
479
+ const userMessageText = params.referenceUserMessage ? extractMessageText(params.referenceUserMessage).trim() : ''
480
+ const readableUploads = params.listReadableUploads()
481
+ const attachmentMetadataContext = buildReadableUploadMetadataContext(readableUploads)
482
+ const hasAttachmentContext = Boolean(attachmentMetadataContext)
483
+ const shouldExtractMemory = params.onboardingActive
484
+ ? shouldEnqueueOnboardingPostChatMemory({
485
+ onboardingActive: params.onboardingActive,
486
+ userMessageText,
487
+ hasAttachmentContext,
488
+ agentMessageCount: agentMessages.length,
489
+ })
490
+ : shouldEnqueueMemoryExtraction({ onboardingActive: params.onboardingActive, turnCount }) &&
491
+ userMessageText.length > 0
492
+
493
+ if (shouldExtractMemory) {
494
+ const memoryUserMessage = userMessageText || 'User uploaded attachment(s).'
495
+ await safeEnqueue(
496
+ () =>
497
+ enqueuePostChatMemory({
498
+ orgId: params.orgIdString,
499
+ workstreamId: params.workstreamIdString,
500
+ sourceId: params.referenceUserMessageId,
501
+ onboardStatus: readOptionalString((params.workspace as { onboardStatus?: unknown }).onboardStatus),
502
+ userMessage: memoryUserMessage,
503
+ historyMessages: historyMessagesForMemory,
504
+ agentMessages,
505
+ memoryBlock: params.memoryBlock.trim() ? params.memoryBlock : undefined,
506
+ attachmentContext: attachmentMetadataContext,
507
+ }),
508
+ { operationName: 'post-chat memory extraction enqueue' },
509
+ )
510
+ }
511
+
512
+ if (params.isUserTurn && params.referenceUserMessage) {
513
+ const conversationSummary = buildConversationSummary({
514
+ userMessageText,
515
+ assistantMessages: params.allAssistantMessages,
516
+ })
517
+ if (conversationSummary) {
518
+ const effectiveAgentId = params.visibleWorkstreamAgentId ?? params.defaultLeadAgentId
519
+ const recentActivityResult = await recentActivityService.recordEvent({
520
+ orgId: params.orgRef,
521
+ userId: params.userRef,
522
+ source: 'system',
523
+ event: {
524
+ sourceEventId: `chat-turn:${params.referenceUserMessageId}`,
525
+ kind: 'chat.turn.completed',
526
+ targetKind: 'workstream',
527
+ targetId: params.workstreamIdString,
528
+ mergeKey: `workstream:${params.workstreamIdString}`,
529
+ title: buildRecentActivityChatSystemTitle({
530
+ workstream: params.workstream,
531
+ visibleAgentId: effectiveAgentId,
532
+ }),
533
+ sourceLabel: agentDisplayNames[effectiveAgentId],
534
+ deepLink: buildRecentActivityChatDeepLink({
535
+ workstream: params.workstream,
536
+ workstreamId: params.workstreamIdString,
537
+ visibleAgentId: effectiveAgentId,
538
+ }),
539
+ metadata: {
540
+ agentId: effectiveAgentId,
541
+ agentName: agentDisplayNames[effectiveAgentId],
542
+ workstreamId: params.workstreamIdString,
543
+ workstreamTitle: params.latestWorkstreamRecord.title ?? params.workstream.title,
544
+ workstreamMode: params.workstream.mode,
545
+ ...(params.workstream.coreType ? { coreType: params.workstream.coreType } : {}),
546
+ userMessageText,
547
+ assistantSummary: conversationSummary,
548
+ messageId: params.referenceUserMessageId,
549
+ },
550
+ occurredAt: toIsoDateTimeString(params.referenceUserMessage.metadata?.createdAt ?? Date.now()),
551
+ },
552
+ })
553
+
554
+ await safeEnqueue(
555
+ async () => {
556
+ const enqueuePostChatOrgAction = getRuntimeAdapters().queues?.enqueuePostChatOrgAction
557
+ if (!enqueuePostChatOrgAction) {
558
+ return
559
+ }
560
+
561
+ await enqueuePostChatOrgAction({
562
+ orgId: params.orgIdString,
563
+ workstreamId: params.workstreamIdString,
564
+ sourceId: params.referenceUserMessageId,
565
+ sourceCreatedAt: params.referenceUserMessage?.metadata?.createdAt ?? Date.now(),
566
+ conversationSummary,
567
+ })
568
+ },
569
+ { operationName: 'post-chat org action enqueue' },
570
+ )
571
+
572
+ if (recentActivityService.isMeaningfulRefinementCandidate(recentActivityResult.item)) {
573
+ await safeEnqueue(() => enqueueRecentActivityTitleRefinement({ activityId: recentActivityResult.item.id }), {
574
+ operationName: 'recent activity title refinement enqueue',
575
+ })
576
+ }
577
+ }
578
+ }
579
+
580
+ if (shouldEnqueueRegularDigestForWorkstream({ onboardingActive: params.onboardingActive, turnCount })) {
581
+ await safeEnqueue(() => enqueueRegularChatMemoryDigest({ orgId: params.orgIdString }), {
582
+ operationName: 'regular chat memory digest enqueue',
583
+ })
584
+ }
585
+
586
+ if (shouldEnqueueSkillExtraction({ onboardingActive: params.onboardingActive, turnCount })) {
587
+ await safeEnqueue(() => enqueueSkillExtraction({ orgId: params.orgIdString }), {
588
+ operationName: 'skill extraction enqueue',
589
+ })
590
+ }
591
+
592
+ if (shouldEnqueueMemoryConsolidation({ onboardingActive: params.onboardingActive, turnCount })) {
593
+ await safeEnqueue(() => enqueueMemoryConsolidation({ scopeId: params.orgIdString }), {
594
+ operationName: 'memory consolidation enqueue',
595
+ })
596
+ }
597
+ }
598
+
303
599
  export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams): Promise<PreparedWorkstreamTurn> {
304
600
  const { workstream, workstreamRef, orgRef, userRef, userName } = params
305
601
  const runtimeAdapters = getRuntimeAdapters()
@@ -320,11 +616,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
320
616
  })
321
617
 
322
618
  let inputMessage: ChatMessage | undefined
323
- const shouldPersistInputMessage = params.kind === 'userTurn' ? params.persistInputMessage !== false : false
619
+ const shouldPersistInputMessage = params.kind === 'userTurn' ? params.skipInputMessagePersistence !== true : false
324
620
  const shouldProcessPostRunSideEffects =
325
621
  params.kind === 'approvalContinuation' || params.kind === 'nativeToolApprovalTurn' || shouldPersistInputMessage
326
622
  if (params.kind === 'userTurn') {
327
- inputMessage = hydrateMessageFileUrls(withMessageCreatedAt(params.inputMessage))
623
+ inputMessage = hydrateMessageFileUrls(withMessageCreatedAt(params.inputMessage, Date.now()))
328
624
  if (inputMessage.role !== 'user') {
329
625
  throw new WorkstreamTurnError('Only user messages can be submitted to the workstream runtime.', 400)
330
626
  }
@@ -358,7 +654,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
358
654
  timer.step('persist-approval-message')
359
655
  }
360
656
 
361
- const initialWorkstreamState = parsePersistedWorkstreamState(workstreamRecord.state)
657
+ const initialWorkstreamState = parseWorkstreamState(workstreamRecord.state)
362
658
  const persistedCompactionCursor = toOptionalTrimmedString(workstreamRecord.lastCompactedMessageId) ?? undefined
363
659
  const persistedLiveHistoryPromise = workstreamMessageService.listMessagesAfterCursor(
364
660
  workstreamRef,
@@ -370,7 +666,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
370
666
  if (inputMessage) {
371
667
  userMessage = {
372
668
  ...inputMessage,
373
- id: inputMessage.id || Bun.randomUUIDv7(),
669
+ id: inputMessage.id,
374
670
  role: 'user',
375
671
  parts: inputMessage.parts,
376
672
  metadata: { ...inputMessage.metadata, createdAt: toTimestamp(inputMessage.metadata?.createdAt) },
@@ -395,14 +691,14 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
395
691
  : validateUIMessages<ChatMessage>({
396
692
  messages: persistedLiveHistory,
397
693
  metadataSchema: messageMetadataSchema,
398
- dataSchemas: dataPartsSchema,
694
+ dataSchemas: dataPartsSchemas,
399
695
  }).then((messages) => messages.map(hydrateMessageFileUrls)),
400
696
  persistedRecentHistory.length === 0
401
697
  ? Promise.resolve([] as ChatMessage[])
402
698
  : validateUIMessages<ChatMessage>({
403
699
  messages: persistedRecentHistory,
404
700
  metadataSchema: messageMetadataSchema,
405
- dataSchemas: dataPartsSchema,
701
+ dataSchemas: dataPartsSchemas,
406
702
  }).then((messages) => messages.map(hydrateMessageFileUrls)),
407
703
  ])
408
704
  timer.step('validate+hydrate-history')
@@ -413,7 +709,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
413
709
  }
414
710
 
415
711
  const originalMessages = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
416
- const allAssistantMessages: ChatMessage[] = []
712
+ let allAssistantMessages: ChatMessage[] = []
417
713
  const referenceUserMessage =
418
714
  params.kind === 'userTurn' && !shouldPersistInputMessage
419
715
  ? [...liveHistory].reverse().find((m) => m.role === 'user')
@@ -425,6 +721,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
425
721
  workstream.mode === 'group' &&
426
722
  !workstream.core &&
427
723
  workstreamRecord.nameGenerated !== true &&
724
+ workstreamRecord.title === WORKSTREAM.DEFAULT_TITLE &&
428
725
  messageText.length > 0
429
726
  ) {
430
727
  void safeEnqueue(
@@ -437,14 +734,8 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
437
734
  if (workstream.core && !workstream.coreType) {
438
735
  throw new WorkstreamTurnError('Core workstreams require a core type.', 400)
439
736
  }
440
- const coreWorkstreamProfile: { config: { agentId: string }; instructions: string; skills?: string[] } | null =
441
- workstream.core && workstream.coreType
442
- ? (getCoreWorkstreamProfile(workstream.coreType) as unknown as {
443
- config: { agentId: string }
444
- instructions: string
445
- skills?: string[]
446
- })
447
- : null
737
+ const coreWorkstreamProfile: CoreWorkstreamProfile | null =
738
+ workstream.core && workstream.coreType ? getCoreWorkstreamProfile(workstream.coreType) : null
448
739
  const defaultLeadAgentId = getLeadAgentId()
449
740
  const visibleWorkstreamAgentId =
450
741
  workstream.mode === 'direct' ? workstream.agentId : (coreWorkstreamProfile?.config.agentId ?? defaultLeadAgentId)
@@ -569,18 +860,21 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
569
860
  let workstreamState = initialWorkstreamState
570
861
  const executionPlanInstructionSectionCache = createExecutionPlanInstructionSectionCache({
571
862
  disabled: onboardingActive,
572
- loadPlan: async () => await executionPlanService.getActivePlanForWorkstream(workstreamRef),
863
+ loadPlans: async () => {
864
+ const runs = await planRunService.getActiveRunRecords(workstreamRef)
865
+ return Promise.all(runs.map((run) => planRunService.toSerializablePlan(run)))
866
+ },
573
867
  })
574
- const getExecutionPlan = async () => await executionPlanInstructionSectionCache.getPlan()
868
+ const getExecutionPlans = async () => await executionPlanInstructionSectionCache.getPlans()
575
869
  const getExecutionPlanInstructionSections = async (): Promise<string[] | undefined> =>
576
870
  await executionPlanInstructionSectionCache.getSections()
577
871
  const invalidateExecutionPlanInstructionSections = () => {
578
872
  executionPlanInstructionSectionCache.invalidate()
579
873
  }
580
874
  const getWorkstreamStateSection = async (): Promise<string | undefined> => {
581
- const executionPlan = await getExecutionPlan()
875
+ const executionPlans = await getExecutionPlans()
582
876
  return contextCompactionRuntime.formatWorkstreamStateForPrompt(
583
- stripExecutionPlanFieldsFromWorkstreamState(workstreamState, Boolean(executionPlan)),
877
+ stripExecutionPlanFieldsFromWorkstreamState(workstreamState, executionPlans.length > 0),
584
878
  )
585
879
  }
586
880
  const respondedBy = recordIdToString(userRef, TABLES.USER)
@@ -626,7 +920,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
626
920
 
627
921
  const section = await learnedSkillService
628
922
  .retrieveForTurn({ orgId: orgIdString, agentId, query: messageText, limit: 3, minConfidence: 0.6 })
629
- .catch((error: unknown) => {
923
+ .catch((error) => {
630
924
  aiLogger.warn`Failed to retrieve learned skills for ${agentId}: ${error}`
631
925
  return undefined
632
926
  })
@@ -691,181 +985,39 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
691
985
  })
692
986
 
693
987
  const commitAssistantResponse = async (response: ChatMessage, agentId: string, agentName: string) => {
694
- const committed = withMessageCreatedAt({
695
- ...response,
696
- metadata: { ...response.metadata, ...buildAgentMetadataPatch(agentId, agentName) },
697
- })
988
+ const committed = withMessageCreatedAt(
989
+ { ...response, metadata: { ...response.metadata, ...buildAgentMetadataPatch(agentId, agentName) } },
990
+ Date.now(),
991
+ )
698
992
 
699
993
  await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [committed] })
700
- const currentMessageIndex = currentMessages.findIndex((item) => item.id === committed.id)
701
- if (currentMessageIndex >= 0) {
702
- currentMessages[currentMessageIndex] = committed
703
- } else {
704
- currentMessages = [...currentMessages, committed]
705
- }
706
-
707
- const assistantIndex = allAssistantMessages.findIndex((item) => item.id === committed.id)
708
- if (assistantIndex >= 0) {
709
- allAssistantMessages[assistantIndex] = committed
710
- } else {
711
- allAssistantMessages.push(committed)
712
- }
994
+ currentMessages = upsertChatHistoryMessage(currentMessages, committed)
995
+ allAssistantMessages = upsertChatHistoryMessage(allAssistantMessages, committed)
713
996
 
714
997
  return committed
715
998
  }
716
999
 
717
- const streamAgentResponse = async (streamParams: {
718
- agentId: string
719
- mode: 'direct' | 'fixedWorkstreamMode' | 'workstreamMode'
720
- messages: ChatMessage[]
721
- tools: ToolSet
722
- observer: ReturnType<typeof createObserver>
723
- skills?: string[]
724
- additionalInstructionSections?: string[]
725
- writer?: UIMessageStreamWriter<ChatMessage>
726
- stopWhen?: StopCondition<ToolSet> | Array<StopCondition<ToolSet>>
727
- prepareStep?: PrepareStepFunction<ToolSet>
728
- abortSignal?: AbortSignal
729
- }): Promise<ChatMessage> => {
730
- const agentTimer = lotaDebugLogger.timer(`agent:${streamParams.agentId}`)
731
- const executionPlanInstructionSections = await getExecutionPlanInstructionSections()
732
- agentTimer.step('get-execution-plan')
733
- const agentResolution = asRecord(
734
- await turnHooks.resolveAgent?.({
735
- agentId: streamParams.agentId,
736
- mode: streamParams.mode,
737
- workstream,
738
- workstreamRef,
739
- orgRef,
740
- userRef,
741
- userName,
742
- onboardingActive,
743
- linearInstalled,
744
- githubInstalled,
745
- reasoningProfile: reasoningProfile.name,
746
- skills: streamParams.skills,
747
- additionalInstructionSections: streamParams.additionalInstructionSections,
748
- context: buildContextResult,
749
- }),
750
- )
751
- agentTimer.step('hook:resolveAgent')
752
- const resolvedAgentId = readOptionalString(agentResolution?.agentId) ?? streamParams.agentId
753
- const [preSeededMemoriesSection, workstreamStateSection, learnedSkillsSection] = await Promise.all([
754
- getPreSeededMemoriesSection(resolvedAgentId),
755
- getWorkstreamStateSection(),
756
- getLearnedSkillsSection(resolvedAgentId),
757
- ])
758
- agentTimer.step('parallel-fetch(memories+state+skills)')
759
- const config = getAgentRuntimeConfig({
760
- agentId: resolvedAgentId,
761
- workstreamMode: workstream.mode,
762
- mode: streamParams.mode,
763
- skills: streamParams.skills,
764
- onboardingActive,
765
- linearInstalled,
766
- reasoningProfile: reasoningProfile.name,
767
- systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
768
- preSeededMemoriesSection,
769
- retrievedKnowledgeSection,
770
- workstreamMemoryBlock: memoryBlock,
771
- workstreamStateSection,
772
- learnedSkillsSection,
773
- additionalInstructionSections: mergeInstructionSections(
774
- executionPlanInstructionSections,
775
- streamParams.additionalInstructionSections,
776
- hookInstructionSections,
777
- readInstructionSections(agentResolution?.additionalInstructionSections),
778
- optionalInstructionSection(agentResolution?.extraInstructions),
779
- ),
780
- context: buildContextResult,
781
- }) as AgentRuntimeConfig
782
- agentTimer.step('build-agent-config')
783
- const modelMessages = await convertToModelMessages(streamParams.messages, {
784
- ignoreIncompleteToolCalls: true,
785
- })
786
- agentTimer.step('convert-model-messages')
787
- const agent = (createAgent as unknown as AgentFactory)[config.id as string]({
788
- mode: streamParams.mode,
789
- tools: streamParams.tools,
790
- extraInstructions: config.extraInstructions,
791
- stopWhen: (agentResolution?.stopWhen as
792
- | StopCondition<ToolSet>
793
- | Array<StopCondition<ToolSet>>
794
- | undefined) ??
795
- streamParams.stopWhen ?? [stepCountIs(config.maxSteps as number)],
796
- prepareStep:
797
- (agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
798
- })
799
- const agentAbortSignal = streamParams.abortSignal ?? runAbort.signal
800
- agentTimer.step('agent-construction')
801
-
802
- let result: unknown
803
- try {
804
- result = await streamParams.observer.run(() =>
805
- agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal }),
806
- )
807
- agentTimer.step('agent.stream()-resolved')
808
- } catch (error) {
809
- if (agentAbortSignal.aborted) {
810
- streamParams.observer.recordAbort(error)
811
- } else {
812
- streamParams.observer.recordError(error)
813
- }
814
- throw error
815
- }
816
- if (!hasUIMessageStream(result)) {
817
- throw new Error(`Agent run for ${resolvedAgentId} did not expose a UI message stream.`)
818
- }
819
-
820
- let responseMessage: ChatMessage | null = null
821
- let resolveFinishedStream!: () => void
822
- const finishedStream = new Promise<void>((resolve) => {
823
- resolveFinishedStream = resolve
824
- })
825
-
826
- const uiStream = result.toUIMessageStream({
827
- generateMessageId: () => Bun.randomUUIDv7(),
828
- originalMessages: streamParams.messages,
829
- sendReasoning: true,
830
- sendSources: true,
831
- messageMetadata: createAgentMessageMetadata({
832
- agentId: resolvedAgentId,
833
- agentName: config.displayName as string,
834
- }),
835
- onFinish: ({ responseMessage: finishedResponseMessage }: { responseMessage: ChatMessage }) => {
836
- responseMessage = withMessageCreatedAt(finishedResponseMessage)
837
- resolveFinishedStream()
838
- },
839
- }) as ReadableStream<ChatStreamChunk>
840
- const reader = uiStream.getReader()
841
- let firstChunkLogged = false
842
- try {
843
- for (;;) {
844
- const { done, value } = await reader.read()
845
- if (done) break
846
- if (!firstChunkLogged) {
847
- agentTimer.step('first-stream-chunk')
848
- firstChunkLogged = true
849
- }
850
- if (streamParams.writer) {
851
- streamParams.writer.write(value)
852
- }
853
- }
854
- } finally {
855
- reader.releaseLock()
856
- }
857
- agentTimer.step('stream-complete')
858
-
859
- const finalizedResponseMessage = await finishedStream.then(() => responseMessage)
860
- if (finalizedResponseMessage === null) {
861
- throw new Error(`Agent run for ${resolvedAgentId} did not produce a response message.`)
862
- }
863
-
864
- for (const toolError of collectToolOutputErrors({ responseMessage: finalizedResponseMessage })) {
865
- aiLogger.warn`Tool execution failed (agent=${resolvedAgentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
866
- }
867
-
868
- return finalizedResponseMessage
1000
+ const streamCtx: StreamAgentResponseContext = {
1001
+ turnHooks,
1002
+ workstream,
1003
+ workstreamRef,
1004
+ orgRef,
1005
+ userRef,
1006
+ userName,
1007
+ onboardingActive,
1008
+ linearInstalled,
1009
+ githubInstalled,
1010
+ reasoningProfileName: reasoningProfile.name,
1011
+ buildContextResult,
1012
+ getExecutionPlanInstructionSections,
1013
+ getPreSeededMemoriesSection,
1014
+ getWorkstreamStateSection,
1015
+ getLearnedSkillsSection,
1016
+ promptContext,
1017
+ retrievedKnowledgeSection,
1018
+ memoryBlock,
1019
+ hookInstructionSections,
1020
+ runAbortSignal: runAbort.signal,
869
1021
  }
870
1022
 
871
1023
  const runVisibleAgent = async (runParams: {
@@ -878,6 +1030,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
878
1030
  }): Promise<ChatMessage> => {
879
1031
  const visibleTimer = lotaDebugLogger.timer(`visible:${runParams.agentId}`)
880
1032
  let runMemoryBlock = memoryBlock
1033
+ const includeExecutionPlanTools = runParams.mode !== 'fixedWorkstreamMode' && !onboardingActive
881
1034
  const tools: ToolSet = {
882
1035
  ...((await buildAgentTools({
883
1036
  agentId: runParams.agentId,
@@ -899,7 +1052,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
899
1052
  runMemoryBlock = value
900
1053
  },
901
1054
  availableUploads: listReadableUploads(runParams.extraMessages),
902
- includeExecutionPlanTools: runParams.mode !== 'fixedWorkstreamMode',
1055
+ includeExecutionPlanTools,
903
1056
  onExecutionPlanChanged: invalidateExecutionPlanInstructionSections,
904
1057
  context: buildContextResult,
905
1058
  })) as ToolSet),
@@ -907,7 +1060,8 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
907
1060
  ...runParams.extraTools,
908
1061
  }
909
1062
  visibleTimer.step('build-agent-tools')
910
- const responseMessage = await streamAgentResponse({
1063
+ streamCtx.memoryBlock = memoryBlock
1064
+ const responseMessage = await streamAgentResponse(streamCtx, {
911
1065
  agentId: runParams.agentId,
912
1066
  mode: runParams.mode,
913
1067
  messages: buildRunInputMessages(runParams.extraMessages),
@@ -921,7 +1075,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
921
1075
  visibleTimer.step('stream-agent-response')
922
1076
  memoryBlock = runMemoryBlock
923
1077
 
924
- return await commitAssistantResponse(
1078
+ return commitAssistantResponse(
925
1079
  responseMessage,
926
1080
  runParams.agentId,
927
1081
  agentDisplayNames[runParams.agentId] ?? runParams.agentId,
@@ -996,14 +1150,14 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
996
1150
  hookInstructionSections,
997
1151
  ),
998
1152
  context: buildContextResult,
999
- }) as AgentRuntimeConfig
1153
+ }) as Record<string, unknown>
1000
1154
  const observer = createObserver(agentId)
1001
- const agent = (createAgent as unknown as AgentFactory)[specialistConfig.id as string]({
1155
+ const agent = createAgent[specialistConfig.id as string]({
1002
1156
  mode: 'fixedWorkstreamMode',
1003
1157
  tools: { ...(specialistTools as ToolSet), ...toolProviders },
1004
1158
  extraInstructions: specialistConfig.extraInstructions,
1005
1159
  stopWhen: [stepCountIs(specialistConfig.maxSteps as number)],
1006
- })
1160
+ }) as ToolLoopAgent<never, ToolSet>
1007
1161
  const modelMessages = await convertToModelMessages(buildRunInputMessages([specialistTaskMessage]), {
1008
1162
  ignoreIncompleteToolCalls: true,
1009
1163
  })
@@ -1039,7 +1193,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1039
1193
  }),
1040
1194
  }) as ReadableStream<never>,
1041
1195
  })) {
1042
- finalMessage = withMessageCreatedAt(message)
1196
+ finalMessage = withMessageCreatedAt(message, Date.now())
1043
1197
  yield finalMessage
1044
1198
  }
1045
1199
 
@@ -1115,7 +1269,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1115
1269
  } finally {
1116
1270
  try {
1117
1271
  const latestWorkstreamRecord = await workstreamService.getById(workstreamRef)
1118
- const latestPersistedState = parsePersistedWorkstreamState(latestWorkstreamRecord.state)
1272
+ const latestPersistedState = parseWorkstreamState(latestWorkstreamRecord.state)
1119
1273
 
1120
1274
  await finalizeTurnRun({
1121
1275
  serverRunId,
@@ -1126,14 +1280,15 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1126
1280
  contextCompactionRuntime.shouldCompactHistory({
1127
1281
  summaryText,
1128
1282
  liveMessages: messages,
1129
- contextSize: CONTEXT_SIZE,
1283
+ contextSize: CONTEXT_WINDOW_TOKENS,
1130
1284
  }),
1131
- enqueueCompaction: () =>
1132
- enqueueContextCompaction({
1285
+ enqueueCompaction: async () => {
1286
+ await enqueueContextCompaction({
1133
1287
  domain: 'workstream',
1134
1288
  entityId: workstreamIdString,
1135
- contextSize: CONTEXT_SIZE,
1136
- }).then(() => {}),
1289
+ contextSize: CONTEXT_WINDOW_TOKENS,
1290
+ })
1291
+ },
1137
1292
  unregisterRun: (runId) => chatRunRegistry.unregister(runId),
1138
1293
  clearActiveRunId: (runId) => workstreamService.clearActiveRunIdIfMatches(workstreamRef, runId),
1139
1294
  disposeAbort: () => runAbort.dispose(),
@@ -1143,130 +1298,30 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1143
1298
  })
1144
1299
 
1145
1300
  if (allAssistantMessages.length > 0 && shouldProcessPostRunSideEffects) {
1146
- const turnCount = await workstreamService.incrementTurnCount(workstreamRef)
1147
- const agentMessages = buildAgentHistoryMessages(allAssistantMessages)
1148
- const historyMessagesForMemory = appendPersistedWorkstreamContextToHistoryMessages(
1149
- toHistoryMessages(recentHistory),
1150
- { compactionSummary: latestWorkstreamRecord.compactionSummary, persistedState: latestPersistedState },
1151
- )
1152
-
1153
- const userMessageText = referenceUserMessage ? extractMessageText(referenceUserMessage).trim() : ''
1154
- const readableUploads = listReadableUploads()
1155
- const attachmentMetadataContext = buildReadableUploadMetadataContext(readableUploads)
1156
- const hasAttachmentContext = Boolean(attachmentMetadataContext)
1157
- const shouldExtractMemory = onboardingActive
1158
- ? shouldEnqueueOnboardingPostChatMemory({
1159
- onboardingActive,
1160
- userMessageText,
1161
- hasAttachmentContext,
1162
- agentMessageCount: agentMessages.length,
1163
- })
1164
- : shouldEnqueueMemoryExtraction({ onboardingActive, turnCount }) && userMessageText.length > 0
1165
-
1166
- if (shouldExtractMemory) {
1167
- const memoryUserMessage = userMessageText || 'User uploaded attachment(s).'
1168
- await safeEnqueue(
1169
- () =>
1170
- enqueuePostChatMemory({
1171
- orgId: orgIdString,
1172
- workstreamId: workstreamIdString,
1173
- sourceId: referenceUserMessageId,
1174
- onboardStatus: readOptionalString((workspace as { onboardStatus?: unknown }).onboardStatus),
1175
- userMessage: memoryUserMessage,
1176
- historyMessages: historyMessagesForMemory,
1177
- agentMessages,
1178
- memoryBlock: memoryBlock.trim() ? memoryBlock : undefined,
1179
- attachmentContext: attachmentMetadataContext,
1180
- }),
1181
- { operationName: 'post-chat memory extraction enqueue' },
1182
- )
1183
- }
1184
-
1185
- if (params.kind === 'userTurn' && referenceUserMessage) {
1186
- const conversationSummary = buildConversationSummary({
1187
- userMessageText,
1188
- assistantMessages: allAssistantMessages,
1189
- })
1190
- if (conversationSummary) {
1191
- const recentActivityResult = await recentActivityService.recordEvent({
1192
- orgId: orgRef,
1193
- userId: userRef,
1194
- source: 'system',
1195
- event: {
1196
- sourceEventId: `chat-turn:${referenceUserMessageId}`,
1197
- kind: 'chat.turn.completed',
1198
- targetKind: 'workstream',
1199
- targetId: workstreamIdString,
1200
- mergeKey: `workstream:${workstreamIdString}`,
1201
- title: buildRecentActivityChatSystemTitle({
1202
- workstream,
1203
- visibleAgentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
1204
- }),
1205
- sourceLabel: agentDisplayNames[visibleWorkstreamAgentId ?? defaultLeadAgentId],
1206
- deepLink: buildRecentActivityChatDeepLink({
1207
- workstream,
1208
- workstreamId: workstreamIdString,
1209
- visibleAgentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
1210
- }),
1211
- metadata: {
1212
- agentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
1213
- agentName: agentDisplayNames[visibleWorkstreamAgentId ?? defaultLeadAgentId],
1214
- workstreamId: workstreamIdString,
1215
- workstreamTitle: latestWorkstreamRecord.title ?? workstream.title,
1216
- workstreamMode: workstream.mode,
1217
- ...(workstream.coreType ? { coreType: workstream.coreType } : {}),
1218
- userMessageText,
1219
- assistantSummary: conversationSummary,
1220
- messageId: referenceUserMessageId,
1221
- },
1222
- occurredAt: toIsoDateTimeString(referenceUserMessage.metadata?.createdAt ?? Date.now()),
1223
- },
1224
- })
1225
-
1226
- await safeEnqueue(
1227
- async () => {
1228
- const enqueuePostChatOrgAction = getRuntimeAdapters().queues?.enqueuePostChatOrgAction
1229
- if (!enqueuePostChatOrgAction) {
1230
- return
1231
- }
1232
-
1233
- await enqueuePostChatOrgAction({
1234
- orgId: orgIdString,
1235
- workstreamId: workstreamIdString,
1236
- sourceId: referenceUserMessageId,
1237
- sourceCreatedAt: referenceUserMessage.metadata?.createdAt ?? Date.now(),
1238
- conversationSummary,
1239
- })
1240
- },
1241
- { operationName: 'post-chat org action enqueue' },
1242
- )
1243
-
1244
- if (recentActivityService.isMeaningfulRefinementCandidate(recentActivityResult.item)) {
1245
- await safeEnqueue(
1246
- () => enqueueRecentActivityTitleRefinement({ activityId: recentActivityResult.item.id }),
1247
- { operationName: 'recent activity title refinement enqueue' },
1248
- )
1249
- }
1250
- }
1251
- }
1252
-
1253
- if (shouldEnqueueRegularDigestForWorkstream({ onboardingActive, turnCount })) {
1254
- await safeEnqueue(() => enqueueRegularChatMemoryDigest({ orgId: orgIdString }), {
1255
- operationName: 'regular chat memory digest enqueue',
1256
- })
1257
- }
1258
-
1259
- if (shouldEnqueueSkillExtraction({ onboardingActive, turnCount })) {
1260
- await safeEnqueue(() => enqueueSkillExtraction({ orgId: orgIdString }), {
1261
- operationName: 'skill extraction enqueue',
1262
- })
1263
- }
1264
-
1265
- if (shouldEnqueueMemoryConsolidation({ onboardingActive, turnCount })) {
1266
- await safeEnqueue(() => enqueueMemoryConsolidation({ scopeId: orgIdString }), {
1267
- operationName: 'memory consolidation enqueue',
1268
- })
1269
- }
1301
+ await runPostTurnSideEffects({
1302
+ workstream,
1303
+ workstreamRef,
1304
+ orgRef,
1305
+ userRef,
1306
+ userName,
1307
+ orgIdString,
1308
+ workstreamIdString,
1309
+ onboardingActive,
1310
+ workspace,
1311
+ allAssistantMessages,
1312
+ referenceUserMessage,
1313
+ referenceUserMessageId,
1314
+ recentHistory,
1315
+ listReadableUploads: () => listReadableUploads(),
1316
+ memoryBlock,
1317
+ visibleWorkstreamAgentId,
1318
+ defaultLeadAgentId,
1319
+ latestWorkstreamRecord,
1320
+ latestPersistedState,
1321
+ turnHooks,
1322
+ buildContextResult,
1323
+ isUserTurn: params.kind === 'userTurn',
1324
+ })
1270
1325
  }
1271
1326
 
1272
1327
  if (allAssistantMessages.length > 0) {