@lota-sdk/core 0.4.26 → 0.4.28
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.28",
|
|
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.28",
|
|
36
36
|
"@mendable/firecrawl-js": "^4.20.0",
|
|
37
37
|
"@surrealdb/node": "^3.0.3",
|
|
38
38
|
"ai": "^6.0.170",
|
|
@@ -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) {
|
|
@@ -3,6 +3,7 @@ import type { ChatMessage } from '@lota-sdk/shared'
|
|
|
3
3
|
|
|
4
4
|
import type { ResolvedAgentConfig } from '../config/agent-defaults'
|
|
5
5
|
import { resolveAgentNameAlias } from '../config/agent-defaults'
|
|
6
|
+
import { truncateText } from '../utils/string'
|
|
6
7
|
import type { ChatMessageLike, ReadableUploadMetadataLike } from './chat-types'
|
|
7
8
|
|
|
8
9
|
export interface ThreadHistoryMessage {
|
|
@@ -11,6 +12,9 @@ export interface ThreadHistoryMessage {
|
|
|
11
12
|
agentName?: string
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
const THREAD_HISTORY_MESSAGE_MAX_CHARS = 4_000
|
|
16
|
+
const CONVERSATION_SUMMARY_MAX_CHARS = 12_000
|
|
17
|
+
|
|
14
18
|
export function asRecord(value: unknown): Record<string, unknown> | null {
|
|
15
19
|
return value && typeof value === 'object' ? (value as Record<string, unknown>) : null
|
|
16
20
|
}
|
|
@@ -53,7 +57,7 @@ export function toHistoryMessages(
|
|
|
53
57
|
): ThreadHistoryMessage[] {
|
|
54
58
|
return messages
|
|
55
59
|
.map((message): ThreadHistoryMessage | null => {
|
|
56
|
-
const content = extractMessageText(message)
|
|
60
|
+
const content = truncateText(extractMessageText(message), THREAD_HISTORY_MESSAGE_MAX_CHARS)
|
|
57
61
|
if (!content) return null
|
|
58
62
|
|
|
59
63
|
if (message.role === 'user') {
|
|
@@ -75,15 +79,28 @@ export function buildConversationSummary(params: {
|
|
|
75
79
|
assistantMessages: ChatMessageLike[]
|
|
76
80
|
}): string {
|
|
77
81
|
const lines: string[] = []
|
|
82
|
+
let summaryChars = 0
|
|
83
|
+
|
|
84
|
+
const appendLine = (line: string): boolean => {
|
|
85
|
+
const separatorChars = lines.length === 0 ? 0 : 2
|
|
86
|
+
const remaining = CONVERSATION_SUMMARY_MAX_CHARS - summaryChars - separatorChars
|
|
87
|
+
if (remaining <= 3) return false
|
|
88
|
+
|
|
89
|
+
const nextLine = line.length > remaining ? truncateText(line, remaining) : line
|
|
90
|
+
lines.push(nextLine)
|
|
91
|
+
summaryChars += separatorChars + nextLine.length
|
|
92
|
+
return line.length <= remaining
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
if (params.userMessageText.trim()) {
|
|
79
|
-
|
|
96
|
+
appendLine(`User: ${truncateText(params.userMessageText.trim(), THREAD_HISTORY_MESSAGE_MAX_CHARS)}`)
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
for (const message of params.assistantMessages) {
|
|
83
|
-
const content = extractMessageText(message)
|
|
100
|
+
const content = truncateText(extractMessageText(message), THREAD_HISTORY_MESSAGE_MAX_CHARS)
|
|
84
101
|
if (!content) continue
|
|
85
102
|
const agentName = getAgentName(params.agentConfig, message)
|
|
86
|
-
|
|
103
|
+
if (!appendLine(agentName ? `${agentName}: ${content}` : `Assistant: ${content}`)) break
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
return lines.join('\n\n').trim()
|
|
@@ -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) {
|
|
@@ -191,14 +191,19 @@ export function makeSocialChatHistoryService(
|
|
|
191
191
|
params.cursor?.createdAt.getTime() ??
|
|
192
192
|
(params.onboardingCutoff ? params.onboardingCutoff.getTime() : Number.NEGATIVE_INFINITY)
|
|
193
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)
|
|
195
194
|
const storageKeys = yield* Effect.tryPromise({
|
|
196
|
-
try: () =>
|
|
197
|
-
params.cursor || params.onboardingCutoff
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
try: () => {
|
|
196
|
+
if (params.cursor || params.onboardingCutoff) {
|
|
197
|
+
if (limit === undefined) {
|
|
198
|
+
return conn.zrangebyscore(indexKey, scoreStart, '+inf')
|
|
199
|
+
}
|
|
200
|
+
return conn
|
|
201
|
+
.call('ZRANGEBYSCORE', indexKey, String(scoreStart), '+inf', 'LIMIT', '0', String(limit + 1))
|
|
202
|
+
.then((result) => (Array.isArray(result) ? result.map(String) : []))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return limit === undefined ? conn.zrange(indexKey, 0, -1) : conn.zrange(indexKey, 0, limit - 1)
|
|
206
|
+
},
|
|
202
207
|
catch: (cause) => new SocialChatHistoryError({ message: 'Failed to list workspace message keys.', cause }),
|
|
203
208
|
})
|
|
204
209
|
if (storageKeys.length === 0) return [] as SocialChatHistoryMessage[]
|
|
@@ -95,6 +95,60 @@ export const createWorkerShutdown = (worker: Worker, name: string, logger: typeo
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
interface SandboxChildLike {
|
|
99
|
+
pid?: number
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface SandboxChildPoolLike {
|
|
103
|
+
getAllFree?: () => SandboxChildLike[]
|
|
104
|
+
kill?: (child: SandboxChildLike, signal?: NodeJS.Signals) => Promise<void>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface SandboxedWorkerLike {
|
|
108
|
+
childPool?: SandboxChildPoolLike
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getSandboxChildPool(worker: Worker): SandboxChildPoolLike | null {
|
|
112
|
+
const pool = (worker as unknown as SandboxedWorkerLike).childPool
|
|
113
|
+
return pool && typeof pool.getAllFree === 'function' && typeof pool.kill === 'function' ? pool : null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function recycleIdleSandboxChildren(
|
|
117
|
+
worker: Worker,
|
|
118
|
+
name: string,
|
|
119
|
+
logger: typeof chatLogger = chatLogger,
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
const pool = getSandboxChildPool(worker)
|
|
122
|
+
if (!pool) return Promise.resolve()
|
|
123
|
+
|
|
124
|
+
const idleChildren = pool.getAllFree?.() ?? []
|
|
125
|
+
if (idleChildren.length === 0) return Promise.resolve()
|
|
126
|
+
|
|
127
|
+
return Promise.all(
|
|
128
|
+
idleChildren.map((child) =>
|
|
129
|
+
(pool.kill?.(child, 'SIGTERM') ?? Promise.resolve()).catch((error: unknown) => {
|
|
130
|
+
logger.warn`Failed to recycle idle ${name} sandbox child (${child.pid ?? 'unknown'}): ${error}`
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
133
|
+
).then(() => undefined)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function attachSandboxChildRecycling(
|
|
137
|
+
worker: Worker,
|
|
138
|
+
name: string,
|
|
139
|
+
logger: typeof chatLogger = chatLogger,
|
|
140
|
+
): void {
|
|
141
|
+
const recycle = () => {
|
|
142
|
+
// @effect-diagnostics-next-line globalTimers:off -- BullMQ worker event callback; defer until BullMQ releases the child.
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
void recycleIdleSandboxChildren(worker, name, logger)
|
|
145
|
+
}, 0)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
worker.on('completed', recycle)
|
|
149
|
+
worker.on('failed', recycle)
|
|
150
|
+
}
|
|
151
|
+
|
|
98
152
|
export function createTracedWorkerProcessor<TJob extends TracedWorkerJobLike, TResult = void>(
|
|
99
153
|
queueName: string,
|
|
100
154
|
processor: (job: TJob) => Promise<TResult>,
|