@lota-sdk/core 0.4.10 → 0.4.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 (110) hide show
  1. package/package.json +3 -3
  2. package/src/ai-gateway/ai-gateway.ts +214 -98
  3. package/src/ai-gateway/index.ts +16 -1
  4. package/src/config/agent-defaults.ts +4 -120
  5. package/src/config/logger.ts +18 -34
  6. package/src/config/model-constants.ts +1 -0
  7. package/src/config/thread-defaults.ts +1 -18
  8. package/src/create-runtime.ts +90 -28
  9. package/src/db/base.service.ts +30 -38
  10. package/src/db/service.ts +489 -545
  11. package/src/effect/index.ts +0 -2
  12. package/src/effect/layers.ts +6 -13
  13. package/src/embeddings/provider.ts +2 -7
  14. package/src/index.ts +4 -5
  15. package/src/queues/autonomous-job.queue.ts +159 -113
  16. package/src/queues/context-compaction.queue.ts +39 -25
  17. package/src/queues/delayed-node-promotion.queue.ts +56 -29
  18. package/src/queues/document-processor.queue.ts +5 -3
  19. package/src/queues/index.ts +1 -0
  20. package/src/queues/memory-consolidation.queue.ts +79 -53
  21. package/src/queues/organization-learning.queue.ts +63 -39
  22. package/src/queues/plan-agent-heartbeat.queue.ts +104 -79
  23. package/src/queues/plan-scheduler.queue.ts +100 -84
  24. package/src/queues/post-chat-memory.queue.ts +55 -33
  25. package/src/queues/queue-factory.ts +40 -41
  26. package/src/queues/queues.service.ts +61 -0
  27. package/src/queues/title-generation.queue.ts +42 -31
  28. package/src/redis/org-memory-lock.ts +24 -9
  29. package/src/redis/redis-lease-lock.ts +8 -1
  30. package/src/runtime/agent-identity-overrides.ts +7 -3
  31. package/src/runtime/agent-runtime-policy.ts +9 -4
  32. package/src/runtime/agent-stream-helpers.ts +9 -4
  33. package/src/runtime/context-compaction/context-compaction-runtime.ts +28 -32
  34. package/src/runtime/context-compaction/context-compaction.ts +9 -7
  35. package/src/runtime/domain-layer.ts +15 -4
  36. package/src/runtime/execution-plan-visibility.ts +5 -2
  37. package/src/runtime/graph-designer.ts +0 -22
  38. package/src/runtime/index.ts +2 -0
  39. package/src/runtime/indexed-repositories-policy.ts +2 -6
  40. package/src/runtime/live-turn-trace.ts +344 -0
  41. package/src/runtime/plugin-resolution.ts +29 -12
  42. package/src/runtime/post-turn-side-effects.ts +139 -141
  43. package/src/runtime/runtime-config.ts +0 -6
  44. package/src/runtime/runtime-extensions.ts +0 -54
  45. package/src/runtime/runtime-lifecycle.ts +4 -4
  46. package/src/runtime/runtime-services.ts +125 -53
  47. package/src/runtime/runtime-worker-registry.ts +113 -30
  48. package/src/runtime/social-chat/social-chat-agent-runner.ts +6 -3
  49. package/src/runtime/social-chat/social-chat-history.ts +3 -1
  50. package/src/runtime/social-chat/social-chat.ts +35 -20
  51. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +6 -5
  52. package/src/runtime/team-consultation/team-consultation-prompts.ts +11 -6
  53. package/src/runtime/thread-chat-helpers.ts +18 -9
  54. package/src/runtime/thread-turn-context.ts +7 -47
  55. package/src/runtime/turn-lifecycle.ts +6 -14
  56. package/src/services/agent-activity.service.ts +168 -175
  57. package/src/services/agent-executor.service.ts +35 -16
  58. package/src/services/attachment.service.ts +4 -70
  59. package/src/services/autonomous-job.service.ts +53 -61
  60. package/src/services/context-compaction.service.ts +7 -9
  61. package/src/services/execution-plan/execution-plan-graph.ts +106 -115
  62. package/src/services/execution-plan/execution-plan-schedule.ts +1 -15
  63. package/src/services/execution-plan/execution-plan.service.ts +67 -50
  64. package/src/services/global-orchestrator.service.ts +18 -7
  65. package/src/services/graph-full-routing.ts +7 -6
  66. package/src/services/memory/memory-conversation.ts +10 -5
  67. package/src/services/memory/memory.service.ts +11 -8
  68. package/src/services/ownership-dispatcher.service.ts +16 -5
  69. package/src/services/plan/plan-agent-heartbeat.service.ts +29 -15
  70. package/src/services/plan/plan-agent-query.service.ts +12 -8
  71. package/src/services/plan/plan-completion-side-effects.ts +93 -101
  72. package/src/services/plan/plan-cycle.service.ts +7 -45
  73. package/src/services/plan/plan-deadline.service.ts +28 -17
  74. package/src/services/plan/plan-event-delivery.service.ts +47 -40
  75. package/src/services/plan/plan-executor-context.ts +2 -0
  76. package/src/services/plan/plan-executor-graph.ts +366 -391
  77. package/src/services/plan/plan-executor.service.ts +13 -91
  78. package/src/services/plan/plan-scheduler.service.ts +62 -49
  79. package/src/services/plan/plan-transaction-events.ts +1 -1
  80. package/src/services/recent-activity-title.service.ts +6 -2
  81. package/src/services/thread/thread-bootstrap.ts +11 -9
  82. package/src/services/thread/thread-message.service.ts +6 -5
  83. package/src/services/thread/thread-turn-execution.ts +86 -82
  84. package/src/services/thread/thread-turn-preparation.service.ts +92 -45
  85. package/src/services/thread/thread-turn-streaming.ts +60 -28
  86. package/src/services/thread/thread-turn.ts +212 -46
  87. package/src/services/thread/thread.service.ts +21 -6
  88. package/src/system-agents/recent-activity-title-refiner.agent.ts +8 -5
  89. package/src/system-agents/thread-router.agent.ts +23 -20
  90. package/src/tools/execution-plan.tool.ts +8 -3
  91. package/src/tools/fetch-webpage.tool.ts +10 -9
  92. package/src/tools/firecrawl-client.ts +0 -15
  93. package/src/tools/remember-memory.tool.ts +3 -6
  94. package/src/tools/research-topic.tool.ts +12 -3
  95. package/src/tools/search-web.tool.ts +10 -9
  96. package/src/tools/search.tool.ts +4 -5
  97. package/src/tools/team-think.tool.ts +139 -121
  98. package/src/workers/bootstrap.ts +9 -10
  99. package/src/workers/memory-consolidation.worker.ts +4 -1
  100. package/src/workers/organization-learning.worker.ts +15 -2
  101. package/src/workers/regular-chat-memory-digest.helpers.ts +3 -4
  102. package/src/workers/regular-chat-memory-digest.runner.ts +21 -14
  103. package/src/workers/skill-extraction.runner.ts +13 -15
  104. package/src/workers/worker-utils.ts +6 -18
  105. package/src/effect/awaitable-effect.ts +0 -96
  106. package/src/effect/runtime-ref.ts +0 -25
  107. package/src/effect/runtime.ts +0 -46
  108. package/src/redis/runtime-connection.ts +0 -20
  109. package/src/runtime/runtime-accessors.ts +0 -92
  110. package/src/runtime/runtime-token.ts +0 -47
