@lota-sdk/core 0.4.13 → 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 (138) 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/memory/memory-block.ts +3 -9
  37. package/src/runtime/memory/memory-scope.ts +3 -1
  38. package/src/runtime/plugin-resolution.ts +2 -1
  39. package/src/runtime/post-turn-side-effects.ts +6 -5
  40. package/src/runtime/retrieval-adapters.ts +8 -20
  41. package/src/runtime/runtime-config.ts +3 -9
  42. package/src/runtime/runtime-extensions.ts +2 -4
  43. package/src/runtime/runtime-lifecycle.ts +56 -16
  44. package/src/runtime/runtime-services.ts +180 -102
  45. package/src/runtime/runtime-worker-registry.ts +3 -1
  46. package/src/runtime/social-chat/social-chat-agent-runner.ts +1 -1
  47. package/src/runtime/social-chat/social-chat-history.ts +21 -18
  48. package/src/runtime/social-chat/social-chat.ts +356 -223
  49. package/src/runtime/specialist-runner.ts +3 -1
  50. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +3 -2
  51. package/src/runtime/thread-turn-context.ts +142 -102
  52. package/src/runtime/turn-lifecycle.ts +15 -46
  53. package/src/services/agent-activity.service.ts +1 -1
  54. package/src/services/agent-executor.service.ts +107 -77
  55. package/src/services/autonomous-job.service.ts +354 -293
  56. package/src/services/background-work.service.ts +3 -3
  57. package/src/services/context-compaction.service.ts +7 -2
  58. package/src/services/document-chunk.service.ts +50 -32
  59. package/src/services/execution-plan/execution-plan-schedule.ts +5 -3
  60. package/src/services/execution-plan/execution-plan.service.ts +162 -179
  61. package/src/services/feedback-loop.service.ts +5 -4
  62. package/src/services/graph-full-routing.ts +37 -36
  63. package/src/services/institutional-memory.service.ts +28 -30
  64. package/src/services/learned-skill.service.ts +107 -72
  65. package/src/services/memory/memory-errors.ts +4 -23
  66. package/src/services/memory/memory-org-memory.ts +10 -5
  67. package/src/services/memory/memory-rerank.ts +18 -6
  68. package/src/services/memory/memory.service.ts +170 -111
  69. package/src/services/memory/rerank.service.ts +29 -20
  70. package/src/services/organization-member.service.ts +1 -1
  71. package/src/services/organization.service.ts +69 -75
  72. package/src/services/ownership-dispatcher.service.ts +40 -39
  73. package/src/services/plan/plan-agent-heartbeat.service.ts +26 -23
  74. package/src/services/plan/plan-agent-query.service.ts +39 -31
  75. package/src/services/plan/plan-completion-side-effects.ts +13 -17
  76. package/src/services/plan/plan-coordination.service.ts +2 -1
  77. package/src/services/plan/plan-cycle.service.ts +6 -5
  78. package/src/services/plan/plan-deadline.service.ts +57 -54
  79. package/src/services/plan/plan-event-delivery.service.ts +5 -4
  80. package/src/services/plan/plan-executor-graph.ts +18 -15
  81. package/src/services/plan/plan-executor.service.ts +235 -262
  82. package/src/services/plan/plan-run.service.ts +169 -93
  83. package/src/services/plan/plan-scheduler.service.ts +192 -202
  84. package/src/services/plan/plan-template.service.ts +1 -1
  85. package/src/services/plan/plan-transaction-events.ts +1 -1
  86. package/src/services/plan/plan-workspace.service.ts +23 -14
  87. package/src/services/plugin-executor.service.ts +5 -9
  88. package/src/services/queue-job.service.ts +117 -59
  89. package/src/services/recent-activity-title.service.ts +13 -12
  90. package/src/services/recent-activity.service.ts +6 -1
  91. package/src/services/social-chat-history.service.ts +29 -25
  92. package/src/services/system-executor.service.ts +5 -9
  93. package/src/services/thread/thread-active-run.ts +2 -2
  94. package/src/services/thread/thread-listing.ts +61 -57
  95. package/src/services/thread/thread-memory-block.ts +73 -48
  96. package/src/services/thread/thread-message.service.ts +76 -65
  97. package/src/services/thread/thread-record-store.ts +8 -8
  98. package/src/services/thread/thread-title.service.ts +10 -4
  99. package/src/services/thread/thread-turn-execution.ts +43 -45
  100. package/src/services/thread/thread-turn-preparation.service.ts +257 -135
  101. package/src/services/thread/thread-turn-streaming.ts +82 -85
  102. package/src/services/thread/thread-turn.ts +8 -8
  103. package/src/services/thread/thread.service.ts +135 -100
  104. package/src/services/user.service.ts +45 -48
  105. package/src/storage/attachment-parser.ts +6 -2
  106. package/src/storage/attachment-storage.service.ts +5 -6
  107. package/src/storage/generated-document-storage.service.ts +1 -1
  108. package/src/system-agents/context-compaction.agent.ts +10 -9
  109. package/src/system-agents/delegated-agent-factory.ts +30 -6
  110. package/src/system-agents/memory-reranker.agent.ts +10 -9
  111. package/src/system-agents/memory.agent.ts +10 -9
  112. package/src/system-agents/recent-activity-title-refiner.agent.ts +13 -15
  113. package/src/system-agents/regular-chat-memory-digest.agent.ts +13 -12
  114. package/src/system-agents/skill-extractor.agent.ts +13 -12
  115. package/src/system-agents/skill-manager.agent.ts +13 -12
  116. package/src/system-agents/thread-router.agent.ts +10 -5
  117. package/src/system-agents/title-generator.agent.ts +13 -12
  118. package/src/tools/fetch-webpage.tool.ts +13 -13
  119. package/src/tools/memory-block.tool.ts +3 -1
  120. package/src/tools/plan-approval.tool.ts +4 -2
  121. package/src/tools/read-file-parts.tool.ts +10 -4
  122. package/src/tools/remember-memory.tool.ts +3 -1
  123. package/src/tools/research-topic.tool.ts +9 -5
  124. package/src/tools/search-web.tool.ts +16 -16
  125. package/src/tools/search.tool.ts +20 -5
  126. package/src/tools/team-think.tool.ts +61 -38
  127. package/src/utils/async.ts +5 -5
  128. package/src/utils/errors.ts +19 -18
  129. package/src/utils/sse-keepalive.ts +28 -25
  130. package/src/workers/bootstrap.ts +75 -11
  131. package/src/workers/memory-consolidation.worker.ts +82 -91
  132. package/src/workers/organization-learning.worker.ts +14 -4
  133. package/src/workers/regular-chat-memory-digest.runner.ts +105 -67
  134. package/src/workers/skill-extraction.runner.ts +97 -61
  135. package/src/workers/utils/repo-structure-extractor.ts +13 -8
  136. package/src/workers/utils/thread-message-query.ts +24 -24
  137. package/src/workers/worker-utils.ts +23 -4
  138. package/src/effect/helpers.ts +0 -123
