@lota-sdk/core 0.4.25 → 0.4.26

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.25",
3
+ "version": "0.4.26",
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.25",
35
+ "@lota-sdk/shared": "0.4.26",
36
36
  "@mendable/firecrawl-js": "^4.20.0",
37
37
  "@surrealdb/node": "^3.0.3",
38
38
  "ai": "^6.0.170",
@@ -1117,11 +1117,18 @@ export function normalizeAiGatewayChatProviderOptions(
1117
1117
  return { ...params, providerOptions: nextProviderOptions as AiGatewayCallOptions['providerOptions'] }
1118
1118
  }
1119
1119
 
1120
- function withAiGatewayDevTools<TModel extends AiGatewayLanguageModel>(model: TModel): TModel {
1121
- if (Bun.env.NODE_ENV === 'production') {
1122
- return model
1123
- }
1120
+ function readEnabledEnvFlag(value: string | undefined): boolean {
1121
+ if (!value) return false
1122
+ return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase())
1123
+ }
1124
1124
 
1125
+ export function isAiGatewayDevToolsEnabled(env: Record<string, string | undefined> = Bun.env): boolean {
1126
+ if (env.NODE_ENV === 'production') return false
1127
+ return readEnabledEnvFlag(env.LOTA_AI_GATEWAY_DEVTOOLS) || readEnabledEnvFlag(env.AI_GATEWAY_DEVTOOLS)
1128
+ }
1129
+
1130
+ function withAiGatewayDevTools<TModel extends AiGatewayLanguageModel>(model: TModel): TModel {
1131
+ if (!isAiGatewayDevToolsEnabled()) return model
1125
1132
  return wrapLanguageModel({ model, middleware: devToolsMiddleware() }) as TModel
1126
1133
  }
1127
1134
 
@@ -13,6 +13,7 @@ export {
13
13
  extractAiGatewayChatReasoningText,
14
14
  injectAiGatewayChatReasoningContent,
15
15
  injectAiGatewayChatReasoningStream,
16
+ isAiGatewayDevToolsEnabled,
16
17
  isAiGenerationContentFilterError,
17
18
  normalizeAiGatewayChatProviderOptions,
18
19
  normalizeAiGatewayJsonSchemas,
@@ -182,6 +182,7 @@ export function makeSocialChatHistoryService(
182
182
  workspaceId: string
183
183
  cursor: LotaRuntimeBackgroundCursor | null
184
184
  onboardingCutoff: Date | null
185
+ limit?: number
185
186
  }) =>
