@lota-sdk/core 0.1.9 → 0.1.12

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 (105) hide show
  1. package/infrastructure/schema/00_workstream.surql +1 -0
  2. package/infrastructure/schema/02_execution_plan.surql +202 -52
  3. package/package.json +4 -87
  4. package/src/ai/index.ts +3 -0
  5. package/src/bifrost/bifrost.ts +94 -25
  6. package/src/bifrost/index.ts +1 -0
  7. package/src/config/agent-defaults.ts +30 -7
  8. package/src/config/constants.ts +0 -9
  9. package/src/config/debug-logger.ts +43 -0
  10. package/src/config/index.ts +5 -0
  11. package/src/config/model-constants.ts +8 -9
  12. package/src/config/workstream-defaults.ts +4 -0
  13. package/src/db/cursor-pagination.ts +2 -2
  14. package/src/db/index.ts +10 -0
  15. package/src/db/memory-store.ts +3 -71
  16. package/src/db/memory.ts +9 -15
  17. package/src/db/service.ts +42 -2
  18. package/src/db/tables.ts +9 -2
  19. package/src/document/index.ts +2 -0
  20. package/src/document/parsing.ts +0 -25
  21. package/src/embeddings/provider.ts +102 -22
  22. package/src/index.ts +15 -499
  23. package/src/queues/index.ts +10 -0
  24. package/src/redis/connection-accessor.ts +26 -0
  25. package/src/redis/connection.ts +1 -1
  26. package/src/redis/index.ts +9 -25
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +1 -1
  29. package/src/redis/stream-context.ts +54 -0
  30. package/src/runtime/agent-runtime-policy.ts +9 -5
  31. package/src/runtime/agent-stream-helpers.ts +6 -3
  32. package/src/runtime/agent-types.ts +1 -5
  33. package/src/runtime/approval-continuation.ts +68 -1
  34. package/src/runtime/chat-attachments.ts +1 -1
  35. package/src/runtime/chat-request-routing.ts +6 -2
  36. package/src/runtime/context-compaction-runtime.ts +2 -2
  37. package/src/runtime/context-compaction.ts +1 -1
  38. package/src/runtime/execution-plan.ts +22 -15
  39. package/src/runtime/index.ts +26 -0
  40. package/src/runtime/indexed-repositories-policy.ts +10 -10
  41. package/src/runtime/memory-pipeline.ts +0 -2
  42. package/src/runtime/runtime-config.ts +238 -0
  43. package/src/runtime/runtime-extensions.ts +3 -2
  44. package/src/runtime/runtime-worker-registry.ts +47 -0
  45. package/src/runtime/team-consultation-orchestrator.ts +9 -6
  46. package/src/runtime/team-consultation-prompts.ts +3 -2
  47. package/src/runtime/turn-lifecycle.ts +13 -5
  48. package/src/runtime/workstream-chat-helpers.ts +0 -54
  49. package/src/runtime/workstream-routing-policy.ts +3 -7
  50. package/src/runtime.ts +387 -0
  51. package/src/services/chat-attachments.service.ts +1 -1
  52. package/src/services/context-compaction.service.ts +1 -1
  53. package/src/services/document-chunk.service.ts +2 -2
  54. package/src/services/execution-plan.service.ts +584 -793
  55. package/src/services/index.ts +14 -0
  56. package/src/services/learned-skill.service.ts +82 -39
  57. package/src/services/memory.service.ts +5 -4
  58. package/src/services/mutating-approval.service.ts +1 -1
  59. package/src/services/organization-member.service.ts +1 -1
  60. package/src/services/organization.service.ts +1 -1
  61. package/src/services/plan-approval.service.ts +83 -0
  62. package/src/services/plan-artifact.service.ts +44 -0
  63. package/src/services/plan-builder.service.ts +61 -0
  64. package/src/services/plan-checkpoint.service.ts +53 -0
  65. package/src/services/plan-compiler.service.ts +81 -0
  66. package/src/services/plan-executor.service.ts +1624 -0
  67. package/src/services/plan-run.service.ts +422 -0
  68. package/src/services/plan-validator.service.ts +760 -0
  69. package/src/services/recent-activity-title.service.ts +1 -1
  70. package/src/services/recent-activity.service.ts +14 -16
  71. package/src/services/user.service.ts +2 -2
  72. package/src/services/workstream-message.service.ts +2 -3
  73. package/src/services/workstream-title.service.ts +1 -1
  74. package/src/services/workstream-turn-preparation.ts +156 -59
  75. package/src/services/workstream-turn.ts +26 -1
  76. package/src/services/workstream.service.ts +35 -9
  77. package/src/services/workstream.types.ts +1 -0
  78. package/src/storage/attachment-parser.ts +1 -1
  79. package/src/storage/attachment-storage.service.ts +11 -10
  80. package/src/storage/generated-document-storage.service.ts +7 -6
  81. package/src/storage/index.ts +10 -0
  82. package/src/system-agents/delegated-agent-factory.ts +78 -29
  83. package/src/system-agents/index.ts +4 -0
  84. package/src/system-agents/recent-activity-title-refiner.agent.ts +38 -3
  85. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  86. package/src/system-agents/skill-extractor.agent.ts +1 -1
  87. package/src/system-agents/skill-manager.agent.ts +2 -4
  88. package/src/system-agents/title-generator.agent.ts +2 -2
  89. package/src/tools/execution-plan.tool.ts +22 -48
  90. package/src/tools/firecrawl-client.ts +2 -2
  91. package/src/tools/index.ts +12 -0
  92. package/src/tools/log-hello-world.tool.ts +17 -0
  93. package/src/tools/research-topic.tool.ts +1 -1
  94. package/src/tools/team-think.tool.ts +1 -1
  95. package/src/tools/user-questions.tool.ts +2 -2
  96. package/src/utils/index.ts +6 -0
  97. package/src/workers/bootstrap.ts +8 -16
  98. package/src/workers/index.ts +7 -0
  99. package/src/workers/regular-chat-memory-digest.runner.ts +1 -1
  100. package/src/workers/skill-extraction.runner.ts +3 -3
  101. package/src/workers/utils/{repo-indexer-chunker.ts → file-section-chunker.ts} +23 -52
  102. package/src/workers/utils/repo-structure-extractor.ts +2 -5
  103. package/src/workers/utils/repomix-file-sections.ts +42 -0
  104. package/src/config/env-shapes.ts +0 -121
  105. package/src/runtime/agent-contract.ts +0 -1
