@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
@@ -4,18 +4,18 @@ import { convertToModelMessages, stepCountIs } from 'ai'
4
4
  import type { PrepareStepFunction, StopCondition, ToolSet, UIMessageStreamWriter } from 'ai'
5
5
  import { Effect, Ref, Schema, Stream } from 'effect'
6
6
 
7
- import { getAgentRuntimeConfig, getResolvedAgentFactoryConfig } from '../../config/agent-defaults'
8
7
  import { aiLogger } from '../../config/logger'
9
8
  import type { RecordIdRef } from '../../db/record-id'
10
9
  import { effectTryMaybeAsync, makeEffectTryPromiseWithMessage } from '../../effect/helpers'
11
- import { runPromise } from '../../effect/runtime'
10
+ import { AgentConfigServiceTag, AgentFactoryServiceTag } from '../../effect/services'
12
11
  import {
13
12
  readRuntimeAgentIdentityOverrides,
14
13
  resolveRuntimeAgentDisplayName,
15
14
  } from '../../runtime/agent-identity-overrides'
16
15
  import { createAgentMessageMetadata } from '../../runtime/agent-stream-helpers'
17
16
  import { mergeInstructionSections } from '../../runtime/instruction-sections'
18
- import type { getTurnHooks } from '../../runtime/runtime-extensions'
17
+ import { createLiveTurnTraceStreamObserver } from '../../runtime/live-turn-trace'
18
+ import type { LotaRuntimeTurnHooks } from '../../runtime/runtime-extensions'
19
19
  import {
20
20
  asRecord,
21
21
  collectToolOutputErrors,
@@ -122,7 +122,7 @@ function buildFallbackResponseMessage(
122
122
  }
123
123
 
124
124
  export interface StreamAgentResponseContext {
125
- turnHooks: ReturnType<typeof getTurnHooks>
125
+ turnHooks: LotaRuntimeTurnHooks
126
126
  thread: NormalizedThread
127
127
  threadRef: RecordIdRef
128
128
  orgRef: RecordIdRef
@@ -226,8 +226,9 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
226
226
  (streamParams.skills ?? []).some((skill) => skill.startsWith('cpo-') || skill.startsWith('mentor-')) ||
227
227
  resolvedAgentId === 'cpo' ||
228
228
  resolvedAgentId === 'mentor'
229
- const agentFactoryConfig = getResolvedAgentFactoryConfig()
230
- const config = getAgentRuntimeConfig({
229
+ const agentFactoryConfig = yield* AgentFactoryServiceTag
230
+ const agentConfig = yield* AgentConfigServiceTag
231
+ const config = agentFactoryConfig.getAgentRuntimeConfig({
231
232
  agentId: resolvedAgentId,
232
233
  threadType: ctx.thread.type,
233
234
  mode: streamParams.mode,
@@ -277,6 +278,7 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
277
278
  prepareStep: (agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
278
279
  })
279
280
  const agentAbortSignal = streamParams.abortSignal ?? ctx.runAbortSignal
281
+ const resolvedAgentName = resolveRuntimeAgentDisplayName(agentConfig, agentIdentityOverrides, resolvedAgentId)
280
282
 
281
283
  const generateFallback = (cause: ThreadTurnStreamingError) =>
282
284
  effectTryPromise(
@@ -291,6 +293,34 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
291
293
  Effect.flatMap((result) => buildFallbackResponseMessage(result as ToolLoopGenerateResult)),
292
294
  )
293
295
 
296
+ const generateWithoutUiStream = effectTryPromise(
297
+ () => streamParams.observer.run(() => agent.generate({ messages: modelMessages, abortSignal: agentAbortSignal })),
298
+ `Agent generate failed for ${resolvedAgentId}.`,
299
+ ).pipe(
300
+ Effect.tapError((error) =>
301
+ Effect.sync(() => {
302
+ if (agentAbortSignal.aborted) {
303
+ streamParams.observer.recordAbort(error)
304
+ return
305
+ }
306
+
307
+ streamParams.observer.recordError(error)
308
+ }),
309
+ ),
310
+ Effect.withSpan('ThreadTurnStreaming.startAgentGenerate'),
311
+ Effect.flatMap((result) => buildFallbackResponseMessage(result as ToolLoopGenerateResult)),
312
+ )
313
+
314
+ if (!streamParams.writer) {
315
+ const generatedResponse = yield* generateWithoutUiStream
316
+
317
+ for (const toolError of collectToolOutputErrors({ responseMessage: generatedResponse })) {
318
+ aiLogger.error`Tool execution failed (agent=${resolvedAgentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
319
+ }
320
+
321
+ return generatedResponse
322
+ }
323
+
294
324
  const result = yield* effectTryPromise(
295
325
  () => streamParams.observer.run(() => agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal })),
296
326
  `Agent stream failed for ${resolvedAgentId}.`,
@@ -320,14 +350,19 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
320
350
  originalMessages: streamParams.messages,
321
351
  sendReasoning: true,
322
352
  sendSources: true,
323
- messageMetadata: createAgentMessageMetadata({
324
- agentId: resolvedAgentId,
325
- agentName: resolveRuntimeAgentDisplayName(agentIdentityOverrides, resolvedAgentId),
326
- }),
353
+ messageMetadata: createAgentMessageMetadata({ agentId: resolvedAgentId, agentName: resolvedAgentName }),
327
354
  onFinish: ({ responseMessage: finishedResponseMessage }: { responseMessage: ChatMessage }) => {
328
355
  resolveFinishedStream(withMessageCreatedAt(finishedResponseMessage, nowEpochMillis()))
329
356
  },
330
357
  }) as ReadableStream<ChatStreamChunk>
358
+ const liveTurnTrace = streamParams.writer
359
+ ? createLiveTurnTraceStreamObserver({
360
+ traceId: `trace:${Bun.randomUUIDv7()}`,
361
+ writer: streamParams.writer,
362
+ agentId: resolvedAgentId,
363
+ agentName: resolvedAgentName,
364
+ })
365
+ : null
331
366
  const streamStartedAt = performance.now()
332
367
  const firstVisibleOutputRecorded = yield* Ref.make(false)
333
368
  const firstTextTokenRecorded = yield* Ref.make(false)
@@ -361,6 +396,7 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
361
396
  if (streamParams.writer) {
362
397
  yield* Effect.sync(() => {
363
398
  streamParams.writer?.write(value)
399
+ liveTurnTrace?.observeChunk(value)
364
400
  })
365
401
  }
366
402
  }),
@@ -371,6 +407,7 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
371
407
  ),
372
408
  Effect.catchTag('ThreadTurnStreamingError', generateFallback),
373
409
  )
410
+ liveTurnTrace?.finish()
374
411
 
375
412
  for (const toolError of collectToolOutputErrors({ responseMessage: streamedResponse })) {
376
413
  aiLogger.error`Tool execution failed (agent=${resolvedAgentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
@@ -379,25 +416,20 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
379
416
  return streamedResponse
380
417
  })
381
418
 
382
- export function streamAgentResponse(
383
- ctx: StreamAgentResponseContext,
384
- streamParams: StreamAgentResponseParams,
385
- ): Promise<ChatMessage> {
386
- return runPromise(
387
- streamAgentResponseEffect(ctx, streamParams).pipe(
388
- Effect.annotateSpans(
389
- compactSpanAttributes({
390
- ...buildThreadTurnSpanAttributes({
391
- threadRef: ctx.threadRef,
392
- orgRef: ctx.orgRef,
393
- userRef: ctx.userRef,
394
- agentId: streamParams.agentId,
395
- threadType: ctx.thread.type,
396
- mode: streamParams.mode,
397
- }),
398
- includeExecutionPlanTools: streamParams.includeExecutionPlanTools ?? true,
419
+ export function streamAgentResponse(ctx: StreamAgentResponseContext, streamParams: StreamAgentResponseParams) {
420
+ return streamAgentResponseEffect(ctx, streamParams).pipe(
421
+ Effect.annotateSpans(
422
+ compactSpanAttributes({
423
+ ...buildThreadTurnSpanAttributes({
424
+ threadRef: ctx.threadRef,
425
+ orgRef: ctx.orgRef,
426
+ userRef: ctx.userRef,
427
+ agentId: streamParams.agentId,
428
+ threadType: ctx.thread.type,
429
+ mode: streamParams.mode,
399
430
  }),
400
- ),
431
+ includeExecutionPlanTools: streamParams.includeExecutionPlanTools ?? true,
432
+ }),
401
433
  ),
402
434
  )
403
435
  }
@@ -2,45 +2,84 @@ import type { ChatMessage } from '@lota-sdk/shared'
2
2
  import { createUIMessageStream } from 'ai'
3
3
  import { Context, Schema, Effect, Layer } from 'effect'
4
4
 
5
+ import type { ResolvedAgentConfig } from '../../config/agent-defaults'
5
6
  import { ensureRecordId, recordIdToString } from '../../db/record-id'
6
7
  import { TABLES } from '../../db/tables'
7
- import { BadRequestError } from '../../effect/errors'
8
- import { runPromise } from '../../effect/runtime'
8
+ import { BadRequestError, ForbiddenError } from '../../effect/errors'
9
+ import { AgentConfigServiceTag } from '../../effect/services'
9
10
  import { hasApprovalRespondedParts, isApprovalContinuationRequest } from '../../runtime/approval-continuation'
10
11
  import { shouldPlanNodeUseVisibleTurn } from '../../runtime/execution-plan-visibility'
11
12
  import { wrapResponseWithKeepalive } from '../../utils/sse-keepalive'
13
+ import { BackgroundWorkService } from '../background-work.service'
12
14
  import type { makePlanExecutorService } from '../plan/plan-executor.service'
13
15
  import { PlanExecutorServiceTag } from '../plan/plan-executor.service'
14
16
  import type { makePlanRunService } from '../plan/plan-run.service'
15
17
  import { PlanRunServiceTag } from '../plan/plan-run.service'
16
18
  import type { makeUserService } from '../user.service'
17
19
  import { UserServiceTag } from '../user.service'
20
+ import type { makeThreadMessageService } from './thread-message.service'
21
+ import { ThreadMessageServiceTag } from './thread-message.service'
18
22
  import type {
19
23
  PreparedThreadTurnResult,
20
24
  ThreadApprovalContinuationParams,
21
- ThreadPlanTurnParams,
22
25
  ThreadTurnParams,
26
+ ThreadPlanTurnParams,
23
27
  makeThreadTurnPreparationService,
24
28
  } from './thread-turn-preparation.service'
25
29
  import { ThreadTurnPreparationServiceTag } from './thread-turn-preparation.service'
26
30
  import { buildThreadTurnSpanAttributes, compactSpanAttributes } from './thread-turn-tracing'
27
31
  import type { makeThreadService } from './thread.service'
28
32
  import { ThreadServiceTag } from './thread.service'
33
+ import type { NormalizedThread } from './thread.types'
29
34
 
30
35
  export { hasApprovalRespondedParts, isApprovalContinuationRequest }
31
36
  export { wrapResponseWithKeepalive }
32
37
  export type { PreparedThreadTurnResult }
33
38
  export type { ThreadPlanTurnParams }
34
39
 
40
+ export interface BackgroundThreadLaunchMessage {
41
+ parts: ChatMessage['parts']
42
+ metadata?: ChatMessage['metadata']
43
+ }
44
+
45
+ export interface LaunchBackgroundThreadWorkParams {
46
+ sourceThreadId: Parameters<typeof ensureRecordId>[0]
47
+ orgRef: Parameters<typeof ensureRecordId>[0]
48
+ userRef: Parameters<typeof ensureRecordId>[0]
49
+ userName?: string | null
50
+ targetThreadId?: Parameters<typeof ensureRecordId>[0]
51
+ projectTitle?: string
52
+ targetAgentId?: string
53
+ handoff: BackgroundThreadLaunchMessage
54
+ input: BackgroundThreadLaunchMessage
55
+ abortSignal?: AbortSignal
56
+ streamId?: string
57
+ }
58
+
59
+ export interface LaunchBackgroundThreadWorkResult {
60
+ launched: boolean
61
+ threadId: string
62
+ threadTitle: string
63
+ sourceThreadId: string
64
+ targetAgentId?: string
65
+ handoffMessageId: string
66
+ createdThread: boolean
67
+ message: string
68
+ }
69
+
35
70
  class ThreadTurnServiceError extends Schema.TaggedErrorClass<ThreadTurnServiceError>()('ThreadTurnServiceError', {
36
71
  message: Schema.String,
37
72
  cause: Schema.optional(Schema.Defect),
38
73
  }) {}
39
74
 
40
75
  interface ThreadTurnDeps {
76
+ agentConfig: ResolvedAgentConfig
77
+ background: Context.Service.Shape<typeof BackgroundWorkService>
41
78
  planExecutor: ReturnType<typeof makePlanExecutorService>
42
79
  planRun: ReturnType<typeof makePlanRunService>
80
+ provideCurrentContext: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, never>
43
81
  thread: ReturnType<typeof makeThreadService>
82
+ threadMessage: ReturnType<typeof makeThreadMessageService>
44
83
  threadTurnPreparation: ReturnType<typeof makeThreadTurnPreparationService>
45
84
  user: ReturnType<typeof makeUserService>
46
85
  }
@@ -170,6 +209,129 @@ function runThreadTurnInBackgroundWith(deps: ThreadTurnDeps, params: ThreadTurnP
170
209
  )
171
210
  }
172
211
 
212
+ function buildBackgroundLaunchMessage(params: { createdThread: boolean; threadTitle: string }) {
213
+ return params.createdThread
214
+ ? `Background work launched in "${params.threadTitle}".`
215
+ : `Background work launched in existing thread "${params.threadTitle}".`
216
+ }
217
+
218
+ const launchBackgroundThreadWorkEffect = Effect.fn('ThreadTurn.launchBackgroundThreadWork')(function* (
219
+ deps: ThreadTurnDeps,
220
+ params: LaunchBackgroundThreadWorkParams,
221
+ ) {
222
+ const orgIdString = recordIdToString(params.orgRef, TABLES.ORGANIZATION)
223
+ const userIdString = recordIdToString(params.userRef, TABLES.USER)
224
+ const sourceThreadId = recordIdToString(params.sourceThreadId, TABLES.THREAD)
225
+
226
+ const resolveTargetThread = (): Effect.Effect<
227
+ { thread: NormalizedThread; createdThread: boolean },
228
+ BadRequestError | ForbiddenError | ThreadTurnServiceError
229
+ > =>
230
+ Effect.gen(function* () {
231
+ if (params.targetThreadId) {
232
+ const existingThread = yield* deps.thread
233
+ .getThread(params.targetThreadId)
234
+ .pipe(
235
+ Effect.mapError((cause) => new ThreadTurnServiceError({ message: 'Failed to load target thread.', cause })),
236
+ )
237
+ if (existingThread.organizationId !== orgIdString) {
238
+ return yield* new ForbiddenError({ message: 'Target thread belongs to a different organization.' })
239
+ }
240
+ if (existingThread.userId !== userIdString) {
241
+ return yield* new ForbiddenError({ message: 'Target thread belongs to a different user.' })
242
+ }
243
+ if (existingThread.status !== 'active') {
244
+ return yield* new BadRequestError({ message: 'Target thread must be active.' })
245
+ }
246
+ return { thread: existingThread, createdThread: false }
247
+ }
248
+
249
+ const projectTitle = params.projectTitle?.trim()
250
+ if (!projectTitle) {
251
+ return yield* new BadRequestError({
252
+ message: 'projectTitle is required when launching background work without targetThreadId.',
253
+ })
254
+ }
255
+
256
+ const createdThread = yield* deps.thread
257
+ .createThread({ userId: params.userRef, organizationId: params.orgRef, title: projectTitle, type: 'group' })
258
+ .pipe(
259
+ Effect.mapError((cause) => new ThreadTurnServiceError({ message: 'Failed to create target thread.', cause })),
260
+ )
261
+ return { thread: createdThread, createdThread: true }
262
+ })
263
+
264
+ let createdThreadId: string | null = null
265
+ const cleanupCreatedThread = () =>
266
+ createdThreadId ? deps.thread.deleteThread(createdThreadId).pipe(Effect.catch(() => Effect.void)) : Effect.void
267
+
268
+ return yield* Effect.gen(function* () {
269
+ const { thread: targetThread, createdThread } = yield* resolveTargetThread()
270
+ if (createdThread) {
271
+ createdThreadId = targetThread.id
272
+ }
273
+
274
+ const handoffMessage = yield* deps.threadMessage.addAgentMessage({
275
+ messageId: { tb: TABLES.THREAD_MESSAGE, id: Bun.randomUUIDv7() },
276
+ threadId: ensureRecordId(targetThread.id, TABLES.THREAD),
277
+ parts: params.handoff.parts,
278
+ metadata: params.handoff.metadata,
279
+ })
280
+
281
+ yield* deps.background.run(
282
+ deps.provideCurrentContext(
283
+ runThreadTurnInBackgroundWith(deps, {
284
+ thread: targetThread,
285
+ threadRef: ensureRecordId(targetThread.id, TABLES.THREAD),
286
+ orgRef: params.orgRef,
287
+ userRef: params.userRef,
288
+ userName: params.userName,
289
+ agentIdOverride: params.targetAgentId,
290
+ inputMessage: {
291
+ id: Bun.randomUUIDv7(),
292
+ role: 'user',
293
+ parts: params.input.parts,
294
+ metadata: params.input.metadata,
295
+ },
296
+ skipInputMessagePersistence: true,
297
+ abortSignal: params.abortSignal,
298
+ streamId: params.streamId,
299
+ }),
300
+ ),
301
+ 'thread.launchBackgroundThreadWork',
302
+ )
303
+
304
+ return {
305
+ launched: true,
306
+ threadId: targetThread.id,
307
+ threadTitle: targetThread.title,
308
+ sourceThreadId,
309
+ ...(params.targetAgentId ? { targetAgentId: params.targetAgentId } : {}),
310
+ handoffMessageId: handoffMessage.id,
311
+ createdThread,
312
+ message: buildBackgroundLaunchMessage({ createdThread, threadTitle: targetThread.title }),
313
+ } satisfies LaunchBackgroundThreadWorkResult
314
+ }).pipe(Effect.catch((error) => cleanupCreatedThread().pipe(Effect.andThen(Effect.fail(error)))))
315
+ })
316
+
317
+ function launchBackgroundThreadWorkWith(deps: ThreadTurnDeps, params: LaunchBackgroundThreadWorkParams) {
318
+ return launchBackgroundThreadWorkEffect(deps, params).pipe(
319
+ Effect.annotateSpans(
320
+ compactSpanAttributes({
321
+ ...buildThreadTurnSpanAttributes({
322
+ threadRef: params.sourceThreadId,
323
+ orgRef: params.orgRef,
324
+ userRef: params.userRef,
325
+ kind: 'background-launch',
326
+ streamId: params.streamId,
327
+ agentId: params.targetAgentId,
328
+ }),
329
+ targetThreadId: params.targetThreadId ? recordIdToString(params.targetThreadId, TABLES.THREAD) : undefined,
330
+ }),
331
+ ),
332
+ )
333
+ }
334
+
173
335
  const triggerPlanNodeTurnEffect = Effect.fn('ThreadTurn.triggerPlanNodeTurn')(function* (
174
336
  deps: ThreadTurnDeps,
175
337
  params: { runId: string; nodeId: string; abortSignal?: AbortSignal; streamId?: string },
@@ -178,7 +340,7 @@ const triggerPlanNodeTurnEffect = Effect.fn('ThreadTurn.triggerPlanNodeTurn')(fu
178
340
  const spec = yield* deps.planRun.getPlanSpecById(run.planSpecId)
179
341
  const nodeSpec = yield* deps.planRun.getNodeSpecByNodeId(spec.id, params.nodeId)
180
342
 
181
- if (!shouldPlanNodeUseVisibleTurn(spec, nodeSpec) || nodeSpec.owner.executorType !== 'agent') {
343
+ if (!shouldPlanNodeUseVisibleTurn(deps.agentConfig, spec, nodeSpec) || nodeSpec.owner.executorType !== 'agent') {
182
344
  return yield* new BadRequestError({
183
345
  message: `Plan node "${params.nodeId}" is not eligible for a visible plan turn.`,
184
346
  })
@@ -187,10 +349,13 @@ const triggerPlanNodeTurnEffect = Effect.fn('ThreadTurn.triggerPlanNodeTurn')(fu
187
349
  let activeRun = run
188
350
  let nodeRun = yield* deps.planRun.getNodeRunByNodeId(run.id, params.nodeId)
189
351
  if (nodeRun.status === 'ready') {
190
- yield* Effect.tryPromise({
191
- try: () => deps.planExecutor.transitionNodeToRunning({ runId: params.runId, nodeId: params.nodeId }),
192
- catch: (cause) => new ThreadTurnServiceError({ message: 'Failed to transition plan node to running.', cause }),
193
- })
352
+ yield* deps.planExecutor
353
+ .transitionNodeToRunning({ runId: params.runId, nodeId: params.nodeId })
354
+ .pipe(
355
+ Effect.mapError(
356
+ (cause) => new ThreadTurnServiceError({ message: 'Failed to transition plan node to running.', cause }),
357
+ ),
358
+ )
194
359
  activeRun = yield* deps.planRun.getRunById(params.runId)
195
360
  nodeRun = yield* deps.planRun.getNodeRunByNodeId(run.id, params.nodeId)
196
361
  }
@@ -286,6 +451,9 @@ export function makeThreadTurnService(deps: ThreadTurnDeps) {
286
451
  createThreadTurnStream(params: ThreadTurnParams) {
287
452
  return createThreadTurnStreamWith(deps, params)
288
453
  },
454
+ launchBackgroundThreadWork(params: LaunchBackgroundThreadWorkParams) {
455
+ return launchBackgroundThreadWorkWith(deps, params)
456
+ },
289
457
  runThreadTurnInBackground(params: ThreadTurnParams) {
290
458
  return runThreadTurnInBackgroundWith(deps, params)
291
459
  },
@@ -303,44 +471,67 @@ export class ThreadTurnServiceTag extends Context.Service<
303
471
  export const ThreadTurnServiceLive = Layer.effect(
304
472
  ThreadTurnServiceTag,
305
473
  Effect.gen(function* () {
474
+ const currentContext = yield* Effect.context()
475
+ const provideCurrentContext = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, never> =>
476
+ effect.pipe(Effect.provide(currentContext)) as Effect.Effect<A, E, never>
477
+ const agentConfig = yield* AgentConfigServiceTag
478
+ const background = yield* BackgroundWorkService
306
479
  const planExecutor = yield* PlanExecutorServiceTag
307
480
  const planRun = yield* PlanRunServiceTag
308
481
  const thread = yield* ThreadServiceTag
482
+ const threadMessage = yield* ThreadMessageServiceTag
309
483
  const threadTurnPreparation = yield* ThreadTurnPreparationServiceTag
310
484
  const user = yield* UserServiceTag
311
- return makeThreadTurnService({ planExecutor, planRun, thread, threadTurnPreparation, user })
485
+ return makeThreadTurnService({
486
+ agentConfig,
487
+ background,
488
+ planExecutor,
489
+ planRun,
490
+ provideCurrentContext,
491
+ thread,
492
+ threadMessage,
493
+ threadTurnPreparation,
494
+ user,
495
+ })
312
496
  }),
313
497
  )
314
498
 
315
- const createThreadApprovalContinuationStreamWithRuntime = Effect.fn(
316
- 'ThreadTurn.createApprovalContinuationStreamWithRuntime',
317
- )(function* (params: ThreadApprovalContinuationParams) {
318
- const threadTurnService = yield* ThreadTurnServiceTag
319
- return yield* threadTurnService.createThreadApprovalContinuationStream(params)
320
- })
499
+ export const createThreadApprovalContinuationStream = Effect.fn('ThreadTurn.createApprovalContinuationStream')(
500
+ function* (params: ThreadApprovalContinuationParams) {
501
+ const threadTurnService = yield* ThreadTurnServiceTag
502
+ return yield* threadTurnService.createThreadApprovalContinuationStream(params)
503
+ },
504
+ )
321
505
 
322
- const createThreadNativeToolApprovalStreamWithRuntime = Effect.fn(
323
- 'ThreadTurn.createNativeToolApprovalStreamWithRuntime',
324
- )(function* (params: ThreadApprovalContinuationParams) {
506
+ export const createThreadNativeToolApprovalStream = Effect.fn('ThreadTurn.createNativeToolApprovalStream')(function* (
507
+ params: ThreadApprovalContinuationParams,
508
+ ) {
325
509
  const threadTurnService = yield* ThreadTurnServiceTag
326
510
  return yield* threadTurnService.createThreadNativeToolApprovalStream(params)
327
511
  })
328
512
 
329
- const createThreadTurnStreamWithRuntime = Effect.fn('ThreadTurn.createThreadTurnStreamWithRuntime')(function* (
513
+ export const createThreadTurnStream = Effect.fn('ThreadTurn.createThreadTurnStream')(function* (
330
514
  params: ThreadTurnParams,
331
515
  ) {
332
516
  const threadTurnService = yield* ThreadTurnServiceTag
333
517
  return yield* threadTurnService.createThreadTurnStream(params)
334
518
  })
335
519
 
336
- const runThreadTurnInBackgroundWithRuntime = Effect.fn('ThreadTurn.runThreadTurnInBackgroundWithRuntime')(function* (
520
+ export const launchBackgroundThreadWork = Effect.fn('ThreadTurn.launchBackgroundThreadWork')(function* (
521
+ params: LaunchBackgroundThreadWorkParams,
522
+ ) {
523
+ const threadTurnService = yield* ThreadTurnServiceTag
524
+ return yield* threadTurnService.launchBackgroundThreadWork(params)
525
+ })
526
+
527
+ export const runThreadTurnInBackground = Effect.fn('ThreadTurn.runThreadTurnInBackground')(function* (
337
528
  params: ThreadTurnParams,
338
529
  ) {
339
530
  const threadTurnService = yield* ThreadTurnServiceTag
340
531
  return yield* threadTurnService.runThreadTurnInBackground(params)
341
532
  })
342
533
 
343
- const triggerPlanNodeTurnWithRuntime = Effect.fn('ThreadTurn.triggerPlanNodeTurnWithRuntime')(function* (params: {
534
+ export const triggerPlanNodeTurn = Effect.fn('ThreadTurn.triggerPlanNodeTurn')(function* (params: {
344
535
  runId: string
345
536
  nodeId: string
346
537
  abortSignal?: AbortSignal
@@ -349,28 +540,3 @@ const triggerPlanNodeTurnWithRuntime = Effect.fn('ThreadTurn.triggerPlanNodeTurn
349
540
  const threadTurnService = yield* ThreadTurnServiceTag
350
541
  return yield* threadTurnService.triggerPlanNodeTurn(params)
351
542
  })
352
-
353
- export function createThreadApprovalContinuationStream(params: ThreadApprovalContinuationParams) {
354
- return runPromise(createThreadApprovalContinuationStreamWithRuntime(params))
355
- }
356
-
357
- export function createThreadNativeToolApprovalStream(params: ThreadApprovalContinuationParams) {
358
- return runPromise(createThreadNativeToolApprovalStreamWithRuntime(params))
359
- }
360
-
361
- export function createThreadTurnStream(params: ThreadTurnParams) {
362
- return runPromise(createThreadTurnStreamWithRuntime(params))
363
- }
364
-
365
- export function runThreadTurnInBackground(params: ThreadTurnParams): Promise<PreparedThreadTurnResult> {
366
- return runPromise(runThreadTurnInBackgroundWithRuntime(params))
367
- }
368
-
369
- export function triggerPlanNodeTurn(params: {
370
- runId: string
371
- nodeId: string
372
- abortSignal?: AbortSignal
373
- streamId?: string
374
- }): Promise<PreparedThreadTurnResult> {
375
- return runPromise(triggerPlanNodeTurnWithRuntime(params))
376
- }
@@ -2,14 +2,21 @@ import { THREAD, sdkThreadStatusSchema } from '@lota-sdk/shared'
2
2
  import { Context, Effect, Layer } from 'effect'
3
3
  import { surql } from 'surrealdb'
4
4
 
5
- import { getCoreThreadProfile, isAgentName } from '../../config/agent-defaults'
5
+ import type { ResolvedAgentConfig } from '../../config/agent-defaults'
6
+ import { isAgentName } from '../../config/agent-defaults'
7
+ import type { ResolvedThreadBootstrapConfig } from '../../config/thread-defaults'
6
8
  import { ensureRecordId, isRecordIdInput, recordIdToString } from '../../db/record-id'
7
9
  import type { RecordIdRef } from '../../db/record-id'
8
10
  import type { SurrealDBService } from '../../db/service'
9
11
  import { TABLES } from '../../db/tables'
10
12
  import { BadRequestError, ServiceError } from '../../effect/errors'
11
13
  import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
12
- import { DatabaseServiceTag, RedisServiceTag } from '../../effect/services'
14
+ import {
15
+ AgentConfigServiceTag,
16
+ DatabaseServiceTag,
17
+ RedisServiceTag,
18
+ ThreadConfigServiceTag,
19
+ } from '../../effect/services'
13
20
  import type { RedisConnectionManager } from '../../redis/connection'
14
21
  import { CompactionCoordinationTag } from '../../runtime/chat-run-orchestration'
15
22
  import { toIsoDateTimeString } from '../../utils/date-time'
@@ -35,9 +42,9 @@ function assertMutableThreadEffect(thread: ThreadRecord): Effect.Effect<void, Ba
35
42
  : Effect.void
36
43
  }
37
44
 
38
- function getDefaultTitle(thread: Pick<ThreadRecord, 'type' | 'threadType'>): string {
45
+ function getDefaultTitle(agentConfig: ResolvedAgentConfig, thread: Pick<ThreadRecord, 'type' | 'threadType'>): string {
39
46
  if (thread.type === 'thread' && typeof thread.threadType === 'string') {
40
- return getCoreThreadProfile(thread.threadType).config.title
47
+ return agentConfig.getCoreThreadProfile(thread.threadType).config.title
41
48
  }
42
49
 
43
50
  return THREAD.DEFAULT_TITLE
@@ -63,6 +70,8 @@ type ChatRunRegistry = Context.Service.Shape<typeof ChatRunRegistryTag>
63
70
  type CompactionCoordination = Context.Service.Shape<typeof CompactionCoordinationTag>
64
71
 
65
72
  interface ThreadServiceDeps {
73
+ agentConfig: ResolvedAgentConfig
74
+ threadBootstrapConfig: ResolvedThreadBootstrapConfig
66
75
  db: SurrealDBService
67
76
  redis: RedisConnectionManager
68
77
  chatRunRegistry: ChatRunRegistry
@@ -126,8 +135,8 @@ export function makeThreadService(deps: ThreadServiceDeps) {
126
135
  nameGenerated: thread.nameGenerated,
127
136
  isRunning,
128
137
  isCompacting: thread.isCompacting === true,
129
- ...(isAgentName(thread.agentId) ? { agentId: thread.agentId } : {}),
130
- title: thread.title ?? getDefaultTitle(thread),
138
+ ...(isAgentName(deps.agentConfig, thread.agentId) ? { agentId: thread.agentId } : {}),
139
+ title: thread.title ?? getDefaultTitle(deps.agentConfig, thread),
131
140
  status: thread.status,
132
141
  memoryBlock: formatMemoryBlockForPrompt(thread),
133
142
  members: thread.members,
@@ -149,6 +158,8 @@ export function makeThreadService(deps: ThreadServiceDeps) {
149
158
  }
150
159
 
151
160
  const bootstrap = createThreadBootstrapHelpers({
161
+ agentConfig: deps.agentConfig,
162
+ threadBootstrapConfig: deps.threadBootstrapConfig,
152
163
  threadStore,
153
164
  threadMessageService: deps.threadMessageService,
154
165
  redis: deps.redis,
@@ -324,6 +335,8 @@ export class ThreadServiceTag extends Context.Service<ThreadServiceTag, ReturnTy
324
335
  export const ThreadServiceLive = Layer.effect(
325
336
  ThreadServiceTag,
326
337
  Effect.gen(function* () {
338
+ const agentConfig = yield* AgentConfigServiceTag
339
+ const threadBootstrapConfig = yield* ThreadConfigServiceTag
327
340
  const db = yield* DatabaseServiceTag
328
341
  const redis = yield* RedisServiceTag
329
342
  const chatRunRegistry = yield* ChatRunRegistryTag
@@ -332,6 +345,8 @@ export const ThreadServiceLive = Layer.effect(
332
345
  const compactionCoordination = yield* CompactionCoordinationTag
333
346
  const background = yield* BackgroundWorkService
334
347
  return makeThreadService({
348
+ agentConfig,
349
+ threadBootstrapConfig,
335
350
  db,
336
351
  redis,
337
352
  chatRunRegistry,
@@ -3,7 +3,7 @@ import { ToolLoopAgent } from 'ai'
3
3
 
4
4
  import { aiGatewayChatModel } from '../ai-gateway/ai-gateway'
5
5
  import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
6
- import { getLeadAgentDisplayName } from '../config/agent-defaults'
6
+ import type { ResolvedAgentConfig } from '../config/agent-defaults'
7
7
  import {
8
8
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
9
9
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -12,8 +12,8 @@ import { resolveHelperAgentOptions } from './helper-agent-options'
12
12
 
13
13
  const RECENT_ACTIVITY_TITLE_MAX_TOKENS = 256
14
14
 
15
- export function buildRecentActivityTitleRefinerPrompt(): string {
16
- const leadAgentDisplayName = getLeadAgentDisplayName() || 'the lead agent'
15
+ export function buildRecentActivityTitleRefinerPrompt(agentConfig: ResolvedAgentConfig): string {
16
+ const leadAgentDisplayName = agentConfig.displayNames[agentConfig.leadAgentId] || 'the lead agent'
17
17
 
18
18
  return `<agent-instructions>
19
19
  You are ${leadAgentDisplayName} writing the visible title for a recent activity item.
@@ -76,14 +76,17 @@ Return only the title text. No quotes, labels, JSON, markdown, or explanation.
76
76
  </output>
77
77
  </agent-instructions>`
78
78
 
79
- export function createRecentActivityTitleRefinerAgent(options: CreateHelperToolLoopAgentOptions) {
79
+ export function createRecentActivityTitleRefinerAgent(
80
+ agentConfig: ResolvedAgentConfig,
81
+ options: CreateHelperToolLoopAgentOptions,
82
+ ) {
80
83
  return new ToolLoopAgent({
81
84
  id: 'recent-activity-title-refiner',
82
85
  model: aiGatewayChatModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
83
86
  headers: buildAiGatewayDirectCacheHeaders('lota-sdk'),
84
87
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
85
88
  ...resolveHelperAgentOptions(options, {
86
- instructions: buildRecentActivityTitleRefinerPrompt(),
89
+ instructions: buildRecentActivityTitleRefinerPrompt(agentConfig),
87
90
  maxOutputTokens: RECENT_ACTIVITY_TITLE_MAX_TOKENS,
88
91
  }),
89
92
  })