@lota-sdk/core 0.4.12 → 0.4.14

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 (139) hide show
  1. package/package.json +4 -4
  2. package/src/ai/embedding-cache.ts +17 -11
  3. package/src/ai-gateway/ai-gateway.ts +164 -94
  4. package/src/ai-gateway/index.ts +4 -1
  5. package/src/config/agent-defaults.ts +2 -2
  6. package/src/config/agent-types.ts +1 -1
  7. package/src/create-runtime.ts +259 -200
  8. package/src/db/cursor-pagination.ts +2 -9
  9. package/src/db/memory-store.ts +194 -175
  10. package/src/db/memory.ts +125 -71
  11. package/src/db/schema-fingerprint.ts +5 -4
  12. package/src/db/service-normalization.ts +4 -3
  13. package/src/db/service.ts +3 -2
  14. package/src/db/startup.ts +15 -16
  15. package/src/effect/errors.ts +161 -21
  16. package/src/effect/index.ts +0 -1
  17. package/src/embeddings/provider.ts +15 -7
  18. package/src/queues/autonomous-job.queue.ts +10 -22
  19. package/src/queues/delayed-node-promotion.queue.ts +8 -14
  20. package/src/queues/document-processor.queue.ts +13 -4
  21. package/src/queues/memory-consolidation.queue.ts +26 -14
  22. package/src/queues/plan-agent-heartbeat.queue.ts +10 -9
  23. package/src/queues/plan-scheduler.queue.ts +37 -15
  24. package/src/queues/queue-factory.ts +59 -35
  25. package/src/queues/standalone-worker.ts +3 -2
  26. package/src/redis/connection.ts +10 -3
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +5 -5
  29. package/src/redis/stream-context.ts +1 -1
  30. package/src/runtime/chat-message.ts +64 -1
  31. package/src/runtime/chat-run-orchestration.ts +33 -20
  32. package/src/runtime/context-compaction/context-compaction-runtime.ts +14 -7
  33. package/src/runtime/context-compaction/context-compaction.ts +78 -66
  34. package/src/runtime/domain-layer.ts +13 -7
  35. package/src/runtime/execution-plan.ts +7 -3
  36. package/src/runtime/live-turn-trace.ts +6 -49
  37. package/src/runtime/memory/memory-block.ts +3 -9
  38. package/src/runtime/memory/memory-scope.ts +3 -1
  39. package/src/runtime/plugin-resolution.ts +2 -1
  40. package/src/runtime/post-turn-side-effects.ts +6 -5
  41. package/src/runtime/retrieval-adapters.ts +8 -20
  42. package/src/runtime/runtime-config.ts +3 -9
  43. package/src/runtime/runtime-extensions.ts +2 -4
  44. package/src/runtime/runtime-lifecycle.ts +56 -16
  45. package/src/runtime/runtime-services.ts +180 -102
  46. package/src/runtime/runtime-worker-registry.ts +3 -1
  47. package/src/runtime/social-chat/social-chat-agent-runner.ts +1 -1
  48. package/src/runtime/social-chat/social-chat-history.ts +21 -18
  49. package/src/runtime/social-chat/social-chat.ts +356 -223
  50. package/src/runtime/specialist-runner.ts +3 -1
  51. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +3 -2
  52. package/src/runtime/thread-turn-context.ts +142 -102
  53. package/src/runtime/turn-lifecycle.ts +15 -46
  54. package/src/services/agent-activity.service.ts +1 -1
  55. package/src/services/agent-executor.service.ts +107 -77
  56. package/src/services/autonomous-job.service.ts +354 -293
  57. package/src/services/background-work.service.ts +3 -3
  58. package/src/services/context-compaction.service.ts +7 -2
  59. package/src/services/document-chunk.service.ts +50 -32
  60. package/src/services/execution-plan/execution-plan-schedule.ts +5 -3
  61. package/src/services/execution-plan/execution-plan.service.ts +162 -179
  62. package/src/services/feedback-loop.service.ts +5 -4
  63. package/src/services/graph-full-routing.ts +37 -36
  64. package/src/services/institutional-memory.service.ts +28 -30
  65. package/src/services/learned-skill.service.ts +107 -72
  66. package/src/services/memory/memory-errors.ts +4 -23
  67. package/src/services/memory/memory-org-memory.ts +10 -5
  68. package/src/services/memory/memory-rerank.ts +18 -6
  69. package/src/services/memory/memory.service.ts +170 -111
  70. package/src/services/memory/rerank.service.ts +29 -20
  71. package/src/services/organization-member.service.ts +1 -1
  72. package/src/services/organization.service.ts +69 -75
  73. package/src/services/ownership-dispatcher.service.ts +40 -39
  74. package/src/services/plan/plan-agent-heartbeat.service.ts +26 -23
  75. package/src/services/plan/plan-agent-query.service.ts +39 -31
  76. package/src/services/plan/plan-completion-side-effects.ts +13 -17
  77. package/src/services/plan/plan-coordination.service.ts +2 -1
  78. package/src/services/plan/plan-cycle.service.ts +6 -5
  79. package/src/services/plan/plan-deadline.service.ts +57 -54
  80. package/src/services/plan/plan-event-delivery.service.ts +5 -4
  81. package/src/services/plan/plan-executor-graph.ts +18 -15
  82. package/src/services/plan/plan-executor.service.ts +235 -262
  83. package/src/services/plan/plan-run.service.ts +169 -93
  84. package/src/services/plan/plan-scheduler.service.ts +192 -202
  85. package/src/services/plan/plan-template.service.ts +1 -1
  86. package/src/services/plan/plan-transaction-events.ts +1 -1
  87. package/src/services/plan/plan-workspace.service.ts +23 -14
  88. package/src/services/plugin-executor.service.ts +5 -9
  89. package/src/services/queue-job.service.ts +117 -59
  90. package/src/services/recent-activity-title.service.ts +13 -12
  91. package/src/services/recent-activity.service.ts +6 -1
  92. package/src/services/social-chat-history.service.ts +29 -25
  93. package/src/services/system-executor.service.ts +5 -9
  94. package/src/services/thread/thread-active-run.ts +2 -2
  95. package/src/services/thread/thread-listing.ts +61 -57
  96. package/src/services/thread/thread-memory-block.ts +73 -48
  97. package/src/services/thread/thread-message.service.ts +76 -65
  98. package/src/services/thread/thread-record-store.ts +8 -8
  99. package/src/services/thread/thread-title.service.ts +10 -4
  100. package/src/services/thread/thread-turn-execution.ts +43 -45
  101. package/src/services/thread/thread-turn-preparation.service.ts +257 -135
  102. package/src/services/thread/thread-turn-streaming.ts +82 -85
  103. package/src/services/thread/thread-turn.ts +8 -8
  104. package/src/services/thread/thread.service.ts +135 -100
  105. package/src/services/user.service.ts +45 -48
  106. package/src/storage/attachment-parser.ts +6 -2
  107. package/src/storage/attachment-storage.service.ts +5 -6
  108. package/src/storage/generated-document-storage.service.ts +1 -1
  109. package/src/system-agents/context-compaction.agent.ts +10 -9
  110. package/src/system-agents/delegated-agent-factory.ts +30 -6
  111. package/src/system-agents/memory-reranker.agent.ts +10 -9
  112. package/src/system-agents/memory.agent.ts +10 -9
  113. package/src/system-agents/recent-activity-title-refiner.agent.ts +13 -15
  114. package/src/system-agents/regular-chat-memory-digest.agent.ts +13 -12
  115. package/src/system-agents/skill-extractor.agent.ts +13 -12
  116. package/src/system-agents/skill-manager.agent.ts +13 -12
  117. package/src/system-agents/thread-router.agent.ts +10 -5
  118. package/src/system-agents/title-generator.agent.ts +13 -12
  119. package/src/tools/fetch-webpage.tool.ts +13 -13
  120. package/src/tools/memory-block.tool.ts +3 -1
  121. package/src/tools/plan-approval.tool.ts +4 -2
  122. package/src/tools/read-file-parts.tool.ts +10 -4
  123. package/src/tools/remember-memory.tool.ts +3 -1
  124. package/src/tools/research-topic.tool.ts +9 -5
  125. package/src/tools/search-web.tool.ts +16 -16
  126. package/src/tools/search.tool.ts +20 -5
  127. package/src/tools/team-think.tool.ts +61 -38
  128. package/src/utils/async.ts +5 -5
  129. package/src/utils/errors.ts +19 -18
  130. package/src/utils/sse-keepalive.ts +28 -25
  131. package/src/workers/bootstrap.ts +75 -11
  132. package/src/workers/memory-consolidation.worker.ts +82 -91
  133. package/src/workers/organization-learning.worker.ts +14 -4
  134. package/src/workers/regular-chat-memory-digest.runner.ts +105 -67
  135. package/src/workers/skill-extraction.runner.ts +97 -61
  136. package/src/workers/utils/repo-structure-extractor.ts +13 -8
  137. package/src/workers/utils/thread-message-query.ts +24 -24
  138. package/src/workers/worker-utils.ts +23 -4
  139. package/src/effect/helpers.ts +0 -123
