@lota-sdk/core 0.4.13 → 0.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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/config/constants.ts +1 -1
  8. package/src/create-runtime.ts +259 -200
  9. package/src/db/cursor-pagination.ts +2 -9
  10. package/src/db/memory-store.ts +194 -175
  11. package/src/db/memory.ts +125 -71
  12. package/src/db/schema-fingerprint.ts +5 -4
  13. package/src/db/service-normalization.ts +4 -3
  14. package/src/db/service.ts +3 -2
  15. package/src/db/startup.ts +15 -16
  16. package/src/effect/errors.ts +161 -21
  17. package/src/effect/index.ts +0 -1
  18. package/src/embeddings/provider.ts +15 -7
  19. package/src/queues/autonomous-job.queue.ts +10 -22
  20. package/src/queues/delayed-node-promotion.queue.ts +8 -14
  21. package/src/queues/document-processor.queue.ts +13 -4
  22. package/src/queues/memory-consolidation.queue.ts +26 -14
  23. package/src/queues/plan-agent-heartbeat.queue.ts +48 -31
  24. package/src/queues/plan-scheduler.queue.ts +37 -15
  25. package/src/queues/queue-factory.ts +59 -35
  26. package/src/queues/standalone-worker.ts +3 -2
  27. package/src/redis/connection.ts +10 -3
  28. package/src/redis/org-memory-lock.ts +1 -1
  29. package/src/redis/redis-lease-lock.ts +5 -5
  30. package/src/redis/stream-context.ts +1 -1
  31. package/src/runtime/chat-message.ts +64 -1
  32. package/src/runtime/chat-run-orchestration.ts +33 -20
  33. package/src/runtime/context-compaction/context-compaction-runtime.ts +14 -7
  34. package/src/runtime/context-compaction/context-compaction.ts +78 -66
  35. package/src/runtime/domain-layer.ts +19 -13
  36. package/src/runtime/execution-plan.ts +7 -3
  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 +22 -24
  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 +83 -92
  103. package/src/services/thread/thread-turn.ts +18 -16
  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 +11 -7
  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
@@ -1,6 +1,7 @@
1
1
  import type Firecrawl from '@mendable/firecrawl-js'
2
+ import type { Effect } from 'effect'
2
3
 
3
- import { aiGatewayChatModel } from '../ai-gateway/ai-gateway'
4
+ import type { AiGatewayModels } from '../ai-gateway/ai-gateway'
4
5
  import { buildAiGatewayStrictSemanticCacheHeaders } from '../ai-gateway/cache-headers'
