@lota-sdk/core 0.4.22 → 0.4.24

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.22",
3
+ "version": "0.4.24",
4
4
  "files": [
5
5
  "src",
6
6
  "infrastructure/schema"
@@ -29,9 +29,10 @@
29
29
  "dependencies": {
30
30
  "@ai-sdk/devtools": "^0.0.16",
31
31
  "@ai-sdk/openai": "^3.0.54",
32
+ "@ai-sdk/provider": "^3.0.9",
32
33
  "@chat-adapter/slack": "^4.26.0",
33
34
  "@chat-adapter/state-ioredis": "^4.26.0",
34
- "@lota-sdk/shared": "0.4.22",
35
+ "@lota-sdk/shared": "0.4.24",
35
36
  "@mendable/firecrawl-js": "^4.20.0",
36
37
  "@surrealdb/node": "^3.0.3",
37
38
  "ai": "^6.0.170",
@@ -1,5 +1,6 @@
1
1
  import { devToolsMiddleware } from '@ai-sdk/devtools'
2
2
  import { createOpenAI } from '@ai-sdk/openai'
3
+ import type { JSONSchema7 } from '@ai-sdk/provider'
3
4
  import { wrapEmbeddingModel, wrapLanguageModel } from 'ai'
4
5
  import type { LanguageModelMiddleware } from 'ai'
5
6
  import { Cause, Clock, Context, Duration, Effect, Fiber, Layer, Semaphore } from 'effect'
@@ -16,6 +17,7 @@ type WrapStreamOptions = Parameters<NonNullable<LanguageModelMiddleware['wrapStr
16
17
  type AiGatewayLanguageModel = Parameters<typeof wrapLanguageModel>[0]['model']
17
18
  type AiGatewayEmbeddingModel = Parameters<typeof wrapEmbeddingModel>[0]['model']
18
19
  type AiGatewayCallOptions = WrapStreamOptions['params']
20
+ type AiGatewayFunctionTool = Extract<NonNullable<AiGatewayCallOptions['tools']>[number], { type: 'function' }>
19
21
  type AiGatewayGenerateResult = Awaited<ReturnType<WrapStreamOptions['doGenerate']>>
20
22
  type AiGatewayStreamResult = Awaited<ReturnType<WrapStreamOptions['doStream']>>
21
23
  type AiGatewayGeneratedContent = AiGatewayGenerateResult['content'][number]
@@ -72,6 +74,17 @@ function isAiGenerationError(error: unknown): error is AiGenerationError {
72
74
  return isRecord(error) && error._tag === ERROR_TAGS.AiGenerationError
73
75
  }
74
76
 
77
+ export function isAiGenerationContentFilterError(error: unknown): error is AiGenerationError {
78
+ if (!isAiGenerationError(error)) return false
79
+
80
+ const text = [error.message, error.providerData, error.responseBody]
81
+ .filter((part): part is string => typeof part === 'string' && part.length > 0)
82
+ .join('\n')
83
+ .toLowerCase()
84
+
85
+ return text.includes('content_filter') || text.includes('content management policy')
86
+ }
87
+
75
88
  function getNumericField(value: Record<string, unknown>, key: string): number | null {
76
89
  const field = value[key]
77
90
  if (typeof field === 'number' && Number.isFinite(field)) return field
@@ -900,52 +913,55 @@ const JSON_SCHEMA_PROPERTY_MAP_KEYS = new Set([
900
913
  'properties',
901
914
  ])
902
915
 
903
- function stripJsonSchemaFormatKeywords(value: unknown): unknown {
916
+ function removeJsonSchemaFormatKeywords(value: unknown): void {
904
917
  if (Array.isArray(value)) {
905
- return value.map((item) => stripJsonSchemaFormatKeywords(item))
918
+ for (const item of value) {
919
+ removeJsonSchemaFormatKeywords(item)
920
+ }
921
+ return
906
922
  }
907
923
 
908
924
  if (!isRecord(value)) {
909
- return value
925
+ return
910
926
  }
911
927
 
912
- let changed = false
913
- const nextValue: Record<string, unknown> = {}
928
+ delete value.format
914
929
 
915
930
  for (const [key, child] of Object.entries(value)) {
916
- if (key === 'format') {
917
- changed = true
918
- continue
919
- }
920
-
921
931
  if (JSON_SCHEMA_PROPERTY_MAP_KEYS.has(key) && isRecord(child)) {
922
- let mapChanged = false
923
- const nextMap: Record<string, unknown> = {}
924
- for (const [propertyName, propertySchema] of Object.entries(child)) {
925
- const nextPropertySchema = stripJsonSchemaFormatKeywords(propertySchema)
926
- mapChanged ||= nextPropertySchema !== propertySchema
927
- nextMap[propertyName] = nextPropertySchema
932
+ for (const propertySchema of Object.values(child)) {
933
+ removeJsonSchemaFormatKeywords(propertySchema)
928
934
  }
929
- changed ||= mapChanged
930
- nextValue[key] = mapChanged ? nextMap : child
931
935
  continue
932
936
  }
933
937
 
934
- const nextChild = stripJsonSchemaFormatKeywords(child)
935
- changed ||= nextChild !== child
936
- nextValue[key] = nextChild
938
+ removeJsonSchemaFormatKeywords(child)
937
939
  }
940
+ }
941
+
942
+ function cloneAiGatewayJsonSchema(schema: JSONSchema7): JSONSchema7 {
943
+ // eslint-disable-next-line typescript-eslint/no-unsafe-assignment -- structuredClone preserves the imported JSONSchema7 shape.
944
+ const nextSchema: JSONSchema7 = structuredClone(schema)
945
+ removeJsonSchemaFormatKeywords(nextSchema)
946
+ return nextSchema
947
+ }
938
948
 
939
- return changed ? nextValue : value
949
+ function isAiGatewayFunctionTool(
950
+ tool: NonNullable<AiGatewayCallOptions['tools']>[number],
951
+ ): tool is AiGatewayFunctionTool {
952
+ return isRecord(tool) && tool.type === 'function'
940
953
  }
941
954
 
942
955
  export function normalizeAiGatewayJsonSchemas(params: AiGatewayCallOptions): AiGatewayCallOptions {
943
956
  let nextParams = params
944
957
 
945
958
  if (params.responseFormat?.type === 'json' && params.responseFormat.schema) {
946
- const nextSchema = stripJsonSchemaFormatKeywords(params.responseFormat.schema)
947
- if (nextSchema !== params.responseFormat.schema) {
948
- nextParams = { ...nextParams, responseFormat: { ...params.responseFormat, schema: nextSchema } }
959
+ // eslint-disable-next-line typescript-eslint/no-unsafe-assignment -- cloneAiGatewayJsonSchema returns the same JSONSchema7 type.
960
+ const responseSchema: JSONSchema7 = cloneAiGatewayJsonSchema(params.responseFormat.schema)
961
+ nextParams = {
962
+ ...nextParams,
963
+ // eslint-disable-next-line typescript-eslint/no-unsafe-assignment -- responseSchema is JSONSchema7 for responseFormat.schema.
964
+ responseFormat: { ...params.responseFormat, schema: responseSchema },
949
965
  }
950
966
  }
951
967
 
@@ -955,13 +971,14 @@ export function normalizeAiGatewayJsonSchemas(params: AiGatewayCallOptions): AiG
955
971
  }
956
972
 
957
973
  const nextTools = sourceTools.map((tool) => {
958
- if (!isRecord(tool) || tool.type !== 'function') {
974
+ if (!isAiGatewayFunctionTool(tool)) {
959
975
  return tool
960
976
  }
961
977
 
962
- const inputSchema = (tool as { inputSchema: unknown }).inputSchema
963
- const nextInputSchema = stripJsonSchemaFormatKeywords(inputSchema)
964
- return { ...tool, inputSchema: nextInputSchema }
978
+ // eslint-disable-next-line typescript-eslint/no-unsafe-assignment -- function tool inputSchema is JSONSchema7.
979
+ const inputSchema: JSONSchema7 = cloneAiGatewayJsonSchema(tool.inputSchema)
980
+ // eslint-disable-next-line typescript-eslint/no-unsafe-assignment -- inputSchema is JSONSchema7 for function tool inputSchema.
981
+ return { ...tool, inputSchema }
965
982
  })
966
983
 
967
984
  return { ...nextParams, tools: nextTools as AiGatewayCallOptions['tools'] }
@@ -13,6 +13,7 @@ export {
13
13
  extractAiGatewayChatReasoningText,
14
14
  injectAiGatewayChatReasoningContent,
15
15
  injectAiGatewayChatReasoningStream,
16
+ isAiGenerationContentFilterError,
16
17
  normalizeAiGatewayChatProviderOptions,
17
18
  normalizeAiGatewayJsonSchemas,
18
19
  normalizeAiGatewayUrl,
@@ -1,9 +1,10 @@
1
1
  import { Context, Effect, Layer } from 'effect'
2
2
 
3
- import { AiGatewayModelsTag } from '../ai-gateway/ai-gateway'
3
+ import { AiGatewayModelsTag, isAiGenerationContentFilterError } from '../ai-gateway/ai-gateway'
4
4
  import type { AiGatewayModels } from '../ai-gateway/ai-gateway'
5
5
  import type { ResolvedAgentConfig } from '../config/agent-defaults'
6
- import { ServiceError } from '../effect/errors'
6
+ import { chatLogger } from '../config/logger'
7
+ import { ERROR_TAGS, ServiceError } from '../effect/errors'
7
8
  import { AgentConfigServiceTag } from '../effect/services'
8
9
  import type { HelperModelRuntime } from '../runtime/helper-model'
9
10
  import { HelperModelTag } from '../runtime/helper-model'
@@ -62,20 +63,34 @@ export function makeRecentActivityTitleService(
62
63
  return
63
64
  }
64
65
 
65
- const refinedTitle = normalizeTitle(
66
- yield* Effect.tryPromise({
67
- try: () =>
68
- helperModelRuntime.generateHelperText({
69
- tag: 'recent-activity-title-refinement',
70
- createAgent: refinerAgentFactory,
71
- defaultSystemPrompt: RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
72
- timeoutMs: RECENT_ACTIVITY_TITLE_TIMEOUT_MS,
73
- messages: [{ role: 'user', content: promptInput }],
74
- }),
75
- catch: (cause) =>
76
- new ServiceError({ message: 'Failed to generate recent activity title refinement.', cause }),
77
- }),
66
+ const maybeRefinedTitle = yield* Effect.tryPromise({
67
+ try: () =>
68
+ helperModelRuntime.generateHelperText({
69
+ tag: 'recent-activity-title-refinement',
70
+ createAgent: refinerAgentFactory,
71
+ defaultSystemPrompt: RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
72
+ timeoutMs: RECENT_ACTIVITY_TITLE_TIMEOUT_MS,
73
+ messages: [{ role: 'user', content: promptInput }],
74
+ }),
75
+ catch: (cause) =>
76
+ isAiGenerationContentFilterError(cause)
77
+ ? cause
78
+ : new ServiceError({ message: 'Failed to generate recent activity title refinement.', cause }),
79
+ }).pipe(
80
+ Effect.catchTag(ERROR_TAGS.AiGenerationError, (error) =>
81
+ isAiGenerationContentFilterError(error)
82
+ ? Effect.sync(() => {
83
+ chatLogger.warn`Skipping recent activity title refinement after provider content filter (activityId=${activityId})`
84
+ return null
85
+ })
86
+ : Effect.fail(error),
87
+ ),
78
88
  )
89
+ if (maybeRefinedTitle === null) {
90
+ return
91
+ }
92
+
93
+ const refinedTitle = normalizeTitle(maybeRefinedTitle)
79
94
  if (
80
95
  !recentActivityService.isAgentTitleUseful({
81
96
  currentTitle: candidate.title,