@@ -6,7 +6,7 @@ import { Effect, Ref, Schema, Stream } from 'effect'
6
6
 
7
7
  import { aiLogger } from '../../config/logger'
8
8
  import type { RecordIdRef } from '../../db/record-id'
9
- import { effectTryMaybeAsync, makeEffectTryPromiseWithMessage } from '../../effect/helpers'
9
+ import { ERROR_TAGS } from '../../effect/errors'
10
10
  import { AgentConfigServiceTag, AgentFactoryServiceTag } from '../../effect/services'
11
11
  import {
12
12
  readRuntimeAgentIdentityOverrides,
@@ -40,28 +40,12 @@ interface ToolLoopGenerateResult {
40
40
  }
41
41
 
42
42
  export class ThreadTurnStreamingError extends Schema.TaggedErrorClass<ThreadTurnStreamingError>()(
43
- 'ThreadTurnStreamingError',
43
+ '@lota-sdk/core/ThreadTurnStreamingError',
44
44
  { message: Schema.String, cause: Schema.optional(Schema.Defect) },
45
45
  ) {}
46
46
 
47
- const effectTryPromise = makeEffectTryPromiseWithMessage(
48
- (message, cause) => new ThreadTurnStreamingError({ message, cause }),
49
- )
50
-
51
- function effectFromMaybeEffect<A>(
52
- evaluate: () => A | PromiseLike<A> | Effect.Effect<A, ThreadTurnStreamingError>,
53
- message: string,
54
- ): Effect.Effect<A, ThreadTurnStreamingError> {
55
- return Effect.try({ try: evaluate, catch: (error) => new ThreadTurnStreamingError({ message, cause: error }) }).pipe(
56
- Effect.flatMap((result) =>
57
- Effect.isEffect(result)
58
- ? result.pipe(Effect.mapError((error) => new ThreadTurnStreamingError({ message, cause: error })))
59
- : effectTryMaybeAsync(
60
- () => result,
61
- (error) => new ThreadTurnStreamingError({ message, cause: error }),
62
- ),
63
- ),
64
- )
47
+ function toThreadTurnStreamingError(message: string, cause: unknown): ThreadTurnStreamingError {
48
+ return new ThreadTurnStreamingError({ message, cause })
65
49
  }
66
50
 
67
51
  function hasUIMessageStream(value: unknown): value is UIMessageStreamResult {
@@ -151,7 +135,7 @@ interface StreamAgentResponseParams {
151
135
  messages: ChatMessage[]
152
136
  tools: ToolSet
153
137
  observer: {
154
- run: <T>(fn: () => T | Promise<T>) => Promise<T>
138
+ run: <T>(fn: () => Promise<T>) => Promise<T>
155
139
  recordError: (error: unknown) => void
156
140
  recordAbort: (error: unknown) => void
157
141
  }
@@ -172,45 +156,54 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
172
156
  const executionPlanInstructionSections =
173
157
  streamParams.includeExecutionPlanTools === false
174
158
  ? undefined
175
- : yield* effectFromMaybeEffect(
176
- () => ctx.getExecutionPlanInstructionSections(),
177
- 'Failed to load execution plan instructions.',
178
- ).pipe(Effect.withSpan('ThreadTurnStreaming.loadExecutionPlanInstructions'))
159
+ : yield* ctx.getExecutionPlanInstructionSections().pipe(
160
+ Effect.mapError((cause) => toThreadTurnStreamingError('Failed to load execution plan instructions.', cause)),
161
+ Effect.withSpan('ThreadTurnStreaming.loadExecutionPlanInstructions'),
162
+ )
179
163
 
164
+ const resolveAgent = ctx.turnHooks.resolveAgent
180
165
  const agentResolution = asRecord(
181
- yield* effectTryMaybeAsync(
182
- () =>
183
- ctx.turnHooks.resolveAgent?.({
184
- agentId: streamParams.agentId,
185
- mode: streamParams.mode,
186
- thread: ctx.thread,
187
- threadRef: ctx.threadRef,
188
- orgRef: ctx.orgRef,
189
- userRef: ctx.userRef,
190
- userName: ctx.userName,
191
- onboardingActive: ctx.onboardingActive,
192
- linearInstalled: ctx.linearInstalled,
193
- githubInstalled: ctx.githubInstalled,
194
- skills: streamParams.skills,
195
- additionalInstructionSections: streamParams.additionalInstructionSections,
196
- context: ctx.buildContextResult,
197
- }),
198
- (error) => new ThreadTurnStreamingError({ message: 'Failed to resolve the runtime agent.', cause: error }),
199
- ).pipe(Effect.withSpan('ThreadTurnStreaming.resolveAgent')),
166
+ resolveAgent
167
+ ? yield* Effect.tryPromise({
168
+ try: () =>
169
+ resolveAgent({
170
+ agentId: streamParams.agentId,
171
+ mode: streamParams.mode,
172
+ thread: ctx.thread,
173
+ threadRef: ctx.threadRef,
174
+ orgRef: ctx.orgRef,
175
+ userRef: ctx.userRef,
176
+ userName: ctx.userName,
177
+ onboardingActive: ctx.onboardingActive,
178
+ linearInstalled: ctx.linearInstalled,
179
+ githubInstalled: ctx.githubInstalled,
180
+ skills: streamParams.skills,
181
+ additionalInstructionSections: streamParams.additionalInstructionSections,
182
+ context: ctx.buildContextResult,
183
+ }),
184
+ catch: (cause) => toThreadTurnStreamingError('Failed to resolve the runtime agent.', cause),
185
+ }).pipe(Effect.withSpan('ThreadTurnStreaming.resolveAgent'))
186
+ : undefined,
200
187
  )
201
188
 
202
189
  const resolvedAgentId = readOptionalString(agentResolution?.agentId) ?? streamParams.agentId
203
190
  const latestUserMessage = [...streamParams.messages].reverse().find((message) => message.role === 'user')
204
191
  const latestUserMessageText = latestUserMessage ? extractMessageText(latestUserMessage).trim() : undefined
205
192
  const [preSeededMemoriesSection, learnedSkillsSection] = yield* Effect.all([
206
- effectFromMaybeEffect(
207
- () => ctx.getPreSeededMemoriesSection(resolvedAgentId),
208
- `Failed to load pre-seeded memories for ${resolvedAgentId}.`,
209
- ),
210
- effectFromMaybeEffect(
211
- () => ctx.getLearnedSkillsSection(resolvedAgentId, latestUserMessageText),
212
- `Failed to load learned skills for ${resolvedAgentId}.`,
213
- ),
193
+ ctx
194
+ .getPreSeededMemoriesSection(resolvedAgentId)
195
+ .pipe(
196
+ Effect.mapError((cause) =>
197
+ toThreadTurnStreamingError(`Failed to load pre-seeded memories for ${resolvedAgentId}.`, cause),
198
+ ),
199
+ ),
200
+ ctx
201
+ .getLearnedSkillsSection(resolvedAgentId, latestUserMessageText)
202
+ .pipe(
203
+ Effect.mapError((cause) =>
204
+ toThreadTurnStreamingError(`Failed to load learned skills for ${resolvedAgentId}.`, cause),
205
+ ),
206
+ ),
214
207
  ]).pipe(Effect.withSpan('ThreadTurnStreaming.loadMemoriesAndSkills'))
215
208
 
216
209
  const toolNames = new Set(Object.keys(streamParams.tools))
@@ -259,10 +252,10 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
259
252
  hasRetrievalTools,
260
253
  })
261
254
 
262
- const modelMessages = yield* effectTryPromise(
263
- () => convertToModelMessages(streamParams.messages, { ignoreIncompleteToolCalls: true }),
264
- 'Failed to convert UI messages to model messages.',
265
- ).pipe(Effect.withSpan('ThreadTurnStreaming.convertModelMessages'))
255
+ const modelMessages = yield* Effect.tryPromise({
256
+ try: () => convertToModelMessages(streamParams.messages, { ignoreIncompleteToolCalls: true }),
257
+ catch: (cause) => toThreadTurnStreamingError('Failed to convert UI messages to model messages.', cause),
258
+ }).pipe(Effect.withSpan('ThreadTurnStreaming.convertModelMessages'))
266
259
 
267
260
  const agentFactory = agentFactoryConfig.createAgent[config.id]
268
261
  if (!agentFactory) {
@@ -281,10 +274,11 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
281
274
  const resolvedAgentName = resolveRuntimeAgentDisplayName(agentConfig, agentIdentityOverrides, resolvedAgentId)
282
275
 
283
276
  const generateFallback = (cause: ThreadTurnStreamingError) =>
284
- effectTryPromise(
285
- () => streamParams.observer.run(() => agent.generate({ messages: modelMessages, abortSignal: agentAbortSignal })),
286
- `Agent generate fallback failed for ${resolvedAgentId}.`,
287
- ).pipe(
277
+ Effect.tryPromise({
278
+ try: () =>
279
+ streamParams.observer.run(() => agent.generate({ messages: modelMessages, abortSignal: agentAbortSignal })),
280
+ catch: (error) => toThreadTurnStreamingError(`Agent generate fallback failed for ${resolvedAgentId}.`, error),
281
+ }).pipe(
288
282
  Effect.tap(() =>
289
283
  Effect.sync(() => {
290
284
  aiLogger.warn`Agent stream failed for ${resolvedAgentId}; falling back to generate: ${cause.message}`
@@ -293,10 +287,11 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
293
287
  Effect.flatMap((result) => buildFallbackResponseMessage(result as ToolLoopGenerateResult)),
294
288
  )
295
289
 
296
- const generateWithoutUiStream = effectTryPromise(
297
- () => streamParams.observer.run(() => agent.generate({ messages: modelMessages, abortSignal: agentAbortSignal })),
298
- `Agent generate failed for ${resolvedAgentId}.`,
299
- ).pipe(
290
+ const generateWithoutUiStream = Effect.tryPromise({
291
+ try: () =>
292
+ streamParams.observer.run(() => agent.generate({ messages: modelMessages, abortSignal: agentAbortSignal })),
293
+ catch: (cause) => toThreadTurnStreamingError(`Agent generate failed for ${resolvedAgentId}.`, cause),
294
+ }).pipe(
300
295
  Effect.tapError((error) =>
301
296
  Effect.sync(() => {
302
297
  if (agentAbortSignal.aborted) {
@@ -321,10 +316,12 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
321
316
  return generatedResponse
322
317
  }
323
318
 
324
- const result = yield* effectTryPromise(
325
- () => streamParams.observer.run(() => agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal })),
326
- `Agent stream failed for ${resolvedAgentId}.`,
327
- ).pipe(
319
+ const writer = streamParams.writer
320
+ const result = yield* Effect.tryPromise({
321
+ try: () =>
322
+ streamParams.observer.run(() => agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal })),
323
+ catch: (cause) => toThreadTurnStreamingError(`Agent stream failed for ${resolvedAgentId}.`, cause),
324
+ }).pipe(
328
325
  Effect.tapError((error) =>
329
326
  Effect.sync(() => {
330
327
  if (agentAbortSignal.aborted) {
@@ -355,14 +352,12 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
355
352
  resolveFinishedStream(withMessageCreatedAt(finishedResponseMessage, nowEpochMillis()))
356
353
  },
357
354
  }) 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
355
+ const liveTurnTrace = createLiveTurnTraceStreamObserver({
356
+ traceId: `trace:${Bun.randomUUIDv7()}`,
357
+ writer,
358
+ agentId: resolvedAgentId,
359
+ agentName: resolvedAgentName,
360
+ })
366
361
  const streamStartedAt = performance.now()
367
362
  const firstVisibleOutputRecorded = yield* Ref.make(false)
368
363
  const firstTextTokenRecorded = yield* Ref.make(false)
@@ -393,21 +388,23 @@ const streamAgentResponseEffect = Effect.fn('ThreadTurnStreaming.streamAgentResp
393
388
  yield* Effect.annotateCurrentSpan({ ttftMs: elapsedMs, ttftChunkType: chunkType })
394
389
  }
395
390
 
396
- if (streamParams.writer) {
397
- yield* Effect.sync(() => {
398
- streamParams.writer?.write(value)
399
- liveTurnTrace?.observeChunk(value)
400
- })
401
- }
391
+ yield* Effect.sync(() => {
392
+ writer.write(value)
393
+ liveTurnTrace.observeChunk(value)
394
+ })
402
395
  }),
403
396
  ),
404
397
  Effect.withSpan('ThreadTurnStreaming.consumeUiStream'),
405
398
  Effect.andThen(
406
- effectTryPromise(() => finishedStream, `Agent run for ${resolvedAgentId} did not produce a response message.`),
399
+ Effect.tryPromise({
400
+ try: () => finishedStream,
401
+ catch: (cause) =>
402
+ toThreadTurnStreamingError(`Agent run for ${resolvedAgentId} did not produce a response message.`, cause),
403
+ }),
407
404
  ),
408
- Effect.catchTag('ThreadTurnStreamingError', generateFallback),
405
+ Effect.catchTag(ERROR_TAGS.ThreadTurnStreamingError, generateFallback),
409
406
  )
410
- liveTurnTrace?.finish()
407
+ liveTurnTrace.finish()
411
408
 
412
409
  for (const toolError of collectToolOutputErrors({ responseMessage: streamedResponse })) {
413
410
  aiLogger.error`Tool execution failed (agent=${resolvedAgentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
@@ -5,12 +5,12 @@ import { Context, Schema, Effect, Layer } from 'effect'
5
5
  import type { ResolvedAgentConfig } from '../../config/agent-defaults'
6
6
  import { ensureRecordId, recordIdToString } from '../../db/record-id'
7
7
  import { TABLES } from '../../db/tables'
8
- import { BadRequestError, ForbiddenError } from '../../effect/errors'
8
+ import { ERROR_TAGS, BadRequestError, ForbiddenError } from '../../effect/errors'
9
9
  import { AgentConfigServiceTag } from '../../effect/services'
10
10
  import { hasApprovalRespondedParts, isApprovalContinuationRequest } from '../../runtime/approval-continuation'
11
11
  import { shouldPlanNodeUseVisibleTurn } from '../../runtime/execution-plan-visibility'
12
12
  import { wrapResponseWithKeepalive } from '../../utils/sse-keepalive'
13
- import { BackgroundWorkService } from '../background-work.service'
13
+ import { BackgroundWorkServiceTag } from '../background-work.service'
14
14
  import type { makePlanExecutorService } from '../plan/plan-executor.service'
15
15
  import { PlanExecutorServiceTag } from '../plan/plan-executor.service'
16
16
  import type { makePlanRunService } from '../plan/plan-run.service'
@@ -67,14 +67,14 @@ export interface LaunchBackgroundThreadWorkResult {
67
67
  message: string
68
68
  }
69
69
 
70
- class ThreadTurnServiceError extends Schema.TaggedErrorClass<ThreadTurnServiceError>()('ThreadTurnServiceError', {
71
- message: Schema.String,
72
- cause: Schema.optional(Schema.Defect),
73
- }) {}
70
+ class ThreadTurnServiceError extends Schema.TaggedErrorClass<ThreadTurnServiceError>()(
71
+ ERROR_TAGS.ThreadTurnServiceError,
72
+ { message: Schema.String, cause: Schema.optional(Schema.Defect) },
73
+ ) {}
74
74
 
75
75
  interface ThreadTurnDeps {
76
76
  agentConfig: ResolvedAgentConfig
77
- background: Context.Service.Shape<typeof BackgroundWorkService>
77
+ background: Context.Service.Shape<typeof BackgroundWorkServiceTag>
78
78
  planExecutor: ReturnType<typeof makePlanExecutorService>
79
79
  planRun: ReturnType<typeof makePlanRunService>
80
80
  provideCurrentContext: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, never>
@@ -475,7 +475,7 @@ export const ThreadTurnServiceLive = Layer.effect(
475
475
  const provideCurrentContext = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, never> =>
476
476
  effect.pipe(Effect.provide(currentContext)) as Effect.Effect<A, E, never>
477
477
  const agentConfig = yield* AgentConfigServiceTag
478
- const background = yield* BackgroundWorkService
478
+ const background = yield* BackgroundWorkServiceTag
479
479
  const planExecutor = yield* PlanExecutorServiceTag
480
480
  const planRun = yield* PlanRunServiceTag
481
481
  const thread = yield* ThreadServiceTag
@@ -10,7 +10,6 @@ import type { RecordIdRef } from '../../db/record-id'
10
10
  import type { SurrealDBService } from '../../db/service'
11
11
  import { TABLES } from '../../db/tables'
12
12
  import { BadRequestError, ServiceError } from '../../effect/errors'
13
- import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
14
13
  import {
15
14
  AgentConfigServiceTag,
16
15
  DatabaseServiceTag,
@@ -20,7 +19,7 @@ import {
20
19
  import type { RedisConnectionManager } from '../../redis/connection'
21
20
  import { CompactionCoordinationTag } from '../../runtime/chat-run-orchestration'
22
21
  import { toIsoDateTimeString } from '../../utils/date-time'
23
- import { BackgroundWorkService } from '../background-work.service'
22
+ import { BackgroundWorkServiceTag } from '../background-work.service'
24
23
  import { ChatRunRegistryTag } from '../chat-run-registry.service'
25
24
  import { ContextCompactionServiceTag } from '../context-compaction.service'
26
25
  import type { makeContextCompactionService } from '../context-compaction.service'
@@ -63,8 +62,6 @@ function toPublicThread(thread: NormalizedThread): PublicThread {
63
62
 
64
63
  type ThreadServiceError = ServiceError | BadRequestError
65
64
 
66
- const effectTryPromise = makeEffectTryPromiseWithMessage((message, cause) => new ServiceError({ message, cause }))
67
-
68
65
  type ChatRunRegistry = Context.Service.Shape<typeof ChatRunRegistryTag>
69
66
 
70
67
  type CompactionCoordination = Context.Service.Shape<typeof CompactionCoordinationTag>
@@ -78,7 +75,7 @@ interface ThreadServiceDeps {
78
75
  threadMessageService: ReturnType<typeof makeThreadMessageService>
79
76
  contextCompactionService: ReturnType<typeof makeContextCompactionService>
80
77
  compactionCoordination: CompactionCoordination
81
- background: Context.Service.Shape<typeof BackgroundWorkService>
78
+ background: Context.Service.Shape<typeof BackgroundWorkServiceTag>
82
79
  }
83
80
 
84
81
  export function makeThreadService(deps: ThreadServiceDeps) {
@@ -109,10 +106,11 @@ export function makeThreadService(deps: ThreadServiceDeps) {
109
106
  return Effect.succeed(true)
110
107
  }
111
108
 
112
- return effectTryPromise(
113
- () => activeRun.hasActiveRunLease(ensureRecordId(thread.id, TABLES.THREAD)),
114
- 'Failed to check active thread run lease.',
115
- )
109
+ return activeRun
110
+ .hasActiveRunLease(ensureRecordId(thread.id, TABLES.THREAD))
111
+ .pipe(
112
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to check active thread run lease.', cause })),
113
+ )
116
114
  }
117
115
 
118
116
  function toNormalizedThread(
@@ -174,68 +172,59 @@ export function makeThreadService(deps: ThreadServiceDeps) {
174
172
  background: deps.background,
175
173
  })
176
174
 
177
- function getById(threadId: RecordIdRef) {
178
- return effectTryPromise(() => threadStore.getById(threadId), 'Failed to load thread.')
179
- }
175
+ const getById = Effect.fn('ThreadService.getById')(function* (threadId: RecordIdRef) {
176
+ return yield* threadStore
177
+ .getById(threadId)
178
+ .pipe(Effect.mapError((cause) => new ServiceError({ message: 'Failed to load thread.', cause })))
179
+ })
180
180
 
181
- function getThread(threadId: RecordIdRef) {
182
- return Effect.gen(function* () {
183
- const thread = yield* getById(threadId)
184
- return yield* toNormalizedThread(thread)
185
- })
186
- }
181
+ const getThread = Effect.fn('ThreadService.getThread')(function* (threadId: RecordIdRef) {
182
+ const thread = yield* getById(threadId)
183
+ return yield* toNormalizedThread(thread)
184
+ })
187
185
 
188
- function updateTitle(threadId: RecordIdRef, title: string) {
189
- return Effect.gen(function* () {
190
- const existing = yield* getById(threadId)
191
- yield* assertMutableThreadEffect(existing)
192
- const thread = yield* effectTryPromise(
193
- () => threadStore.update(threadId, { title, nameGenerated: true }),
194
- 'Failed to update thread title.',
195
- )
196
- return yield* toNormalizedThread(thread)
197
- })
198
- }
186
+ const updateTitle = Effect.fn('ThreadService.updateTitle')(function* (threadId: RecordIdRef, title: string) {
187
+ const existing = yield* getById(threadId)
188
+ yield* assertMutableThreadEffect(existing)
189
+ const thread = yield* threadStore
190
+ .update(threadId, { title, nameGenerated: true })
191
+ .pipe(Effect.mapError((cause) => new ServiceError({ message: 'Failed to update thread title.', cause })))
192
+ return yield* toNormalizedThread(thread)
193
+ })
199
194
 
200
- function updateStatus(threadId: RecordIdRef, status: string) {
201
- return Effect.gen(function* () {
202
- const parsedStatus = sdkThreadStatusSchema.safeParse(status)
203
- if (!parsedStatus.success) {
204
- return yield* new BadRequestError({ message: `Invalid thread status: ${status}` })
205
- }
206
- const existing = yield* getById(threadId)
207
- yield* assertMutableThreadEffect(existing)
208
- const thread = yield* effectTryPromise(
209
- () => threadStore.update(threadId, { status: parsedStatus.data }),
210
- 'Failed to update thread status.',
211
- )
212
- return yield* toNormalizedThread(thread)
213
- })
214
- }
195
+ const updateStatus = Effect.fn('ThreadService.updateStatus')(function* (threadId: RecordIdRef, status: string) {
196
+ const parsedStatus = sdkThreadStatusSchema.safeParse(status)
197
+ if (!parsedStatus.success) {
198
+ return yield* new BadRequestError({ message: `Invalid thread status: ${status}` })
199
+ }
200
+ const existing = yield* getById(threadId)
201
+ yield* assertMutableThreadEffect(existing)
202
+ const thread = yield* threadStore
203
+ .update(threadId, { status: parsedStatus.data })
204
+ .pipe(Effect.mapError((cause) => new ServiceError({ message: 'Failed to update thread status.', cause })))
205
+ return yield* toNormalizedThread(thread)
206
+ })
215
207
 
216
- function setCompacting(threadId: RecordIdRef, value: boolean) {
208
+ const setCompacting = Effect.fn('ThreadService.setCompacting')(function* (threadId: RecordIdRef, value: boolean) {
217
209
  const threadRef = ensureRecordId(threadId, TABLES.THREAD)
218
210
  const threadIdString = recordIdToString(threadRef, TABLES.THREAD)
219
- return Effect.asVoid(
220
- effectTryPromise(
221
- () => deps.db.query<unknown>(surql`UPDATE ONLY ${threadRef} SET isCompacting = ${value}`),
222
- 'Failed to update thread compaction flag.',
223
- ).pipe(Effect.tap(() => deps.compactionCoordination.signal(threadIdString, value))),
224
- )
225
- }
226
-
227
- function clearThread(threadId: RecordIdRef) {
228
- return Effect.gen(function* () {
229
- const existing = yield* getById(threadId)
230
- yield* assertMutableThreadEffect(existing)
231
- const threadRef = ensureRecordId(threadId, TABLES.THREAD)
232
- yield* effectTryPromise(
233
- () => deps.db.deleteWhere(TABLES.THREAD_MESSAGE, { threadId: threadRef }),
234
- 'Failed to delete thread messages.',
211
+ yield* deps.db
212
+ .query<unknown>(surql`UPDATE ONLY ${threadRef} SET isCompacting = ${value}`)
213
+ .pipe(
214
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to update thread compaction flag.', cause })),
235
215
  )
236
- yield* effectTryPromise(
237
- () =>
238
- deps.db.query<unknown>(surql`
216
+ yield* deps.compactionCoordination.signal(threadIdString, value)
217
+ })
218
+
219
+ const clearThread = Effect.fn('ThreadService.clearThread')(function* (threadId: RecordIdRef) {
220
+ const existing = yield* getById(threadId)
221
+ yield* assertMutableThreadEffect(existing)
222
+ const threadRef = ensureRecordId(threadId, TABLES.THREAD)
223
+ yield* deps.db
224
+ .deleteWhere(TABLES.THREAD_MESSAGE, { threadId: threadRef })
225
+ .pipe(Effect.mapError((cause) => new ServiceError({ message: 'Failed to delete thread messages.', cause })))
226
+ yield* deps.db
227
+ .query<unknown>(surql`
239
228
  UPDATE ONLY ${threadRef}
240
229
  SET turnCount = 0,
241
230
  compactionSummary = NONE,
@@ -243,68 +232,102 @@ export function makeThreadService(deps: ThreadServiceDeps) {
243
232
  activeRunId = NONE,
244
233
  activeStreamId = NONE,
245
234
  isCompacting = false
246
- `),
247
- 'Failed to reset thread state.',
248
- )
249
- })
250
- }
235
+ `)
236
+ .pipe(Effect.mapError((cause) => new ServiceError({ message: 'Failed to reset thread state.', cause })))
237
+ })
251
238
 
252
- function deleteThread(threadId: RecordIdRef) {
253
- return Effect.gen(function* () {
254
- const existing = yield* getById(threadId)
255
- yield* assertMutableThreadEffect(existing)
256
- yield* effectTryPromise(() => threadStore.deleteById(threadId), 'Failed to delete thread.')
257
- })
258
- }
239
+ const deleteThread = Effect.fn('ThreadService.deleteThread')(function* (threadId: RecordIdRef) {
240
+ const existing = yield* getById(threadId)
241
+ yield* assertMutableThreadEffect(existing)
242
+ yield* threadStore
243
+ .deleteById(threadId)
244
+ .pipe(Effect.mapError((cause) => new ServiceError({ message: 'Failed to delete thread.', cause })))
245
+ })
259
246
 
260
247
  function incrementTurnCount(threadId: RecordIdRef) {
261
248
  const threadRef = ensureRecordId(threadId, TABLES.THREAD)
262
249
  return Effect.gen(function* () {
263
- const result = yield* effectTryPromise(
264
- () =>
265
- deps.db.query<{ turnCount: number }>(
266
- surql`
250
+ const result = yield* deps.db
251
+ .query<{ turnCount: number }>(
252
+ surql`
267
253
  UPDATE ONLY ${threadRef}
268
254
  SET turnCount += 1
269
255
  RETURN turnCount
270
256
  `,
271
- ),
272
- 'Failed to increment thread turn count.',
273
- )
257
+ )
258
+ .pipe(
259
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to increment thread turn count.', cause })),
260
+ )
274
261
  return result[0].turnCount
275
262
  })