@@ -3,10 +3,10 @@ import { SUBMIT_PLAN_TURN_RESULT_TOOL_NAME, toTimestamp, withMessageCreatedAt }
3
3
  import type { ToolSet, UIMessageStreamWriter } from 'ai'
4
4
  import { Schema, Effect } from 'effect'
5
5
 
6
+ import type { ResolvedAgentConfig } from '../../config/agent-defaults'
6
7
  import { aiLogger } from '../../config/logger'
7
8
  import type { RecordIdRef } from '../../db/record-id'
8
9
  import { effectTryMaybeAsync, effectTryPromise as effectTryPromiseShared } from '../../effect/helpers'
9
- import { runPromise } from '../../effect/runtime'
10
10
  import type { readRuntimeAgentIdentityOverrides } from '../../runtime/agent-identity-overrides'
11
11
  import { resolveRuntimeAgentDisplayName } from '../../runtime/agent-identity-overrides'
12
12
  import { OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES } from '../../runtime/agent-runtime-policy'
@@ -67,6 +67,7 @@ interface VisibleAgentRunParams {
67
67
  }
68
68
 
69
69
  interface VisibleAgentRunnerDeps<TBuildTurnToolParams> {
70
+ agentConfig: ResolvedAgentConfig
70
71
  agentFactoryConfig: { buildAgentTools: (params: TBuildTurnToolParams) => Promise<ToolSet> | ToolSet }
71
72
  buildTurnToolParams: (params: {
72
73
  agentId: string
@@ -100,10 +101,10 @@ export function createThreadTurnVisibleAgentRunner<TBuildTurnToolParams>(
100
101
  { message: Schema.String, cause: Schema.optional(Schema.Defect) },
101
102
  ) {}
102
103
 
103
- const effectTryPromise = <A>(
104
- evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
104
+ const effectTryPromise = <A, R = never>(
105
+ evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown, R>,
105
106
  message: string,
106
- ): Effect.Effect<A, ThreadTurnExecutionError> =>
107
+ ): Effect.Effect<A, ThreadTurnExecutionError, R> =>
107
108
  effectTryPromiseShared(evaluate, (error) => new ThreadTurnExecutionError({ message, cause: error }))
108
109
 
109
110
  function effectFromMaybePromise<A>(
@@ -130,14 +131,13 @@ export function createThreadTurnVisibleAgentRunner<TBuildTurnToolParams>(
130
131
  )
131
132
 
132
133
  const createObserver = (agentId: string) => ({
133
- run: <T>(fn: () => T | Promise<T>) =>
134
- runPromise(
135
- effectTryMaybeAsync(
136
- fn,
137
- (error) =>
138
- new ThreadTurnExecutionError({ message: `Agent observer run failed (agent=${agentId}).`, cause: error }),
139
- ),
140
- ),
134
+ run: async <T>(fn: () => T | Promise<T>): Promise<T> => {
135
+ try {
136
+ return await fn()
137
+ } catch (error) {
138
+ throw new ThreadTurnExecutionError({ message: `Agent observer run failed (agent=${agentId}).`, cause: error })
139
+ }
140
+ },
141
141
  recordError: (error: unknown) => {
142
142
  aiLogger.error`Agent run failed (agent=${agentId}): ${error}`
143
143
  },
@@ -151,54 +151,53 @@ export function createThreadTurnVisibleAgentRunner<TBuildTurnToolParams>(
151
151
  agentId: string,
152
152
  agentName: string,
153
153
  metadataPatch?: NonNullable<MessageMetadata>,
154
- ): Promise<ChatMessage> =>
155
- runPromise(
156
- Effect.gen(function* () {
157
- yield* failIfRunAborted()
158
-
159
- const toCommittedAssistantMessage = (
160
- message: ChatMessage,
161
- resolvedAgentId: string,
162
- resolvedAgentName: string,
163
- patch?: NonNullable<MessageMetadata>,
164
- ) =>
165
- withMessageCreatedAt(
166
- {
167
- ...message,
168
- metadata: {
169
- ...message.metadata,
170
- ...buildAgentMetadataPatch(resolvedAgentId, resolvedAgentName),
171
- ...patch,
172
- },
173
- },
174
- toTimestamp(message.metadata?.createdAt) ?? nowEpochMillis(),
175
- )
176
-
177
- const committedConsultMessages = collectCompletedConsultTeamMessages({ responseMessage: response }).flatMap(
178
- (consultMessage) => {
179
- const consultAgentId = readOptionalString(consultMessage.metadata?.agentId)
180
- const consultAgentName = readOptionalString(consultMessage.metadata?.agentName)
181
- if (!consultAgentId || !consultAgentName) {
182
- return []
183
- }
184
-
185
- return [toCommittedAssistantMessage(consultMessage, consultAgentId, consultAgentName)]
154
+ ) =>
155
+ Effect.gen(function* () {
156
+ // Persist the response even if the run is being interrupted (e.g., lease
157
+ // refresh was rejected between the final stream chunk and this commit).
158
+ // The LLM tokens have already been consumed; the message must reach the
159
+ // thread history so callers and post-turn side effects observe the
160
+ // completed turn. `Effect.uninterruptible` shields the persist from the
161
+ // surrounding interrupt without blocking later steps from handling it.
162
+ const toCommittedAssistantMessage = (
163
+ message: ChatMessage,
164
+ resolvedAgentId: string,
165
+ resolvedAgentName: string,
166
+ patch?: NonNullable<MessageMetadata>,
167
+ ) =>
168
+ withMessageCreatedAt(
169
+ {
170
+ ...message,
171
+ metadata: { ...message.metadata, ...buildAgentMetadataPatch(resolvedAgentId, resolvedAgentName), ...patch },
186
172
  },
173
+ toTimestamp(message.metadata?.createdAt) ?? nowEpochMillis(),
187
174
  )
188
175
 
189
- const committed = toCommittedAssistantMessage(response, agentId, agentName, metadataPatch)
190
- const messagesToPersist = [...committedConsultMessages, committed]
191
- yield* deps.threadMessageService.upsertMessagesEffect({ threadId: deps.threadRef, messages: messagesToPersist })
176
+ const committedConsultMessages = collectCompletedConsultTeamMessages({ responseMessage: response }).flatMap(
177
+ (consultMessage) => {
178
+ const consultAgentId = readOptionalString(consultMessage.metadata?.agentId)
179
+ const consultAgentName = readOptionalString(consultMessage.metadata?.agentName)
180
+ if (!consultAgentId || !consultAgentName) {
181
+ return []
182
+ }
192
183
 
193
- for (const persistedMessage of messagesToPersist) {
194
- deps.threadTurnMessageContext.appendMessages([persistedMessage])
195
- deps.onPersistedMessage(persistedMessage)
196
- }
197
- yield* failIfRunAborted()
184
+ return [toCommittedAssistantMessage(consultMessage, consultAgentId, consultAgentName)]
185
+ },
186
+ )
198
187
 
199
- return committed
200
- }),
201
- )
188
+ const committed = toCommittedAssistantMessage(response, agentId, agentName, metadataPatch)
189
+ const messagesToPersist = [...committedConsultMessages, committed]
190
+ yield* Effect.uninterruptible(
191
+ deps.threadMessageService.upsertMessagesEffect({ threadId: deps.threadRef, messages: messagesToPersist }),
192
+ )
193
+
194
+ for (const persistedMessage of messagesToPersist) {
195
+ deps.threadTurnMessageContext.appendMessages([persistedMessage])
196
+ deps.onPersistedMessage(persistedMessage)
197
+ }
198
+
199
+ return committed
200
+ })
202
201
 
203
202
  const runVisibleAgentEffect = Effect.fn('ThreadTurnExecution.runVisibleAgent')(function* (
204
203
  runParams: VisibleAgentRunParams,
@@ -243,36 +242,41 @@ export function createThreadTurnVisibleAgentRunner<TBuildTurnToolParams>(
243
242
  ).pipe(Effect.withSpan('ThreadTurnExecution.streamVisibleAgent'))
244
243
  deps.setMemoryBlock(runMemoryBlock)
245
244
 
246
- return yield* effectTryPromise(
247
- () =>
248
- commitAssistantResponse(
249
- responseMessage,
250
- runParams.agentId,
251
- resolveRuntimeAgentDisplayName(deps.agentIdentityOverrides, runParams.agentId),
252
- runParams.metadataPatch,
253
- ),
254
- `Failed to commit assistant response for ${runParams.agentId}.`,
255
- ).pipe(Effect.withSpan('ThreadTurnExecution.commitAssistantResponse'))
245
+ // Commit the response under `uninterruptible` so a late lease-lost or
246
+ // stop signal arriving after the stream completed still writes the
247
+ // already-generated assistant message into the thread history. Without
248
+ // this, a race between stream completion and lease refresh rejection
249
+ // swallows the response without any error trace.
250
+ return yield* Effect.uninterruptible(
251
+ effectTryPromise(
252
+ () =>
253
+ commitAssistantResponse(
254
+ responseMessage,
255
+ runParams.agentId,
256
+ resolveRuntimeAgentDisplayName(deps.agentConfig, deps.agentIdentityOverrides, runParams.agentId),
257
+ runParams.metadataPatch,
258
+ ),
259
+ `Failed to commit assistant response for ${runParams.agentId}.`,
260
+ ).pipe(Effect.withSpan('ThreadTurnExecution.commitAssistantResponse')),
261
+ )
256
262
  })
257
263
 
258
- const runVisibleAgent = (runParams: VisibleAgentRunParams): Promise<ChatMessage> =>
259
- runPromise(
260
- runVisibleAgentEffect(runParams).pipe(
261
- Effect.annotateSpans(
262
- compactSpanAttributes({
263
- ...buildThreadTurnSpanAttributes({
264
- threadRef: deps.threadRef,
265
- orgRef: deps.streamCtx.orgRef,
266
- userRef: deps.streamCtx.userRef,
267
- agentId: runParams.agentId,
268
- threadType: deps.streamCtx.thread.type,
269
- mode: runParams.mode,
270
- }),
271
- includeExecutionPlanTools: runParams.includeExecutionPlanTools ?? runParams.mode !== 'fixedThreadMode',
272
- extraMessageCount: runParams.extraMessages?.length ?? 0,
273
- extraToolCount: runParams.extraTools ? Object.keys(runParams.extraTools).length : 0,
264
+ const runVisibleAgent = (runParams: VisibleAgentRunParams) =>
265
+ runVisibleAgentEffect(runParams).pipe(
266
+ Effect.annotateSpans(
267
+ compactSpanAttributes({
268
+ ...buildThreadTurnSpanAttributes({
269
+ threadRef: deps.threadRef,
270
+ orgRef: deps.streamCtx.orgRef,
271
+ userRef: deps.streamCtx.userRef,
272
+ agentId: runParams.agentId,
273
+ threadType: deps.streamCtx.thread.type,
274
+ mode: runParams.mode,
274
275
  }),
275
- ),
276
+ includeExecutionPlanTools: runParams.includeExecutionPlanTools ?? runParams.mode !== 'fixedThreadMode',
277
+ extraMessageCount: runParams.extraMessages?.length ?? 0,
278
+ extraToolCount: runParams.extraTools ? Object.keys(runParams.extraTools).length : 0,
279
+ }),
276
280
  ),
277
281
  )
278
282
 
@@ -13,12 +13,6 @@ import type { UIMessageStreamWriter } from 'ai'
13
13
  import { Clock, Context, Schema, Effect, Layer } from 'effect'
14
14
 
15
15
  import type { CoreThreadProfile } from '../../config/agent-defaults'
16
- import {
17
- getAgentRoster,
18
- getLeadAgentId,
19
- getCoreThreadProfile,
20
- getResolvedAgentFactoryConfig,
21
- } from '../../config/agent-defaults'
22
16
  import { aiLogger } from '../../config/logger'
23
17
  import type { RecordIdRef } from '../../db/record-id'
24
18
  import { recordIdToString } from '../../db/record-id'
@@ -26,8 +20,15 @@ import { TABLES } from '../../db/tables'
26
20
  import type { DatabaseError, ServiceError } from '../../effect/errors'
27
21
  import { ThreadTurnError } from '../../effect/errors'
28
22
  import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
29
- import { enqueueContextCompaction } from '../../queues/context-compaction.queue'
30
- import { enqueueThreadTitleGeneration } from '../../queues/title-generation.queue'
23
+ import {
24
+ AgentConfigServiceTag,
25
+ AgentFactoryServiceTag,
26
+ RuntimeAdaptersServiceTag,
27
+ TurnHooksServiceTag,
28
+ } from '../../effect/services'
29
+ import type { ContextCompactionQueueRuntime } from '../../queues/context-compaction.queue'
30
+ import { LotaQueuesServiceTag } from '../../queues/queues.service'
31
+ import type { TitleGenerationQueueRuntime } from '../../queues/title-generation.queue'
31
32
  import {
32
33
  readRuntimeAgentIdentityOverrides,
33
34
  resolveRuntimeAgentDisplayName,
@@ -42,7 +43,6 @@ import { createExecutionPlanInstructionSectionCache, toExecutionPlanCacheError }
42
43
  import type { HelperModelRuntime } from '../../runtime/helper-model'
43
44
  import { HelperModelTag } from '../../runtime/helper-model'
44
45
  import { runPostTurnSideEffects } from '../../runtime/post-turn-side-effects'
45
- import { getRuntimeAdapters, getTurnHooks } from '../../runtime/runtime-extensions'
46
46
  import { extractMessageText, toOptionalTrimmedString } from '../../runtime/thread-chat-helpers'
47
47
  import {
48
48
  buildPlanTurnInstructionSections,
@@ -93,6 +93,8 @@ interface ThreadTurnPreparationServiceContext {
93
93
  threadMessageService: ReturnType<typeof makeThreadMessageService>
94
94
  threadService: ReturnType<typeof makeThreadService>
95
95
  contextCompactionRuntime: ReturnType<typeof createWiredContextCompactionRuntime>
96
+ contextCompactionQueue: ContextCompactionQueueRuntime
97
+ titleGenerationQueue: TitleGenerationQueueRuntime
96
98
  }
97
99
 
98
100
  type ThreadTurnPreparationRuntimeDeps = ThreadTurnPreparationServiceContext
@@ -365,8 +367,10 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
365
367
  deps: ThreadTurnPreparationRuntimeDeps,
366
368
  params: ThreadRunCoreParams,
367
369
  ) {
368
- const runtimeAdapters = getRuntimeAdapters()
369
- const turnHooks = getTurnHooks()
370
+ const runtimeAdapters = yield* RuntimeAdaptersServiceTag
371
+ const turnHooks = yield* TurnHooksServiceTag
372
+ const agentConfig = yield* AgentConfigServiceTag
373
+ const agentFactoryConfig = yield* AgentFactoryServiceTag
370
374
  const workspaceProvider = runtimeAdapters.workspaceProvider
371
375
  const workspacePromise =
372
376
  params.kind !== 'approvalContinuation' && workspaceProvider
@@ -488,17 +492,19 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
488
492
  threadRecord.title === THREAD.DEFAULT_TITLE &&
489
493
  messageText.length > 0
490
494
  ) {
491
- void safeEnqueue(() => enqueueThreadTitleGeneration({ threadId: threadIdString, sourceText: messageText }), {
492
- operationName: 'thread-title-generation',
493
- })
495
+ void safeEnqueue(
496
+ () =>
497
+ deps.titleGenerationQueue.enqueueThreadTitleGeneration({ threadId: threadIdString, sourceText: messageText }),
498
+ { operationName: 'thread-title-generation' },
499
+ )
494
500
  }
495
501
 
496
502
  if (thread.type === 'thread' && !thread.threadType) {
497
503
  return yield* new ThreadTurnError({ message: 'Core threads require a thread type.', reason: 'bad-request' })
498
504
  }
499
505
  const coreThreadProfile: CoreThreadProfile | null =
500
- thread.type === 'thread' && thread.threadType ? getCoreThreadProfile(thread.threadType) : null
501
- const defaultLeadAgentId = getLeadAgentId()
506
+ thread.type === 'thread' && thread.threadType ? agentConfig.getCoreThreadProfile(thread.threadType) : null
507
+ const defaultLeadAgentId = agentConfig.leadAgentId
502
508
  const visibleThreadAgentId =
503
509
  params.agentIdOverride ??
504
510
  (thread.type === 'default' ? thread.agentId : (coreThreadProfile?.config.agentId ?? defaultLeadAgentId))
@@ -517,6 +523,8 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
517
523
  workspacePromise,
518
524
  workspaceProvider,
519
525
  turnHooks,
526
+ runtimeAdapters,
527
+ pluginRuntime: agentFactoryConfig.pluginRuntime,
520
528
  }),
521
529
  'Failed to assemble thread turn context.',
522
530
  ).pipe(Effect.withSpan('ThreadTurnPreparation.assembleThreadTurnContext'))
@@ -674,7 +682,11 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
674
682
  serverRunId: string
675
683
  runAbort: ReturnType<typeof createServerRunAbortController>
676
684
  writer?: UIMessageStreamWriter<ChatMessage>
677
- }): Effect.Effect<PreparedThreadTurnResult | void, DatabaseError | ThreadTurnError | ThreadTurnPreparationError> =>
685
+ }): Effect.Effect<
686
+ PreparedThreadTurnResult | void,
687
+ DatabaseError | ThreadTurnError | ThreadTurnPreparationError,
688
+ unknown
689
+ > =>
678
690
  Effect.ensuring(
679
691
  Effect.gen(function* () {
680
692
  const currentRunAbort = runParams.runAbort
@@ -693,8 +705,6 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
693
705
  .pipe(Effect.withSpan('ThreadTurnPreparation.setActiveTurn'))
694
706
  }
695
707
 
696
- const agentFactoryConfig = getResolvedAgentFactoryConfig()
697
-
698
708
  const streamCtx: StreamAgentResponseContext = {
699
709
  turnHooks,
700
710
  thread,
@@ -717,6 +727,7 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
717
727
  }
718
728
 
719
729
  const { runVisibleAgent } = createThreadTurnVisibleAgentRunner({
730
+ agentConfig,
720
731
  agentFactoryConfig: { buildAgentTools: agentFactoryConfig.buildAgentTools },
721
732
  buildTurnToolParams,
722
733
  threadTurnMessageContext,
@@ -778,7 +789,7 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
778
789
  ).pipe(Effect.withSpan('ThreadTurnPreparation.executeDirectThread'))
779
790
  } else {
780
791
  const wsMembers = (thread as { members?: string[] }).members ?? []
781
- const members = wsMembers.length > 0 ? wsMembers : [...getAgentRoster()]
792
+ const members = wsMembers.length > 0 ? wsMembers : [...agentConfig.roster]
782
793
  const fallbackAgentId = coreThreadProfile?.config.agentId ?? defaultLeadAgentId
783
794
  yield* failIfRunAborted()
784
795
  writeMultiAgentEvent(runParams.writer, { phase: 'routing', note: 'Routing this turn to the right agent.' })
@@ -791,6 +802,7 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
791
802
  const triageResult = yield* effectTryPromise(
792
803
  () =>
793
804
  triageThreadMessage({
805
+ agentConfig,
794
806
  threadTitle: thread.title,
795
807
  members,
796
808
  messageText,
@@ -841,6 +853,7 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
841
853
  const checkResult = yield* effectTryPromise(
842
854
  () =>
843
855
  checkForNextAgent({
856
+ agentConfig,
844
857
  threadTitle: thread.title,
845
858
  members,
846
859
  messageText,
@@ -859,7 +872,7 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
859
872
  writeMultiAgentEvent(runParams.writer, {
860
873
  phase: 'waiting-for-agent',
861
874
  agentId: checkResult.agentId,
862
- agentName: resolveRuntimeAgentDisplayName(agentIdentityOverrides, checkResult.agentId),
875
+ agentName: resolveRuntimeAgentDisplayName(agentConfig, agentIdentityOverrides, checkResult.agentId),
863
876
  note: checkResult.routingContext ?? undefined,
864
877
  })
865
878
 
@@ -890,7 +903,7 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
890
903
  writeMultiAgentEvent(runParams.writer, {
891
904
  phase: 'agent-message-persisted',
892
905
  agentId: checkResult.agentId,
893
- agentName: resolveRuntimeAgentDisplayName(agentIdentityOverrides, checkResult.agentId),
906
+ agentName: resolveRuntimeAgentDisplayName(agentConfig, agentIdentityOverrides, checkResult.agentId),
894
907
  messageId: lastResponse.id,
895
908
  })
896
909
  }
@@ -919,7 +932,7 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
919
932
  contextSize: CONTEXT_WINDOW_TOKENS,
920
933
  }),
921
934
  enqueueCompaction: () =>
922
- enqueueContextCompaction({
935
+ deps.contextCompactionQueue.enqueueContextCompaction({
923
936
  domain: 'thread',
924
937
  entityId: threadIdString,
925
938
  contextSize: CONTEXT_WINDOW_TOKENS,
@@ -980,27 +993,9 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
980
993
  ).pipe(Effect.withSpan('ThreadTurnPreparation.runPostTurnSideEffects'))
981
994
  }
982
995
 
983
- if (allAssistantMessages.length > 0 && params.kind !== 'planTurn') {
984
- const afterTurn = turnHooks.afterTurn
985
- if (afterTurn) {
986
- yield* effectTryPromise(
987
- () =>
988
- afterTurn({
989
- thread,
990
- threadRef,
991
- orgRef,
992
- userRef,
993
- userName,
994
- onboardingActive,
995
- referenceUserMessage,
996
- assistantMessages: allAssistantMessages,
997
- latestThreadRecord,
998
- context: buildContextResult,
999
- }),
1000
- 'Failed to run afterTurn hook.',
1001
- ).pipe(Effect.withSpan('ThreadTurnPreparation.afterTurnHook'))
1002
- }
1003
- }
996
+ yield* Effect.sync(() => {
997
+ launchAfterTurnHook(latestThreadRecord)
998
+ })
1004
999
  }).pipe(
1005
1000
  Effect.catch((postRunError) =>
1006
1001
  Effect.sync(() => {
@@ -1021,6 +1016,48 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
1021
1016
  assistantMessages: [...allAssistantMessages],
1022
1017
  })
1023
1018
 
1019
+ const launchAfterTurnHook = (latestThreadRecord: typeof threadRecord) => {
1020
+ if (allAssistantMessages.length === 0 || params.kind === 'planTurn') {
1021
+ return
1022
+ }
1023
+
1024
+ const afterTurn = turnHooks.afterTurn
1025
+ if (!afterTurn) {
1026
+ return
1027
+ }
1028
+
1029
+ // `afterTurn` is host-owned follow-up work. Launch it detached so the
1030
+ // streamed turn closes after persistence/finalization instead of waiting on
1031
+ // onboarding/map side effects in the request path.
1032
+ void runPromiseWithCurrentContext(
1033
+ effectTryPromise(
1034
+ () =>
1035
+ afterTurn({
1036
+ thread,
1037
+ threadRef,
1038
+ orgRef,
1039
+ userRef,
1040
+ userName,
1041
+ onboardingActive,
1042
+ referenceUserMessage,
1043
+ assistantMessages: allAssistantMessages,
1044
+ latestThreadRecord,
1045
+ context: buildContextResult,
1046
+ }),
1047
+ 'Failed to run afterTurn hook.',
1048
+ ).pipe(
1049
+ Effect.withSpan('ThreadTurnPreparation.afterTurnHook'),
1050
+ Effect.catch((error) =>
1051
+ Effect.sync(() => {
1052
+ aiLogger.error`Thread afterTurn hook failed: ${error}`
1053
+ }),
1054
+ ),
1055
+ ),
1056
+ ).catch((error) => {
1057
+ aiLogger.error`Thread afterTurn hook scheduling failed: ${error}`
1058
+ })
1059
+ }
1060
+
1024
1061
  const run = (writer?: UIMessageStreamWriter<ChatMessage>) => {
1025
1062
  const serverRunId = Bun.randomUUIDv7()
1026
1063
 
@@ -1049,7 +1086,10 @@ const prepareThreadRunCoreEffect = Effect.fn('ThreadTurnPreparation.prepareThrea
1049
1086
  }),
1050
1087
  )
1051
1088
 
1052
- return runEffect.pipe(Effect.withSpan('ThreadTurnPreparation.executeRun'), Effect.provide(currentContext))
1089
+ return runEffect.pipe(
1090
+ Effect.withSpan('ThreadTurnPreparation.executeRun'),
1091
+ Effect.provide(currentContext),
1092
+ ) as Effect.Effect<PreparedThreadTurnResult, DatabaseError | ThreadTurnError | ThreadTurnPreparationError, never>
1053
1093
  }
1054
1094
 
1055
1095
  return { originalMessages, run }
@@ -1066,6 +1106,8 @@ interface ThreadTurnPreparationDeps {
1066
1106
  threadMessage: ReturnType<typeof makeThreadMessageService>
1067
1107
  thread: ReturnType<typeof makeThreadService>
1068
1108
  helperModelRuntime: HelperModelRuntime
1109
+ contextCompactionQueue: ContextCompactionQueueRuntime
1110
+ titleGenerationQueue: TitleGenerationQueueRuntime
1069
1111
  }
1070
1112
 
1071
1113
  export function makeThreadTurnPreparationService(deps: ThreadTurnPreparationDeps) {
@@ -1084,6 +1126,8 @@ export function makeThreadTurnPreparationService(deps: ThreadTurnPreparationDeps
1084
1126
  now: nowEpochMillis,
1085
1127
  randomId: () => Bun.randomUUIDv7(),
1086
1128
  }),
1129
+ contextCompactionQueue: deps.contextCompactionQueue,
1130
+ titleGenerationQueue: deps.titleGenerationQueue,
1087
1131
  }
1088
1132
  const annotateTurnSpans = <A, E, R>(params: ThreadRunCoreParams, effect: Effect.Effect<A, E, R>) =>
1089
1133
  effect.pipe(
@@ -1132,6 +1176,7 @@ export const ThreadTurnPreparationServiceLive = Layer.effect(
1132
1176
  const planRun = yield* PlanRunServiceTag
1133
1177
  const threadMessage = yield* ThreadMessageServiceTag
1134
1178
  const thread = yield* ThreadServiceTag
1179
+ const queues = yield* LotaQueuesServiceTag
1135
1180
  return makeThreadTurnPreparationService({
1136
1181
  attachment,
1137
1182
  chatRunRegistry,
@@ -1143,6 +1188,8 @@ export const ThreadTurnPreparationServiceLive = Layer.effect(
1143
1188
  threadMessage,
1144
1189
  thread,
1145
1190
  helperModelRuntime: yield* HelperModelTag,
1191
+ contextCompactionQueue: queues.contextCompaction,
1192
+ titleGenerationQueue: queues.titleGeneration,
1146
1193
  })
1147
1194
  }),
1148
1195
  )