@@ -1,7 +1,7 @@
1
1
  import type { CreateHelperToolLoopAgentOptions } from '@lota-sdk/shared'
2
2
  import { ToolLoopAgent } from 'ai'
3
3
 
4
- import { aiGatewayChatModel } from '../ai-gateway/ai-gateway'
4
+ import type { AiGatewayModels } from '../ai-gateway/ai-gateway'
5
5
  import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
6
6
  import {
7
7
  OPENROUTER_FAST_REASONING_MODEL_ID,
@@ -30,15 +30,16 @@ Return only the title text. No quotes, no labels, no explanation.
30
30
  </output-format>
31
31
  </agent-instructions>`
32
32
 
33
- export function createThreadTitleGeneratorAgent(options: CreateHelperToolLoopAgentOptions) {
34
- return new ToolLoopAgent({
35
- id: 'thread-title-generator',
36
- model: aiGatewayChatModel(OPENROUTER_FAST_REASONING_MODEL_ID),
37
- headers: buildAiGatewayDirectCacheHeaders('lota-sdk'),
38
- providerOptions: OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
39
- ...resolveHelperAgentOptions(options, {
40
- instructions: THREAD_TITLE_GENERATOR_PROMPT,
41
- maxOutputTokens: THREAD_TITLE_MAX_TOKENS,
42
- }),
43
- })
33
+ export function makeThreadTitleGeneratorAgentFactory(models: AiGatewayModels) {
34
+ return (options: CreateHelperToolLoopAgentOptions) =>
35
+ new ToolLoopAgent({
36
+ id: 'thread-title-generator',
37
+ model: models.chatModel(OPENROUTER_FAST_REASONING_MODEL_ID),
38
+ headers: buildAiGatewayDirectCacheHeaders('lota-sdk'),
39
+ providerOptions: OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
40
+ ...resolveHelperAgentOptions(options, {
41
+ instructions: THREAD_TITLE_GENERATOR_PROMPT,
42
+ maxOutputTokens: THREAD_TITLE_MAX_TOKENS,
43
+ }),
44
+ })
44
45
  }
@@ -12,6 +12,7 @@ import { toRecord, WEB_TOOL_TIMEOUT_MS } from './web-tool-shared'
12
12
 
13
13
  export interface FetchWebpageToolContext {
14
14
  firecrawl: Firecrawl
15
+ runPromise: <A, E>(effect: Effect.Effect<A, E, never>, options?: { signal?: AbortSignal }) => Promise<A>
15
16
  }
16
17
 
17
18
  class FetchWebpageToolError extends Schema.TaggedErrorClass<FetchWebpageToolError>()(
@@ -102,7 +103,7 @@ function buildFetchCitations(url: string, document: unknown): WebCitation[] {
102
103
 
103
104
  export const fetchWebpageTool = {
104
105
  name: 'fetchWebpage',
105
- create: ({ firecrawl }: FetchWebpageToolContext) =>
106
+ create: ({ firecrawl, runPromise }: FetchWebpageToolContext) =>
106
107
  tool({
107
108
  description: 'Retrieve and parse a single webpage.',
108
109
  inputSchema: z
@@ -113,18 +114,16 @@ export const fetchWebpageTool = {
113
114
  maxAge: z.number().int().min(1).optional(),
114
115
  })
115
116
  .strict(),
116
- execute: ({
117
- url,
118
- formats,
119
- onlyMainContent,
120
- maxAge,
121
- }: {
122
- url: string
123
- formats?: z.infer<typeof FormatSchema>[]
124
- onlyMainContent?: boolean
125
- maxAge?: number
126
- }) =>
127
- Effect.runPromise(
117
+ execute: (
118
+ {
119
+ url,
120
+ formats,
121
+ onlyMainContent,
122
+ maxAge,
123
+ }: { url: string; formats?: z.infer<typeof FormatSchema>[]; onlyMainContent?: boolean; maxAge?: number },
124
+ { abortSignal }: { abortSignal?: AbortSignal } = {},
125
+ ) =>
126
+ runPromise(
128
127
  Effect.gen(function* () {
129
128
  const result = yield* Effect.tryPromise({
130
129
  try: () =>
@@ -141,6 +140,7 @@ export const fetchWebpageTool = {
141
140
  })
142
141
  return { url, document: summarizeDocument(url, result), citations: buildFetchCitations(url, result) }
143
142
  }).pipe(Effect.withSpan('tool.fetchWebpage.execute')),
143
+ abortSignal ? { signal: abortSignal } : undefined,
144
144
  ),
145
145
  }),
146
146
  } as const satisfies ToolDefinition<FetchWebpageToolContext>
@@ -18,12 +18,14 @@ export function createMemoryBlockTool({
18
18
  threadId,
19
19
  agentLabel,
20
20
  threadService,
21
+ runPromise,
21
22
  getCurrentBlock,
22
23
  onAppend,
23
24
  }: {
24
25
  threadId: RecordIdRef
25
26
  agentLabel: string
26
27
  threadService: MemoryBlockThreadService
28
+ runPromise: <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A>
27
29
  getCurrentBlock?: () => string
28
30
  onAppend?: (value: string) => void
29
31
  }) {
@@ -48,7 +50,7 @@ export function createMemoryBlockTool({
48
50
 
49
51
  void safeEnqueue(
50
52
  () =>
51
- Effect.runPromise(
53
+ runPromise(
52
54
  Effect.gen(function* () {
53
55
  const updated = yield* threadService.appendMemoryBlock(threadId, prepared.formatted)
54
56
  onAppend?.(updated)
@@ -21,14 +21,15 @@ export function createPlanApprovalTool(params: {
21
21
  threadId: RecordIdRef
22
22
  actorId: string
23
23
  executionPlanService: PlanApprovalExecutionPlanService
24
+ runPromise: <A, E>(effect: Effect.Effect<A, E, never>, options?: { signal?: AbortSignal }) => Promise<A>
24
25
  }) {
25
26
  return tool({
26
27
  description:
27
28
  'Approve, reject, or request changes to a plan that is pending approval. Use approve to start execution, reject to abort it, or modify to request a revised plan.',
28
29
  inputSchema: PlanApprovalArgsSchema,
29
30
  outputSchema: PlanApprovalToolResultDataSchema,
30
- execute: (input) =>
31
- Effect.runPromise(
31
+ execute: (input, { abortSignal }: { abortSignal?: AbortSignal } = {}) =>
32
+ params.runPromise(
32
33
  Effect.gen(function* () {
33
34
  switch (input.action) {
34
35
  case 'approve': {
@@ -71,6 +72,7 @@ export function createPlanApprovalTool(params: {
71
72
  }
72
73
  }
73
74
  }),
75
+ abortSignal ? { signal: abortSignal } : undefined,
74
76
  ),
75
77
  })
76
78
  }
@@ -3,6 +3,7 @@ import { Schema, Effect } from 'effect'
3
3
  import { z } from 'zod'
4
4
 
5
5
  import type { ToolDefinition } from '../ai/definitions'
6
+ import { ERROR_TAGS } from '../effect/errors'
6
7
  import type { makeAttachmentService } from '../services/attachment.service'
7
8
  import type { ReadableUploadMetadata } from '../storage/attachment-types'
8
9
 
@@ -15,6 +16,7 @@ export interface ReadFilePartsToolContext {
15
16
  userId: string
16
17
  uploads: ReadableUploadMetadata[]
17
18
  attachmentService: ReadFilePartsAttachmentService
19
+ runPromise: <A, E>(effect: Effect.Effect<A, E, never>, options?: { signal?: AbortSignal }) => Promise<A>
18
20
  }
19
21
 
20
22
  function toUploadSummary(upload: ReadableUploadMetadata) {
@@ -26,7 +28,7 @@ function toUploadSummary(upload: ReadableUploadMetadata) {
26
28
  }
27
29
  }
28
30
 
29
- class ReadFilePartsError extends Schema.TaggedErrorClass<ReadFilePartsError>()('ReadFilePartsError', {
31
+ class ReadFilePartsError extends Schema.TaggedErrorClass<ReadFilePartsError>()(ERROR_TAGS.ReadFilePartsError, {
30
32
  message: Schema.String,
31
33
  }) {}
32
34
 
@@ -47,15 +49,18 @@ function resolveUploadTargetEffect(params: {
47
49
 
48
50
  export const readFilePartsTool = {
49
51
  name: 'readFileParts',
50
- create: ({ orgId, userId, uploads, attachmentService }: ReadFilePartsToolContext) =>
52
+ create: ({ orgId, userId, uploads, attachmentService, runPromise }: ReadFilePartsToolContext) =>
51
53
  tool({
52
54
  description:
53
55
  'Read uploaded file content by part. Call with no args to list uploads metadata, then call with storageKey and part (25 pages per part).',
54
56
  inputSchema: z
55
57
  .object({ storageKey: z.string().trim().min(1).optional(), part: z.number().int().positive().default(1) })
56
58
  .strict(),
57
- execute: ({ storageKey, part }: { storageKey?: string; part?: number }) =>
58
- Effect.runPromise(
59
+ execute: (
60
+ { storageKey, part }: { storageKey?: string; part?: number },
61
+ { abortSignal }: { abortSignal?: AbortSignal } = {},
62
+ ) =>
63
+ runPromise(
59
64
  Effect.gen(function* () {
60
65
  const availableUploads = uploads.map(toUploadSummary)
61
66
  const selected = yield* resolveUploadTargetEffect({ uploads, storageKey })
@@ -86,6 +91,7 @@ export const readFilePartsTool = {
86
91
 
87
92
  return { availableUploads, selectedUpload: toUploadSummary(selected), ...parts }
88
93
  }),
94
+ abortSignal ? { signal: abortSignal } : undefined,
89
95
  ),
90
96
  }),
91
97
  } as const satisfies ToolDefinition<ReadFilePartsToolContext>
@@ -24,10 +24,12 @@ export function createRememberMemoryTool({
24
24
  orgId,
25
25
  agentName,
26
26
  memoryService,
27
+ runPromise,
27
28
  }: {
28
29
  orgId: RecordIdRef
29
30
  agentName: string
30
31
  memoryService: RememberMemoryService
32
+ runPromise: <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A>
31
33
  }) {
32
34
  return tool({
33
35
  description:
@@ -42,7 +44,7 @@ export function createRememberMemoryTool({
42
44
  const orgIdString = recordIdToString(orgId, TABLES.ORGANIZATION)
43
45
  void safeEnqueue(
44
46
  () =>
45
- Effect.runPromise(
47
+ runPromise(
46
48
  Effect.gen(function* () {
47
49
  const assessment = yield* memoryService.assessMemoryCandidate({ orgId: orgIdString, content: trimmed })
48
50
  if (!assessment) {
@@ -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