@lota-sdk/core 0.4.29 → 0.4.31
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 +2 -2
- package/src/ai-gateway/ai-gateway.ts +41 -2
- package/src/db/memory-store.helpers.ts +26 -3
- package/src/db/startup.ts +18 -11
- package/src/db/transaction-conflict.ts +37 -11
- package/src/queues/queue-factory.ts +2 -1
- package/src/runtime/context-compaction/context-compaction-runtime.ts +10 -1
- package/src/runtime/helper-model.ts +4 -1
- package/src/services/queue-job.service.ts +2 -2
- package/src/workers/worker-utils.ts +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.31",
|
|
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.31",
|
|
36
36
|
"@mendable/firecrawl-js": "^4.20.0",
|
|
37
37
|
"@surrealdb/node": "^3.0.3",
|
|
38
38
|
"ai": "^6.0.170",
|
|
@@ -992,10 +992,47 @@ function removeJsonSchemaFormatKeywords(value: unknown): void {
|
|
|
992
992
|
}
|
|
993
993
|
}
|
|
994
994
|
|
|
995
|
-
function
|
|
995
|
+
function requireAllJsonSchemaObjectProperties(value: unknown): void {
|
|
996
|
+
if (Array.isArray(value)) {
|
|
997
|
+
for (const item of value) {
|
|
998
|
+
requireAllJsonSchemaObjectProperties(item)
|
|
999
|
+
}
|
|
1000
|
+
return
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (!isRecord(value)) {
|
|
1004
|
+
return
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (isRecord(value.properties)) {
|
|
1008
|
+
const propertyNames = Object.keys(value.properties)
|
|
1009
|
+
if (propertyNames.length > 0) {
|
|
1010
|
+
const currentRequired = Array.isArray(value.required)
|
|
1011
|
+
? value.required.filter((item): item is string => typeof item === 'string')
|
|
1012
|
+
: []
|
|
1013
|
+
value.required = [...new Set([...currentRequired, ...propertyNames])]
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1018
|
+
if (JSON_SCHEMA_PROPERTY_MAP_KEYS.has(key) && isRecord(child)) {
|
|
1019
|
+
for (const propertySchema of Object.values(child)) {
|
|
1020
|
+
requireAllJsonSchemaObjectProperties(propertySchema)
|
|
1021
|
+
}
|
|
1022
|
+
continue
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
requireAllJsonSchemaObjectProperties(child)
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function cloneAiGatewayJsonSchema(schema: JSONSchema7, options?: { requireAllProperties?: boolean }): JSONSchema7 {
|
|
996
1030
|
// eslint-disable-next-line typescript-eslint/no-unsafe-assignment -- structuredClone preserves the imported JSONSchema7 shape.
|
|
997
1031
|
const nextSchema: JSONSchema7 = structuredClone(schema)
|
|
998
1032
|
removeJsonSchemaFormatKeywords(nextSchema)
|
|
1033
|
+
if (options?.requireAllProperties) {
|
|
1034
|
+
requireAllJsonSchemaObjectProperties(nextSchema)
|
|
1035
|
+
}
|
|
999
1036
|
return nextSchema
|
|
1000
1037
|
}
|
|
1001
1038
|
|
|
@@ -1010,7 +1047,9 @@ export function normalizeAiGatewayJsonSchemas(params: AiGatewayCallOptions): AiG
|
|
|
1010
1047
|
|
|
1011
1048
|
if (params.responseFormat?.type === 'json' && params.responseFormat.schema) {
|
|
1012
1049
|
// eslint-disable-next-line typescript-eslint/no-unsafe-assignment -- cloneAiGatewayJsonSchema returns the same JSONSchema7 type.
|
|
1013
|
-
const responseSchema: JSONSchema7 = cloneAiGatewayJsonSchema(params.responseFormat.schema
|
|
1050
|
+
const responseSchema: JSONSchema7 = cloneAiGatewayJsonSchema(params.responseFormat.schema, {
|
|
1051
|
+
requireAllProperties: true,
|
|
1052
|
+
})
|
|
1014
1053
|
nextParams = {
|
|
1015
1054
|
...nextParams,
|
|
1016
1055
|
// eslint-disable-next-line typescript-eslint/no-unsafe-assignment -- responseSchema is JSONSchema7 for responseFormat.schema.
|
|
@@ -6,9 +6,32 @@ import { recordIdToString } from './record-id'
|
|
|
6
6
|
import { TABLES } from './tables'
|
|
7
7
|
|
|
8
8
|
export function isUniqueIndexConflict(error: unknown, indexName: string): boolean {
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
9
|
+
const visited = new Set<unknown>()
|
|
10
|
+
const stack: unknown[] = [error]
|
|
11
|
+
|
|
12
|
+
while (stack.length > 0) {
|
|
13
|
+
const current = stack.pop()
|
|
14
|
+
if (current === null || current === undefined || visited.has(current)) continue
|
|
15
|
+
visited.add(current)
|
|
16
|
+
|
|
17
|
+
if (typeof current === 'string') {
|
|
18
|
+
if (current.includes(indexName) && current.includes('already contains')) return true
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (current instanceof Error) {
|
|
23
|
+
if (current.message.includes(indexName) && current.message.includes('already contains')) return true
|
|
24
|
+
stack.push(current.cause)
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof current === 'object') {
|
|
29
|
+
const record = current as Record<string, unknown>
|
|
30
|
+
stack.push(record.message, record.cause)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false
|
|
12
35
|
}
|
|
13
36
|
|
|
14
37
|
const coerceDate = (value: unknown): Date => {
|
package/src/db/startup.ts
CHANGED
|
@@ -9,11 +9,17 @@ import { getErrorMessage } from '../utils/errors'
|
|
|
9
9
|
import type { SurrealDBService, SurrealDatabaseLogger } from './service'
|
|
10
10
|
import type { SurrealDBError } from './service-normalization'
|
|
11
11
|
import { TABLES } from './tables'
|
|
12
|
+
import { isRetriableTransactionConflict } from './transaction-conflict'
|
|
12
13
|
|
|
13
14
|
const DATABASE_BOOTSTRAP_KEY = 'database-schema-ready'
|
|
14
15
|
const DEFAULT_RETRY_DELAY_MS = 1_000
|
|
15
16
|
const DEFAULT_MAX_WAIT_MS = 3 * 60 * 1_000
|
|
16
17
|
const RETRY_LOG_INTERVAL = 5
|
|
18
|
+
const BOOTSTRAP_PUBLISH_RETRY_OPTIONS = {
|
|
19
|
+
times: 8,
|
|
20
|
+
schedule: Schedule.jittered(Schedule.exponential(Duration.millis(50), 2)),
|
|
21
|
+
while: isRetriableTransactionConflict,
|
|
22
|
+
} as const
|
|
17
23
|
|
|
18
24
|
const RuntimeBootstrapRecordSchema = z.object({
|
|
19
25
|
id: recordIdSchema,
|
|
@@ -212,21 +218,22 @@ export function publishDatabaseBootstrapEffect(params: {
|
|
|
212
218
|
schemaFingerprint: string
|
|
213
219
|
}): Effect.Effect<void, DatabaseError> {
|
|
214
220
|
return Effect.asVoid(
|
|
215
|
-
|
|
216
|
-
.upsert(
|
|
221
|
+
Effect.suspend(() =>
|
|
222
|
+
params.databaseService.upsert(
|
|
217
223
|
TABLES.RUNTIME_BOOTSTRAP,
|
|
218
224
|
new RecordId(TABLES.RUNTIME_BOOTSTRAP, DATABASE_BOOTSTRAP_KEY),
|
|
219
225
|
{ key: DATABASE_BOOTSTRAP_KEY, schemaFingerprint: params.schemaFingerprint, readyAt: nowDate() },
|
|
220
226
|
RuntimeBootstrapRecordSchema,
|
|
221
|
-
)
|
|
222
|
-
.pipe(
|
|
223
|
-
Effect.mapError(
|
|
224
|
-
(error) =>
|
|
225
|
-
new DatabaseError({
|
|
226
|
-
message: `Failed to publish database bootstrap: ${getErrorMessage(error)}`,
|
|
227
|
-
cause: error,
|
|
228
|
-
}),
|
|
229
|
-
),
|
|
230
227
|
),
|
|
228
|
+
).pipe(
|
|
229
|
+
Effect.retry(BOOTSTRAP_PUBLISH_RETRY_OPTIONS),
|
|
230
|
+
Effect.mapError(
|
|
231
|
+
(error) =>
|
|
232
|
+
new DatabaseError({
|
|
233
|
+
message: `Failed to publish database bootstrap: ${getErrorMessage(error)}`,
|
|
234
|
+
cause: error,
|
|
235
|
+
}),
|
|
236
|
+
),
|
|
237
|
+
),
|
|
231
238
|
)
|
|
232
239
|
}
|
|
@@ -1,15 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
if (
|
|
1
|
+
function readErrorMessage(error: unknown): string | undefined {
|
|
2
|
+
if (error instanceof Error) {
|
|
3
|
+
return error.message
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (typeof error === 'object' && error !== null && typeof (error as { message?: unknown }).message === 'string') {
|
|
7
|
+
return (error as { message: string }).message
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readErrorCause(error: unknown): unknown {
|
|
14
|
+
if (typeof error !== 'object' || error === null || !('cause' in error)) {
|
|
15
|
+
return undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (error as { cause?: unknown }).cause
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isRetriableTransactionConflict(error: unknown, seen = new Set<unknown>()): boolean {
|
|
22
|
+
if (seen.has(error)) {
|
|
3
23
|
return false
|
|
4
24
|
}
|
|
25
|
+
seen.add(error)
|
|
26
|
+
|
|
27
|
+
const message = readErrorMessage(error)?.toLowerCase()
|
|
28
|
+
if (
|
|
29
|
+
message?.includes('transaction conflict') ||
|
|
30
|
+
message?.includes('transaction read conflict') ||
|
|
31
|
+
message?.includes('read or write conflict') ||
|
|
32
|
+
message?.includes('write conflict') ||
|
|
33
|
+
message?.includes('resource busy') ||
|
|
34
|
+
message?.includes('this transaction can be retried')
|
|
35
|
+
) {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
5
38
|
|
|
6
|
-
const
|
|
7
|
-
return (
|
|
8
|
-
message.includes('transaction conflict') ||
|
|
9
|
-
message.includes('transaction read conflict') ||
|
|
10
|
-
message.includes('read or write conflict') ||
|
|
11
|
-
message.includes('write conflict') ||
|
|
12
|
-
message.includes('resource busy') ||
|
|
13
|
-
message.includes('this transaction can be retried')
|
|
14
|
-
)
|
|
39
|
+
const cause = readErrorCause(error)
|
|
40
|
+
return cause !== undefined && isRetriableTransactionConflict(cause, seen)
|
|
15
41
|
}
|
|
@@ -42,6 +42,7 @@ interface QueueFactoryConfigBase {
|
|
|
42
42
|
lockDuration?: number
|
|
43
43
|
stalledInterval?: number
|
|
44
44
|
maxStalledCount?: number
|
|
45
|
+
shutdownTimeoutMs?: number
|
|
45
46
|
defaultJobOptions?: JobsOptions
|
|
46
47
|
recycleSandboxChildren?: boolean
|
|
47
48
|
connectionProvider: () => IORedis
|
|
@@ -285,7 +286,7 @@ function createQueueFactoryRuntime<TJob>(config: QueueFactoryConfigBase): {
|
|
|
285
286
|
if ((config.recycleSandboxChildren ?? true) && workerConfig.processorPath) {
|
|
286
287
|
attachSandboxChildRecycling(worker, config.displayName, logger)
|
|
287
288
|
}
|
|
288
|
-
const shutdown = createWorkerShutdown(worker, config.displayName, logger)
|
|
289
|
+
const shutdown = createWorkerShutdown(worker, config.displayName, logger, config.shutdownTimeoutMs)
|
|
289
290
|
|
|
290
291
|
if (registerSignals) {
|
|
291
292
|
registerShutdownSignals({ name: config.displayName, shutdown, logger })
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema, Effect } from 'effect'
|
|
1
|
+
import { Duration, Schema, Effect, Schedule } from 'effect'
|
|
2
2
|
|
|
3
3
|
import type { AiGatewayModels } from '../../ai-gateway/ai-gateway'
|
|
4
4
|
import { makeContextCompactionAgentFactory } from '../../system-agents/context-compaction.agent'
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
} from './context-compaction-constants'
|
|
23
23
|
|
|
24
24
|
const CONTEXT_COMPACTION_MAX_OUTPUT_TOKENS = 512
|
|
25
|
+
const MEMORY_BLOCK_COMPACTION_RETRY_ATTEMPTS = 2
|
|
26
|
+
const MEMORY_BLOCK_COMPACTION_RETRY_BASE_DELAY_MS = 100
|
|
25
27
|
|
|
26
28
|
interface HelperModelRuntime {
|
|
27
29
|
generateHelperStructured<T>(params: GenerateHelperStructuredParams<T>): Promise<T>
|
|
@@ -103,6 +105,13 @@ export function createWiredContextCompactionRuntime(deps: CreateContextCompactio
|
|
|
103
105
|
messages: [{ role: 'user', content: buildMemoryBlockCompactionPrompt({ previousSummary, newEntriesText }) }],
|
|
104
106
|
maxOutputTokens: CONTEXT_COMPACTION_MAX_OUTPUT_TOKENS,
|
|
105
107
|
}),
|
|
108
|
+
).pipe(
|
|
109
|
+
Effect.retry({
|
|
110
|
+
times: MEMORY_BLOCK_COMPACTION_RETRY_ATTEMPTS,
|
|
111
|
+
schedule: Schedule.jittered(
|
|
112
|
+
Schedule.exponential(Duration.millis(MEMORY_BLOCK_COMPACTION_RETRY_BASE_DELAY_MS), 2),
|
|
113
|
+
),
|
|
114
|
+
}),
|
|
106
115
|
)
|
|
107
116
|
}
|
|
108
117
|
|
|
@@ -29,6 +29,7 @@ export interface HelperMessage {
|
|
|
29
29
|
|
|
30
30
|
interface HelperAgentGenerateParams {
|
|
31
31
|
messages: Array<{ role: 'user' | 'assistant'; content: string }>
|
|
32
|
+
abortSignal?: AbortSignal
|
|
32
33
|
timeout?: TimeoutConfiguration
|
|
33
34
|
onStepFinish?: ToolLoopAgentOnStepFinishCallback<ToolSet>
|
|
34
35
|
onFinish?: ToolLoopAgentOnFinishCallback<ToolSet>
|
|
@@ -49,6 +50,7 @@ export interface GenerateHelperTextParams {
|
|
|
49
50
|
temperature?: number
|
|
50
51
|
maxOutputTokens?: number
|
|
51
52
|
timeoutMs?: number
|
|
53
|
+
abortSignal?: AbortSignal
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export interface GenerateHelperStructuredParams<T> extends Omit<GenerateHelperTextParams, 'tag'> {
|
|
@@ -149,12 +151,13 @@ function createHelperAgent(params: {
|
|
|
149
151
|
function generateWithHelperAgent(
|
|
150
152
|
tag: string,
|
|
151
153
|
agent: HelperAgent,
|
|
152
|
-
params: { messages: HelperMessage[]; timeoutMs?: number },
|
|
154
|
+
params: { messages: HelperMessage[]; timeoutMs?: number; abortSignal?: AbortSignal },
|
|
153
155
|
): Effect.Effect<{ text?: unknown; output?: unknown }, AiGenerationError> {
|
|
154
156
|
return Effect.tryPromise({
|
|
155
157
|
try: () =>
|
|
156
158
|
agent.generate({
|
|
157
159
|
messages: toModelMessages(params.messages),
|
|
160
|
+
...(params.abortSignal ? { abortSignal: params.abortSignal } : {}),
|
|
158
161
|
...(typeof params.timeoutMs === 'number' ? { timeout: params.timeoutMs } : {}),
|
|
159
162
|
}),
|
|
160
163
|
catch: (error) => formatGenerationError(tag, error),
|
|
@@ -56,8 +56,8 @@ const QueueJobAttemptRowSchema = z.object({
|
|
|
56
56
|
updatedAt: z.coerce.date(),
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
const PERSISTENCE_MAX_ATTEMPTS =
|
|
60
|
-
const PERSISTENCE_RETRY_BASE_DELAY_MS =
|
|
59
|
+
const PERSISTENCE_MAX_ATTEMPTS = 8
|
|
60
|
+
const PERSISTENCE_RETRY_BASE_DELAY_MS = 50
|
|
61
61
|
|
|
62
62
|
export interface TrackedBullJobLike {
|
|
63
63
|
queueName: string
|
|
@@ -108,13 +108,18 @@ export const attachWorkerEvents = (worker: Worker, name: string, logger: typeof
|
|
|
108
108
|
})
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
export const createWorkerShutdown = (
|
|
111
|
+
export const createWorkerShutdown = (
|
|
112
|
+
worker: Worker,
|
|
113
|
+
name: string,
|
|
114
|
+
logger: typeof chatLogger = chatLogger,
|
|
115
|
+
timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
116
|
+
) => {
|
|
112
117
|
return () => {
|
|
113
118
|
logger.info`Shutting down ${name} worker`
|
|
114
119
|
return Effect.runPromise(
|
|
115
120
|
Effect.asVoid(
|
|
116
121
|
Effect.tryPromise({
|
|
117
|
-
try: () => closeWorkerWithTimeout(worker, name, logger),
|
|
122
|
+
try: () => closeWorkerWithTimeout(worker, name, logger, timeoutMs),
|
|
118
123
|
catch: (cause) => new QueueWorkerError({ phase: 'close', cause }),
|
|
119
124
|
}),
|
|
120
125
|
),
|