@lota-sdk/core 0.4.30 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lota-sdk/core",
3
- "version": "0.4.30",
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.30",
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 cloneAiGatewayJsonSchema(schema: JSONSchema7): JSONSchema7 {
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.
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
- params.databaseService
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
- export function isRetriableTransactionConflict(error: unknown): boolean {
2
- if (!(error instanceof Error)) {
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 message = error.message.toLowerCase()
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 = 4
60
- const PERSISTENCE_RETRY_BASE_DELAY_MS = 25
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 = (worker: Worker, name: string, logger: typeof chatLogger = chatLogger) => {
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
  ),