@lota-sdk/core 0.4.27 → 0.4.29
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.29",
|
|
4
4
|
"files": [
|
|
5
5
|
"src",
|
|
6
6
|
"infrastructure/schema"
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@ai-sdk/provider": "^3.0.9",
|
|
33
33
|
"@chat-adapter/slack": "^4.26.0",
|
|
34
34
|
"@chat-adapter/state-ioredis": "^4.26.0",
|
|
35
|
-
"@lota-sdk/shared": "0.4.
|
|
35
|
+
"@lota-sdk/shared": "0.4.29",
|
|
36
36
|
"@mendable/firecrawl-js": "^4.20.0",
|
|
37
37
|
"@surrealdb/node": "^3.0.3",
|
|
38
38
|
"ai": "^6.0.170",
|
|
@@ -38,8 +38,8 @@ class AiGatewayStreamAttemptTag extends Context.Service<
|
|
|
38
38
|
|
|
39
39
|
const EXPECTED_GATEWAY_KEY_PREFIX = 'sk-bf-'
|
|
40
40
|
const AI_GATEWAY_VIRTUAL_KEY_HEADER = 'x-bf-vk'
|
|
41
|
-
const AI_GATEWAY_TIMEOUT_MS =
|
|
42
|
-
const AI_GATEWAY_STREAM_IDLE_TIMEOUT_MS =
|
|
41
|
+
const AI_GATEWAY_TIMEOUT_MS = 360_000
|
|
42
|
+
const AI_GATEWAY_STREAM_IDLE_TIMEOUT_MS = 180_000
|
|
43
43
|
const AI_GATEWAY_MAX_RETRIES = 4
|
|
44
44
|
const AI_GATEWAY_MAX_RETRY_DELAY_MS = 15_000
|
|
45
45
|
const OPENAI_RESPONSES_PROVIDER_ID = 'openai.responses'
|
|
@@ -748,13 +748,60 @@ function isOpenRouterModel(modelId: string): boolean {
|
|
|
748
748
|
return modelId.trim().toLowerCase().startsWith('openrouter/')
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
+
function mergeAbortSignals(signals: Array<AbortSignal | undefined>): { signal?: AbortSignal; cleanup: () => void } {
|
|
752
|
+
const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal))
|
|
753
|
+
if (activeSignals.length === 0) return { cleanup: () => undefined }
|
|
754
|
+
if (activeSignals.length === 1) return { signal: activeSignals[0], cleanup: () => undefined }
|
|
755
|
+
|
|
756
|
+
const controller = new AbortController()
|
|
757
|
+
const listeners: Array<() => void> = []
|
|
758
|
+
const abortFrom = (signal: AbortSignal) => {
|
|
759
|
+
if (!controller.signal.aborted) controller.abort(signal.reason)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
for (const signal of activeSignals) {
|
|
763
|
+
if (signal.aborted) {
|
|
764
|
+
abortFrom(signal)
|
|
765
|
+
continue
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const listener = () => abortFrom(signal)
|
|
769
|
+
signal.addEventListener('abort', listener, { once: true })
|
|
770
|
+
listeners.push(() => signal.removeEventListener('abort', listener))
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return {
|
|
774
|
+
signal: controller.signal,
|
|
775
|
+
cleanup: () => {
|
|
776
|
+
for (const cleanup of listeners) cleanup()
|
|
777
|
+
},
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function callAbortableProvider<T>(
|
|
782
|
+
params: AiGatewayCallOptions,
|
|
783
|
+
effectSignal: AbortSignal,
|
|
784
|
+
evaluate: (params: AiGatewayCallOptions) => PromiseLike<T>,
|
|
785
|
+
): Promise<T> {
|
|
786
|
+
const { signal, cleanup } = mergeAbortSignals([effectSignal, params.abortSignal])
|
|
787
|
+
try {
|
|
788
|
+
return await evaluate(signal ? { ...params, abortSignal: signal } : params)
|
|
789
|
+
} finally {
|
|
790
|
+
cleanup()
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
751
794
|
function attemptAiGatewayGenerate(
|
|
752
795
|
source: string,
|
|
753
|
-
|
|
796
|
+
params: AiGatewayCallOptions,
|
|
797
|
+
evaluate: (params: AiGatewayCallOptions) => PromiseLike<AiGatewayGenerateResult>,
|
|
754
798
|
): Effect.Effect<AiGatewayAttemptResult<AiGatewayGenerateResult>, AiGenerationError> {
|
|
755
799
|
return withAiGatewayResilience(
|
|
756
800
|
source,
|
|
757
|
-
Effect.tryPromise({
|
|
801
|
+
Effect.tryPromise({
|
|
802
|
+
try: (signal) => callAbortableProvider(params, signal, evaluate),
|
|
803
|
+
catch: (cause) => classifyAiGatewayError(source, cause),
|
|
804
|
+
}),
|
|
758
805
|
).pipe(
|
|
759
806
|
Effect.map((result) => ({ source, result })),
|
|
760
807
|
Effect.withSpan('AiGateway.generateAttempt'),
|
|
@@ -764,11 +811,15 @@ function attemptAiGatewayGenerate(
|
|
|
764
811
|
|
|
765
812
|
function attemptAiGatewayStream(
|
|
766
813
|
source: string,
|
|
767
|
-
|
|
814
|
+
params: AiGatewayCallOptions,
|
|
815
|
+
evaluate: (params: AiGatewayCallOptions) => PromiseLike<AiGatewayStreamResult>,
|
|
768
816
|
): Effect.Effect<AiGatewayAttemptResult<AiGatewayStreamResult>, AiGenerationError> {
|
|
769
817
|
return withAiGatewayResilience(
|
|
770
818
|
source,
|
|
771
|
-
Effect.tryPromise({
|
|
819
|
+
Effect.tryPromise({
|
|
820
|
+
try: (signal) => callAbortableProvider(params, signal, evaluate),
|
|
821
|
+
catch: (cause) => classifyAiGatewayError(source, cause),
|
|
822
|
+
}),
|
|
772
823
|
).pipe(
|
|
773
824
|
Effect.map((result) => ({ source, result })),
|
|
774
825
|
Effect.withSpan('AiGateway.streamAttempt'),
|
|
@@ -778,10 +829,11 @@ function attemptAiGatewayStream(
|
|
|
778
829
|
|
|
779
830
|
function executeGenerateAttemptPlan(
|
|
780
831
|
modelId: string,
|
|
781
|
-
|
|
832
|
+
params: AiGatewayCallOptions,
|
|
833
|
+
doGenerate: (params: AiGatewayCallOptions) => PromiseLike<AiGatewayGenerateResult>,
|
|
782
834
|
): Effect.Effect<AiGatewayAttemptResult<AiGatewayGenerateResult>, AiGenerationError> {
|
|
783
835
|
const primary = Layer.succeed(AiGatewayGenerateAttemptTag, {
|
|
784
|
-
execute: attemptAiGatewayGenerate('ai-gateway.generate', doGenerate),
|
|
836
|
+
execute: attemptAiGatewayGenerate('ai-gateway.generate', params, doGenerate),
|
|
785
837
|
})
|
|
786
838
|
const effect = Effect.gen(function* () {
|
|
787
839
|
const attempt = yield* AiGatewayGenerateAttemptTag
|
|
@@ -797,10 +849,11 @@ function executeGenerateAttemptPlan(
|
|
|
797
849
|
|
|
798
850
|
function executeStreamAttemptPlan(
|
|
799
851
|
modelId: string,
|
|
800
|
-
|
|
852
|
+
params: AiGatewayCallOptions,
|
|
853
|
+
doStream: (params: AiGatewayCallOptions) => PromiseLike<AiGatewayStreamResult>,
|
|
801
854
|
): Effect.Effect<AiGatewayAttemptResult<AiGatewayStreamResult>, AiGenerationError> {
|
|
802
855
|
const primary = Layer.succeed(AiGatewayStreamAttemptTag, {
|
|
803
|
-
execute: attemptAiGatewayStream('ai-gateway.stream', doStream),
|
|
856
|
+
execute: attemptAiGatewayStream('ai-gateway.stream', params, doStream),
|
|
804
857
|
})
|
|
805
858
|
const effect = Effect.gen(function* () {
|
|
806
859
|
const attempt = yield* AiGatewayStreamAttemptTag
|
|
@@ -1046,7 +1099,7 @@ function createAiGatewayLanguageModelMiddleware(
|
|
|
1046
1099
|
const model = resolveProviderModel(resolvedDeps.gateway.provider, modelId, providerId)
|
|
1047
1100
|
return resolvedDeps.runPromise(
|
|
1048
1101
|
withAiGatewayConcurrency(
|
|
1049
|
-
executeGenerateAttemptPlan(modelId, () => model.doGenerate(
|
|
1102
|
+
executeGenerateAttemptPlan(modelId, params, (attemptParams) => model.doGenerate(attemptParams)).pipe(
|
|
1050
1103
|
Effect.map(({ result }) => ({
|
|
1051
1104
|
...result,
|
|
1052
1105
|
content: injectAiGatewayChatReasoningContent(
|
|
@@ -1063,7 +1116,7 @@ function createAiGatewayLanguageModelMiddleware(
|
|
|
1063
1116
|
const model = resolveProviderModel(resolvedDeps.gateway.provider, modelId, providerId)
|
|
1064
1117
|
return resolvedDeps.runPromise(
|
|
1065
1118
|
withAiGatewayStreamConcurrency(
|
|
1066
|
-
executeStreamAttemptPlan(modelId, () => model.doStream(
|
|
1119
|
+
executeStreamAttemptPlan(modelId, params, (attemptParams) => model.doStream(attemptParams)).pipe(
|
|
1067
1120
|
Effect.map((attempt) => ({
|
|
1068
1121
|
...attempt,
|
|
1069
1122
|
result: isReasoningEnabled(params)
|
|
@@ -66,11 +66,13 @@ export function createDocumentProcessorQueueRuntime<TJob extends DocumentProcess
|
|
|
66
66
|
queueName?: string
|
|
67
67
|
workerName?: string
|
|
68
68
|
concurrency?: number
|
|
69
|
+
attempts?: number
|
|
69
70
|
lockDuration?: number
|
|
70
71
|
}): { enqueue: (job: TJob) => Promise<void>; startWorker: (options?: { registerSignals?: boolean }) => WorkerHandle } {
|
|
71
72
|
const queueName = params.queueName ?? DEFAULT_DOCUMENT_PROCESSOR_QUEUE
|
|
72
73
|
const workerName = params.workerName ?? DEFAULT_WORKER_NAME
|
|
73
74
|
const concurrency = params.concurrency ?? 10
|
|
75
|
+
const attempts = params.attempts ?? 3
|
|
74
76
|
const lockDuration = params.lockDuration ?? 300_000
|
|
75
77
|
const queueRuntime = createQueueFactory<TJob>({
|
|
76
78
|
name: queueName,
|
|
@@ -80,7 +82,7 @@ export function createDocumentProcessorQueueRuntime<TJob extends DocumentProcess
|
|
|
80
82
|
lockDuration,
|
|
81
83
|
logger: params.logger,
|
|
82
84
|
connectionProvider: params.getConnectionForBullMQ,
|
|
83
|
-
defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts
|
|
85
|
+
defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts, backoff: { type: 'exponential', delay: 1000 } },
|
|
84
86
|
processorPath: params.getWorkerPath(),
|
|
85
87
|
queueJobService: params.queueJobService,
|
|
86
88
|
})
|
|
@@ -9,6 +9,7 @@ import { ERROR_TAGS } from '../effect/errors'
|
|
|
9
9
|
import type { TrackedBullJobLike } from '../services/queue-job.service'
|
|
10
10
|
import {
|
|
11
11
|
attachWorkerEvents,
|
|
12
|
+
attachSandboxChildRecycling,
|
|
12
13
|
createTracedWorkerProcessor,
|
|
13
14
|
createWorkerShutdown,
|
|
14
15
|
DEFAULT_JOB_RETENTION,
|
|
@@ -42,6 +43,7 @@ interface QueueFactoryConfigBase {
|
|
|
42
43
|
stalledInterval?: number
|
|
43
44
|
maxStalledCount?: number
|
|
44
45
|
defaultJobOptions?: JobsOptions
|
|
46
|
+
recycleSandboxChildren?: boolean
|
|
45
47
|
connectionProvider: () => IORedis
|
|
46
48
|
queueJobService: QueueJobService
|
|
47
49
|
}
|
|
@@ -280,6 +282,9 @@ function createQueueFactoryRuntime<TJob>(config: QueueFactoryConfigBase): {
|
|
|
280
282
|
// leaks.
|
|
281
283
|
try {
|
|
282
284
|
attachWorkerEvents(worker, config.displayName, logger)
|
|
285
|
+
if ((config.recycleSandboxChildren ?? true) && workerConfig.processorPath) {
|
|
286
|
+
attachSandboxChildRecycling(worker, config.displayName, logger)
|
|
287
|
+
}
|
|
283
288
|
const shutdown = createWorkerShutdown(worker, config.displayName, logger)
|
|
284
289
|
|
|
285
290
|
if (registerSignals) {
|
|
@@ -4,7 +4,7 @@ import { AiGatewayModelsTag, isAiGenerationContentFilterError } from '../ai-gate
|
|
|
4
4
|
import type { AiGatewayModels } from '../ai-gateway/ai-gateway'
|
|
5
5
|
import type { ResolvedAgentConfig } from '../config/agent-defaults'
|
|
6
6
|
import { chatLogger } from '../config/logger'
|
|
7
|
-
import {
|
|
7
|
+
import { ServiceError } from '../effect/errors'
|
|
8
8
|
import { AgentConfigServiceTag } from '../effect/services'
|
|
9
9
|
import type { HelperModelRuntime } from '../runtime/helper-model'
|
|
10
10
|
import { HelperModelTag } from '../runtime/helper-model'
|
|
@@ -13,10 +13,19 @@ import {
|
|
|
13
13
|
makeRecentActivityTitleRefinerAgentFactory,
|
|
14
14
|
RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
|
|
15
15
|
} from '../system-agents/recent-activity-title-refiner.agent'
|
|
16
|
+
import { compactWhitespace, truncateText } from '../utils/string'
|
|
16
17
|
import type { makeRecentActivityService } from './recent-activity.service'
|
|
17
18
|
import { RecentActivityServiceTag } from './recent-activity.service'
|
|
18
19
|
|
|
19
20
|
const RECENT_ACTIVITY_TITLE_TIMEOUT_MS = 60_000
|
|
21
|
+
const RECENT_ACTIVITY_TITLE_FIELD_MAX_CHARS = 800
|
|
22
|
+
|
|
23
|
+
function formatPromptField(label: string, value: string | undefined): string | null {
|
|
24
|
+
if (!value) return null
|
|
25
|
+
const normalized = compactWhitespace(value)
|
|
26
|
+
if (!normalized) return null
|
|
27
|
+
return `${label}=${truncateText(normalized, RECENT_ACTIVITY_TITLE_FIELD_MAX_CHARS)}`
|
|
28
|
+
}
|
|
20
29
|
|
|
21
30
|
function buildRefinementPromptInput(
|
|
22
31
|
candidate: {
|
|
@@ -32,12 +41,12 @@ function buildRefinementPromptInput(
|
|
|
32
41
|
|
|
33
42
|
const metadata = candidate.metadata
|
|
34
43
|
const lines = [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
formatPromptField('sourceLabel', candidate.sourceLabel),
|
|
45
|
+
formatPromptField('systemTitle', candidate.systemTitle),
|
|
46
|
+
formatPromptField('agentName', metadata.agentName),
|
|
47
|
+
formatPromptField('threadTitle', metadata.threadTitle),
|
|
48
|
+
formatPromptField('userMessage', metadata.userMessageText),
|
|
49
|
+
formatPromptField('assistantSummary', metadata.assistantSummary),
|
|
41
50
|
].filter((line): line is string => Boolean(line))
|
|
42
51
|
|
|
43
52
|
if (lines.length === 0) return null
|
|
@@ -77,13 +86,12 @@ export function makeRecentActivityTitleService(
|
|
|
77
86
|
? cause
|
|
78
87
|
: new ServiceError({ message: 'Failed to generate recent activity title refinement.', cause }),
|
|
79
88
|
}).pipe(
|
|
80
|
-
Effect.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
: Effect.fail(error),
|
|
89
|
+
Effect.catch((error) =>
|
|
90
|
+
Effect.sync(() => {
|
|
91
|
+
const reason = isAiGenerationContentFilterError(error) ? 'provider content filter' : 'non-fatal error'
|
|
92
|
+
chatLogger.warn`Skipping recent activity title refinement after ${reason} (activityId=${activityId}): ${error}`
|
|
93
|
+
return null
|
|
94
|
+
}),
|
|
87
95
|
),
|
|
88
96
|
)
|
|
89
97
|
if (maybeRefinedTitle === null) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
1
2
|
import { fileURLToPath } from 'node:url'
|
|
2
3
|
|
|
3
4
|
import type { Job, Worker } from 'bullmq'
|
|
@@ -30,6 +31,32 @@ export interface WorkerHandle {
|
|
|
30
31
|
shutdown: () => Promise<void>
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
async function closeWorkerWithTimeout(
|
|
35
|
+
worker: Worker,
|
|
36
|
+
name: string,
|
|
37
|
+
logger: typeof chatLogger,
|
|
38
|
+
timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const gracefulClose = worker.close(false)
|
|
41
|
+
const gracefulOutcome = gracefulClose.then(
|
|
42
|
+
() => ({ status: 'closed' as const }),
|
|
43
|
+
(error: unknown) => ({ status: 'failed' as const, error }),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const outcome = await Promise.race([gracefulOutcome, delay(timeoutMs).then(() => ({ status: 'timed_out' as const }))])
|
|
47
|
+
|
|
48
|
+
if (outcome.status === 'closed') return
|
|
49
|
+
|
|
50
|
+
if (outcome.status === 'timed_out') {
|
|
51
|
+
logger.warn`${name} worker did not close within ${timeoutMs}ms; force-closing`
|
|
52
|
+
} else {
|
|
53
|
+
logger.warn`${name} worker graceful close failed; force-closing: ${outcome.error}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await worker.close(true)
|
|
57
|
+
if (outcome.status === 'failed') throw outcome.error
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
interface TracedWorkerJobLike {
|
|
34
61
|
id?: unknown
|
|
35
62
|
name: string
|
|
@@ -87,7 +114,7 @@ export const createWorkerShutdown = (worker: Worker, name: string, logger: typeo
|
|
|
87
114
|
return Effect.runPromise(
|
|
88
115
|
Effect.asVoid(
|
|
89
116
|
Effect.tryPromise({
|
|
90
|
-
try: () => worker
|
|
117
|
+
try: () => closeWorkerWithTimeout(worker, name, logger),
|
|
91
118
|
catch: (cause) => new QueueWorkerError({ phase: 'close', cause }),
|
|
92
119
|
}),
|
|
93
120
|
),
|
|
@@ -95,6 +122,60 @@ export const createWorkerShutdown = (worker: Worker, name: string, logger: typeo
|
|
|
95
122
|
}
|
|
96
123
|
}
|
|
97
124
|
|
|
125
|
+
interface SandboxChildLike {
|
|
126
|
+
pid?: number
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface SandboxChildPoolLike {
|
|
130
|
+
getAllFree?: () => SandboxChildLike[]
|
|
131
|
+
kill?: (child: SandboxChildLike, signal?: NodeJS.Signals) => Promise<void>
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface SandboxedWorkerLike {
|
|
135
|
+
childPool?: SandboxChildPoolLike
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getSandboxChildPool(worker: Worker): SandboxChildPoolLike | null {
|
|
139
|
+
const pool = (worker as unknown as SandboxedWorkerLike).childPool
|
|
140
|
+
return pool && typeof pool.getAllFree === 'function' && typeof pool.kill === 'function' ? pool : null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function recycleIdleSandboxChildren(
|
|
144
|
+
worker: Worker,
|
|
145
|
+
name: string,
|
|
146
|
+
logger: typeof chatLogger = chatLogger,
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
const pool = getSandboxChildPool(worker)
|
|
149
|
+
if (!pool) return Promise.resolve()
|
|
150
|
+
|
|
151
|
+
const idleChildren = pool.getAllFree?.() ?? []
|
|
152
|
+
if (idleChildren.length === 0) return Promise.resolve()
|
|
153
|
+
|
|
154
|
+
return Promise.all(
|
|
155
|
+
idleChildren.map((child) =>
|
|
156
|
+
(pool.kill?.(child, 'SIGTERM') ?? Promise.resolve()).catch((error: unknown) => {
|
|
157
|
+
logger.warn`Failed to recycle idle ${name} sandbox child (${child.pid ?? 'unknown'}): ${error}`
|
|
158
|
+
}),
|
|
159
|
+
),
|
|
160
|
+
).then(() => undefined)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function attachSandboxChildRecycling(
|
|
164
|
+
worker: Worker,
|
|
165
|
+
name: string,
|
|
166
|
+
logger: typeof chatLogger = chatLogger,
|
|
167
|
+
): void {
|
|
168
|
+
const recycle = () => {
|
|
169
|
+
// @effect-diagnostics-next-line globalTimers:off -- BullMQ worker event callback; defer until BullMQ releases the child.
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
void recycleIdleSandboxChildren(worker, name, logger)
|
|
172
|
+
}, 0)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
worker.on('completed', recycle)
|
|
176
|
+
worker.on('failed', recycle)
|
|
177
|
+
}
|
|
178
|
+
|
|
98
179
|
export function createTracedWorkerProcessor<TJob extends TracedWorkerJobLike, TResult = void>(
|
|
99
180
|
queueName: string,
|
|
100
181
|
processor: (job: TJob) => Promise<TResult>,
|