@lota-sdk/core 0.1.11 → 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 (95) hide show
  1. package/package.json +2 -87
  2. package/src/ai/index.ts +3 -0
  3. package/src/bifrost/index.ts +1 -0
  4. package/src/config/agent-defaults.ts +30 -7
  5. package/src/config/constants.ts +0 -9
  6. package/src/config/debug-logger.ts +43 -0
  7. package/src/config/index.ts +5 -0
  8. package/src/config/model-constants.ts +0 -3
  9. package/src/config/workstream-defaults.ts +4 -0
  10. package/src/db/cursor-pagination.ts +2 -2
  11. package/src/db/index.ts +10 -0
  12. package/src/db/memory.ts +9 -15
  13. package/src/document/index.ts +2 -0
  14. package/src/document/parsing.ts +0 -25
  15. package/src/embeddings/provider.ts +17 -8
  16. package/src/index.ts +15 -505
  17. package/src/queues/index.ts +10 -0
  18. package/src/redis/connection-accessor.ts +26 -0
  19. package/src/redis/connection.ts +1 -1
  20. package/src/redis/index.ts +9 -25
  21. package/src/redis/org-memory-lock.ts +1 -1
  22. package/src/redis/redis-lease-lock.ts +1 -1
  23. package/src/redis/stream-context.ts +12 -2
  24. package/src/runtime/agent-runtime-policy.ts +9 -5
  25. package/src/runtime/agent-stream-helpers.ts +6 -3
  26. package/src/runtime/agent-types.ts +1 -5
  27. package/src/runtime/approval-continuation.ts +9 -1
  28. package/src/runtime/chat-attachments.ts +1 -1
  29. package/src/runtime/chat-request-routing.ts +1 -1
  30. package/src/runtime/context-compaction-runtime.ts +2 -2
  31. package/src/runtime/context-compaction.ts +1 -1
  32. package/src/runtime/execution-plan.ts +1 -1
  33. package/src/runtime/index.ts +26 -0
  34. package/src/runtime/indexed-repositories-policy.ts +10 -10
  35. package/src/runtime/memory-pipeline.ts +0 -2
  36. package/src/runtime/runtime-config.ts +238 -0
  37. package/src/runtime/runtime-extensions.ts +3 -2
  38. package/src/runtime/runtime-worker-registry.ts +47 -0
  39. package/src/runtime/team-consultation-orchestrator.ts +9 -6
  40. package/src/runtime/team-consultation-prompts.ts +3 -2
  41. package/src/runtime/turn-lifecycle.ts +1 -1
  42. package/src/runtime/workstream-chat-helpers.ts +0 -54
  43. package/src/runtime/workstream-routing-policy.ts +3 -7
  44. package/src/runtime.ts +387 -0
  45. package/src/services/chat-attachments.service.ts +1 -1
  46. package/src/services/context-compaction.service.ts +1 -1
  47. package/src/services/execution-plan.service.ts +14 -16
  48. package/src/services/index.ts +14 -0
  49. package/src/services/learned-skill.service.ts +80 -37
  50. package/src/services/memory.service.ts +5 -4
  51. package/src/services/mutating-approval.service.ts +1 -1
  52. package/src/services/organization-member.service.ts +1 -1
  53. package/src/services/organization.service.ts +1 -1
  54. package/src/services/plan-approval.service.ts +2 -2
  55. package/src/services/plan-artifact.service.ts +2 -3
  56. package/src/services/plan-builder.service.ts +1 -1
  57. package/src/services/plan-checkpoint.service.ts +2 -2
  58. package/src/services/plan-compiler.service.ts +2 -2
  59. package/src/services/plan-executor.service.ts +10 -9
  60. package/src/services/plan-run.service.ts +2 -2
  61. package/src/services/plan-validator.service.ts +4 -4
  62. package/src/services/recent-activity-title.service.ts +1 -1
  63. package/src/services/recent-activity.service.ts +14 -16
  64. package/src/services/user.service.ts +2 -2
  65. package/src/services/workstream-message.service.ts +2 -3
  66. package/src/services/workstream-title.service.ts +1 -1
  67. package/src/services/workstream-turn-preparation.ts +105 -50
  68. package/src/services/workstream-turn.ts +14 -1
  69. package/src/services/workstream.service.ts +9 -9
  70. package/src/storage/attachment-parser.ts +1 -1
  71. package/src/storage/attachment-storage.service.ts +11 -10
  72. package/src/storage/generated-document-storage.service.ts +7 -6
  73. package/src/storage/index.ts +10 -0
  74. package/src/system-agents/delegated-agent-factory.ts +78 -29
  75. package/src/system-agents/index.ts +4 -0
  76. package/src/system-agents/recent-activity-title-refiner.agent.ts +38 -3
  77. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  78. package/src/system-agents/skill-extractor.agent.ts +1 -1
  79. package/src/system-agents/skill-manager.agent.ts +2 -4
  80. package/src/tools/execution-plan.tool.ts +2 -2
  81. package/src/tools/firecrawl-client.ts +2 -2
  82. package/src/tools/index.ts +12 -0
  83. package/src/tools/research-topic.tool.ts +1 -1
  84. package/src/tools/team-think.tool.ts +1 -1
  85. package/src/tools/user-questions.tool.ts +2 -2
  86. package/src/utils/index.ts +6 -0
  87. package/src/workers/bootstrap.ts +8 -16
  88. package/src/workers/index.ts +7 -0
  89. package/src/workers/regular-chat-memory-digest.runner.ts +1 -1
  90. package/src/workers/skill-extraction.runner.ts +1 -1
  91. package/src/workers/utils/{repo-indexer-chunker.ts → file-section-chunker.ts} +23 -52
  92. package/src/workers/utils/repo-structure-extractor.ts +2 -5
  93. package/src/workers/utils/repomix-file-sections.ts +42 -0
  94. package/src/config/env-shapes.ts +0 -121
  95. 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,11 @@ 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 function stripExecutionPlanFieldsFromWorkstreamState(
147
+ function stripExecutionPlanFieldsFromWorkstreamState(
144
148
  state: WorkstreamState | null | undefined,
145
149
  hasExecutionPlan: boolean,
146
150
  ): WorkstreamState | null | undefined {
@@ -149,7 +153,7 @@ export function stripExecutionPlanFieldsFromWorkstreamState(
149
153
  return { ...state, currentPlan: null, tasks: [], artifacts: [] }
150
154
  }
151
155
 
152
- export async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
156
+ async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
153
157
  return await waitForCompactionIfNeeded({
154
158
  entityId: recordIdToString(workstreamId, TABLES.WORKSTREAM),
155
159
  entityLabel: 'Workstream',
@@ -158,13 +162,13 @@ export async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordId
158
162
  })
159
163
  }
160
164
 
161
- export function parseChatMessageCandidate(value: unknown): ChatMessage | undefined {
165
+ function parseChatMessageCandidate(value: unknown): ChatMessage | undefined {
162
166
  const parsed = baseChatMessageSchema.safeParse(value)
163
167
  if (!parsed.success) return undefined
164
168
  return parsed.data as ChatMessage
165
169
  }
166
170
 
167
- export function getChatMessageFromToolOutput(output: unknown): ChatMessage | undefined {
171
+ function getChatMessageFromToolOutput(output: unknown): ChatMessage | undefined {
168
172
  const directCandidate = parseChatMessageCandidate(output)
169
173
  if (directCandidate) return directCandidate
170
174
 
@@ -182,7 +186,7 @@ export function getChatMessageFromToolOutput(output: unknown): ChatMessage | und
182
186
  return undefined
183
187
  }
184
188
 
185
- export class WorkstreamTurnError extends AppError {
189
+ class WorkstreamTurnError extends AppError {
186
190
  constructor(
187
191
  message: string,
188
192
  readonly statusCode: 400 | 409,
@@ -192,19 +196,19 @@ export class WorkstreamTurnError extends AppError {
192
196
  }
193
197
  }
194
198
 
195
- export function asRecord(value: unknown): Record<string, unknown> | null {
199
+ function asRecord(value: unknown): Record<string, unknown> | null {
196
200
  return value && typeof value === 'object' ? (value as Record<string, unknown>) : null
197
201
  }
198
202
 
199
- export function readOptionalString(value: unknown): string | undefined {
203
+ function readOptionalString(value: unknown): string | undefined {
200
204
  return typeof value === 'string' && value.trim().length > 0 ? value : undefined
201
205
  }
202
206
 
203
- export function readOptionalBoolean(value: unknown): boolean | undefined {
207
+ function readOptionalBoolean(value: unknown): boolean | undefined {
204
208
  return typeof value === 'boolean' ? value : undefined
205
209
  }
206
210
 
207
- export function readInstructionSections(value: unknown): string[] {
211
+ function readInstructionSections(value: unknown): string[] {
208
212
  if (!Array.isArray(value)) {
209
213
  return []
210
214
  }
@@ -215,7 +219,7 @@ export function readInstructionSections(value: unknown): string[] {
215
219
  .filter((section) => section.length > 0)
216
220
  }
217
221
 
218
- export function optionalInstructionSection(value: unknown): string[] | undefined {
222
+ function optionalInstructionSection(value: unknown): string[] | undefined {
219
223
  const section = readOptionalString(value)
220
224
  return section ? [section] : undefined
221
225
  }
@@ -243,7 +247,7 @@ export interface WorkstreamApprovalContinuationParams {
243
247
  streamId?: string
244
248
  }
245
249
 
246
- export type WorkstreamRunCoreParams = {
250
+ type WorkstreamRunCoreParams = {
247
251
  workstream: NormalizedWorkstream
248
252
  workstreamRef: RecordIdRef
249
253
  orgRef: RecordIdRef
@@ -257,12 +261,12 @@ export type WorkstreamRunCoreParams = {
257
261
  | { kind: 'nativeToolApprovalTurn'; approvalMessages: ChatMessage[] }
258
262
  )
259
263
 
260
- export interface PreparedWorkstreamTurn {
264
+ interface PreparedWorkstreamTurn {
261
265
  originalMessages: ChatMessage[]
262
266
  run: (writer?: UIMessageStreamWriter<ChatMessage>) => Promise<void>
263
267
  }
264
268
 
265
- export function upsertChatHistoryMessage(messages: ChatMessage[], nextMessage: ChatMessage): ChatMessage[] {
269
+ function upsertChatHistoryMessage(messages: ChatMessage[], nextMessage: ChatMessage): ChatMessage[] {
266
270
  const existingIndex = messages.findIndex((message) => message.id === nextMessage.id)
267
271
  if (existingIndex === -1) {
268
272
  return [...messages, nextMessage]
@@ -273,22 +277,19 @@ export function upsertChatHistoryMessage(messages: ChatMessage[], nextMessage: C
273
277
  return nextMessages
274
278
  }
275
279
 
276
- export function buildRecentActivityChatDeepLink(params: {
280
+ function buildRecentActivityChatDeepLink(params: {
277
281
  workstream: NormalizedWorkstream
278
282
  workstreamId: string
279
283
  visibleAgentId: string
280
- }): { route: '/chat'; search: { chat?: string; agent?: string; tab: 'cos' | 'team' | 'workstreams' } } {
284
+ }): { route: string; search: Record<string, string> } {
281
285
  if (params.workstream.mode === 'direct') {
282
- return {
283
- route: '/chat',
284
- search: { agent: params.visibleAgentId, tab: params.visibleAgentId === 'chief' ? 'cos' : 'team' },
285
- }
286
+ return { route: 'direct-workstream', search: { workstreamId: params.workstreamId, agentId: params.visibleAgentId } }
286
287
  }
287
288
 
288
- return { route: '/chat', search: { chat: params.workstreamId, tab: 'workstreams' } }
289
+ return { route: 'group-workstream', search: { workstreamId: params.workstreamId } }
289
290
  }
290
291
 
291
- export function buildRecentActivityChatSystemTitle(params: {
292
+ function buildRecentActivityChatSystemTitle(params: {
292
293
  workstream: NormalizedWorkstream
293
294
  visibleAgentId: string
294
295
  }): string {
@@ -335,7 +336,13 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
335
336
  }
336
337
  }
337
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
+
338
344
  const workstreamRecord = await waitForWorkstreamCompactionIfNeeded(workstreamRef)
345
+ timer.step('compaction-gate')
339
346
  if (toOptionalTrimmedString(workstreamRecord.activeRunId)) {
340
347
  throw new WorkstreamTurnError('A chat run is already active.', 409)
341
348
  }
@@ -348,9 +355,9 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
348
355
  throw new WorkstreamTurnError('No approval-responded message found.', 400)
349
356
  }
350
357
  await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [approvedAssistantMessage] })
358
+ timer.step('persist-approval-message')
351
359
  }
352
360
 
353
- const workspacePromise = workspaceProvider ? workspaceProvider.getWorkspace(orgRef) : Promise.resolve({})
354
361
  const initialWorkstreamState = parsePersistedWorkstreamState(workstreamRecord.state)
355
362
  const persistedCompactionCursor = toOptionalTrimmedString(workstreamRecord.lastCompactedMessageId) ?? undefined
356
363
  const persistedLiveHistoryPromise = workstreamMessageService.listMessagesAfterCursor(
@@ -375,10 +382,13 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
375
382
  persistedLiveHistoryPromise,
376
383
  persistedRecentHistoryPromise,
377
384
  ])
385
+ timer.step('fetch-workspace+history')
378
386
  const workspaceLifecycleState = workspaceProvider ? await workspaceProvider.getLifecycleState?.(workspace) : undefined
387
+ timer.step('workspace-lifecycle-state')
379
388
  const workspaceProfileState = workspaceProvider
380
389
  ? await workspaceProvider.readProfileProjectionState?.(workspace)
381
390
  : undefined
391
+ timer.step('workspace-profile-state')
382
392
  const [liveHistory, recentHistory] = await Promise.all([
383
393
  persistedLiveHistory.length === 0
384
394
  ? Promise.resolve([] as ChatMessage[])
@@ -395,9 +405,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
395
405
  dataSchemas: dataPartsSchema,
396
406
  }).then((messages) => messages.map(hydrateMessageFileUrls)),
397
407
  ])
408
+ timer.step('validate+hydrate-history')
398
409
 
399
410
  if (userMessage && shouldPersistInputMessage) {
400
411
  await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [userMessage] })
412
+ timer.step('persist-user-message')
401
413
  }
402
414
 
403
415
  const originalMessages = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
@@ -433,8 +445,9 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
433
445
  skills?: string[]
434
446
  })
435
447
  : null
448
+ const defaultLeadAgentId = getLeadAgentId()
436
449
  const visibleWorkstreamAgentId =
437
- workstream.mode === 'direct' ? workstream.agentId : (coreWorkstreamProfile?.config.agentId ?? 'chief')
450
+ workstream.mode === 'direct' ? workstream.agentId : (coreWorkstreamProfile?.config.agentId ?? defaultLeadAgentId)
438
451
  const coreInstructionSections = coreWorkstreamProfile ? [coreWorkstreamProfile.instructions] : undefined
439
452
  const getLinearInstallationByOrgId = getPluginService([
440
453
  'linear',
@@ -456,6 +469,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
456
469
  forceDeep: highImpactAssessment.classes.length > 0 || policyAssessment.classes.length > 0,
457
470
  explicitProfile: onboardingActive ? 'standard' : undefined,
458
471
  })
472
+ timer.step('reasoning-classification')
459
473
 
460
474
  const [linearInstallation, githubInstallation, indexedRepoContext, recentDomainEvents, promptSummary] =
461
475
  await Promise.all([
@@ -469,6 +483,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
469
483
  ? workspaceProvider.buildPromptSummary(orgRef).catch(() => undefined)
470
484
  : Promise.resolve(undefined),
471
485
  ])
486
+ timer.step('parallel-context-fetch(plugins+repos+events+summary)')
472
487
  let linearInstalled = Boolean(linearInstallation)
473
488
  let githubInstalled = Boolean(githubInstallation)
474
489
  let promptContext = buildAgentPromptContext({
@@ -487,6 +502,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
487
502
  userId: userIdString,
488
503
  query: messageText,
489
504
  })
505
+ timer.step('rag-knowledge-retrieval')
490
506
  const buildContextResult = asRecord(
491
507
  await turnHooks.buildContext?.({
492
508
  workstream,
@@ -508,6 +524,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
508
524
  retrievedKnowledgeSection,
509
525
  }),
510
526
  )
527
+ timer.step('hook:buildContext')
511
528
  const buildContextPromptDetails = readOptionalString(buildContextResult?.systemWorkspaceDetails)
512
529
  if (buildContextPromptDetails) {
513
530
  promptContext = { systemWorkspaceDetails: buildContextPromptDetails }
@@ -546,6 +563,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
546
563
  context: buildContextResult,
547
564
  }),
548
565
  )
566
+ timer.step('hook:buildExtraInstructionSections')
549
567
 
550
568
  let memoryBlock = workstreamService.formatMemoryBlockForPrompt(workstreamRecord)
551
569
  let workstreamState = initialWorkstreamState
@@ -585,6 +603,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
585
603
  invalidateExecutionPlanInstructionSections()
586
604
  }
587
605
  }
606
+ timer.step('execution-plan-input')
588
607
  const preSeededMemoriesByAgent = new Map<string, string | undefined>()
589
608
  const getPreSeededMemoriesSection = async (agentId: string): Promise<string | undefined> => {
590
609
  if (preSeededMemoriesByAgent.has(agentId)) {
@@ -635,17 +654,21 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
635
654
  uploadMetadataText: buildReadableUploadMetadataText(listReadableUploads(extraMessages)),
636
655
  })
637
656
 
657
+ timer.step('preparation-complete')
658
+
638
659
  return {
639
660
  originalMessages,
640
661
  run: async (writer?: UIMessageStreamWriter<ChatMessage>) => {
641
662
  const executeRun = async () => {
663
+ const runTimer = lotaDebugLogger.timer('run')
642
664
  const serverRunId = Bun.randomUUIDv7()
643
665
  const runAbort = createServerRunAbortController(params.abortSignal)
644
- await workstreamService.setActiveRunId(workstreamRef, serverRunId)
645
- if (params.streamId) {
646
- await workstreamService.setActiveStreamId(workstreamRef, params.streamId)
647
- }
666
+ await Promise.all([
667
+ workstreamService.setActiveRunId(workstreamRef, serverRunId),
668
+ params.streamId ? workstreamService.setActiveStreamId(workstreamRef, params.streamId) : undefined,
669
+ ])
648
670
  chatRunRegistry.register(serverRunId, runAbort.controller)
671
+ runTimer.step('set-active-run+stream')
649
672
 
650
673
  try {
651
674
  const buildAgentMetadataPatch = (agentId: string, agentName: string): NonNullable<MessageMetadata> => ({
@@ -704,7 +727,9 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
704
727
  prepareStep?: PrepareStepFunction<ToolSet>
705
728
  abortSignal?: AbortSignal
706
729
  }): Promise<ChatMessage> => {
730
+ const agentTimer = lotaDebugLogger.timer(`agent:${streamParams.agentId}`)
707
731
  const executionPlanInstructionSections = await getExecutionPlanInstructionSections()
732
+ agentTimer.step('get-execution-plan')
708
733
  const agentResolution = asRecord(
709
734
  await turnHooks.resolveAgent?.({
710
735
  agentId: streamParams.agentId,
@@ -723,7 +748,14 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
723
748
  context: buildContextResult,
724
749
  }),
725
750
  )
751
+ agentTimer.step('hook:resolveAgent')
726
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)')
727
759
  const config = getAgentRuntimeConfig({
728
760
  agentId: resolvedAgentId,
729
761
  workstreamMode: workstream.mode,
@@ -733,11 +765,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
733
765
  linearInstalled,
734
766
  reasoningProfile: reasoningProfile.name,
735
767
  systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
736
- preSeededMemoriesSection: await getPreSeededMemoriesSection(resolvedAgentId),
768
+ preSeededMemoriesSection,
737
769
  retrievedKnowledgeSection,
738
770
  workstreamMemoryBlock: memoryBlock,
739
- workstreamStateSection: await getWorkstreamStateSection(),
740
- learnedSkillsSection: await getLearnedSkillsSection(resolvedAgentId),
771
+ workstreamStateSection,
772
+ learnedSkillsSection,
741
773
  additionalInstructionSections: mergeInstructionSections(
742
774
  executionPlanInstructionSections,
743
775
  streamParams.additionalInstructionSections,
@@ -747,9 +779,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
747
779
  ),
748
780
  context: buildContextResult,
749
781
  }) as AgentRuntimeConfig
782
+ agentTimer.step('build-agent-config')
750
783
  const modelMessages = await convertToModelMessages(streamParams.messages, {
751
784
  ignoreIncompleteToolCalls: true,
752
785
  })
786
+ agentTimer.step('convert-model-messages')
753
787
  const agent = (createAgent as unknown as AgentFactory)[config.id as string]({
754
788
  mode: streamParams.mode,
755
789
  tools: streamParams.tools,
@@ -763,12 +797,14 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
763
797
  (agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
764
798
  })
765
799
  const agentAbortSignal = streamParams.abortSignal ?? runAbort.signal
800
+ agentTimer.step('agent-construction')
766
801
 
767
802
  let result: unknown
768
803
  try {
769
804
  result = await streamParams.observer.run(() =>
770
805
  agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal }),
771
806
  )
807
+ agentTimer.step('agent.stream()-resolved')
772
808
  } catch (error) {
773
809
  if (agentAbortSignal.aborted) {
774
810
  streamParams.observer.recordAbort(error)
@@ -802,10 +838,15 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
802
838
  },
803
839
  }) as ReadableStream<ChatStreamChunk>
804
840
  const reader = uiStream.getReader()
841
+ let firstChunkLogged = false
805
842
  try {
806
843
  for (;;) {
807
844
  const { done, value } = await reader.read()
808
845
  if (done) break
846
+ if (!firstChunkLogged) {
847
+ agentTimer.step('first-stream-chunk')
848
+ firstChunkLogged = true
849
+ }
809
850
  if (streamParams.writer) {
810
851
  streamParams.writer.write(value)
811
852
  }
@@ -813,6 +854,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
813
854
  } finally {
814
855
  reader.releaseLock()
815
856
  }
857
+ agentTimer.step('stream-complete')
816
858
 
817
859
  const finalizedResponseMessage = await finishedStream.then(() => responseMessage)
818
860
  if (finalizedResponseMessage === null) {
@@ -834,6 +876,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
834
876
  extraMessages?: ChatMessage[]
835
877
  extraTools?: ToolSet
836
878
  }): Promise<ChatMessage> => {
879
+ const visibleTimer = lotaDebugLogger.timer(`visible:${runParams.agentId}`)
837
880
  let runMemoryBlock = memoryBlock
838
881
  const tools: ToolSet = {
839
882
  ...((await buildAgentTools({
@@ -863,6 +906,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
863
906
  ...toolProviders,
864
907
  ...runParams.extraTools,
865
908
  }
909
+ visibleTimer.step('build-agent-tools')
866
910
  const responseMessage = await streamAgentResponse({
867
911
  agentId: runParams.agentId,
868
912
  mode: runParams.mode,
@@ -874,6 +918,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
874
918
  writer,
875
919
  })
876
920
 
921
+ visibleTimer.step('stream-agent-response')
877
922
  memoryBlock = runMemoryBlock
878
923
 
879
924
  return await commitAssistantResponse(
@@ -921,7 +966,17 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
921
966
  context: buildContextResult,
922
967
  })
923
968
 
924
- 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
+ ])
925
980
  const specialistConfig = getAgentRuntimeConfig({
926
981
  agentId,
927
982
  workstreamMode: workstream.mode,
@@ -930,11 +985,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
930
985
  linearInstalled,
931
986
  reasoningProfile: reasoningProfile.name,
932
987
  systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
933
- preSeededMemoriesSection: await getPreSeededMemoriesSection(agentId),
988
+ preSeededMemoriesSection: specialistPreSeededMemories,
934
989
  retrievedKnowledgeSection,
935
990
  workstreamMemoryBlock: specialistMemoryBlock,
936
- workstreamStateSection: await getWorkstreamStateSection(),
937
- learnedSkillsSection: await getLearnedSkillsSection(agentId),
991
+ workstreamStateSection: specialistWorkstreamState,
992
+ learnedSkillsSection: specialistLearnedSkills,
938
993
  additionalInstructionSections: mergeInstructionSections(
939
994
  specialistExecutionPlanInstructionSections,
940
995
  coreInstructionSections,
@@ -1047,7 +1102,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1047
1102
  await runVisibleAgent({ agentId: workstream.agentId, mode: 'direct' })
1048
1103
  } else {
1049
1104
  await runVisibleAgent({
1050
- agentId: visibleWorkstreamAgentId ?? 'chief',
1105
+ agentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
1051
1106
  mode: 'workstreamMode',
1052
1107
  skills: coreWorkstreamProfile?.skills ? [...coreWorkstreamProfile.skills] : undefined,
1053
1108
  additionalInstructionSections: mergeInstructionSections(coreInstructionSections, hookInstructionSections),
@@ -1145,17 +1200,17 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1145
1200
  mergeKey: `workstream:${workstreamIdString}`,
1146
1201
  title: buildRecentActivityChatSystemTitle({
1147
1202
  workstream,
1148
- visibleAgentId: visibleWorkstreamAgentId ?? 'chief',
1203
+ visibleAgentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
1149
1204
  }),
1150
- sourceLabel: agentDisplayNames[visibleWorkstreamAgentId ?? 'chief'],
1205
+ sourceLabel: agentDisplayNames[visibleWorkstreamAgentId ?? defaultLeadAgentId],
1151
1206
  deepLink: buildRecentActivityChatDeepLink({
1152
1207
  workstream,
1153
1208
  workstreamId: workstreamIdString,
1154
- visibleAgentId: visibleWorkstreamAgentId ?? 'chief',
1209
+ visibleAgentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
1155
1210
  }),
1156
1211
  metadata: {
1157
- agentId: visibleWorkstreamAgentId ?? 'chief',
1158
- agentName: agentDisplayNames[visibleWorkstreamAgentId ?? 'chief'],
1212
+ agentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
1213
+ agentName: agentDisplayNames[visibleWorkstreamAgentId ?? defaultLeadAgentId],
1159
1214
  workstreamId: workstreamIdString,
1160
1215
  workstreamTitle: latestWorkstreamRecord.title ?? workstream.title,
1161
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,42 +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')
20
24
  },
21
25
  })
22
26
  }
23
27
 
24
28
  export async function createWorkstreamNativeToolApprovalStream(params: WorkstreamApprovalContinuationParams) {
29
+ const timer = lotaDebugLogger.timer('turn:native-tool-approval')
25
30
  const prepared = await prepareWorkstreamRunCore({ ...params, kind: 'nativeToolApprovalTurn' })
31
+ timer.step('prepare')
26
32
 
27
33
  return createUIMessageStream<ChatMessage>({
28
34
  originalMessages: prepared.originalMessages,
29
35
  onError: (error) => (error instanceof Error ? error.message : 'Native tool approval stream failed.'),
30
36
  execute: async ({ writer }) => {
31
37
  await prepared.run(writer)
38
+ timer.step('run')
32
39
  },
33
40
  })
34
41
  }
35
42
 
36
43
  export async function createWorkstreamTurnStream(params: WorkstreamTurnParams) {
44
+ const timer = lotaDebugLogger.timer('turn:user-turn')
37
45
  const prepared = await prepareWorkstreamRunCore({ ...params, kind: 'userTurn' })
46
+ timer.step('prepare')
38
47
 
39
48
  return createUIMessageStream<ChatMessage>({
40
49
  originalMessages: prepared.originalMessages,
41
50
  onError: (error) => (error instanceof Error ? error.message : 'Chat stream failed.'),
42
51
  execute: async ({ writer }) => {
43
52
  await prepared.run(writer)
53
+ timer.step('run')
44
54
  },
45
55
  })
46
56
  }
47
57
 
48
58
  export async function runWorkstreamTurnInBackground(params: WorkstreamTurnParams): Promise<void> {
59
+ const timer = lotaDebugLogger.timer('turn:background')
49
60
  const prepared = await prepareWorkstreamRunCore({ ...params, kind: 'userTurn' })
61
+ timer.step('prepare')
50
62
  await prepared.run()
63
+ timer.step('run')
51
64
  }
@@ -1,4 +1,4 @@
1
- import { WORKSTREAM } from '@lota-sdk/shared/constants/workstream'
1
+ import { WORKSTREAM } from '@lota-sdk/shared'
2
2
  import { BoundQuery, RecordId, StringRecordId, surql } from 'surrealdb'
3
3
 
4
4
  import { agentDisplayNames, getCoreWorkstreamProfile, isAgentName } from '../config/agent-defaults'
@@ -345,19 +345,19 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
345
345
 
346
346
  const onboardingWelcome = bootstrapConfig.onboardingWelcome
347
347
  if (!onboardingCompleted && onboardingWelcome) {
348
- const createdChiefWorkstream = createdWorkstreams.find(
348
+ const createdOnboardingOwnerWorkstream = createdWorkstreams.find(
349
349
  (workstream) => workstream.mode === 'direct' && workstream.agentId === onboardingWelcome.directAgentId,
350
350
  )
351
- const existingChiefWorkstream = directWorkstreamsByAgent.get(onboardingWelcome.directAgentId)
351
+ const existingOnboardingOwnerWorkstream = directWorkstreamsByAgent.get(onboardingWelcome.directAgentId)
352
352
 
353
- const chiefWorkstreamId =
354
- createdChiefWorkstream?.id ??
355
- (existingChiefWorkstream ? this.normalizeWorkstreamId(existingChiefWorkstream.id) : null)
353
+ const onboardingOwnerWorkstreamId =
354
+ createdOnboardingOwnerWorkstream?.id ??
355
+ (existingOnboardingOwnerWorkstream ? this.normalizeWorkstreamId(existingOnboardingOwnerWorkstream.id) : null)
356
356
 
357
- if (chiefWorkstreamId) {
358
- const chiefWorkstreamRef = ensureRecordId(chiefWorkstreamId, TABLES.WORKSTREAM)
357
+ if (onboardingOwnerWorkstreamId) {
358
+ const onboardingOwnerWorkstreamRef = ensureRecordId(onboardingOwnerWorkstreamId, TABLES.WORKSTREAM)
359
359
  await workstreamMessageService.ensureBootstrapWelcomeMessage({
360
- workstreamId: chiefWorkstreamRef,
360
+ workstreamId: onboardingOwnerWorkstreamRef,
361
361
  agentId: onboardingWelcome.directAgentId,
362
362
  text: onboardingWelcome.buildMessageText({ userName: options?.userName }),
363
363
  })
@@ -1,4 +1,4 @@
1
- import { SUPPORTED_ATTACHMENT_MIME_TYPES, getAttachmentExtension } from '@lota-sdk/shared/constants/attachments'
1
+ import { SUPPORTED_ATTACHMENT_MIME_TYPES, getAttachmentExtension } from '@lota-sdk/shared'
2
2
  import mammoth from 'mammoth'
3
3
  import { PDFParse } from 'pdf-parse'
4
4
 
@@ -1,8 +1,8 @@
1
- import { inferContentType } from '@lota-sdk/shared/constants/attachments'
1
+ import { inferContentType } from '@lota-sdk/shared'
2
2
  import { S3Client } from 'bun'
3
3
 
4
- import { env } from '../config/env-shapes'
5
4
  import { serverLogger } from '../config/logger'
5
+ import { getRuntimeConfig } from '../runtime/runtime-config'
6
6
  import { readString } from '../utils/string'
7
7
  import {
8
8
  extractAttachmentText,
@@ -41,17 +41,18 @@ export class AttachmentStorageService {
41
41
  private readonly client: S3Client
42
42
 
43
43
  constructor() {
44
+ const config = getRuntimeConfig()
44
45
  this.client = new S3Client({
45
- accessKeyId: env.S3_ACCESS_KEY_ID,
46
- secretAccessKey: env.S3_SECRET_ACCESS_KEY,
47
- bucket: env.S3_BUCKET,
48
- endpoint: env.S3_ENDPOINT,
49
- region: env.S3_REGION,
46
+ accessKeyId: config.s3.accessKeyId,
47
+ secretAccessKey: config.s3.secretAccessKey,
48
+ bucket: config.s3.bucket,
49
+ endpoint: config.s3.endpoint,
50
+ region: config.s3.region,
50
51
  })
51
52
  }
52
53
 
53
54
  getAttachmentUrl(storageKey: string): string {
54
- return this.client.file(storageKey).presign({ expiresIn: env.ATTACHMENT_URL_EXPIRES_IN })
55
+ return this.client.file(storageKey).presign({ expiresIn: getRuntimeConfig().s3.attachmentUrlExpiresIn })
55
56
  }
56
57
 
57
58
  async writeOrganizationDocument({
@@ -102,7 +103,7 @@ export class AttachmentStorageService {
102
103
  sizeBytes: file.size,
103
104
  storageKey,
104
105
  url: this.getAttachmentUrl(storageKey),
105
- expiresInSeconds: env.ATTACHMENT_URL_EXPIRES_IN,
106
+ expiresInSeconds: getRuntimeConfig().s3.attachmentUrlExpiresIn,
106
107
  }
107
108
  }
108
109
 
@@ -128,7 +129,7 @@ export class AttachmentStorageService {
128
129
  sizeBytes,
129
130
  storageKey,
130
131
  url: this.getAttachmentUrl(storageKey),
131
- expiresInSeconds: env.ATTACHMENT_URL_EXPIRES_IN,
132
+ expiresInSeconds: getRuntimeConfig().s3.attachmentUrlExpiresIn,
132
133
  }
133
134
  }
134
135