@@ -1,12 +1,14 @@
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
1
  import {
6
- CONSULT_TEAM_TOOL_NAME,
2
+ baseChatMessageSchema,
7
3
  CONSULT_SPECIALIST_TOOL_NAME,
4
+ CONSULT_TEAM_TOOL_NAME,
8
5
  ConsultSpecialistArgsSchema,
9
- } from '@lota-sdk/shared/schemas/tools'
6
+ dataPartsSchema,
7
+ messageMetadataSchema,
8
+ toTimestamp,
9
+ withMessageCreatedAt,
10
+ } from '@lota-sdk/shared'
11
+ import type { ChatMessage, MessageMetadata } from '@lota-sdk/shared'
10
12
  import { convertToModelMessages, readUIMessageStream, stepCountIs, tool as createTool, validateUIMessages } from 'ai'
11
13
  import type { PrepareStepFunction, StopCondition, ToolSet, UIMessageStreamWriter } from 'ai'
12
14
  import type { z } from 'zod'
@@ -15,10 +17,12 @@ import {
15
17
  agentDisplayNames,
16
18
  buildAgentTools,
17
19
  createAgent,
20
+ getLeadAgentId,
18
21
  getCoreWorkstreamProfile,
19
22
  getAgentRuntimeConfig,
20
23
  pluginRuntime,
21
24
  } from '../config/agent-defaults'
25
+ import { lotaDebugLogger } from '../config/debug-logger'
22
26
  import { aiLogger } from '../config/logger'
23
27
  import type { RecordIdRef } from '../db/record-id'
24
28
  import { recordIdToString } from '../db/record-id'
@@ -93,7 +97,7 @@ interface UIMessageStreamResult {
93
97
  toUIMessageStream(options: Record<string, unknown>): ReadableStream<unknown>
94
98
  }
95
99
 
