@lota-sdk/core 0.1.13 → 0.1.15

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 +5 -5
  2. package/src/ai/embedding-cache.ts +7 -6
  3. package/src/ai/index.ts +1 -0
  4. package/src/bifrost/bifrost.ts +12 -7
  5. package/src/config/agent-defaults.ts +1 -1
  6. package/src/config/logger.ts +7 -9
  7. package/src/{runtime.ts → create-runtime.ts} +6 -6
  8. package/src/db/cursor-pagination.ts +1 -1
  9. package/src/db/memory-store.ts +10 -6
  10. package/src/db/memory.ts +6 -4
  11. package/src/db/schema-fingerprint.ts +1 -0
  12. package/src/db/service.ts +45 -51
  13. package/src/db/startup.ts +3 -3
  14. package/src/index.ts +1 -1
  15. package/src/queues/context-compaction.queue.ts +4 -8
  16. package/src/queues/document-processor.queue.ts +7 -7
  17. package/src/queues/memory-consolidation.queue.ts +7 -8
  18. package/src/queues/post-chat-memory.queue.ts +2 -6
  19. package/src/queues/recent-activity-title-refinement.queue.ts +2 -6
  20. package/src/queues/regular-chat-memory-digest.queue.ts +4 -7
  21. package/src/queues/skill-extraction.queue.ts +4 -7
  22. package/src/queues/workstream-title-generation.queue.ts +2 -6
  23. package/src/redis/connection.ts +6 -3
  24. package/src/redis/index.ts +1 -0
  25. package/src/redis/org-memory-lock.ts +1 -1
  26. package/src/redis/redis-lease-lock.ts +41 -8
  27. package/src/runtime/agent-stream-helpers.ts +2 -1
  28. package/src/runtime/context-compaction-constants.ts +1 -1
  29. package/src/runtime/context-compaction-runtime.ts +6 -4
  30. package/src/runtime/context-compaction.ts +19 -38
  31. package/src/runtime/execution-plan.ts +2 -2
  32. package/src/runtime/helper-model.ts +3 -1
  33. package/src/runtime/index.ts +12 -1
  34. package/src/runtime/memory-block.ts +3 -2
  35. package/src/runtime/memory-pipeline.ts +24 -5
  36. package/src/runtime/plugin-types.ts +1 -1
  37. package/src/runtime/runtime-extensions.ts +89 -13
  38. package/src/runtime/title-helpers.ts +11 -2
  39. package/src/runtime/workstream-chat-helpers.ts +5 -6
  40. package/src/runtime/workstream-routing-policy.ts +0 -30
  41. package/src/runtime/workstream-state.ts +17 -7
  42. package/src/services/attachment.service.ts +1 -1
  43. package/src/services/context-compaction.service.ts +3 -3
  44. package/src/services/document-chunk.service.ts +37 -32
  45. package/src/services/execution-plan.service.ts +2 -0
  46. package/src/services/learned-skill.service.ts +6 -10
  47. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -8
  48. package/src/services/memory.service.ts +21 -18
  49. package/src/services/organization-member.service.ts +1 -1
  50. package/src/services/plan-artifact.service.ts +1 -0
  51. package/src/services/plan-executor.service.ts +2 -18
  52. package/src/services/plan-helpers.ts +15 -0
  53. package/src/services/plan-validator.service.ts +3 -18
  54. package/src/services/recent-activity-title.service.ts +3 -10
  55. package/src/services/recent-activity.service.ts +6 -12
  56. package/src/services/workstream-message.service.ts +26 -16
  57. package/src/services/workstream-title.service.ts +1 -9
  58. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +401 -314
  59. package/src/services/workstream-turn.ts +2 -2
  60. package/src/services/workstream.service.ts +22 -10
  61. package/src/services/workstream.types.ts +7 -16
  62. package/src/storage/attachment-storage.service.ts +4 -4
  63. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +1 -4
  64. package/src/storage/index.ts +2 -2
  65. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  66. package/src/system-agents/delegated-agent-factory.ts +3 -2
  67. package/src/system-agents/index.ts +8 -0
  68. package/src/system-agents/memory-reranker.agent.ts +1 -1
  69. package/src/system-agents/memory.agent.ts +1 -1
  70. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  71. package/src/tools/execution-plan.tool.ts +6 -2
  72. package/src/tools/fetch-webpage.tool.ts +20 -18
  73. package/src/tools/index.ts +2 -2
  74. package/src/tools/read-file-parts.tool.ts +1 -1
  75. package/src/tools/search-web.tool.ts +18 -15
  76. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  77. package/src/tools/team-think.tool.ts +9 -5
  78. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  79. package/src/utils/async.ts +1 -1
  80. package/src/utils/errors.ts +15 -0
  81. package/src/utils/hono-error-handler.ts +1 -2
  82. package/src/utils/index.ts +10 -2
  83. package/src/utils/string.ts +14 -0
  84. package/src/workers/bootstrap.ts +2 -2
  85. package/src/workers/memory-consolidation.worker.ts +12 -12
  86. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  87. package/src/workers/regular-chat-memory-digest.runner.ts +9 -103
  88. package/src/workers/skill-extraction.runner.ts +7 -101
  89. package/src/workers/utils/file-section-chunker.ts +5 -3
  90. package/src/workers/utils/workstream-message-query.ts +106 -0
  91. package/src/workers/worker-utils.ts +4 -0
  92. package/src/runtime/retrieval-pipeline.ts +0 -3
  93. package/src/utils/error.ts +0 -10
  94. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  95. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -3,7 +3,7 @@ import {
3
3
  CONSULT_SPECIALIST_TOOL_NAME,
4
4
  CONSULT_TEAM_TOOL_NAME,
5
5
  ConsultSpecialistArgsSchema,
6
- dataPartsSchema,
6
+ dataPartsSchemas,
7
7
  messageMetadataSchema,
8
8
  toTimestamp,
9
9
  withMessageCreatedAt,
@@ -13,6 +13,7 @@ import { convertToModelMessages, readUIMessageStream, stepCountIs, tool as creat
13
13
  import type { PrepareStepFunction, StopCondition, ToolSet, UIMessageStreamWriter } from 'ai'
14
14
  import type { z } from 'zod'
15
15
 
16
+ import type { CoreWorkstreamProfile } from '../config/agent-defaults'
16
17
  import {
17
18
  agentDisplayNames,
18
19
  buildAgentTools,
@@ -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 {
@@ -81,7 +82,7 @@ import { toIsoDateTimeString } from '../utils/date-time'
81
82
  import { AppError } from '../utils/errors'
82
83
  import { attachmentService } from './attachment.service'
83
84
  import { listReadableUploadsFromChatMessages } from './chat-attachments.service'
84
- import { contextCompactionRuntime } from './context-compaction-runtime'
85
+ import { contextCompactionRuntime } from './context-compaction-runtime.singleton'
85
86
  import { executionPlanService } from './execution-plan.service'
86
87
  import { learnedSkillService } from './learned-skill.service'
87
88
  import { memoryService } from './memory.service'
@@ -231,7 +232,7 @@ export interface WorkstreamTurnParams {
231
232
  userRef: RecordIdRef
232
233
  userName?: string | null
233
234
  inputMessage: ChatMessage
234
- persistInputMessage?: boolean
235
+ skipInputMessagePersistence?: boolean
235
236
  abortSignal?: AbortSignal
236
237
  streamId?: string
237
238
  }
@@ -256,7 +257,7 @@ type WorkstreamRunCoreParams = {
256
257
  abortSignal?: AbortSignal
257
258
  streamId?: string
258
259
  } & (
259
- | { kind: 'userTurn'; inputMessage: ChatMessage; persistInputMessage?: boolean }
260
+ | { kind: 'userTurn'; inputMessage: ChatMessage; skipInputMessagePersistence?: boolean }
260
261
  | { kind: 'approvalContinuation'; approvalMessages: ChatMessage[] }
261
262
  | { kind: 'nativeToolApprovalTurn'; approvalMessages: ChatMessage[] }
262
263
  )
@@ -300,6 +301,338 @@ function buildRecentActivityChatSystemTitle(params: {
300
301
  return params.workstream.title.trim() || 'Workstream update'
301
302
  }
302
303
 
304
+ interface StreamAgentResponseContext {
305
+ turnHooks: ReturnType<typeof getTurnHooks>
306
+ workstream: NormalizedWorkstream
307
+ workstreamRef: RecordIdRef
308
+ orgRef: RecordIdRef
309
+ userRef: RecordIdRef
310
+ userName?: string | null
311
+ onboardingActive: boolean
312
+ linearInstalled: boolean
313
+ githubInstalled: boolean
314
+ reasoningProfileName: string
315
+ buildContextResult: Record<string, unknown> | null
316
+ getExecutionPlanInstructionSections: () => Promise<string[] | undefined>
317
+ getPreSeededMemoriesSection: (agentId: string) => Promise<string | undefined>
318
+ getWorkstreamStateSection: () => Promise<string | undefined>
319
+ getLearnedSkillsSection: (agentId: string) => Promise<string | undefined>
320
+ promptContext: { systemWorkspaceDetails?: string }
321
+ retrievedKnowledgeSection: string | undefined
322
+ memoryBlock: string
323
+ hookInstructionSections: string[]
324
+ runAbortSignal: AbortSignal
325
+ }
326
+
327
+ interface StreamAgentResponseParams {
328
+ agentId: string
329
+ mode: 'direct' | 'fixedWorkstreamMode' | 'workstreamMode'
330
+ messages: ChatMessage[]
331
+ tools: ToolSet
332
+ observer: {
333
+ run: <T>(fn: () => T | Promise<T>) => Promise<T>
334
+ recordError: (error: unknown) => void
335
+ recordAbort: (error: unknown) => void
336
+ }
337
+ skills?: string[]
338
+ additionalInstructionSections?: string[]
339
+ writer?: UIMessageStreamWriter<ChatMessage>
340
+ stopWhen?: StopCondition<ToolSet> | Array<StopCondition<ToolSet>>
341
+ prepareStep?: PrepareStepFunction<ToolSet>
342
+ abortSignal?: AbortSignal
343
+ }
344
+
345
+ async function streamAgentResponse(
346
+ ctx: StreamAgentResponseContext,
347
+ streamParams: StreamAgentResponseParams,
348
+ ): Promise<ChatMessage> {
349
+ const agentTimer = lotaDebugLogger.timer(`agent:${streamParams.agentId}`)
350
+ const executionPlanInstructionSections = await ctx.getExecutionPlanInstructionSections()
351
+ agentTimer.step('get-execution-plan')
352
+ const agentResolution = asRecord(
353
+ await ctx.turnHooks.resolveAgent?.({
354
+ agentId: streamParams.agentId,
355
+ mode: streamParams.mode,
356
+ workstream: ctx.workstream,
357
+ workstreamRef: ctx.workstreamRef,
358
+ orgRef: ctx.orgRef,
359
+ userRef: ctx.userRef,
360
+ userName: ctx.userName,
361
+ onboardingActive: ctx.onboardingActive,
362
+ linearInstalled: ctx.linearInstalled,
363
+ githubInstalled: ctx.githubInstalled,
364
+ reasoningProfile: ctx.reasoningProfileName,
365
+ skills: streamParams.skills,
366
+ additionalInstructionSections: streamParams.additionalInstructionSections,
367
+ context: ctx.buildContextResult,
368
+ }),
369
+ )
370
+ agentTimer.step('hook:resolveAgent')
371
+ const resolvedAgentId = readOptionalString(agentResolution?.agentId) ?? streamParams.agentId
372
+ const [preSeededMemoriesSection, workstreamStateSection, learnedSkillsSection] = await Promise.all([
373
+ ctx.getPreSeededMemoriesSection(resolvedAgentId),
374
+ ctx.getWorkstreamStateSection(),
375
+ ctx.getLearnedSkillsSection(resolvedAgentId),
376
+ ])
377
+ agentTimer.step('parallel-fetch(memories+state+skills)')
378
+ const config = getAgentRuntimeConfig({
379
+ agentId: resolvedAgentId,
380
+ workstreamMode: ctx.workstream.mode,
381
+ mode: streamParams.mode,
382
+ skills: streamParams.skills,
383
+ onboardingActive: ctx.onboardingActive,
384
+ linearInstalled: ctx.linearInstalled,
385
+ reasoningProfile: ctx.reasoningProfileName,
386
+ systemWorkspaceDetails: ctx.promptContext.systemWorkspaceDetails,
387
+ preSeededMemoriesSection,
388
+ retrievedKnowledgeSection: ctx.retrievedKnowledgeSection,
389
+ workstreamMemoryBlock: ctx.memoryBlock,
390
+ workstreamStateSection,
391
+ learnedSkillsSection,
392
+ additionalInstructionSections: mergeInstructionSections(
393
+ executionPlanInstructionSections,
394
+ streamParams.additionalInstructionSections,
395
+ ctx.hookInstructionSections,
396
+ readInstructionSections(agentResolution?.additionalInstructionSections),
397
+ optionalInstructionSection(agentResolution?.extraInstructions),
398
+ ),
399
+ context: ctx.buildContextResult,
400
+ }) as AgentRuntimeConfig
401
+ agentTimer.step('build-agent-config')
402
+ const modelMessages = await convertToModelMessages(streamParams.messages, { ignoreIncompleteToolCalls: true })
403
+ agentTimer.step('convert-model-messages')
404
+ const agent = (createAgent as unknown as AgentFactory)[config.id as string]({
405
+ mode: streamParams.mode,
406
+ tools: streamParams.tools,
407
+ extraInstructions: config.extraInstructions,
408
+ stopWhen: (agentResolution?.stopWhen as StopCondition<ToolSet> | Array<StopCondition<ToolSet>> | undefined) ??
409
+ streamParams.stopWhen ?? [stepCountIs(config.maxSteps as number)],
410
+ prepareStep: (agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
411
+ })
412
+ const agentAbortSignal = streamParams.abortSignal ?? ctx.runAbortSignal
413
+ agentTimer.step('agent-construction')
414
+
415
+ let result: unknown
416
+ try {
417
+ result = await streamParams.observer.run(() =>
418
+ agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal }),
419
+ )
420
+ agentTimer.step('agent.stream()-resolved')
421
+ } catch (error) {
422
+ if (agentAbortSignal.aborted) {
423
+ streamParams.observer.recordAbort(error)
424
+ } else {
425
+ streamParams.observer.recordError(error)
426
+ }
427
+ throw error
428
+ }
429
+ if (!hasUIMessageStream(result)) {
430
+ throw new Error(`Agent run for ${resolvedAgentId} did not expose a UI message stream.`)
431
+ }
432
+
433
+ let responseMessage: ChatMessage | null = null
434
+ let resolveFinishedStream!: () => void
435
+ const finishedStream = new Promise<void>((resolve) => {
436
+ resolveFinishedStream = resolve
437
+ })
438
+
439
+ const uiStream = result.toUIMessageStream({
440
+ generateMessageId: () => Bun.randomUUIDv7(),
441
+ originalMessages: streamParams.messages,
442
+ sendReasoning: true,
443
+ sendSources: true,
444
+ messageMetadata: createAgentMessageMetadata({ agentId: resolvedAgentId, agentName: config.displayName as string }),
445
+ onFinish: ({ responseMessage: finishedResponseMessage }: { responseMessage: ChatMessage }) => {
446
+ responseMessage = withMessageCreatedAt(finishedResponseMessage)
447
+ resolveFinishedStream()
448
+ },
449
+ }) as ReadableStream<ChatStreamChunk>
450
+ const reader = uiStream.getReader()
451
+ let firstChunkLogged = false
452
+ try {
453
+ for (;;) {
454
+ const { done, value } = await reader.read()
455
+ if (done) break
456
+ if (!firstChunkLogged) {
457
+ agentTimer.step('first-stream-chunk')
458
+ firstChunkLogged = true
459
+ }
460
+ if (streamParams.writer) {
461
+ streamParams.writer.write(value)
462
+ }
463
+ }
464
+ } finally {
465
+ reader.releaseLock()
466
+ }
467
+ agentTimer.step('stream-complete')
468
+
469
+ await finishedStream
470
+ // responseMessage is set inside the stream callback — linter cannot track cross-callback assignment
471
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
472
+ if (responseMessage === null) {
473
+ throw new Error(`Agent run for ${resolvedAgentId} did not produce a response message.`)
474
+ }
475
+
476
+ for (const toolError of collectToolOutputErrors({ responseMessage: responseMessage })) {
477
+ aiLogger.warn`Tool execution failed (agent=${resolvedAgentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
478
+ }
479
+
480
+ return responseMessage
481
+ }
482
+
483
+ interface PostTurnSideEffectsParams {
484
+ workstream: NormalizedWorkstream
485
+ workstreamRef: RecordIdRef
486
+ orgRef: RecordIdRef
487
+ userRef: RecordIdRef
488
+ userName?: string | null
489
+ orgIdString: string
490
+ workstreamIdString: string
491
+ onboardingActive: boolean
492
+ workspace: unknown
493
+ allAssistantMessages: ChatMessage[]
494
+ referenceUserMessage: ChatMessage | undefined
495
+ referenceUserMessageId: string
496
+ recentHistory: ChatMessage[]
497
+ listReadableUploads: () => ReturnType<typeof listReadableUploadsFromChatMessages>
498
+ memoryBlock: string
499
+ visibleWorkstreamAgentId: string | null | undefined
500
+ defaultLeadAgentId: string
501
+ latestWorkstreamRecord: WorkstreamRecord
502
+ latestPersistedState: WorkstreamState | null
503
+ turnHooks: ReturnType<typeof getTurnHooks>
504
+ buildContextResult: Record<string, unknown> | null
505
+ isUserTurn: boolean
506
+ }
507
+
508
+ async function runPostTurnSideEffects(params: PostTurnSideEffectsParams): Promise<void> {
509
+ const turnCount = await workstreamService.incrementTurnCount(params.workstreamRef)
510
+ const agentMessages = buildAgentHistoryMessages(params.allAssistantMessages)
511
+ const historyMessagesForMemory = appendPersistedWorkstreamContextToHistoryMessages(
512
+ toHistoryMessages(params.recentHistory),
513
+ { compactionSummary: params.latestWorkstreamRecord.compactionSummary, persistedState: params.latestPersistedState },
514
+ )
515
+
516
+ const userMessageText = params.referenceUserMessage ? extractMessageText(params.referenceUserMessage).trim() : ''
517
+ const readableUploads = params.listReadableUploads()
518
+ const attachmentMetadataContext = buildReadableUploadMetadataContext(readableUploads)
519
+ const hasAttachmentContext = Boolean(attachmentMetadataContext)
520
+ const shouldExtractMemory = params.onboardingActive
521
+ ? shouldEnqueueOnboardingPostChatMemory({
522
+ onboardingActive: params.onboardingActive,
523
+ userMessageText,
524
+ hasAttachmentContext,
525
+ agentMessageCount: agentMessages.length,
526
+ })
527
+ : shouldEnqueueMemoryExtraction({ onboardingActive: params.onboardingActive, turnCount }) &&
528
+ userMessageText.length > 0
529
+
530
+ if (shouldExtractMemory) {
531
+ const memoryUserMessage = userMessageText || 'User uploaded attachment(s).'
532
+ await safeEnqueue(
533
+ () =>
534
+ enqueuePostChatMemory({
535
+ orgId: params.orgIdString,
536
+ workstreamId: params.workstreamIdString,
537
+ sourceId: params.referenceUserMessageId,
538
+ onboardStatus: readOptionalString((params.workspace as { onboardStatus?: unknown }).onboardStatus),
539
+ userMessage: memoryUserMessage,
540
+ historyMessages: historyMessagesForMemory,
541
+ agentMessages,
542
+ memoryBlock: params.memoryBlock.trim() ? params.memoryBlock : undefined,
543
+ attachmentContext: attachmentMetadataContext,
544
+ }),
545
+ { operationName: 'post-chat memory extraction enqueue' },
546
+ )
547
+ }
548
+
549
+ if (params.isUserTurn && params.referenceUserMessage) {
550
+ const conversationSummary = buildConversationSummary({
551
+ userMessageText,
552
+ assistantMessages: params.allAssistantMessages,
553
+ })
554
+ if (conversationSummary) {
555
+ const effectiveAgentId = params.visibleWorkstreamAgentId ?? params.defaultLeadAgentId
556
+ const recentActivityResult = await recentActivityService.recordEvent({
557
+ orgId: params.orgRef,
558
+ userId: params.userRef,
559
+ source: 'system',
560
+ event: {
561
+ sourceEventId: `chat-turn:${params.referenceUserMessageId}`,
562
+ kind: 'chat.turn.completed',
563
+ targetKind: 'workstream',
564
+ targetId: params.workstreamIdString,
565
+ mergeKey: `workstream:${params.workstreamIdString}`,
566
+ title: buildRecentActivityChatSystemTitle({
567
+ workstream: params.workstream,
568
+ visibleAgentId: effectiveAgentId,
569
+ }),
570
+ sourceLabel: agentDisplayNames[effectiveAgentId],
571
+ deepLink: buildRecentActivityChatDeepLink({
572
+ workstream: params.workstream,
573
+ workstreamId: params.workstreamIdString,
574
+ visibleAgentId: effectiveAgentId,
575
+ }),
576
+ metadata: {
577
+ agentId: effectiveAgentId,
578
+ agentName: agentDisplayNames[effectiveAgentId],
579
+ workstreamId: params.workstreamIdString,
580
+ workstreamTitle: params.latestWorkstreamRecord.title ?? params.workstream.title,
581
+ workstreamMode: params.workstream.mode,
582
+ ...(params.workstream.coreType ? { coreType: params.workstream.coreType } : {}),
583
+ userMessageText,
584
+ assistantSummary: conversationSummary,
585
+ messageId: params.referenceUserMessageId,
586
+ },
587
+ occurredAt: toIsoDateTimeString(params.referenceUserMessage.metadata?.createdAt ?? Date.now()),
588
+ },
589
+ })
590
+
591
+ await safeEnqueue(
592
+ async () => {
593
+ const enqueuePostChatOrgAction = getRuntimeAdapters().queues?.enqueuePostChatOrgAction
594
+ if (!enqueuePostChatOrgAction) {
595
+ return
596
+ }
597
+
598
+ await enqueuePostChatOrgAction({
599
+ orgId: params.orgIdString,
600
+ workstreamId: params.workstreamIdString,
601
+ sourceId: params.referenceUserMessageId,
602
+ sourceCreatedAt: params.referenceUserMessage?.metadata?.createdAt ?? Date.now(),
603
+ conversationSummary,
604
+ })
605
+ },
606
+ { operationName: 'post-chat org action enqueue' },
607
+ )
608
+
609
+ if (recentActivityService.isMeaningfulRefinementCandidate(recentActivityResult.item)) {
610
+ await safeEnqueue(() => enqueueRecentActivityTitleRefinement({ activityId: recentActivityResult.item.id }), {
611
+ operationName: 'recent activity title refinement enqueue',
612
+ })
613
+ }
614
+ }
615
+ }
616
+
617
+ if (shouldEnqueueRegularDigestForWorkstream({ onboardingActive: params.onboardingActive, turnCount })) {
618
+ await safeEnqueue(() => enqueueRegularChatMemoryDigest({ orgId: params.orgIdString }), {
619
+ operationName: 'regular chat memory digest enqueue',
620
+ })
621
+ }
622
+
623
+ if (shouldEnqueueSkillExtraction({ onboardingActive: params.onboardingActive, turnCount })) {
624
+ await safeEnqueue(() => enqueueSkillExtraction({ orgId: params.orgIdString }), {
625
+ operationName: 'skill extraction enqueue',
626
+ })
627
+ }
628
+
629
+ if (shouldEnqueueMemoryConsolidation({ onboardingActive: params.onboardingActive, turnCount })) {
630
+ await safeEnqueue(() => enqueueMemoryConsolidation({ scopeId: params.orgIdString }), {
631
+ operationName: 'memory consolidation enqueue',
632
+ })
633
+ }
634
+ }
635
+
303
636
  export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams): Promise<PreparedWorkstreamTurn> {
304
637
  const { workstream, workstreamRef, orgRef, userRef, userName } = params
305
638
  const runtimeAdapters = getRuntimeAdapters()
@@ -320,7 +653,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
320
653
  })
321
654
 
322
655
  let inputMessage: ChatMessage | undefined
323
- const shouldPersistInputMessage = params.kind === 'userTurn' ? params.persistInputMessage !== false : false
656
+ const shouldPersistInputMessage = params.kind === 'userTurn' ? params.skipInputMessagePersistence !== true : false
324
657
  const shouldProcessPostRunSideEffects =
325
658
  params.kind === 'approvalContinuation' || params.kind === 'nativeToolApprovalTurn' || shouldPersistInputMessage
326
659
  if (params.kind === 'userTurn') {
@@ -373,7 +706,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
373
706
  id: inputMessage.id || Bun.randomUUIDv7(),
374
707
  role: 'user',
375
708
  parts: inputMessage.parts,
376
- metadata: { ...inputMessage.metadata, createdAt: toTimestamp(inputMessage.metadata?.createdAt) },
709
+ metadata: { ...inputMessage.metadata, createdAt: toTimestamp(inputMessage.metadata?.createdAt) ?? Date.now() },
377
710
  }
378
711
  }
379
712
 
@@ -395,14 +728,14 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
395
728
  : validateUIMessages<ChatMessage>({
396
729
  messages: persistedLiveHistory,
397
730
  metadataSchema: messageMetadataSchema,
398
- dataSchemas: dataPartsSchema,
731
+ dataSchemas: dataPartsSchemas,
399
732
  }).then((messages) => messages.map(hydrateMessageFileUrls)),
400
733
  persistedRecentHistory.length === 0
401
734
  ? Promise.resolve([] as ChatMessage[])
402
735
  : validateUIMessages<ChatMessage>({
403
736
  messages: persistedRecentHistory,
404
737
  metadataSchema: messageMetadataSchema,
405
- dataSchemas: dataPartsSchema,
738
+ dataSchemas: dataPartsSchemas,
406
739
  }).then((messages) => messages.map(hydrateMessageFileUrls)),
407
740
  ])
408
741
  timer.step('validate+hydrate-history')
@@ -413,7 +746,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
413
746
  }
414
747
 
415
748
  const originalMessages = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
416
- const allAssistantMessages: ChatMessage[] = []
749
+ let allAssistantMessages: ChatMessage[] = []
417
750
  const referenceUserMessage =
418
751
  params.kind === 'userTurn' && !shouldPersistInputMessage
419
752
  ? [...liveHistory].reverse().find((m) => m.role === 'user')
@@ -437,14 +770,8 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
437
770
  if (workstream.core && !workstream.coreType) {
438
771
  throw new WorkstreamTurnError('Core workstreams require a core type.', 400)
439
772
  }
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
773
+ const coreWorkstreamProfile: CoreWorkstreamProfile | null =
774
+ workstream.core && workstream.coreType ? getCoreWorkstreamProfile(workstream.coreType) : null
448
775
  const defaultLeadAgentId = getLeadAgentId()
449
776
  const visibleWorkstreamAgentId =
450
777
  workstream.mode === 'direct' ? workstream.agentId : (coreWorkstreamProfile?.config.agentId ?? defaultLeadAgentId)
@@ -626,7 +953,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
626
953
 
627
954
  const section = await learnedSkillService
628
955
  .retrieveForTurn({ orgId: orgIdString, agentId, query: messageText, limit: 3, minConfidence: 0.6 })
629
- .catch((error: unknown) => {
956
+ .catch((error) => {
630
957
  aiLogger.warn`Failed to retrieve learned skills for ${agentId}: ${error}`
631
958
  return undefined
632
959
  })
@@ -697,175 +1024,33 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
697
1024
  })
698
1025
 
699
1026
  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
- }
1027
+ currentMessages = upsertChatHistoryMessage(currentMessages, committed)
1028
+ allAssistantMessages = upsertChatHistoryMessage(allAssistantMessages, committed)
713
1029
 
714
1030
  return committed
715
1031
  }
716
1032
 
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
1033
+ const streamCtx: StreamAgentResponseContext = {
1034
+ turnHooks,
1035
+ workstream,
1036
+ workstreamRef,
1037
+ orgRef,
1038
+ userRef,
1039
+ userName,
1040
+ onboardingActive,
1041
+ linearInstalled,
1042
+ githubInstalled,
1043
+ reasoningProfileName: reasoningProfile.name,
1044
+ buildContextResult,
1045
+ getExecutionPlanInstructionSections,
1046
+ getPreSeededMemoriesSection,
1047
+ getWorkstreamStateSection,
1048
+ getLearnedSkillsSection,
1049
+ promptContext,
1050
+ retrievedKnowledgeSection,
1051
+ memoryBlock,
1052
+ hookInstructionSections,
1053
+ runAbortSignal: runAbort.signal,
869
1054
  }
870
1055
 
871
1056
  const runVisibleAgent = async (runParams: {
@@ -907,7 +1092,8 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
907
1092
  ...runParams.extraTools,
908
1093
  }
909
1094
  visibleTimer.step('build-agent-tools')
910
- const responseMessage = await streamAgentResponse({
1095
+ streamCtx.memoryBlock = memoryBlock
1096
+ const responseMessage = await streamAgentResponse(streamCtx, {
911
1097
  agentId: runParams.agentId,
912
1098
  mode: runParams.mode,
913
1099
  messages: buildRunInputMessages(runParams.extraMessages),
@@ -1126,14 +1312,15 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1126
1312
  contextCompactionRuntime.shouldCompactHistory({
1127
1313
  summaryText,
1128
1314
  liveMessages: messages,
1129
- contextSize: CONTEXT_SIZE,
1315
+ contextSize: CONTEXT_WINDOW_TOKENS,
1130
1316
  }),
1131
- enqueueCompaction: () =>
1132
- enqueueContextCompaction({
1317
+ enqueueCompaction: async () => {
1318
+ await enqueueContextCompaction({
1133
1319
  domain: 'workstream',
1134
1320
  entityId: workstreamIdString,
1135
- contextSize: CONTEXT_SIZE,
1136
- }).then(() => {}),
1321
+ contextSize: CONTEXT_WINDOW_TOKENS,
1322
+ })
1323
+ },
1137
1324
  unregisterRun: (runId) => chatRunRegistry.unregister(runId),
1138
1325
  clearActiveRunId: (runId) => workstreamService.clearActiveRunIdIfMatches(workstreamRef, runId),
1139
1326
  disposeAbort: () => runAbort.dispose(),
@@ -1143,130 +1330,30 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1143
1330
  })
1144
1331
 
1145
1332
  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
- }
1333
+ await runPostTurnSideEffects({
1334
+ workstream,
1335
+ workstreamRef,
1336
+ orgRef,
1337
+ userRef,
1338
+ userName,
1339
+ orgIdString,
1340
+ workstreamIdString,
1341
+ onboardingActive,
1342
+ workspace,
1343
+ allAssistantMessages,
1344
+ referenceUserMessage,
1345
+ referenceUserMessageId,
1346
+ recentHistory,
1347
+ listReadableUploads: () => listReadableUploads(),
1348
+ memoryBlock,
1349
+ visibleWorkstreamAgentId,
1350
+ defaultLeadAgentId,
1351
+ latestWorkstreamRecord,
1352
+ latestPersistedState,
1353
+ turnHooks,
1354
+ buildContextResult,
1355
+ isUserTurn: params.kind === 'userTurn',
1356
+ })
1270
1357
  }
1271
1358
 
1272
1359
  if (allAssistantMessages.length > 0) {