276
263
  }
277
264
 
278
265
  return {
279
266
  findById: (...args: Parameters<typeof threadStore.findById>) =>
280
- effectTryPromise(() => threadStore.findById(...args), 'Failed to find thread.'),
267
+ threadStore.findById(...args).pipe(
268
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to find thread.', cause })),
269
+ Effect.withSpan('ThreadService.findById'),
270
+ ),
281
271
  getById,
282
272
  findAll: (...args: Parameters<typeof threadStore.findAll>) =>
283
- effectTryPromise(() => threadStore.findAll(...args), 'Failed to find all threads.'),
273
+ threadStore.findAll(...args).pipe(
274
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to find all threads.', cause })),
275
+ Effect.withSpan('ThreadService.findAll'),
276
+ ),
284
277
  create: (...args: Parameters<typeof threadStore.create>) =>
285
- effectTryPromise(() => threadStore.create(...args), 'Failed to create thread.'),
278
+ threadStore.create(...args).pipe(
279
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to create thread.', cause })),
280
+ Effect.withSpan('ThreadService.create'),
281
+ ),
286
282
  update: (...args: Parameters<typeof threadStore.update>) =>
287
- effectTryPromise(() => threadStore.update(...args), 'Failed to update thread.'),
283
+ threadStore.update(...args).pipe(
284
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to update thread.', cause })),
285
+ Effect.withSpan('ThreadService.update'),
286
+ ),
288
287
  delete: (...args: Parameters<typeof threadStore.deleteById>) =>
289
- effectTryPromise(() => threadStore.deleteById(...args), 'Failed to delete thread.'),
288
+ threadStore.deleteById(...args).pipe(
289
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to delete thread.', cause })),
290
+ Effect.withSpan('ThreadService.delete'),
291
+ ),
290
292
  getOrCreateDefault: (...args: Parameters<typeof bootstrap.getOrCreateDefault>) =>
291
- effectTryPromise(() => bootstrap.getOrCreateDefault(...args), 'Failed to get or create default thread.'),
293
+ bootstrap.getOrCreateDefault(...args).pipe(
294
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to get or create default thread.', cause })),
295
+ Effect.withSpan('ThreadService.getOrCreateDefault'),
296
+ ),
292
297
  getOrCreateThread: (...args: Parameters<typeof bootstrap.getOrCreateThread>) =>
293
- effectTryPromise(() => bootstrap.getOrCreateThread(...args), 'Failed to get or create thread.'),
298
+ bootstrap.getOrCreateThread(...args).pipe(
299
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to get or create thread.', cause })),
300
+ Effect.withSpan('ThreadService.getOrCreateThread'),
301
+ ),
294
302
  createThread: (...args: Parameters<typeof bootstrap.createThread>) =>
295
- effectTryPromise(() => bootstrap.createThread(...args), 'Failed to create thread.'),
303
+ bootstrap.createThread(...args).pipe(
304
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to create thread.', cause })),
305
+ Effect.withSpan('ThreadService.createThread'),
306
+ ),
296
307
  ensureBootstrapThreads: (...args: Parameters<typeof bootstrap.ensureBootstrapThreads>) =>
297
- effectTryPromise(() => bootstrap.ensureBootstrapThreads(...args), 'Failed to ensure bootstrap threads.'),
308
+ bootstrap.ensureBootstrapThreads(...args).pipe(
309
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to ensure bootstrap threads.', cause })),
310
+ Effect.withSpan('ThreadService.ensureBootstrapThreads'),
311
+ ),
298
312
  listThreads: (...args: Parameters<typeof listing.listThreads>) =>
299
- effectTryPromise(() => listing.listThreads(...args), 'Failed to list threads.'),
313
+ listing.listThreads(...args).pipe(
314
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to list threads.', cause })),
315
+ Effect.withSpan('ThreadService.listThreads'),
316
+ ),
300
317
  listOrganizationThreads: (...args: Parameters<typeof listing.listOrganizationThreads>) =>
301
- effectTryPromise(() => listing.listOrganizationThreads(...args), 'Failed to list organization threads.'),
318
+ listing.listOrganizationThreads(...args).pipe(
319
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to list organization threads.', cause })),
320
+ Effect.withSpan('ThreadService.listOrganizationThreads'),
321
+ ),
302
322
  getThread,
303
323
  updateTitle,
304
324
  updateStatus,
305
325
  setActiveTurn: activeRun.setActiveTurn,
306
326
  getActiveTurn: (...args: Parameters<typeof activeRun.getActiveTurn>) =>
307
- effectTryPromise(() => activeRun.getActiveTurn(...args), 'Failed to get active turn.'),
327
+ activeRun.getActiveTurn(...args).pipe(
328
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to get active turn.', cause })),
329
+ Effect.withSpan('ThreadService.getActiveTurn'),
330
+ ),
308
331
  getActiveRunId: activeRun.getActiveRunId,
309
332
  hasActiveRunLease: activeRun.hasActiveRunLease,
310
333
  withActiveRunLease: activeRun.withActiveRunLease,
@@ -312,16 +335,28 @@ export function makeThreadService(deps: ThreadServiceDeps) {
312
335
  clearActiveTurn: activeRun.clearActiveTurn,
313
336
  clearStaleActiveRunIfMissingFromRegistry: activeRun.clearStaleActiveRunIfMissingFromRegistry,
314
337
  stopActiveRun: (...args: Parameters<typeof activeRun.stopActiveRun>) =>
315
- effectTryPromise(() => activeRun.stopActiveRun(...args), 'Failed to stop active run.'),
338
+ activeRun.stopActiveRun(...args).pipe(
339
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to stop active run.', cause })),
340
+ Effect.withSpan('ThreadService.stopActiveRun'),
341
+ ),
316
342
  setCompacting,
317
343
  appendMemoryBlock: (...args: Parameters<typeof memory.appendMemoryBlock>) =>
318
- effectTryPromise(() => memory.appendMemoryBlock(...args), 'Failed to append memory block.'),
344
+ memory.appendMemoryBlock(...args).pipe(
345
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to append memory block.', cause })),
346
+ Effect.withSpan('ThreadService.appendMemoryBlock'),
347
+ ),
319
348
  compactMemoryBlock: (...args: Parameters<typeof memory.compactMemoryBlock>) =>
320
- effectTryPromise(() => memory.compactMemoryBlock(...args), 'Failed to compact memory block.'),
349
+ memory.compactMemoryBlock(...args).pipe(
350
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to compact memory block.', cause })),
351
+ Effect.withSpan('ThreadService.compactMemoryBlock'),
352
+ ),
321
353
  clearThread,
322
354
  deleteThread,
323
355
  listRecentThreads: (...args: Parameters<typeof listing.listRecentThreads>) =>
324
- effectTryPromise(() => listing.listRecentThreads(...args), 'Failed to list recent threads.'),
356
+ listing.listRecentThreads(...args).pipe(
357
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to list recent threads.', cause })),
358
+ Effect.withSpan('ThreadService.listRecentThreads'),
359
+ ),
325
360
  formatMemoryBlockForPrompt,
326
361
  toPublicThread,
327
362
  incrementTurnCount,
@@ -343,7 +378,7 @@ export const ThreadServiceLive = Layer.effect(
343
378
  const threadMessageService = yield* ThreadMessageServiceTag
344
379
  const contextCompactionService = yield* ContextCompactionServiceTag
345
380
  const compactionCoordination = yield* CompactionCoordinationTag
346
- const background = yield* BackgroundWorkService
381
+ const background = yield* BackgroundWorkServiceTag
347
382
  return makeThreadService({
348
383
  agentConfig,
349
384
  threadBootstrapConfig,