5
6
  import {
6
7
  OPENROUTER_FAST_REASONING_MODEL_ID,
@@ -13,19 +14,22 @@ import { searchWebTool } from './search-web.tool'
13
14
 
14
15
  export interface ResearchTopicToolContext {
15
16
  firecrawl: Firecrawl
17
+ aiGatewayModels: AiGatewayModels
18
+ runPromise: <A, E>(effect: Effect.Effect<A, E, never>, options?: { signal?: AbortSignal }) => Promise<A>
16
19
  }
17
20
 
18
21
  export const researchTopicTool = createDelegatedAgentToolWithContext<ResearchTopicToolContext>({
19
22
  id: 'researchTopic',
20
23
  description:
21
24
  'Delegate a research task to a dedicated research agent that searches the web, fetches pages, and returns a synthesized markdown report.',
22
- model: () => aiGatewayChatModel(OPENROUTER_FAST_REASONING_MODEL_ID),
25
+ model: ({ aiGatewayModels }) => aiGatewayModels.chatModel(OPENROUTER_FAST_REASONING_MODEL_ID),
23
26
  providerOptions: OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
24
27
  headers: buildAiGatewayStrictSemanticCacheHeaders('researchTopic'),
25
28
  instructions: RESEARCHER_PROMPT,
26
- createTools: ({ firecrawl }) => ({
27
- searchWeb: searchWebTool.create({ firecrawl }),
28
- fetchWebpage: fetchWebpageTool.create({ firecrawl }),
29
+ createTools: ({ firecrawl, runPromise }) => ({
30
+ searchWeb: searchWebTool.create({ firecrawl, runPromise }),
31
+ fetchWebpage: fetchWebpageTool.create({ firecrawl, runPromise }),
29
32
  }),
30
33
  maxSteps: 6,
34
+ getRunPromise: (context) => context.runPromise,
31
35
  })
@@ -4,6 +4,7 @@ import { Effect, Schema } from 'effect'
4
4
  import { z } from 'zod'
5
5
 
6
6
  import type { ToolDefinition } from '../ai/definitions'
7
+ import { ERROR_TAGS } from '../effect/errors'
7
8
  import { withTimeout } from '../utils/async'
8
9
  import { nowIsoDateTimeString } from '../utils/date-time'
9
10
  import { readStringField, truncateOptionalText } from '../utils/string'
@@ -12,9 +13,10 @@ import { toRecord, WEB_TOOL_TIMEOUT_MS } from './web-tool-shared'
12
13
 
13
14
  export interface SearchWebToolContext {
14
15
  firecrawl: Firecrawl
16
+ runPromise: <A, E>(effect: Effect.Effect<A, E, never>, options?: { signal?: AbortSignal }) => Promise<A>
15
17
  }
16
18
 
17
- class SearchWebToolError extends Schema.TaggedErrorClass<SearchWebToolError>()('@lota-sdk/core/SearchWebToolError', {
19
+ class SearchWebToolError extends Schema.TaggedErrorClass<SearchWebToolError>()(ERROR_TAGS.SearchWebToolError, {
18
20
  message: Schema.String,
19
21
  cause: Schema.optional(Schema.Defect),
20
22
  }) {}
@@ -139,7 +141,7 @@ function buildWebCitations(results: { web?: unknown[]; news?: unknown[]; images?
139
141
 
140
142
  export const searchWebTool = {
141
143
  name: 'searchWeb',
142
- create: ({ firecrawl }: SearchWebToolContext) =>
144
+ create: ({ firecrawl, runPromise }: SearchWebToolContext) =>
143
145
  tool({
144
146
  description: 'Search the web for real-time information.',
145
147
  inputSchema: z
@@ -151,20 +153,17 @@ export const searchWebTool = {
151
153
  tbs: z.string().optional(),
152
154
  })
153
155
  .strict(),
154
- execute: ({
155
- query,
156
- limit,
157
- sources,
158
- location,
159
- tbs,
160
- }: {
161
- query: string
162
- limit?: number
163
- sources?: ('web' | 'news' | 'images')[]
164
- location?: string
165
- tbs?: string
166
- }) =>
167
- Effect.runPromise(
156
+ execute: (
157
+ {
158
+ query,
159
+ limit,
160
+ sources,
161
+ location,
162
+ tbs,
163
+ }: { query: string; limit?: number; sources?: ('web' | 'news' | 'images')[]; location?: string; tbs?: string },
164
+ { abortSignal }: { abortSignal?: AbortSignal } = {},
165
+ ) =>
166
+ runPromise(
168
167
  Effect.gen(function* () {
169
168
  const results = yield* Effect.tryPromise({
170
169
  try: () =>
@@ -185,6 +184,7 @@ export const searchWebTool = {
185
184
  citations: buildWebCitations(results),
186
185
  }
187
186
  }).pipe(Effect.withSpan('tool.searchWeb.execute')),
187
+ abortSignal ? { signal: abortSignal } : undefined,
188
188
  ),
189
189
  }),
190
190
  } as const satisfies ToolDefinition<SearchWebToolContext>
@@ -15,18 +15,24 @@ const ConversationSearchInputSchema = z.object({ query: z.string().min(1), type:
15
15
  type MemorySearchService = Pick<ReturnType<typeof createMemoryService>, 'searchAllMemoriesBatched'>
16
16
  type ConversationSearchService = Pick<ReturnType<typeof makeThreadMessageService>, 'searchMessagesEffect'>
17
17
 
18
+ type SearchToolRunPromise = <A, E>(effect: Effect.Effect<A, E, never>, options?: { signal?: AbortSignal }) => Promise<A>
19
+
18
20
  export function createMemorySearchTool(
19
21
  agentConfig: ResolvedAgentConfig,
20
22
  orgIdString: string,
21
23
  memoryService: MemorySearchService,
24
+ runPromise: SearchToolRunPromise,
22
25
  agentName?: string,
23
26
  options?: { fastMode?: boolean; allowMultiScopeRerank?: boolean },
24
27
  ) {
25
28
  return tool({
26
29
  description: 'Search organization and agent memories relevant to a query.',
27
30
  inputSchema: MemorySearchInputSchema,
28
- execute: ({ query }: z.infer<typeof MemorySearchInputSchema>) =>
29
- Effect.runPromise(
31
+ execute: (
32
+ { query }: z.infer<typeof MemorySearchInputSchema>,
33
+ { abortSignal }: { abortSignal?: AbortSignal } = {},
34
+ ) =>
35
+ runPromise(
30
36
  Effect.gen(function* () {
31
37
  const normalizedQuery = query.trim()
32
38
  const retrieval = yield* memoryService.searchAllMemoriesBatched({
@@ -41,16 +47,24 @@ export function createMemorySearchTool(
41
47
  const reminder = `Remember: you searched for "${normalizedQuery}". Use the highest-relevance retrieved lines above to answer.`
42
48
  return { query: normalizedQuery, retrieval, reminder }
43
49
  }),
50
+ abortSignal ? { signal: abortSignal } : undefined,
44
51
  ),
45
52
  })
46
53
  }
47
54
 
48
- export function createConversationSearchTool(threadId: RecordIdRef, threadMessageService: ConversationSearchService) {
55
+ export function createConversationSearchTool(
56
+ threadId: RecordIdRef,
57
+ threadMessageService: ConversationSearchService,
58
+ runPromise: SearchToolRunPromise,
59
+ ) {
49
60
  return tool({
50
61
  description: 'Search prior chat messages by role and query text.',
51
62
  inputSchema: ConversationSearchInputSchema,
52
- execute: ({ query, type }: z.infer<typeof ConversationSearchInputSchema>) =>
53
- Effect.runPromise(
63
+ execute: (
64
+ { query, type }: z.infer<typeof ConversationSearchInputSchema>,
65
+ { abortSignal }: { abortSignal?: AbortSignal } = {},
66
+ ) =>
67
+ runPromise(
54
68
  Effect.gen(function* () {
55
69
  const normalizedQuery = query.trim()
56
70
  const results = yield* threadMessageService.searchMessagesEffect({
@@ -61,6 +75,7 @@ export function createConversationSearchTool(threadId: RecordIdRef, threadMessag
61
75
  })
62
76
  return { query: normalizedQuery, type, count: results.length, results }
63
77
  }),
78
+ abortSignal ? { signal: abortSignal } : undefined,
64
79
  ),
65
80
  })
66
81
  }
@@ -7,7 +7,7 @@ import { aiLogger } from '../config/logger'
7
7
  import type { RecordIdRef } from '../db/record-id'
8
8
  import { recordIdToString } from '../db/record-id'
9
9
  import { TABLES } from '../db/tables'
10
- import { effectTryMaybeAsync as effectTryMaybeAsyncShared } from '../effect/helpers'
10
+ import { ERROR_TAGS } from '../effect/errors'
11
11
  import {
12
12
  AgentConfigServiceTag,
13
13
  AgentFactoryServiceTag,
@@ -35,29 +35,25 @@ function buildTeamThinkAgentToolsEffect(
35
35
  return Effect.succeed({ tools: {} })
36
36
  }
37
37
 
38
- return effectTryMaybeAsync(() => builder(params), 'Failed to build team-think agent tools.')
38
+ return Effect.tryPromise({
39
+ try: () => builder(params),
40
+ catch: (error) => new TeamThinkRuntimeError({ message: 'Failed to build team-think agent tools.', cause: error }),
41
+ })
39
42
  }
40
43
 
41
44
  const TEAM_THINK_AGENT_MAX_RETRIES = 1
42
45
  const TEAM_THINK_AGENT_MAX_STEPS = 3
43
46
 
44
47
  class TeamThinkAgentFactoryNotConfiguredError extends Schema.TaggedErrorClass<TeamThinkAgentFactoryNotConfiguredError>()(
45
- 'TeamThinkAgentFactoryNotConfiguredError',
48
+ '@lota-sdk/core/TeamThinkAgentFactoryNotConfiguredError',
46
49
  { agentId: Schema.String },
47
50
  ) {}
48
51
 
49
- class TeamThinkRuntimeError extends Schema.TaggedErrorClass<TeamThinkRuntimeError>()('TeamThinkRuntimeError', {
52
+ class TeamThinkRuntimeError extends Schema.TaggedErrorClass<TeamThinkRuntimeError>()(ERROR_TAGS.TeamThinkRuntimeError, {
50
53
  message: Schema.String,
51
54
  cause: Schema.optional(Schema.Defect),
52
55
  }) {}
53
56
 
54
- function effectTryMaybeAsync<A>(
55
- evaluate: () => A | PromiseLike<A>,
56
- message: string,
57
- ): Effect.Effect<A, TeamThinkRuntimeError> {
58
- return effectTryMaybeAsyncShared(evaluate, (error) => new TeamThinkRuntimeError({ message, cause: error }))
59
- }
60
-
61
57
  export function createTeamThinkTool(params: {
62
58
  historyMessages: ChatMessage[]
63
59
  latestUserMessageId: string
@@ -76,6 +72,7 @@ export function createTeamThinkTool(params: {
76
72
  context?: unknown
77
73
  toolProviders?: ToolSet
78
74
  abortSignal: AbortSignal
75
+ runPromise: <A, E>(effect: Effect.Effect<A, E, never>, options?: { signal?: AbortSignal }) => Promise<A>
79
76
  }) {
80
77
  return Effect.gen(function* () {
81
78
  const turnHooks = yield* TurnHooksServiceTag
@@ -87,35 +84,54 @@ export function createTeamThinkTool(params: {
87
84
  )
88
85
  const participantRunner: TeamConsultationParticipantRunner = {
89
86
  buildParticipantAgent(agentId, runParams) {
90
- return Effect.runPromise(
87
+ return params.runPromise(
91
88
  Effect.gen(function* () {
89
+ // Capture the fiber context of the managed runtime and replay it
90
+ // for observer callbacks that cross the Effect → Promise boundary
91
+ // (AI SDK agent observers). This preserves spans/loggers across
92
+ // the callback edge — it is NOT an ambient-runtime slot; the
93
+ // captured context lives only for this participant's fiber.
92
94
  const currentContext = yield* Effect.context()
93
95
  const runPromiseWithCurrentContext = Effect.runPromiseWith(currentContext)
94
- const dynamicInstructionSections = yield* effectTryMaybeAsync(
95
- () => params.getAdditionalInstructionSections?.(),
96
- 'Failed to load dynamic team-think instruction sections.',
97
- )
96
+ const getAdditionalInstructionSections = params.getAdditionalInstructionSections
97
+ const dynamicInstructionSections = getAdditionalInstructionSections
98
+ ? yield* Effect.tryPromise({
99
+ try: () => getAdditionalInstructionSections(),
100
+ catch: (error) =>
101
+ new TeamThinkRuntimeError({
102
+ message: 'Failed to load dynamic team-think instruction sections.',
103
+ cause: error,
104
+ }),
105
+ })
106
+ : undefined
107
+ const resolveAgent = turnHooks.resolveAgent
98
108
  const agentResolution = asRecord(
99
- yield* effectTryMaybeAsync(
100
- () =>
101
- turnHooks.resolveAgent?.({
102
- agentId,
103
- mode: 'fixedThreadMode',
104
- thread: null,
105
- threadRef: params.threadId,
106
- orgRef: params.orgId,
107
- userRef: params.userId,
108
- onboardingActive: false,
109
- linearInstalled: false,
110
- githubInstalled: params.githubInstalled,
111
- additionalInstructionSections: mergeInstructionSections(
112
- dynamicInstructionSections,
113
- params.additionalInstructionSections,
114
- ),
115
- context: (params.context as Record<string, unknown> | null | undefined) ?? null,
116
- }),
117
- 'Failed to resolve team-think participant agent.',
118
- ),
109
+ resolveAgent
110
+ ? yield* Effect.tryPromise({
111
+ try: () =>
112
+ resolveAgent({
113
+ agentId,
114
+ mode: 'fixedThreadMode',
115
+ thread: null,
116
+ threadRef: params.threadId,
117
+ orgRef: params.orgId,
118
+ userRef: params.userId,
119
+ onboardingActive: false,
120
+ linearInstalled: false,
121
+ githubInstalled: params.githubInstalled,
122
+ additionalInstructionSections: mergeInstructionSections(
123
+ dynamicInstructionSections,
124
+ params.additionalInstructionSections,
125
+ ),
126
+ context: (params.context as Record<string, unknown> | null | undefined) ?? null,
127
+ }),
128
+ catch: (error) =>
129
+ new TeamThinkRuntimeError({
130
+ message: 'Failed to resolve team-think participant agent.',
131
+ cause: error,
132
+ }),
133
+ })
134
+ : undefined,
119
135
  )
120
136
  const resolvedAgentId = readOptionalString(agentResolution?.agentId) ?? agentId
121
137
  const config = agentFactoryConfig.getAgentRuntimeConfig({
@@ -169,9 +185,16 @@ export function createTeamThinkTool(params: {
169
185
  stopWhen: [stepCountIs(maxSteps)],
170
186
  })
171
187
  const observer = {
172
- run: <T>(fn: () => T | Promise<T>): Promise<T> =>
188
+ run: <T>(fn: () => Promise<T>): Promise<T> =>
173
189
  runPromiseWithCurrentContext(
174
- effectTryMaybeAsync(fn, `Team-think participant run failed (${agentId}).`),
190
+ Effect.tryPromise({
191
+ try: () => fn(),
192
+ catch: (error) =>
193
+ new TeamThinkRuntimeError({
194
+ message: `Team-think participant run failed (${agentId}).`,
195
+ cause: error,
196
+ }),
197
+ }),
175
198
  ),
176
199
  recordError: (error: unknown) => {
177
200
  aiLogger.error`Team-think participant failed (${agentId}): ${error}`
@@ -1,16 +1,16 @@
1
1
  import { Cause, Duration, Effect, Exit, Schema } from 'effect'
2
2
 
3
3
  import { serverLogger } from '../config/logger'
4
- import { TimeoutError } from '../effect/errors'
4
+ import { ERROR_TAGS, TimeoutError } from '../effect/errors'
5
5
  import { getErrorMessage, toError } from './errors'
6
6
 
7
- class TimedOperationError extends Schema.TaggedErrorClass<TimedOperationError>()('TimedOperationError', {
7
+ class TimedOperationError extends Schema.TaggedErrorClass<TimedOperationError>()(ERROR_TAGS.TimedOperationError, {
8
8
  operation: Schema.String,
9
9
  cause: Schema.Defect,
10
10
  }) {}
11
11
 
12
12
  function isTimedOperationError(error: unknown): error is TimedOperationError {
13
- return typeof error === 'object' && error !== null && '_tag' in error && error._tag === 'TimedOperationError'
13
+ return typeof error === 'object' && error !== null && '_tag' in error && error._tag === ERROR_TAGS.TimedOperationError
14
14
  }
15
15
 
16
16
  export function withTimeout<T>(promise: Promise<T>, ms: number, operation: string): Promise<T> {
@@ -46,8 +46,8 @@ export function createSafeEnqueue(logger: { warn: (message: string) => void }) {
46
46
 
47
47
  const _defaultSafeEnqueue = createSafeEnqueue({ warn: (message: string) => serverLogger.warn`${message}` })
48
48
  export function safeEnqueue<T>(
49
- operation: () => T | Promise<T>,
49
+ operation: () => Promise<T>,
50
50
  options: { operationName: string; onError?: (error: unknown) => void; logPrefix?: string },
51
51
  ): Promise<T | void> {
52
- return _defaultSafeEnqueue(() => Promise.resolve(operation()), options)
52
+ return _defaultSafeEnqueue(operation, options)
53
53
  }
@@ -1,8 +1,8 @@
1
1
  export { getErrorMessage } from '@lota-sdk/shared'
2
2
  import { Data, Match } from 'effect'
3
3
 
4
- import type { EffectError, ValidationIssue } from '../effect/errors'
5
- import { BaseServicePersistenceError, isEffectError } from '../effect/errors'
4
+ import type { BaseServicePersistenceError, EffectError, ValidationIssue } from '../effect/errors'
5
+ import { ERROR_TAGS, isEffectError } from '../effect/errors'
6
6
 
7
7
  export function toError(value: unknown): Error {
8
8
  return value instanceof Error ? value : new Error(String(value))
@@ -129,29 +129,30 @@ export function toAppErrorResponse(error: AppErrorLike): AppErrorResponse {
129
129
  }
130
130
 
131
131
  function isBaseServicePersistenceError(error: EffectError): error is BaseServicePersistenceError {
132
- return error._tag === BaseServicePersistenceError.name
132
+ return error._tag === ERROR_TAGS.BaseServicePersistenceError
133
133
  }
134
134
 
135
135
  const toHttpErrorMatch = Match.type<Exclude<EffectError, BaseServicePersistenceError>>().pipe(
136
- Match.tag('NotFoundError', (e) => httpError(e.message, 'NOT_FOUND', 404)),
137
- Match.tag('BadRequestError', (e) => httpError(e.message, 'BAD_REQUEST', 400)),
138
- Match.tag('ValidationError', (e) => httpError(e.message, 'VALIDATION_ERROR', 400)),
139
- Match.tag('ConflictError', (e) => httpError(e.message, 'CONFLICT', 409)),
140
- Match.tag('ForbiddenError', (e) => httpError(e.message, 'FORBIDDEN', 403)),
141
- Match.tag('ThreadTurnError', (e) =>
136
+ Match.tag(ERROR_TAGS.NotFoundError, (e) => httpError(e.message, 'NOT_FOUND', 404)),
137
+ Match.tag(ERROR_TAGS.BadRequestError, (e) => httpError(e.message, 'BAD_REQUEST', 400)),
138
+ Match.tag(ERROR_TAGS.ValidationError, (e) => httpError(e.message, 'VALIDATION_ERROR', 400)),
139
+ Match.tag(ERROR_TAGS.ConflictError, (e) => httpError(e.message, 'CONFLICT', 409)),
140
+ Match.tag(ERROR_TAGS.ForbiddenError, (e) => httpError(e.message, 'FORBIDDEN', 403)),
141
+ Match.tag(ERROR_TAGS.ThreadTurnError, (e) =>
142
142
  httpError(e.message, e.reason === 'conflict' ? 'CONFLICT' : 'BAD_REQUEST', e.reason === 'conflict' ? 409 : 400),
143
143
  ),
144
- Match.tag('ActiveThreadRunConflictError', (e) => httpError(e.message, 'CONFLICT', 409)),
145
- Match.tag('ConfigurationError', (e) => httpError(e.message, 'INTERNAL_SERVER_ERROR', 500)),
146
- Match.tag('DatabaseError', () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
147
- Match.tag('RedisError', () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
148
- Match.tag('TimeoutError', (e) =>
144
+ Match.tag(ERROR_TAGS.ActiveThreadRunConflictError, (e) => httpError(e.message, 'CONFLICT', 409)),
145
+ Match.tag(ERROR_TAGS.ConfigurationError, (e) => httpError(e.message, 'INTERNAL_SERVER_ERROR', 500)),
146
+ Match.tag(ERROR_TAGS.DatabaseError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
147
+ Match.tag(ERROR_TAGS.RedisError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
148
+ Match.tag(ERROR_TAGS.RuntimeLifecycleError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
149
+ Match.tag(ERROR_TAGS.TimeoutError, (e) =>
149
150
  httpError(`Operation "${e.operation}" timed out after ${e.ms}ms`, 'INTERNAL_SERVER_ERROR', 500),
150
151
  ),
151
- Match.tag('LockAcquisitionError', () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
152
- Match.tag('LockLostError', () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
153
- Match.tag('AiGenerationError', (e) => httpError(e.message, 'INTERNAL_SERVER_ERROR', 500)),
154
- Match.tag('ServiceError', () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
152
+ Match.tag(ERROR_TAGS.LockAcquisitionError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
153
+ Match.tag(ERROR_TAGS.LockLostError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
154
+ Match.tag(ERROR_TAGS.AiGenerationError, (e) => httpError(e.message, 'INTERNAL_SERVER_ERROR', 500)),
155
+ Match.tag(ERROR_TAGS.ServiceError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
155
156
  Match.exhaustive,
156
157
  )
157
158
 
@@ -1,12 +1,25 @@
1
- import { Duration, Effect, Fiber } from 'effect'
1
+ import type { Fiber } from 'effect'
2
+ import { Duration, Effect, Exit, Schema, Scope } from 'effect'
3
+
4
+ import { ERROR_TAGS } from '../effect/errors'
2
5
 
3
6
  const KEEPALIVE_COMMENT = new TextEncoder().encode(': keepalive\n\n')
4
7
  const DEFAULT_KEEPALIVE_INTERVAL_MS = 20_000
5
8
 
9
+ class SseKeepaliveError extends Schema.TaggedErrorClass<SseKeepaliveError>()(ERROR_TAGS.SseKeepaliveError, {
10
+ phase: Schema.Literals(['read']),
11
+ cause: Schema.Defect,
12
+ }) {}
13
+
6
14
  /**
7
15
  * Wraps an SSE Response body with periodic keepalive comments.
8
16
  * SSE comments (`: keepalive\n\n`) are ignored by standard SSE parsers,
9
17
  * so no client changes are needed.
18
+ *
19
+ * Lifecycle: a function-local `Scope` owns both the keepalive and body-pump
20
+ * fibers via `Effect.forkScoped`. Stream `cancel` closes the scope, which
21
+ * auto-interrupts both fibers and runs their `ensuring` finalizers. No
22
+ * manual `Fiber.interrupt` bookkeeping.
10
23
  */
11
24
  export function wrapResponseWithKeepalive(response: Response, intervalMs = DEFAULT_KEEPALIVE_INTERVAL_MS): Response {
12
25
  const body = response.body
@@ -14,24 +27,10 @@ export function wrapResponseWithKeepalive(response: Response, intervalMs = DEFAU
14
27
 
15
28
  let closed = false
16
29
  let reader: ReadableStreamDefaultReader<Uint8Array> | null = null
17
- let keepaliveFiber: ReturnType<typeof Effect.runFork> | null = null
18
- let bodyPumpFiber: ReturnType<typeof Effect.runFork> | null = null
19
-
20
- const interruptFiber = (fiber: ReturnType<typeof Effect.runFork> | null) => {
21
- if (!fiber) return
22
- void Effect.runFork(Fiber.interrupt(fiber))
23
- }
30
+ const scope = Scope.makeUnsafe()
24
31
 
25
- const stopKeepalive = () => {
26
- const fiber = keepaliveFiber
27
- keepaliveFiber = null
28
- interruptFiber(fiber)
29
- }
30
-
31
- const stopBodyPump = () => {
32
- const fiber = bodyPumpFiber
33
- bodyPumpFiber = null
34
- interruptFiber(fiber)
32
+ const closeScope = () => {
33
+ void Effect.runPromise(Scope.close(scope, Exit.void))
35
34
  }
36
35
 
37
36
  const releaseReader = (bodyReader: ReadableStreamDefaultReader<Uint8Array>) => {
@@ -90,7 +89,10 @@ export function wrapResponseWithKeepalive(response: Response, intervalMs = DEFAU
90
89
  for (;;) {
91
90
  if (closed) return
92
91
 
93
- const { done, value } = yield* Effect.tryPromise(() => bodyReader.read())
92
+ const { done, value } = yield* Effect.tryPromise({
93
+ try: () => bodyReader.read(),
94
+ catch: (cause) => new SseKeepaliveError({ phase: 'read', cause }),
95
+ })
94
96
  if (done) {
95
97
  yield* Effect.sync(() => closeStream(controller))
96
98
  return
@@ -105,25 +107,26 @@ export function wrapResponseWithKeepalive(response: Response, intervalMs = DEFAU
105
107
  Effect.ensuring(
106
108
  Effect.sync(() => {
107
109
  closed = true
108
- stopKeepalive()
109
- bodyPumpFiber = null
110
110
  reader = null
111
111
  releaseReader(bodyReader)
112
+ closeScope()
112
113
  }),
113
114
  ),
114
115
  )
115
116
 
117
+ const startFiber = <A, E>(effect: Effect.Effect<A, E>): Fiber.Fiber<Fiber.Fiber<A, E>, never> =>
118
+ Effect.runFork(Scope.provide(Effect.forkScoped(effect), scope))
119
+
116
120
  const transformed = new ReadableStream<Uint8Array>({
117
121
  start(controller) {
118
122
  const bodyReader = body.getReader()
119
123
  reader = bodyReader
120
- keepaliveFiber = Effect.runFork(keepaliveEffect(controller))
121
- bodyPumpFiber = Effect.runFork(pumpBodyEffect(bodyReader, controller))
124
+ startFiber(keepaliveEffect(controller))
125
+ startFiber(pumpBodyEffect(bodyReader, controller))
122
126
  },
123
127
  cancel(reason) {
124
128
  closed = true
125
- stopKeepalive()
126
- stopBodyPump()
129
+ closeScope()
127
130
 
128
131
  const bodyReader = reader
129
132
  reader = null
@@ -1,17 +1,25 @@
1
- import { ConfigProvider, Option, Schema, Effect, Layer, ManagedRuntime, Redacted } from 'effect'
2
-
3
- import { AiGatewayLive } from '../ai-gateway/ai-gateway'
1
+ import { ConfigProvider, Deferred, Option, Schema, Effect, Layer, ManagedRuntime, Redacted } from 'effect'
2
+
3
+ import {
4
+ AiGatewayModelsTag,
5
+ AiGatewayTag,
6
+ RuntimeBridgeTag,
7
+ createAiGatewayModels,
8
+ makeAiGatewayService,
9
+ } from '../ai-gateway/ai-gateway'
10
+ import type { AiGatewayModels, RuntimeBridge } from '../ai-gateway/ai-gateway'
4
11
  import { EmbeddingCacheLive } from '../ai/embedding-cache'
5
12
  import { serverLogger } from '../config/logger'
6
13
  import { connectWithStartupRetry, waitForDatabaseBootstrap } from '../db/startup'
7
14
  import { buildWorkerInfrastructureLayer } from '../effect'
15
+ import { ERROR_TAGS } from '../effect/errors'
8
16
  import { DatabaseServiceTag, RuntimeAdaptersServiceTag } from '../effect/services'
9
17
  import { lotaRuntimeEnvConfig, parseLotaRuntimeConfig, parseWorkerBootstrapEnv } from '../runtime/runtime-config'
10
18
  import { FirecrawlLive } from '../tools/firecrawl-client'
11
19
  import { getErrorMessage } from '../utils/errors'
12
20
 
13
21
  class SandboxedWorkerBootstrapError extends Schema.TaggedErrorClass<SandboxedWorkerBootstrapError>()(
14
- 'SandboxedWorkerBootstrapError',
22
+ ERROR_TAGS.SandboxedWorkerBootstrapError,
15
23
  {
16
24
  stage: Schema.Literals(['setup', 'initialize', 'connect-db', 'connect-plugin-db', 'bootstrap-wait']),
17
25
  message: Schema.String,
@@ -86,12 +94,35 @@ function ensureSandboxedWorkerRuntimeConfigured(): Promise<WorkerManagedRuntime>
86
94
  const runtimeConfig = yield* buildSandboxedWorkerRuntimeConfigEffect()
87
95
 
88
96
  const infrastructureLayer = buildWorkerInfrastructureLayer(runtimeConfig)
89
-
90
- const layer = Layer.mergeAll(infrastructureLayer, Layer.provide(AiGatewayLive, infrastructureLayer))
91
-
92
- const fullLayer = Layer.mergeAll(layer, Layer.provide(Layer.mergeAll(EmbeddingCacheLive, FirecrawlLive), layer))
97
+ const aiGateway = yield* makeAiGatewayService(runtimeConfig).pipe(
98
+ Effect.mapError((error) => toSandboxedWorkerBootstrapError('setup', error)),
99
+ )
100
+ const aiGatewayModelsDeferred = yield* Deferred.make<AiGatewayModels>()
101
+ const runtimeBridgeDeferred = yield* Deferred.make<RuntimeBridge>()
102
+ const bridgeLayer = Layer.mergeAll(
103
+ Layer.succeed(AiGatewayTag, aiGateway),
104
+ Layer.effect(AiGatewayModelsTag, Deferred.await(aiGatewayModelsDeferred)),
105
+ Layer.effect(RuntimeBridgeTag, Deferred.await(runtimeBridgeDeferred)),
106
+ )
107
+ const layerWithBridge = Layer.mergeAll(infrastructureLayer, bridgeLayer)
108
+ const fullLayer = Layer.mergeAll(
109
+ layerWithBridge,
110
+ Layer.provide(Layer.mergeAll(EmbeddingCacheLive, FirecrawlLive), layerWithBridge),
111
+ )
93
112
 
94
113
  const managedRuntime = ManagedRuntime.make(fullLayer)
114
+ const runtimeBridge: RuntimeBridge = {
115
+ runPromise: (effect, options) => managedRuntime.runPromise(effect, options),
116
+ runFork: (effect) => managedRuntime.runFork(effect),
117
+ }
118
+ const aiGatewayModels = createAiGatewayModels({
119
+ gateway: aiGateway,
120
+ runtimeConfig,
121
+ runPromise: runtimeBridge.runPromise,
122
+ runFork: runtimeBridge.runFork,
123
+ })
124
+ yield* Deferred.succeed(runtimeBridgeDeferred, runtimeBridge)
125
+ yield* Deferred.succeed(aiGatewayModelsDeferred, aiGatewayModels)
95
126
 
96
127
  return managedRuntime
97
128
  }),
@@ -111,7 +142,10 @@ export function initializeSandboxedWorkerRuntime(): Promise<WorkerManagedRuntime
111
142
  // Assign before the async kicks off so concurrent callers observe the in-flight promise.
112
143
  sandboxedWorkerInitPromise = Effect.runPromise(
113
144
  Effect.gen(function* () {
114
- const env = parseWorkerBootstrapEnv(Bun.env)
145
+ const env = yield* Effect.try({
146
+ try: () => parseWorkerBootstrapEnv(Bun.env),
147
+ catch: (error) => toSandboxedWorkerBootstrapError('setup', error),
148
+ })
115
149
  const runtime = yield* Effect.tryPromise({
116
150
  try: () => ensureSandboxedWorkerRuntimeConfigured(),
117
151
  catch: (error) => toSandboxedWorkerBootstrapError('setup', error),
@@ -124,7 +158,7 @@ export function initializeSandboxedWorkerRuntime(): Promise<WorkerManagedRuntime
124
158
  yield* Effect.tryPromise({
125
159
  try: () =>
126
160
  connectWithStartupRetry({
127
- connect: () => db.connect(),
161
+ connect: () => runtime.runPromise(db.connect()),
128
162
  label: 'sandboxed worker AI database runtime',
129
163
  logger: serverLogger,
130
164
  }),
@@ -156,7 +190,7 @@ export function initializeSandboxedWorkerRuntime(): Promise<WorkerManagedRuntime
156
190
  expectedFingerprint: env.DB_SCHEMA_FINGERPRINT,
157
191
  label: 'sandboxed worker runtime',
158
192
  logger: serverLogger,
159
- connect: () => db.connect(),
193
+ connect: () => runtime.runPromise(db.connect()),
160
194
  }),
161
195
  catch: (error) => toSandboxedWorkerBootstrapError('bootstrap-wait', error),
162
196
  })
@@ -170,3 +204,33 @@ export function initializeSandboxedWorkerRuntime(): Promise<WorkerManagedRuntime
170
204
 
171
205
  return sandboxedWorkerInitPromise
172
206
  }
207
+
208
+ /**
209
+ * Dispose the sandboxed worker runtime and clear both module-level caches so
210
+ * a subsequent initialize call rebuilds a fresh runtime. Registered as a
211
+ * SIGTERM/SIGINT handler when the worker process runs as the main entrypoint.
212
+ */
213
+ // @effect-diagnostics-next-line asyncFunction:off -- host-boundary dispose handler returns Promise by design.
214
+ export async function disposeSandboxedWorkerRuntime(): Promise<void> {
215
+ const setupPromise = sandboxedWorkerSetupPromise
216
+ sandboxedWorkerSetupPromise = null
217
+ sandboxedWorkerInitPromise = null
218
+ if (!setupPromise) return
219
+ try {
220
+ const runtime = await setupPromise
221
+ await runtime.dispose()
222
+ } catch (error) {
223
+ serverLogger.warn`Failed to dispose sandboxed worker runtime: ${error}`
224
+ }
225
+ }
226
+
227
+ let sandboxedWorkerShutdownHandlersRegistered = false
228
+ export function registerSandboxedWorkerShutdownHandlers(): void {
229
+ if (sandboxedWorkerShutdownHandlersRegistered) return
230
+ sandboxedWorkerShutdownHandlersRegistered = true
231
+ const handle = () => {
232
+ void disposeSandboxedWorkerRuntime().catch(() => undefined)
233
+ }
234
+ process.once('SIGTERM', handle)
235
+ process.once('SIGINT', handle)
236
+ }