186
187
  Effect.gen(function* () {
187
188
  const conn = redis.getConnection()
@@ -189,11 +190,15 @@ export function makeSocialChatHistoryService(
189
190
  const scoreStart =
190
191
  params.cursor?.createdAt.getTime() ??
191
192
  (params.onboardingCutoff ? params.onboardingCutoff.getTime() : Number.NEGATIVE_INFINITY)
193
+ const limit = typeof params.limit === 'number' ? Math.max(1, Math.trunc(params.limit)) : undefined
194
+ const rangeLimitArgs = limit === undefined ? [] : (['LIMIT', 0, limit + 1] as const)
192
195
  const storageKeys = yield* Effect.tryPromise({
193
196
  try: () =>
194
197
  params.cursor || params.onboardingCutoff
195
- ? conn.zrangebyscore(indexKey, scoreStart, '+inf')
196
- : conn.zrange(indexKey, 0, -1),
198
+ ? conn.zrangebyscore(indexKey, scoreStart, '+inf', ...rangeLimitArgs)
199
+ : limit === undefined
200
+ ? conn.zrange(indexKey, 0, -1)
201
+ : conn.zrange(indexKey, 0, limit - 1),
197
202
  catch: (cause) => new SocialChatHistoryError({ message: 'Failed to list workspace message keys.', cause }),
198
203
  })
199
204
  if (storageKeys.length === 0) return [] as SocialChatHistoryMessage[]
@@ -226,6 +231,7 @@ export function makeSocialChatHistoryService(
226
231
  workspaceId: params.workspaceId,
227
232
  cursor: params.cursor,
228
233
  onboardingCutoff: params.onboardingCutoff,
234
+ limit: 1,
229
235
  }).pipe(Effect.map((messages) => messages.length > 0))
230
236
 
231
237
  const getBackgroundCursorEffect = (kind: LotaRuntimeBackgroundCursorKind, workspaceId: string) =>
@@ -1,9 +1,12 @@
1
- import { compactWhitespace, readRecord, readString } from '../utils/string'
1
+ import { compactWhitespace, readRecord, readString, truncateText } from '../utils/string'
2
2
  import type { DigestMessage } from './utils/thread-message-query'
3
3
 
4
4
  type DigestMessageForTranscript = Pick<DigestMessage, 'source' | 'sourceId' | 'role' | 'parts' | 'metadata'>
5
5
  type DigestMessagePart = DigestMessageForTranscript['parts'][number]
6
6
 
7
+ const DIGEST_TRANSCRIPT_MAX_CHARS = 60_000
8
+ const DIGEST_TRANSCRIPT_MESSAGE_MAX_CHARS = 4_000
9
+
7
10
  function normalizeFilePartMetadata(part: DigestMessagePart): string | null {
8
11
  if (part.type !== 'file') return null
9
12
  const partRecord = readRecord(part)
@@ -41,6 +44,10 @@ function extractAssistantLabel(
41
44
  return 'assistant'
42
45
  }
43
46
 
47
+ function normalizeTranscriptText(value: string): string {
48
+ return truncateText(compactWhitespace(value), DIGEST_TRANSCRIPT_MESSAGE_MAX_CHARS)
49
+ }
50
+
44
51
  export function buildDigestTranscript(params: {
45
52
  messages: DigestMessageForTranscript[]
46
53
  isKnownAgentName: (value: string) => boolean
@@ -48,13 +55,27 @@ export function buildDigestTranscript(params: {
48
55
  const lines: string[] = []
49
56
  const involvedAgentNames = new Set<string>()
50
57
  const { isKnownAgentName } = params
58
+ let transcriptChars = 0
59
+
60
+ const appendLine = (line: string): boolean => {
61
+ const separatorChars = lines.length === 0 ? 0 : 1
62
+ const remaining = DIGEST_TRANSCRIPT_MAX_CHARS - transcriptChars - separatorChars
63
+ if (remaining <= 3) return false
64
+
65
+ const nextLine = line.length > remaining ? truncateText(line, remaining) : line
66
+ lines.push(nextLine)
67
+ transcriptChars += separatorChars + nextLine.length
68
+ return line.length <= remaining
69
+ }
51
70
 
52
71
  for (const message of params.messages) {
53
72
  if (message.role !== 'user' && message.role !== 'assistant') continue
54
73
 
55
74
  const sourcePrefix = `[${message.source}:${message.sourceId}]`
56
75
  const textParts = message.parts
57
- .flatMap((part) => (part.type === 'text' && typeof part.text === 'string' ? [compactWhitespace(part.text)] : []))
76
+ .flatMap((part) =>
77
+ part.type === 'text' && typeof part.text === 'string' ? [normalizeTranscriptText(part.text)] : [],
78
+ )
58
79
  .filter((value) => value.length > 0)
59
80
  const fileParts = message.parts
60
81
  .map((part) => normalizeFilePartMetadata(part))
@@ -62,10 +83,10 @@ export function buildDigestTranscript(params: {
62
83
 
63
84
  if (message.role === 'user') {
64
85
  for (const textPart of textParts) {
65
- lines.push(`${sourcePrefix} User: ${textPart}`)
86
+ if (!appendLine(`${sourcePrefix} User: ${textPart}`)) break
66
87
  }
67
88
  if (fileParts.length > 0) {
68
- lines.push(`${sourcePrefix} User files: ${fileParts.join('; ')}`)
89
+ if (!appendLine(`${sourcePrefix} User files: ${fileParts.join('; ')}`)) break
69
90
  }
70
91
  continue
71
92
  }
@@ -76,10 +97,10 @@ export function buildDigestTranscript(params: {
76
97
  }
77
98
 
78
99
  for (const textPart of textParts) {
79
- lines.push(`${sourcePrefix} [${assistantLabel}] ${textPart}`)
100
+ if (!appendLine(`${sourcePrefix} [${assistantLabel}] ${textPart}`)) break
80
101
  }
81
102
  if (fileParts.length > 0) {
82
- lines.push(`${sourcePrefix} [${assistantLabel}] files: ${fileParts.join('; ')}`)
103
+ if (!appendLine(`${sourcePrefix} [${assistantLabel}] files: ${fileParts.join('; ')}`)) break
83
104
  }
84
105
  }
85
106
 
@@ -22,9 +22,10 @@ import type { MemoryServiceTag } from '../services/memory/memory.service'
22
22
  import type { SocialChatHistoryServiceTag } from '../services/social-chat-history.service'
23
23
  import { makeRegularChatMemoryDigestAgentFactory } from '../system-agents/regular-chat-memory-digest.agent'
24
24
  import { nowIsoDateTimeString } from '../utils/date-time'
25
- import { compactWhitespace } from '../utils/string'
25
+ import { compactWhitespace, truncateText } from '../utils/string'
26
26
  import { buildDigestTranscript, resolveWorkspaceBootstrapCutoff } from './regular-chat-memory-digest.helpers'
27
27
  import {
28
+ BACKGROUND_LEARNING_MESSAGE_BATCH_LIMIT,
28
29
  compareDigestMessageOrder,
29
30
  listEligibleThreadMessages,
30
31
  listThreadIdsForOrg,
@@ -87,7 +88,7 @@ function buildMemoryContext(memories: Array<{ content: string }>): string {
87
88
  .map((memory, index) => {
88
89
  const content = compactWhitespace(memory.content)
89
90
  if (!content) return ''
90
- return `${index + 1}. ${content}`
91
+ return `${index + 1}. ${truncateText(content, 500)}`
91
92
  })
92
93
  .filter((line) => line.length > 0)
93
94
  .join('\n')
@@ -177,7 +178,7 @@ function loadExistingOrganizationMemories(db: SurrealDBService, orgId: string):
177
178
  AND archivedAt IS NONE
178
179
  AND (validUntil IS NONE OR validUntil > time::now())
179
180
  ORDER BY createdAt DESC, id DESC
180
- LIMIT 250`,
181
+ LIMIT 100`,
181
182
  { orgId },
182
183
  ),
183
184
  WorkspaceMemoryRowSchema,
@@ -242,6 +243,7 @@ function runRegularChatMemoryDigestEffect(
242
243
  threadIds,
243
244
  cursor: existingThreadCursor,
244
245
  onboardingCutoff: threadOnboardingCutoff,
246
+ limit: BACKGROUND_LEARNING_MESSAGE_BATCH_LIMIT,
245
247
  }),
246
248
  catch: (cause) => new Cause.UnknownError(cause),
247
249
  })
@@ -257,6 +259,7 @@ function runRegularChatMemoryDigestEffect(
257
259
  workspaceId: orgId,
258
260
  cursor: existingSocialCursor,
259
261
  onboardingCutoff: socialOnboardingCutoff,
262
+ limit: BACKGROUND_LEARNING_MESSAGE_BATCH_LIMIT,
260
263
  })
261
264
 
262
265
  if (threadMessages.length === 0 && socialMessages.length === 0) {
@@ -20,6 +20,7 @@ import type { SkillCandidate } from '../system-agents/skill-extractor.agent'
20
20
  import { makeSkillManagerAgentFactory, SkillManagerOutputSchema } from '../system-agents/skill-manager.agent'
21
21
  import { buildDigestTranscript, resolveWorkspaceBootstrapCutoff } from './regular-chat-memory-digest.helpers'
22
22
  import {
23
+ BACKGROUND_LEARNING_MESSAGE_BATCH_LIMIT,
23
24
  compareDigestMessageOrder,
24
25
  listEligibleThreadMessages,
25
26
  listThreadIdsForOrg,
@@ -157,6 +158,7 @@ function runSkillExtractionEffect(
157
158
  threadIds,
158
159
  cursor: existingCursor,
159
160
  onboardingCutoff,
161
+ limit: BACKGROUND_LEARNING_MESSAGE_BATCH_LIMIT,
160
162
  }),
161
163
  catch: (cause) => new Cause.UnknownError(cause),
162
164
  })
@@ -164,6 +166,7 @@ function runSkillExtractionEffect(
164
166
  workspaceId: orgId,
165
167
  cursor: existingSocialCursor,
166
168
  onboardingCutoff: socialOnboardingCutoff,
169
+ limit: BACKGROUND_LEARNING_MESSAGE_BATCH_LIMIT,
167
170
  })
168
171
  const messages = [...threadMessages, ...socialMessages]
169
172
 
@@ -16,6 +16,8 @@ import type { LotaRuntimeBackgroundCursor } from '../../runtime/runtime-extensio
16
16
  import type { SocialChatHistoryMessage } from '../../services/social-chat-history.service'
17
17
  import { unsafeDateFrom } from '../../utils/date-time'
18
18
 
19
+ export const BACKGROUND_LEARNING_MESSAGE_BATCH_LIMIT = 80
20
+
19
21
  interface ThreadDigestMessage {
20
22
  source: 'thread'
21
23
  sourceId: string
@@ -103,11 +105,13 @@ export function listEligibleThreadMessages(params: {
103
105
  threadIds: RecordIdRef[]
104
106
  cursor: LotaRuntimeBackgroundCursor | null
105
107
  onboardingCutoff: Date | null
108
+ limit?: number
106
109
  }): Promise<DigestMessage[]> {
107
110
  if (params.threadIds.length === 0) return Promise.resolve([])
108
111
 
109
112
  return Effect.runPromise(
110
113
  Effect.gen(function* () {
114
+ const limit = Math.max(1, Math.trunc(params.limit ?? BACKGROUND_LEARNING_MESSAGE_BATCH_LIMIT))
111
115
  const query = params.cursor
112
116
  ? new BoundQuery(
113
117
  `SELECT type::string(id) AS id, type::string(threadId) AS threadId, role, parts, metadata, createdAt FROM ${TABLES.THREAD_MESSAGE}
@@ -116,11 +120,13 @@ export function listEligibleThreadMessages(params: {
116
120
  createdAt > $cursorCreatedAt
117
121
  OR (createdAt = $cursorCreatedAt AND id > $cursorRowId)
118
122
  )
119
- ORDER BY createdAt ASC, id ASC`,
123
+ ORDER BY createdAt ASC, id ASC
124
+ LIMIT $limit`,
120
125
  {
121
126
  threadIds: params.threadIds,
122
127
  cursorCreatedAt: params.cursor.createdAt,
123
128
  cursorRowId: ensureRecordId(params.cursor.id, TABLES.THREAD_MESSAGE),
129
+ limit,
124
130
  },
125
131
  )
126
132
  : params.onboardingCutoff
@@ -128,8 +134,9 @@ export function listEligibleThreadMessages(params: {
128
134
  `SELECT type::string(id) AS id, type::string(threadId) AS threadId, role, parts, metadata, createdAt FROM ${TABLES.THREAD_MESSAGE}
129
135
  WHERE threadId IN $threadIds
130
136
  AND createdAt > $onboardingCutoff
131
- ORDER BY createdAt ASC, id ASC`,
132
- { threadIds: params.threadIds, onboardingCutoff: params.onboardingCutoff },
137
+ ORDER BY createdAt ASC, id ASC
138
+ LIMIT $limit`,
139
+ { threadIds: params.threadIds, onboardingCutoff: params.onboardingCutoff, limit },
133
140
  )
134
141
  : null
135
142