96
- export function hasUIMessageStream(value: unknown): value is UIMessageStreamResult {
100
+ function hasUIMessageStream(value: unknown): value is UIMessageStreamResult {
97
101
  return (
98
102
  typeof value === 'object' &&
99
103
  value !== null &&
@@ -101,7 +105,7 @@ export function hasUIMessageStream(value: unknown): value is UIMessageStreamResu
101
105
  )
102
106
  }
103
107
 
104
- export function getPluginService(path: string[]): ((...args: unknown[]) => unknown) | undefined {
108
+ function getPluginService(path: string[]): ((...args: unknown[]) => unknown) | undefined {
105
109
  let current: unknown = pluginRuntime
106
110
  let owner: unknown = undefined
107
111
  for (const key of path) {
@@ -118,7 +122,7 @@ export function getPluginService(path: string[]): ((...args: unknown[]) => unkno
118
122
  : (current as (...args: unknown[]) => unknown)
119
123
  }
120
124
 
121
- export async function buildIndexedRepositoriesContext(
125
+ async function buildIndexedRepositoriesContext(
122
126
  workspaceId: string,
123
127
  ): Promise<{ provideRepoTool: boolean; defaultSectionsByAgent: Record<string, unknown>; context: string }> {
124
128
  const buildContext = getRuntimeAdapters().workstream?.buildIndexedRepositoriesContext
@@ -136,11 +140,20 @@ export async function buildIndexedRepositoriesContext(
136
140
 
137
141
  const PRESEEDED_MEMORY_LOOKUP_LIMIT = 3
138
142
 
139
- export function parsePersistedWorkstreamState(value: unknown): WorkstreamState | null {
143
+ function parsePersistedWorkstreamState(value: unknown): WorkstreamState | null {
140
144
  return parseWorkstreamState(value)
141
145
  }
142
146
 
143
- export async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
147
+ function stripExecutionPlanFieldsFromWorkstreamState(
148
+ state: WorkstreamState | null | undefined,
149
+ hasExecutionPlan: boolean,
150
+ ): WorkstreamState | null | undefined {
151
+ if (!state || !hasExecutionPlan) return state
152
+
153
+ return { ...state, currentPlan: null, tasks: [], artifacts: [] }
154
+ }
155
+
156
+ async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
144
157
  return await waitForCompactionIfNeeded({
145
158
  entityId: recordIdToString(workstreamId, TABLES.WORKSTREAM),
146
159
  entityLabel: 'Workstream',
@@ -149,13 +162,13 @@ export async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordId
149
162
  })
150
163
  }
151
164
 
152
- export function parseChatMessageCandidate(value: unknown): ChatMessage | undefined {
165
+ function parseChatMessageCandidate(value: unknown): ChatMessage | undefined {
153
166
  const parsed = baseChatMessageSchema.safeParse(value)
154
167
  if (!parsed.success) return undefined
155
168
  return parsed.data as ChatMessage
156
169
  }
157
170
 
158
- export function getChatMessageFromToolOutput(output: unknown): ChatMessage | undefined {
171
+ function getChatMessageFromToolOutput(output: unknown): ChatMessage | undefined {
159
172
  const directCandidate = parseChatMessageCandidate(output)
160
173
  if (directCandidate) return directCandidate
161
174
 
@@ -173,7 +186,7 @@ export function getChatMessageFromToolOutput(output: unknown): ChatMessage | und
173
186
  return undefined
174
187
  }
175
188
 
176
- export class WorkstreamTurnError extends AppError {
189
+ class WorkstreamTurnError extends AppError {
177
190
  constructor(
178
191
  message: string,
179
192
  readonly statusCode: 400 | 409,
@@ -183,19 +196,19 @@ export class WorkstreamTurnError extends AppError {
183
196
  }
184
197
  }
185
198
 
186
- export function asRecord(value: unknown): Record<string, unknown> | null {
199
+ function asRecord(value: unknown): Record<string, unknown> | null {
187
200
  return value && typeof value === 'object' ? (value as Record<string, unknown>) : null
188
201
  }
189
202
 
190
- export function readOptionalString(value: unknown): string | undefined {
203
+ function readOptionalString(value: unknown): string | undefined {
191
204
  return typeof value === 'string' && value.trim().length > 0 ? value : undefined
192
205
  }
193
206
 
194
- export function readOptionalBoolean(value: unknown): boolean | undefined {
207
+ function readOptionalBoolean(value: unknown): boolean | undefined {
195
208
  return typeof value === 'boolean' ? value : undefined
196
209
  }
197
210
 
198
- export function readInstructionSections(value: unknown): string[] {
211
+ function readInstructionSections(value: unknown): string[] {
199
212
  if (!Array.isArray(value)) {
200
213
  return []
201
214
  }
@@ -206,7 +219,7 @@ export function readInstructionSections(value: unknown): string[] {
206
219
  .filter((section) => section.length > 0)
207
220
  }
208
221
 
209
- export function optionalInstructionSection(value: unknown): string[] | undefined {
222
+ function optionalInstructionSection(value: unknown): string[] | undefined {
210
223
  const section = readOptionalString(value)
211
224
  return section ? [section] : undefined
212
225
  }
@@ -220,6 +233,7 @@ export interface WorkstreamTurnParams {
220
233
  inputMessage: ChatMessage
221
234
  persistInputMessage?: boolean
222
235
  abortSignal?: AbortSignal
236
+ streamId?: string
223
237
  }
224
238
 
225
239
  export interface WorkstreamApprovalContinuationParams {
@@ -230,26 +244,29 @@ export interface WorkstreamApprovalContinuationParams {
230
244
  userName?: string | null
231
245
  approvalMessages: ChatMessage[]
232
246
  abortSignal?: AbortSignal
247
+ streamId?: string
233
248
  }
234
249
 
235
- export type WorkstreamRunCoreParams = {
250
+ type WorkstreamRunCoreParams = {
236
251
  workstream: NormalizedWorkstream
237
252
  workstreamRef: RecordIdRef
238
253
  orgRef: RecordIdRef
239
254
  userRef: RecordIdRef
240
255
  userName?: string | null
241
256
  abortSignal?: AbortSignal
257
+ streamId?: string
242
258
  } & (
243
259
  | { kind: 'userTurn'; inputMessage: ChatMessage; persistInputMessage?: boolean }
244
260
  | { kind: 'approvalContinuation'; approvalMessages: ChatMessage[] }
261
+ | { kind: 'nativeToolApprovalTurn'; approvalMessages: ChatMessage[] }
245
262
  )
246
263
 
247
- export interface PreparedWorkstreamTurn {
264
+ interface PreparedWorkstreamTurn {
248
265
  originalMessages: ChatMessage[]
249
266
  run: (writer?: UIMessageStreamWriter<ChatMessage>) => Promise<void>
250
267
  }
251
268
 
252
- export function upsertChatHistoryMessage(messages: ChatMessage[], nextMessage: ChatMessage): ChatMessage[] {
269
+ function upsertChatHistoryMessage(messages: ChatMessage[], nextMessage: ChatMessage): ChatMessage[] {
253
270
  const existingIndex = messages.findIndex((message) => message.id === nextMessage.id)
254
271
  if (existingIndex === -1) {
255
272
  return [...messages, nextMessage]
@@ -260,22 +277,19 @@ export function upsertChatHistoryMessage(messages: ChatMessage[], nextMessage: C
260
277
  return nextMessages
261
278
  }
262
279
 
263
- export function buildRecentActivityChatDeepLink(params: {
280
+ function buildRecentActivityChatDeepLink(params: {
264
281
  workstream: NormalizedWorkstream
265
282
  workstreamId: string
266
283
  visibleAgentId: string
267
- }): { route: '/chat'; search: { chat?: string; agent?: string; tab: 'cos' | 'team' | 'workstreams' } } {
284
+ }): { route: string; search: Record<string, string> } {
268
285
  if (params.workstream.mode === 'direct') {
269
- return {
270
- route: '/chat',
271
- search: { agent: params.visibleAgentId, tab: params.visibleAgentId === 'chief' ? 'cos' : 'team' },
272
- }
286
+ return { route: 'direct-workstream', search: { workstreamId: params.workstreamId, agentId: params.visibleAgentId } }
273
287
  }
274
288
 
275
- return { route: '/chat', search: { chat: params.workstreamId, tab: 'workstreams' } }
289
+ return { route: 'group-workstream', search: { workstreamId: params.workstreamId } }
276
290
  }
277
291
 
278
- export function buildRecentActivityChatSystemTitle(params: {
292
+ function buildRecentActivityChatSystemTitle(params: {
279
293
  workstream: NormalizedWorkstream
280
294
  visibleAgentId: string
281
295
  }): string {
@@ -307,7 +321,8 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
307
321
 
308
322
  let inputMessage: ChatMessage | undefined
309
323
  const shouldPersistInputMessage = params.kind === 'userTurn' ? params.persistInputMessage !== false : false
310
- const shouldProcessPostRunSideEffects = params.kind === 'approvalContinuation' || shouldPersistInputMessage
324
+ const shouldProcessPostRunSideEffects =
325
+ params.kind === 'approvalContinuation' || params.kind === 'nativeToolApprovalTurn' || shouldPersistInputMessage
311
326
  if (params.kind === 'userTurn') {
312
327
  inputMessage = hydrateMessageFileUrls(withMessageCreatedAt(params.inputMessage))
313
328
  if (inputMessage.role !== 'user') {
@@ -321,12 +336,18 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
321
336
  }
322
337
  }
323
338
 
339
+ const timer = lotaDebugLogger.timer('prepare')
340
+
341
+ // Start workspace fetch early — does not depend on compaction gate
342
+ const workspacePromise = workspaceProvider ? workspaceProvider.getWorkspace(orgRef) : Promise.resolve({})
343
+
324
344
  const workstreamRecord = await waitForWorkstreamCompactionIfNeeded(workstreamRef)
345
+ timer.step('compaction-gate')
325
346
  if (toOptionalTrimmedString(workstreamRecord.activeRunId)) {
326
347
  throw new WorkstreamTurnError('A chat run is already active.', 409)
327
348
  }
328
349
 
329
- if (params.kind === 'approvalContinuation') {
350
+ if (params.kind === 'approvalContinuation' || params.kind === 'nativeToolApprovalTurn') {
330
351
  const approvedAssistantMessage = [...params.approvalMessages]
331
352
  .reverse()
332
353
  .find((m) => m.role === 'assistant' && hasApprovalRespondedParts(m))
@@ -334,9 +355,9 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
334
355
  throw new WorkstreamTurnError('No approval-responded message found.', 400)
335
356
  }
336
357
  await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [approvedAssistantMessage] })
358
+ timer.step('persist-approval-message')
337
359
  }
338
360
 
339
- const workspacePromise = workspaceProvider ? workspaceProvider.getWorkspace(orgRef) : Promise.resolve({})
340
361
  const initialWorkstreamState = parsePersistedWorkstreamState(workstreamRecord.state)
341
362
  const persistedCompactionCursor = toOptionalTrimmedString(workstreamRecord.lastCompactedMessageId) ?? undefined
342
363
  const persistedLiveHistoryPromise = workstreamMessageService.listMessagesAfterCursor(
@@ -361,10 +382,13 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
361
382
  persistedLiveHistoryPromise,
362
383
  persistedRecentHistoryPromise,
363
384
  ])
385
+ timer.step('fetch-workspace+history')
364
386
  const workspaceLifecycleState = workspaceProvider ? await workspaceProvider.getLifecycleState?.(workspace) : undefined
387
+ timer.step('workspace-lifecycle-state')
365
388
  const workspaceProfileState = workspaceProvider
366
389
  ? await workspaceProvider.readProfileProjectionState?.(workspace)
367
390
  : undefined
391
+ timer.step('workspace-profile-state')
368
392
  const [liveHistory, recentHistory] = await Promise.all([
369
393
  persistedLiveHistory.length === 0
370
394
  ? Promise.resolve([] as ChatMessage[])
@@ -381,9 +405,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
381
405
  dataSchemas: dataPartsSchema,
382
406
  }).then((messages) => messages.map(hydrateMessageFileUrls)),
383
407
  ])
408
+ timer.step('validate+hydrate-history')
384
409
 
385
410
  if (userMessage && shouldPersistInputMessage) {
386
411
  await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [userMessage] })
412
+ timer.step('persist-user-message')
387
413
  }
388
414
 
389
415
  const originalMessages = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
@@ -419,8 +445,9 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
419
445
  skills?: string[]
420
446
  })
421
447
  : null
448
+ const defaultLeadAgentId = getLeadAgentId()
422
449
  const visibleWorkstreamAgentId =
423
- workstream.mode === 'direct' ? workstream.agentId : (coreWorkstreamProfile?.config.agentId ?? 'chief')
450
+ workstream.mode === 'direct' ? workstream.agentId : (coreWorkstreamProfile?.config.agentId ?? defaultLeadAgentId)
424
451
  const coreInstructionSections = coreWorkstreamProfile ? [coreWorkstreamProfile.instructions] : undefined
425
452
  const getLinearInstallationByOrgId = getPluginService([
426
453
  'linear',
@@ -442,6 +469,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
442
469
  forceDeep: highImpactAssessment.classes.length > 0 || policyAssessment.classes.length > 0,
443
470
  explicitProfile: onboardingActive ? 'standard' : undefined,
444
471
  })
472
+ timer.step('reasoning-classification')
445
473
 
446
474
  const [linearInstallation, githubInstallation, indexedRepoContext, recentDomainEvents, promptSummary] =
447
475
  await Promise.all([
@@ -455,6 +483,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
455
483
  ? workspaceProvider.buildPromptSummary(orgRef).catch(() => undefined)
456
484
  : Promise.resolve(undefined),
457
485
  ])
486
+ timer.step('parallel-context-fetch(plugins+repos+events+summary)')
458
487
  let linearInstalled = Boolean(linearInstallation)
459
488
  let githubInstalled = Boolean(githubInstallation)
460
489
  let promptContext = buildAgentPromptContext({
@@ -473,6 +502,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
473
502
  userId: userIdString,
474
503
  query: messageText,
475
504
  })
505
+ timer.step('rag-knowledge-retrieval')
476
506
  const buildContextResult = asRecord(
477
507
  await turnHooks.buildContext?.({
478
508
  workstream,
@@ -494,6 +524,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
494
524
  retrievedKnowledgeSection,
495
525
  }),
496
526
  )
527
+ timer.step('hook:buildContext')
497
528
  const buildContextPromptDetails = readOptionalString(buildContextResult?.systemWorkspaceDetails)
498
529
  if (buildContextPromptDetails) {
499
530
  promptContext = { systemWorkspaceDetails: buildContextPromptDetails }
@@ -532,6 +563,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
532
563
  context: buildContextResult,
533
564
  }),
534
565
  )
566
+ timer.step('hook:buildExtraInstructionSections')
535
567
 
536
568
  let memoryBlock = workstreamService.formatMemoryBlockForPrompt(workstreamRecord)
537
569
  let workstreamState = initialWorkstreamState
@@ -539,11 +571,39 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
539
571
  disabled: onboardingActive,
540
572
  loadPlan: async () => await executionPlanService.getActivePlanForWorkstream(workstreamRef),
541
573
  })
574
+ const getExecutionPlan = async () => await executionPlanInstructionSectionCache.getPlan()
542
575
  const getExecutionPlanInstructionSections = async (): Promise<string[] | undefined> =>
543
576
  await executionPlanInstructionSectionCache.getSections()
544
577
  const invalidateExecutionPlanInstructionSections = () => {
545
578
  executionPlanInstructionSectionCache.invalidate()
546
579
  }
580
+ const getWorkstreamStateSection = async (): Promise<string | undefined> => {
581
+ const executionPlan = await getExecutionPlan()
582
+ return contextCompactionRuntime.formatWorkstreamStateForPrompt(
583
+ stripExecutionPlanFieldsFromWorkstreamState(workstreamState, Boolean(executionPlan)),
584
+ )
585
+ }
586
+ const respondedBy = recordIdToString(userRef, TABLES.USER)
587
+ if (params.kind === 'approvalContinuation') {
588
+ const appliedApproval = await executionPlanService.applyApprovalResponseFromMessages({
589
+ workstreamId: workstreamRef,
590
+ approvalMessages: params.approvalMessages,
591
+ respondedBy,
592
+ })
593
+ if (appliedApproval) {
594
+ invalidateExecutionPlanInstructionSections()
595
+ }
596
+ } else if (userMessage) {
597
+ const appliedHumanInput = await executionPlanService.applyHumanInputFromUserMessage({
598
+ workstreamId: workstreamRef,
599
+ message: userMessage,
600
+ respondedBy,
601
+ })
602
+ if (appliedHumanInput) {
603
+ invalidateExecutionPlanInstructionSections()
604
+ }
605
+ }
606
+ timer.step('execution-plan-input')
547
607
  const preSeededMemoriesByAgent = new Map<string, string | undefined>()
548
608
  const getPreSeededMemoriesSection = async (agentId: string): Promise<string | undefined> => {
549
609
  if (preSeededMemoriesByAgent.has(agentId)) {
@@ -594,14 +654,21 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
594
654
  uploadMetadataText: buildReadableUploadMetadataText(listReadableUploads(extraMessages)),
595
655
  })
596
656
 
657
+ timer.step('preparation-complete')
658
+
597
659
  return {
598
660
  originalMessages,
599
661
  run: async (writer?: UIMessageStreamWriter<ChatMessage>) => {
600
662
  const executeRun = async () => {
663
+ const runTimer = lotaDebugLogger.timer('run')
601
664
  const serverRunId = Bun.randomUUIDv7()
602
665
  const runAbort = createServerRunAbortController(params.abortSignal)
603
- await workstreamService.setActiveRunId(workstreamRef, serverRunId)
666
+ await Promise.all([
667
+ workstreamService.setActiveRunId(workstreamRef, serverRunId),
668
+ params.streamId ? workstreamService.setActiveStreamId(workstreamRef, params.streamId) : undefined,
669
+ ])
604
670
  chatRunRegistry.register(serverRunId, runAbort.controller)
671
+ runTimer.step('set-active-run+stream')
605
672
 
606
673
  try {
607
674
  const buildAgentMetadataPatch = (agentId: string, agentName: string): NonNullable<MessageMetadata> => ({
@@ -660,7 +727,9 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
660
727
  prepareStep?: PrepareStepFunction<ToolSet>
661
728
  abortSignal?: AbortSignal
662
729
  }): Promise<ChatMessage> => {
730
+ const agentTimer = lotaDebugLogger.timer(`agent:${streamParams.agentId}`)
663
731
  const executionPlanInstructionSections = await getExecutionPlanInstructionSections()
732
+ agentTimer.step('get-execution-plan')
664
733
  const agentResolution = asRecord(
665
734
  await turnHooks.resolveAgent?.({
666
735
  agentId: streamParams.agentId,
@@ -679,7 +748,14 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
679
748
  context: buildContextResult,
680
749
  }),
681
750
  )
751
+ agentTimer.step('hook:resolveAgent')
682
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)')
683
759
  const config = getAgentRuntimeConfig({
684
760
  agentId: resolvedAgentId,
685
761
  workstreamMode: workstream.mode,
@@ -689,11 +765,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
689
765
  linearInstalled,
690
766
  reasoningProfile: reasoningProfile.name,
691
767
  systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
692
- preSeededMemoriesSection: await getPreSeededMemoriesSection(resolvedAgentId),
768
+ preSeededMemoriesSection,
693
769
  retrievedKnowledgeSection,
694
770
  workstreamMemoryBlock: memoryBlock,
695
- workstreamStateSection: contextCompactionRuntime.formatWorkstreamStateForPrompt(workstreamState),
696
- learnedSkillsSection: await getLearnedSkillsSection(resolvedAgentId),
771
+ workstreamStateSection,
772
+ learnedSkillsSection,
697
773
  additionalInstructionSections: mergeInstructionSections(
698
774
  executionPlanInstructionSections,
699
775
  streamParams.additionalInstructionSections,
@@ -703,9 +779,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
703
779
  ),
704
780
  context: buildContextResult,
705
781
  }) as AgentRuntimeConfig
782
+ agentTimer.step('build-agent-config')
706
783
  const modelMessages = await convertToModelMessages(streamParams.messages, {
707
784
  ignoreIncompleteToolCalls: true,
708
785
  })
786
+ agentTimer.step('convert-model-messages')
709
787
  const agent = (createAgent as unknown as AgentFactory)[config.id as string]({
710
788
  mode: streamParams.mode,
711
789
  tools: streamParams.tools,
@@ -719,12 +797,14 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
719
797
  (agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
720
798
  })
721
799
  const agentAbortSignal = streamParams.abortSignal ?? runAbort.signal
800
+ agentTimer.step('agent-construction')
722
801
 
723
802
  let result: unknown
724
803
  try {
725
804
  result = await streamParams.observer.run(() =>
726
805
  agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal }),
727
806
  )
807
+ agentTimer.step('agent.stream()-resolved')
728
808
  } catch (error) {
729
809
  if (agentAbortSignal.aborted) {
730
810
  streamParams.observer.recordAbort(error)
@@ -758,10 +838,15 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
758
838
  },
759
839
  }) as ReadableStream<ChatStreamChunk>
760
840
  const reader = uiStream.getReader()
841
+ let firstChunkLogged = false
761
842
  try {
762
843
  for (;;) {
763
844
  const { done, value } = await reader.read()
764
845
  if (done) break
846
+ if (!firstChunkLogged) {
847
+ agentTimer.step('first-stream-chunk')
848
+ firstChunkLogged = true
849
+ }
765
850
  if (streamParams.writer) {
766
851
  streamParams.writer.write(value)
767
852
  }
@@ -769,6 +854,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
769
854
  } finally {
770
855
  reader.releaseLock()
771
856
  }
857
+ agentTimer.step('stream-complete')
772
858
 
773
859
  const finalizedResponseMessage = await finishedStream.then(() => responseMessage)
774
860
  if (finalizedResponseMessage === null) {
@@ -790,6 +876,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
790
876
  extraMessages?: ChatMessage[]
791
877
  extraTools?: ToolSet
792
878
  }): Promise<ChatMessage> => {
879
+ const visibleTimer = lotaDebugLogger.timer(`visible:${runParams.agentId}`)
793
880
  let runMemoryBlock = memoryBlock
794
881
  const tools: ToolSet = {
795
882
  ...((await buildAgentTools({
@@ -819,6 +906,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
819
906
  ...toolProviders,
820
907
  ...runParams.extraTools,
821
908
  }
909
+ visibleTimer.step('build-agent-tools')
822
910
  const responseMessage = await streamAgentResponse({
823
911
  agentId: runParams.agentId,
824
912
  mode: runParams.mode,
@@ -830,6 +918,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
830
918
  writer,
831
919
  })
832
920
 
921
+ visibleTimer.step('stream-agent-response')
833
922
  memoryBlock = runMemoryBlock
834
923
 
835
924
  return await commitAssistantResponse(
@@ -839,6 +928,12 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
839
928
  )
840
929
  }
841
930
 
931
+ // Execution-plan approval continuations mutate plan state and persist the approval message,
932
+ // but they do not begin a new visible agent turn.
933
+ if (params.kind === 'approvalContinuation') {
934
+ return
935
+ }
936
+
842
937
  const consultSpecialistTool = createTool({
843
938
  description: 'Consult one specialist teammate for domain-specific guidance before replying to the user.',
844
939
  inputSchema: ConsultSpecialistArgsSchema,
@@ -871,7 +966,17 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
871
966
  context: buildContextResult,
872
967
  })
873
968
 
874
- const specialistExecutionPlanInstructionSections = await getExecutionPlanInstructionSections()
969
+ const [
970
+ specialistExecutionPlanInstructionSections,
971
+ specialistPreSeededMemories,
972
+ specialistWorkstreamState,
973
+ specialistLearnedSkills,
974
+ ] = await Promise.all([
975
+ getExecutionPlanInstructionSections(),
976
+ getPreSeededMemoriesSection(agentId),
977
+ getWorkstreamStateSection(),
978
+ getLearnedSkillsSection(agentId),
979
+ ])
875
980
  const specialistConfig = getAgentRuntimeConfig({
876
981
  agentId,
877
982
  workstreamMode: workstream.mode,
@@ -880,11 +985,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
880
985
  linearInstalled,
881
986
  reasoningProfile: reasoningProfile.name,
882
987
  systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
883
- preSeededMemoriesSection: await getPreSeededMemoriesSection(agentId),
988
+ preSeededMemoriesSection: specialistPreSeededMemories,
884
989
  retrievedKnowledgeSection,
885
990
  workstreamMemoryBlock: specialistMemoryBlock,
886
- workstreamStateSection: contextCompactionRuntime.formatWorkstreamStateForPrompt(workstreamState),
887
- learnedSkillsSection: await getLearnedSkillsSection(agentId),
991
+ workstreamStateSection: specialistWorkstreamState,
992
+ learnedSkillsSection: specialistLearnedSkills,
888
993
  additionalInstructionSections: mergeInstructionSections(
889
994
  specialistExecutionPlanInstructionSections,
890
995
  coreInstructionSections,
@@ -995,20 +1100,9 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
995
1100
  throw new WorkstreamTurnError('Direct workstreams require an assigned agent.', 400)
996
1101
  }
997
1102
  await runVisibleAgent({ agentId: workstream.agentId, mode: 'direct' })
998
- } else if (params.kind === 'userTurn') {
999
- await runVisibleAgent({
1000
- agentId: visibleWorkstreamAgentId ?? 'chief',
1001
- mode: 'workstreamMode',
1002
- skills: coreWorkstreamProfile?.skills ? [...coreWorkstreamProfile.skills] : undefined,
1003
- additionalInstructionSections: mergeInstructionSections(coreInstructionSections, hookInstructionSections),
1004
- extraTools: {
1005
- [CONSULT_SPECIALIST_TOOL_NAME]: consultSpecialistTool,
1006
- ...(teamThinkTool ? { [CONSULT_TEAM_TOOL_NAME]: teamThinkTool } : {}),
1007
- },
1008
- })
1009
1103
  } else {
1010
1104
  await runVisibleAgent({
1011
- agentId: visibleWorkstreamAgentId ?? 'chief',
1105
+ agentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
1012
1106
  mode: 'workstreamMode',
1013
1107
  skills: coreWorkstreamProfile?.skills ? [...coreWorkstreamProfile.skills] : undefined,
1014
1108
  additionalInstructionSections: mergeInstructionSections(coreInstructionSections, hookInstructionSections),
@@ -1043,6 +1137,9 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1043
1137
  unregisterRun: (runId) => chatRunRegistry.unregister(runId),
1044
1138
  clearActiveRunId: (runId) => workstreamService.clearActiveRunIdIfMatches(workstreamRef, runId),
1045
1139
  disposeAbort: () => runAbort.dispose(),
1140
+ activeStreamId: params.streamId,
1141
+ clearActiveStreamId: (streamId) =>
1142
+ workstreamService.clearActiveStreamIdIfMatches(workstreamRef, streamId),
1046
1143
  })
1047
1144
 
1048
1145
  if (allAssistantMessages.length > 0 && shouldProcessPostRunSideEffects) {
@@ -1103,17 +1200,17 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1103
1200
  mergeKey: `workstream:${workstreamIdString}`,
1104
1201
  title: buildRecentActivityChatSystemTitle({
1105
1202
  workstream,
1106
- visibleAgentId: visibleWorkstreamAgentId ?? 'chief',
1203
+ visibleAgentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
1107
1204
  }),
1108
- sourceLabel: agentDisplayNames[visibleWorkstreamAgentId ?? 'chief'],
1205
+ sourceLabel: agentDisplayNames[visibleWorkstreamAgentId ?? defaultLeadAgentId],
1109
1206
  deepLink: buildRecentActivityChatDeepLink({
1110
1207
  workstream,
1111
1208
  workstreamId: workstreamIdString,
1112
- visibleAgentId: visibleWorkstreamAgentId ?? 'chief',
1209
+ visibleAgentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
1113
1210
  }),
1114
1211
  metadata: {
1115
- agentId: visibleWorkstreamAgentId ?? 'chief',
1116
- agentName: agentDisplayNames[visibleWorkstreamAgentId ?? 'chief'],
1212
+ agentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
1213
+ agentName: agentDisplayNames[visibleWorkstreamAgentId ?? defaultLeadAgentId],
1117
1214
  workstreamId: workstreamIdString,
1118
1215
  workstreamTitle: latestWorkstreamRecord.title ?? workstream.title,
1119
1216
  workstreamMode: workstream.mode,
@@ -1,6 +1,7 @@
1
- import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
1
+ import type { ChatMessage } from '@lota-sdk/shared'
2
2
  import { createUIMessageStream } from 'ai'
3
3
 
4
+ import { lotaDebugLogger } from '../config/debug-logger'
4
5
  import { hasApprovalRespondedParts, isApprovalContinuationRequest } from '../runtime/approval-continuation'
5
6
  import { wrapResponseWithKeepalive } from '../utils/sse-keepalive'
6
7
  import { prepareWorkstreamRunCore } from './workstream-turn-preparation'
@@ -10,30 +11,54 @@ export { hasApprovalRespondedParts, isApprovalContinuationRequest }
10
11
  export { wrapResponseWithKeepalive }
11
12
 
12
13
  export async function createWorkstreamApprovalContinuationStream(params: WorkstreamApprovalContinuationParams) {
14
+ const timer = lotaDebugLogger.timer('turn:approval-continuation')
13
15
  const prepared = await prepareWorkstreamRunCore({ ...params, kind: 'approvalContinuation' })
16
+ timer.step('prepare')
14
17
 
15
18
  return createUIMessageStream<ChatMessage>({
16
19
  originalMessages: prepared.originalMessages,
17
20
  onError: (error) => (error instanceof Error ? error.message : 'Approval continuation stream failed.'),
18
21
  execute: async ({ writer }) => {
19
22
  await prepared.run(writer)
23
+ timer.step('run')
24
+ },
25
+ })
26
+ }
27
+
28
+ export async function createWorkstreamNativeToolApprovalStream(params: WorkstreamApprovalContinuationParams) {
29
+ const timer = lotaDebugLogger.timer('turn:native-tool-approval')
30
+ const prepared = await prepareWorkstreamRunCore({ ...params, kind: 'nativeToolApprovalTurn' })
31
+ timer.step('prepare')
32
+
33
+ return createUIMessageStream<ChatMessage>({
34
+ originalMessages: prepared.originalMessages,
35
+ onError: (error) => (error instanceof Error ? error.message : 'Native tool approval stream failed.'),
36
+ execute: async ({ writer }) => {
37
+ await prepared.run(writer)
38
+ timer.step('run')
20
39
  },
21
40
  })
22
41
  }
23
42
 
24
43
  export async function createWorkstreamTurnStream(params: WorkstreamTurnParams) {
44
+ const timer = lotaDebugLogger.timer('turn:user-turn')
25
45
  const prepared = await prepareWorkstreamRunCore({ ...params, kind: 'userTurn' })
46
+ timer.step('prepare')
26
47
 
27
48
  return createUIMessageStream<ChatMessage>({
28
49
  originalMessages: prepared.originalMessages,
29
50
  onError: (error) => (error instanceof Error ? error.message : 'Chat stream failed.'),
30
51
  execute: async ({ writer }) => {
31
52
  await prepared.run(writer)
53
+ timer.step('run')
32
54
  },
33
55
  })
34
56
  }
35
57
 
36
58
  export async function runWorkstreamTurnInBackground(params: WorkstreamTurnParams): Promise<void> {
59
+ const timer = lotaDebugLogger.timer('turn:background')
37
60
  const prepared = await prepareWorkstreamRunCore({ ...params, kind: 'userTurn' })
61
+ timer.step('prepare')
38
62
  await prepared.run()
63
+ timer.step('run')
39
64
  }