@lota-sdk/core 0.1.6 → 0.1.7
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/infrastructure/schema/00_workstream.surql +1 -0
- package/package.json +1 -1
- package/src/ai/embedding-cache.ts +48 -0
- package/src/config/background-processing.ts +33 -0
- package/src/config/model-constants.ts +4 -0
- package/src/db/memory-store.ts +110 -19
- package/src/db/memory-types.ts +11 -0
- package/src/db/memory.ts +11 -1
- package/src/index.ts +28 -2
- package/src/queues/memory-consolidation.queue.ts +6 -0
- package/src/runtime/agent-types.ts +5 -22
- package/src/runtime/memory-digest-policy.ts +30 -2
- package/src/runtime/skill-extraction-policy.ts +9 -2
- package/src/services/memory.service.ts +35 -0
- package/src/services/workstream-turn-preparation.ts +22 -10
- package/src/services/workstream-turn.ts +2 -0
- package/src/services/workstream.service.ts +52 -0
- package/src/services/workstream.types.ts +1 -0
- package/src/tools/research-topic.tool.ts +5 -1
- package/src/utils/sse-keepalive.ts +40 -0
|
@@ -18,6 +18,7 @@ DEFINE FIELD IF NOT EXISTS lastCompactedMessageId ON TABLE workstream TYPE optio
|
|
|
18
18
|
DEFINE FIELD IF NOT EXISTS nameGenerated ON TABLE workstream TYPE bool DEFAULT false;
|
|
19
19
|
DEFINE FIELD IF NOT EXISTS isCompacting ON TABLE workstream TYPE bool DEFAULT false;
|
|
20
20
|
DEFINE FIELD IF NOT EXISTS state ON TABLE workstream TYPE option<object> FLEXIBLE;
|
|
21
|
+
DEFINE FIELD IF NOT EXISTS turnCount ON TABLE workstream TYPE int DEFAULT 0;
|
|
21
22
|
|
|
22
23
|
DEFINE INDEX IF NOT EXISTS workstreamOrgIdx ON TABLE workstream COLUMNS organizationId;
|
|
23
24
|
DEFINE INDEX IF NOT EXISTS workstreamUserIdx ON TABLE workstream COLUMNS userId;
|
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import type IORedis from 'ioredis'
|
|
4
|
+
|
|
5
|
+
import { aiLogger } from '../config/logger'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TTL_SECONDS = 3600
|
|
8
|
+
|
|
9
|
+
export class EmbeddingCache {
|
|
10
|
+
constructor(
|
|
11
|
+
private redis: IORedis,
|
|
12
|
+
private ttlSeconds: number = DEFAULT_TTL_SECONDS,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
private buildKey(model: string, text: string): string {
|
|
16
|
+
const hash = createHash('sha256').update(text).digest('hex')
|
|
17
|
+
return `emb:${model}:${hash}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async get(model: string, text: string): Promise<number[] | null> {
|
|
21
|
+
try {
|
|
22
|
+
const cached = await this.redis.getBuffer(this.buildKey(model, text))
|
|
23
|
+
if (!cached) return null
|
|
24
|
+
return JSON.parse(cached.toString()) as number[]
|
|
25
|
+
} catch (error) {
|
|
26
|
+
aiLogger.debug`Embedding cache get failed: ${error}`
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async set(model: string, text: string, embedding: number[]): Promise<void> {
|
|
32
|
+
try {
|
|
33
|
+
await this.redis.set(this.buildKey(model, text), JSON.stringify(embedding), 'EX', this.ttlSeconds)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
aiLogger.debug`Embedding cache set failed: ${error}`
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let embeddingCacheInstance: EmbeddingCache | null = null
|
|
41
|
+
|
|
42
|
+
export function configureEmbeddingCache(redis: IORedis, ttlSeconds?: number): void {
|
|
43
|
+
embeddingCacheInstance = new EmbeddingCache(redis, ttlSeconds ?? DEFAULT_TTL_SECONDS)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getEmbeddingCache(): EmbeddingCache | null {
|
|
47
|
+
return embeddingCacheInstance
|
|
48
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface BackgroundProcessingConfig {
|
|
2
|
+
memoryExtractionFrequency: number
|
|
3
|
+
skillExtractionFrequency: number
|
|
4
|
+
memoryDigestFrequency: number
|
|
5
|
+
memoryConsolidationFrequency: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG: BackgroundProcessingConfig = {
|
|
9
|
+
memoryExtractionFrequency: 3,
|
|
10
|
+
skillExtractionFrequency: 5,
|
|
11
|
+
memoryDigestFrequency: 1,
|
|
12
|
+
memoryConsolidationFrequency: 10,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let resolvedConfig: BackgroundProcessingConfig = { ...DEFAULT_CONFIG }
|
|
16
|
+
|
|
17
|
+
export function configureBackgroundProcessing(config?: Partial<BackgroundProcessingConfig>): void {
|
|
18
|
+
resolvedConfig = {
|
|
19
|
+
memoryExtractionFrequency: config?.memoryExtractionFrequency ?? DEFAULT_CONFIG.memoryExtractionFrequency,
|
|
20
|
+
skillExtractionFrequency: config?.skillExtractionFrequency ?? DEFAULT_CONFIG.skillExtractionFrequency,
|
|
21
|
+
memoryDigestFrequency: config?.memoryDigestFrequency ?? DEFAULT_CONFIG.memoryDigestFrequency,
|
|
22
|
+
memoryConsolidationFrequency: config?.memoryConsolidationFrequency ?? DEFAULT_CONFIG.memoryConsolidationFrequency,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getBackgroundProcessingConfig(): BackgroundProcessingConfig {
|
|
27
|
+
return resolvedConfig
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function shouldRunAtFrequency(turnCount: number, frequency: number): boolean {
|
|
31
|
+
if (frequency <= 1) return true
|
|
32
|
+
return turnCount > 0 && turnCount % frequency === 0
|
|
33
|
+
}
|
|
@@ -22,6 +22,10 @@ export const OPENROUTER_XHIGH_REASONING_PROVIDER_OPTIONS = {
|
|
|
22
22
|
openai: { forceReasoning: true, reasoningEffort: 'xhigh', reasoningSummary: 'auto' },
|
|
23
23
|
} as const
|
|
24
24
|
|
|
25
|
+
export const OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS = {
|
|
26
|
+
openai: { forceReasoning: true, reasoningEffort: 'medium', reasoningSummary: 'auto' },
|
|
27
|
+
} as const
|
|
28
|
+
|
|
25
29
|
export const OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS = {
|
|
26
30
|
openai: { forceReasoning: true, reasoningEffort: 'low', reasoningSummary: 'auto' },
|
|
27
31
|
} as const
|
package/src/db/memory-store.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { BoundQuery, eq, inside } from 'surrealdb'
|
|
2
2
|
|
|
3
|
+
import { getEmbeddingCache } from '../ai/embedding-cache'
|
|
4
|
+
import { env } from '../config/env-shapes'
|
|
3
5
|
import { aiLogger } from '../config/logger'
|
|
4
6
|
import { DEFAULT_MEMORY_SEARCH_LIMIT } from '../config/search'
|
|
5
7
|
import { createDefaultEmbeddings } from '../embeddings/provider'
|
|
@@ -7,7 +9,14 @@ import { memoryQueryBuilder } from './memory-query-builder'
|
|
|
7
9
|
import type { RelationCounts } from './memory-store.helpers'
|
|
8
10
|
import { hashContent, mapRowToMemoryRecord, processGraphAwareRows } from './memory-store.helpers'
|
|
9
11
|
import type { BasicSearchRow, SurrealMemoryRow } from './memory-store.rows'
|
|
10
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
LinearNormalization,
|
|
14
|
+
MemoryEvent,
|
|
15
|
+
MemoryListOptions,
|
|
16
|
+
MemoryRecord,
|
|
17
|
+
MemorySearchResult,
|
|
18
|
+
RelationType,
|
|
19
|
+
} from './memory-types'
|
|
11
20
|
import { ensureRecordId, recordIdToString } from './record-id'
|
|
12
21
|
import type { RecordIdInput, RecordIdRef } from './record-id'
|
|
13
22
|
import { databaseService } from './service'
|
|
@@ -33,6 +42,18 @@ export class SurrealMemoryStore {
|
|
|
33
42
|
|
|
34
43
|
constructor(private embeddings: EmbeddingClient) {}
|
|
35
44
|
|
|
45
|
+
private toMetadataFieldPath(key: string): string {
|
|
46
|
+
const segments = key.split('.').map((segment) => segment.trim())
|
|
47
|
+
if (
|
|
48
|
+
segments.length === 0 ||
|
|
49
|
+
segments.some((segment) => segment.length === 0 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment))
|
|
50
|
+
) {
|
|
51
|
+
throw new Error(`Invalid memory metadata filter key: ${key}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `metadata.${segments.join('.')}`
|
|
55
|
+
}
|
|
56
|
+
|
|
36
57
|
private tokenizeQuery(query: string): string[] {
|
|
37
58
|
return query
|
|
38
59
|
.toLowerCase()
|
|
@@ -188,6 +209,7 @@ export class SurrealMemoryStore {
|
|
|
188
209
|
const now = Date.now()
|
|
189
210
|
this.pruneEmbeddingCache(now)
|
|
190
211
|
|
|
212
|
+
// L1: in-memory cache
|
|
191
213
|
const cached = this.embeddingCache.get(cacheKey)
|
|
192
214
|
if (cached && now - cached.ts <= EMBEDDING_CACHE_TTL_MS) {
|
|
193
215
|
return cached.embedding
|
|
@@ -198,16 +220,32 @@ export class SurrealMemoryStore {
|
|
|
198
220
|
return await inFlight
|
|
199
221
|
}
|
|
200
222
|
|
|
201
|
-
const request =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
const request = (async () => {
|
|
224
|
+
// L2: Redis cache
|
|
225
|
+
const redisCache = getEmbeddingCache()
|
|
226
|
+
if (redisCache) {
|
|
227
|
+
const model = env.AI_EMBEDDING_MODEL
|
|
228
|
+
const redisCached = await redisCache.get(model, normalized)
|
|
229
|
+
if (redisCached) {
|
|
230
|
+
this.embeddingCache.set(cacheKey, { embedding: redisCached, ts: Date.now() })
|
|
231
|
+
return redisCached
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// L3: API call
|
|
236
|
+
const embedding = await this.embeddings.embedQuery(normalized)
|
|
237
|
+
this.embeddingCache.set(cacheKey, { embedding, ts: Date.now() })
|
|
238
|
+
this.pruneEmbeddingCache(Date.now())
|
|
239
|
+
|
|
240
|
+
// Backfill Redis
|
|
241
|
+
if (redisCache) {
|
|
242
|
+
void redisCache.set(env.AI_EMBEDDING_MODEL, normalized, embedding)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return embedding
|
|
246
|
+
})().finally(() => {
|
|
247
|
+
this.embeddingInFlight.delete(cacheKey)
|
|
248
|
+
})
|
|
211
249
|
|
|
212
250
|
this.embeddingInFlight.set(cacheKey, request)
|
|
213
251
|
return await request
|
|
@@ -558,6 +596,8 @@ export class SurrealMemoryStore {
|
|
|
558
596
|
return processed
|
|
559
597
|
}
|
|
560
598
|
|
|
599
|
+
private static HYBRID_SEARCH_TIMEOUT_MS = 2000
|
|
600
|
+
|
|
561
601
|
async hybridSearchWeighted(
|
|
562
602
|
query: string,
|
|
563
603
|
options: {
|
|
@@ -569,6 +609,7 @@ export class SurrealMemoryStore {
|
|
|
569
609
|
fastMode?: boolean
|
|
570
610
|
},
|
|
571
611
|
): Promise<MemorySearchResult[]> {
|
|
612
|
+
const searchStart = performance.now()
|
|
572
613
|
const queryEmbedding = await this.generateEmbedding(query)
|
|
573
614
|
|
|
574
615
|
const tokens = this.tokenizeQuery(query)
|
|
@@ -601,7 +642,25 @@ export class SurrealMemoryStore {
|
|
|
601
642
|
|
|
602
643
|
type LinearRow = BasicSearchRow & { linearScore: number }
|
|
603
644
|
|
|
604
|
-
|
|
645
|
+
let results: LinearRow[]
|
|
646
|
+
try {
|
|
647
|
+
results = await Promise.race([
|
|
648
|
+
this.queryFinalStatement<LinearRow>(sql, bindVars),
|
|
649
|
+
new Promise<never>((_, reject) =>
|
|
650
|
+
setTimeout(() => reject(new Error('Hybrid search timeout')), SurrealMemoryStore.HYBRID_SEARCH_TIMEOUT_MS),
|
|
651
|
+
),
|
|
652
|
+
])
|
|
653
|
+
} catch {
|
|
654
|
+
const elapsed = performance.now() - searchStart
|
|
655
|
+
aiLogger.warn`Hybrid search timed out after ${elapsed.toFixed(0)}ms (scopeId: ${options.scopeId}). Falling back to vector-only.`
|
|
656
|
+
return this.vectorSearchWithEmbedding({
|
|
657
|
+
embedding: queryEmbedding,
|
|
658
|
+
scopeId: options.scopeId,
|
|
659
|
+
limit: options.limit,
|
|
660
|
+
memoryType: options.memoryType,
|
|
661
|
+
fastMode: options.fastMode,
|
|
662
|
+
})
|
|
663
|
+
}
|
|
605
664
|
|
|
606
665
|
if (results.length === 0) {
|
|
607
666
|
aiLogger.debug`Weighted hybrid search returned 0 raw results (scopeId: ${options.scopeId})`
|
|
@@ -644,7 +703,8 @@ export class SurrealMemoryStore {
|
|
|
644
703
|
})
|
|
645
704
|
}
|
|
646
705
|
|
|
647
|
-
|
|
706
|
+
const elapsed = performance.now() - searchStart
|
|
707
|
+
aiLogger.info`[SUCCESS_WEIGHTED_SEARCH] Weighted hybrid search succeeded (scopeId: ${options.scopeId}, rawResults: ${results.length}, returned: ${processed.length}, weights: ${weights.join(',')}, normalization: ${normalization}, latencyMs: ${elapsed.toFixed(0)})`
|
|
648
708
|
this.touchMemories(processed.map((row) => row.id))
|
|
649
709
|
return processed
|
|
650
710
|
}
|
|
@@ -709,16 +769,47 @@ export class SurrealMemoryStore {
|
|
|
709
769
|
await this.recordHistory(id, existing.content, null, 'DELETE')
|
|
710
770
|
}
|
|
711
771
|
|
|
712
|
-
async list(
|
|
713
|
-
const
|
|
772
|
+
async list(options: MemoryListOptions): Promise<MemoryRecord[]> {
|
|
773
|
+
const whereClauses = [
|
|
774
|
+
'scopeId = $scopeId',
|
|
775
|
+
'archivedAt IS NONE',
|
|
776
|
+
'(validUntil IS NONE OR validUntil > time::now())',
|
|
777
|
+
]
|
|
778
|
+
const bindVars: Record<string, unknown> = { scopeId: options.scopeId }
|
|
779
|
+
|
|
780
|
+
if (options.memoryType) {
|
|
781
|
+
whereClauses.push('memoryType = $memoryType')
|
|
782
|
+
bindVars.memoryType = options.memoryType
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
for (const [index, [key, value]] of Object.entries(options.metadataEquals ?? {}).entries()) {
|
|
786
|
+
const fieldPath = this.toMetadataFieldPath(key)
|
|
787
|
+
const bindKey = `metadataEquals_${index}`
|
|
788
|
+
whereClauses.push(`${fieldPath} = $${bindKey}`)
|
|
789
|
+
bindVars[bindKey] = value
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
for (const [index, [key, value]] of Object.entries(options.metadataNotEquals ?? {}).entries()) {
|
|
793
|
+
const fieldPath = this.toMetadataFieldPath(key)
|
|
794
|
+
const bindKey = `metadataNotEquals_${index}`
|
|
795
|
+
whereClauses.push(`(${fieldPath} IS NONE OR ${fieldPath} != $${bindKey})`)
|
|
796
|
+
bindVars[bindKey] = value
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const sortDirection = options.sort === 'createdAtAsc' ? 'ASC' : 'DESC'
|
|
800
|
+
const limitClause = typeof options.limit === 'number' ? 'LIMIT $limit' : ''
|
|
801
|
+
if (typeof options.limit === 'number') {
|
|
802
|
+
bindVars.limit = options.limit
|
|
803
|
+
}
|
|
804
|
+
|
|
714
805
|
const sql = `
|
|
715
806
|
SELECT * FROM ${MEMORY_TABLE}
|
|
716
|
-
WHERE
|
|
717
|
-
|
|
718
|
-
|
|
807
|
+
WHERE ${whereClauses.join('\n AND ')}
|
|
808
|
+
ORDER BY createdAt ${sortDirection}
|
|
809
|
+
${limitClause}
|
|
719
810
|
`
|
|
720
811
|
|
|
721
|
-
const results = await databaseService.query<SurrealMemoryRow>(new BoundQuery(sql,
|
|
812
|
+
const results = await databaseService.query<SurrealMemoryRow>(new BoundQuery(sql, bindVars))
|
|
722
813
|
|
|
723
814
|
return results.map((row) => mapRowToMemoryRecord(row))
|
|
724
815
|
}
|
package/src/db/memory-types.ts
CHANGED
|
@@ -55,6 +55,17 @@ export interface SearchOptions {
|
|
|
55
55
|
pointInTime?: string
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export type MemoryListScalar = boolean | null | number | string
|
|
59
|
+
|
|
60
|
+
export interface MemoryListOptions {
|
|
61
|
+
scopeId: string
|
|
62
|
+
limit?: number
|
|
63
|
+
memoryType?: MemoryType
|
|
64
|
+
metadataEquals?: Record<string, MemoryListScalar>
|
|
65
|
+
metadataNotEquals?: Record<string, MemoryListScalar>
|
|
66
|
+
sort?: 'createdAtAsc' | 'createdAtDesc'
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
export type LinearNormalization = 'minmax' | 'zscore'
|
|
59
70
|
|
|
60
71
|
export interface WeightedSearchOptions extends SearchOptions {
|
package/src/db/memory.ts
CHANGED
|
@@ -21,11 +21,13 @@ import type {
|
|
|
21
21
|
Durability,
|
|
22
22
|
ExtractedFact,
|
|
23
23
|
MemoryConfig,
|
|
24
|
+
MemoryListOptions,
|
|
24
25
|
MemorySearchResult,
|
|
25
26
|
MemoryType,
|
|
26
27
|
MemoryUpdateOutput,
|
|
27
28
|
Message,
|
|
28
29
|
MemoryRecord,
|
|
30
|
+
RelationType,
|
|
29
31
|
SearchOptions,
|
|
30
32
|
WeightedSearchOptions,
|
|
31
33
|
} from './memory-types'
|
|
@@ -152,10 +154,18 @@ export class Memory {
|
|
|
152
154
|
return await this.store.listTopMemories(options)
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
async list(options: MemoryListOptions): Promise<MemoryRecord[]> {
|
|
158
|
+
return await this.store.list(options)
|
|
159
|
+
}
|
|
160
|
+
|
|
155
161
|
async updateMemory(id: string, newContent: string): Promise<void> {
|
|
156
162
|
await this.store.update(id, newContent)
|
|
157
163
|
}
|
|
158
164
|
|
|
165
|
+
async addRelation(fromId: string, toId: string, relationType: RelationType, confidence = 1): Promise<void> {
|
|
166
|
+
await this.store.addRelation(fromId, toId, relationType, confidence)
|
|
167
|
+
}
|
|
168
|
+
|
|
159
169
|
async getStaleMemories(scopeId: string, limit?: number): Promise<MemorySearchResult[]> {
|
|
160
170
|
return await this.store.getStaleMemories(scopeId, limit)
|
|
161
171
|
}
|
|
@@ -237,7 +247,7 @@ export class Memory {
|
|
|
237
247
|
private async prepareFactsForScope(facts: ExtractedFact[], options: AddOptions): Promise<PreparedScopeUpdate> {
|
|
238
248
|
const factMaps = buildMemoryFactMaps(facts)
|
|
239
249
|
|
|
240
|
-
const existingMemories = await this.store.list(options.scopeId, options.memoryType)
|
|
250
|
+
const existingMemories = await this.store.list({ scopeId: options.scopeId, memoryType: options.memoryType })
|
|
241
251
|
const oldMemoryFormat = existingMemories.map((m) => ({ id: m.id, text: m.content }))
|
|
242
252
|
|
|
243
253
|
const factContents = facts.map((f) => f.content)
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { routeWorkstreamChatMessages } from './runtime/chat-request-routing
|
|
|
19
19
|
import type { LotaPlugin } from './runtime/plugin-types'
|
|
20
20
|
import type { LotaRuntimeAdapters, LotaRuntimeTurnHooks } from './runtime/runtime-extensions'
|
|
21
21
|
import type { attachmentService } from './services/attachment.service'
|
|
22
|
+
import type { documentChunkService } from './services/document-chunk.service'
|
|
22
23
|
import type { executionPlanService } from './services/execution-plan.service'
|
|
23
24
|
import type { memoryService } from './services/memory.service'
|
|
24
25
|
import type { verifyMutatingApproval } from './services/mutating-approval.service'
|
|
@@ -74,8 +75,14 @@ export interface LotaRuntimeConfig {
|
|
|
74
75
|
}
|
|
75
76
|
firecrawl: { apiKey: string; apiBaseUrl?: string }
|
|
76
77
|
logging?: { level?: 'trace' | 'debug' | 'info' | 'warning' | 'error' | 'fatal' }
|
|
77
|
-
memory?: { searchK?: number }
|
|
78
|
+
memory?: { searchK?: number; embeddingCacheTtlSeconds?: number }
|
|
78
79
|
workstreams?: LotaWorkstreamConfig
|
|
80
|
+
backgroundProcessing?: {
|
|
81
|
+
memoryExtractionFrequency?: number
|
|
82
|
+
skillExtractionFrequency?: number
|
|
83
|
+
memoryDigestFrequency?: number
|
|
84
|
+
memoryConsolidationFrequency?: number
|
|
85
|
+
}
|
|
79
86
|
|
|
80
87
|
agents: {
|
|
81
88
|
roster: readonly string[]
|
|
@@ -103,6 +110,7 @@ export interface LotaRuntime {
|
|
|
103
110
|
redis: RedisConnectionManager
|
|
104
111
|
closeRedisConnection: () => Promise<void>
|
|
105
112
|
attachmentService: typeof attachmentService
|
|
113
|
+
documentChunkService: typeof documentChunkService
|
|
106
114
|
generatedDocumentStorageService: typeof generatedDocumentStorageService
|
|
107
115
|
memoryService: typeof memoryService
|
|
108
116
|
verifyMutatingApproval: typeof verifyMutatingApproval
|
|
@@ -200,6 +208,20 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
200
208
|
const { configureWorkstreams } = await import('./config/workstream-defaults')
|
|
201
209
|
const { configureRuntimeExtensions } = await import('./runtime/runtime-extensions')
|
|
202
210
|
const { routeWorkstreamChatMessages } = await import('./runtime/chat-request-routing')
|
|
211
|
+
const { configureBackgroundProcessing } = await import('./config/background-processing')
|
|
212
|
+
const { configureEmbeddingCache } = await import('./ai/embedding-cache')
|
|
213
|
+
|
|
214
|
+
// Resolve config defaults
|
|
215
|
+
const memory = {
|
|
216
|
+
searchK: config.memory?.searchK ?? 6,
|
|
217
|
+
embeddingCacheTtlSeconds: config.memory?.embeddingCacheTtlSeconds ?? 3600,
|
|
218
|
+
}
|
|
219
|
+
const backgroundProcessing = {
|
|
220
|
+
memoryExtractionFrequency: config.backgroundProcessing?.memoryExtractionFrequency ?? 3,
|
|
221
|
+
skillExtractionFrequency: config.backgroundProcessing?.skillExtractionFrequency ?? 5,
|
|
222
|
+
memoryDigestFrequency: config.backgroundProcessing?.memoryDigestFrequency ?? 1,
|
|
223
|
+
memoryConsolidationFrequency: config.backgroundProcessing?.memoryConsolidationFrequency ?? 10,
|
|
224
|
+
}
|
|
203
225
|
|
|
204
226
|
setEnv({
|
|
205
227
|
AI_GATEWAY_URL: config.aiGateway.url,
|
|
@@ -217,7 +239,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
217
239
|
ATTACHMENT_URL_EXPIRES_IN: config.s3.attachmentUrlExpiresIn ?? 1800,
|
|
218
240
|
FIRECRAWL_API_KEY: config.firecrawl.apiKey,
|
|
219
241
|
FIRECRAWL_API_BASE_URL: config.firecrawl.apiBaseUrl,
|
|
220
|
-
MEMORY_SEARCH_K:
|
|
242
|
+
MEMORY_SEARCH_K: memory.searchK,
|
|
221
243
|
})
|
|
222
244
|
|
|
223
245
|
await configureLogger(config.logging?.level ?? 'info')
|
|
@@ -233,6 +255,8 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
233
255
|
|
|
234
256
|
const redisManager = createRedisConnectionManager({ url: config.redis.url })
|
|
235
257
|
setRedisConnectionManager(redisManager)
|
|
258
|
+
configureEmbeddingCache(redisManager.getConnection(), memory.embeddingCacheTtlSeconds)
|
|
259
|
+
configureBackgroundProcessing(backgroundProcessing)
|
|
236
260
|
|
|
237
261
|
configureAgents({
|
|
238
262
|
roster: config.agents.roster,
|
|
@@ -253,6 +277,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
253
277
|
}
|
|
254
278
|
|
|
255
279
|
const { attachmentService } = await import('./services/attachment.service')
|
|
280
|
+
const { documentChunkService } = await import('./services/document-chunk.service')
|
|
256
281
|
const { recentActivityService } = await import('./services/recent-activity.service')
|
|
257
282
|
const { recentActivityTitleService } = await import('./services/recent-activity-title.service')
|
|
258
283
|
const { executionPlanService } = await import('./services/execution-plan.service')
|
|
@@ -392,6 +417,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
392
417
|
redis: redisManager,
|
|
393
418
|
closeRedisConnection: async () => await redisManager.closeConnection(),
|
|
394
419
|
attachmentService,
|
|
420
|
+
documentChunkService,
|
|
395
421
|
generatedDocumentStorageService,
|
|
396
422
|
memoryService,
|
|
397
423
|
verifyMutatingApproval,
|
|
@@ -32,6 +32,12 @@ function getMemoryConsolidationQueue(): Queue<MemoryConsolidationJob> {
|
|
|
32
32
|
return _memoryConsolidationQueue
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export async function enqueueMemoryConsolidation(job: MemoryConsolidationJob = {}) {
|
|
36
|
+
await getMemoryConsolidationQueue().add('consolidate-turn', job, {
|
|
37
|
+
jobId: job.scopeId ? `consolidate-turn:${job.scopeId}` : undefined,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
export async function scheduleRecurringConsolidation() {
|
|
36
42
|
await getMemoryConsolidationQueue().add(
|
|
37
43
|
'consolidate',
|
|
@@ -1,22 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
mode: ChatMode
|
|
7
|
-
tools: TTools
|
|
8
|
-
extraInstructions?: string
|
|
9
|
-
stopWhen?: StopCondition<TTools> | Array<StopCondition<TTools>>
|
|
10
|
-
prepareStep?: PrepareStepFunction<TTools>
|
|
11
|
-
maxRetries?: number
|
|
12
|
-
modelOverride?: { model: unknown; providerOptions?: Record<string, unknown> }
|
|
13
|
-
onFinish?: ToolLoopAgentOnFinishCallback<TTools>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface CreateHelperToolLoopAgentOptions {
|
|
17
|
-
instructions?: string
|
|
18
|
-
maxOutputTokens?: number
|
|
19
|
-
temperature?: number
|
|
20
|
-
output?: Output.Output
|
|
21
|
-
maxRetries?: number
|
|
22
|
-
}
|
|
1
|
+
export type {
|
|
2
|
+
ChatMode,
|
|
3
|
+
CreateHelperToolLoopAgentOptions,
|
|
4
|
+
CreateRoutedAgentOptions,
|
|
5
|
+
} from '@lota-sdk/shared/runtime/agent-types'
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getBackgroundProcessingConfig, shouldRunAtFrequency } from '../config/background-processing'
|
|
2
|
+
|
|
1
3
|
export function shouldEnqueueOnboardingPostChatMemory(params: {
|
|
2
4
|
onboardingActive: boolean
|
|
3
5
|
userMessageText: string
|
|
@@ -9,6 +11,32 @@ export function shouldEnqueueOnboardingPostChatMemory(params: {
|
|
|
9
11
|
return params.userMessageText.trim().length > 0 || params.hasAttachmentContext
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export function shouldEnqueueRegularDigestForWorkstream(params: {
|
|
13
|
-
|
|
14
|
+
export function shouldEnqueueRegularDigestForWorkstream(params: {
|
|
15
|
+
onboardingActive: boolean
|
|
16
|
+
turnCount?: number
|
|
17
|
+
}): boolean {
|
|
18
|
+
if (params.onboardingActive) return false
|
|
19
|
+
const { memoryDigestFrequency } = getBackgroundProcessingConfig()
|
|
20
|
+
if (typeof params.turnCount === 'number') {
|
|
21
|
+
return shouldRunAtFrequency(params.turnCount, memoryDigestFrequency)
|
|
22
|
+
}
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function shouldEnqueueMemoryExtraction(params: { onboardingActive: boolean; turnCount?: number }): boolean {
|
|
27
|
+
if (params.onboardingActive) return true
|
|
28
|
+
const { memoryExtractionFrequency } = getBackgroundProcessingConfig()
|
|
29
|
+
if (typeof params.turnCount === 'number') {
|
|
30
|
+
return shouldRunAtFrequency(params.turnCount, memoryExtractionFrequency)
|
|
31
|
+
}
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function shouldEnqueueMemoryConsolidation(params: { onboardingActive: boolean; turnCount?: number }): boolean {
|
|
36
|
+
if (params.onboardingActive) return false
|
|
37
|
+
const { memoryConsolidationFrequency } = getBackgroundProcessingConfig()
|
|
38
|
+
if (typeof params.turnCount === 'number') {
|
|
39
|
+
return shouldRunAtFrequency(params.turnCount, memoryConsolidationFrequency)
|
|
40
|
+
}
|
|
41
|
+
return false
|
|
14
42
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { getBackgroundProcessingConfig, shouldRunAtFrequency } from '../config/background-processing'
|
|
2
|
+
|
|
3
|
+
export function shouldEnqueueSkillExtraction(params: { onboardingActive: boolean; turnCount?: number }): boolean {
|
|
4
|
+
if (params.onboardingActive) return false
|
|
5
|
+
const { skillExtractionFrequency } = getBackgroundProcessingConfig()
|
|
6
|
+
if (typeof params.turnCount === 'number') {
|
|
7
|
+
return shouldRunAtFrequency(params.turnCount, skillExtractionFrequency)
|
|
8
|
+
}
|
|
9
|
+
return true
|
|
3
10
|
}
|
|
@@ -8,10 +8,12 @@ import { isUniqueIndexConflict } from '../db/memory-store.helpers'
|
|
|
8
8
|
import type {
|
|
9
9
|
AddOptions,
|
|
10
10
|
ExtractedFact,
|
|
11
|
+
MemoryListScalar,
|
|
11
12
|
MemoryRecord,
|
|
12
13
|
MemorySearchResult,
|
|
13
14
|
MemoryType,
|
|
14
15
|
Message,
|
|
16
|
+
RelationType,
|
|
15
17
|
} from '../db/memory-types'
|
|
16
18
|
import { withOrgMemoryLock } from '../redis/org-memory-lock'
|
|
17
19
|
import { createHelperModelRuntime } from '../runtime/helper-model'
|
|
@@ -413,6 +415,19 @@ class MemoryService {
|
|
|
413
415
|
return `Agent memory (${agentName}):\n${agentResult}\n\nGlobal org memory:\n${orgResult}`
|
|
414
416
|
}
|
|
415
417
|
|
|
418
|
+
async listOrganizationMemoryRecords(params: {
|
|
419
|
+
orgId: string
|
|
420
|
+
limit?: number
|
|
421
|
+
memoryType?: MemoryType
|
|
422
|
+
metadataEquals?: Record<string, MemoryListScalar>
|
|
423
|
+
metadataNotEquals?: Record<string, MemoryListScalar>
|
|
424
|
+
sort?: 'createdAtAsc' | 'createdAtDesc'
|
|
425
|
+
}): Promise<MemoryRecord[]> {
|
|
426
|
+
const { orgId, ...listOptions } = params
|
|
427
|
+
const orgMemory = this.getOrgMemory(orgId)
|
|
428
|
+
return await orgMemory.list({ scopeId: scopeId(ORG_SCOPE_PREFIX, orgId), ...listOptions })
|
|
429
|
+
}
|
|
430
|
+
|
|
416
431
|
async getTopMemories(params: { orgId: string; agentName?: string; limit?: number }): Promise<string | undefined> {
|
|
417
432
|
const orgMemory = this.getOrgMemory(params.orgId)
|
|
418
433
|
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, params.orgId)
|
|
@@ -568,12 +583,14 @@ class MemoryService {
|
|
|
568
583
|
memoryType,
|
|
569
584
|
metadata,
|
|
570
585
|
importance,
|
|
586
|
+
durability,
|
|
571
587
|
}: {
|
|
572
588
|
orgId: string
|
|
573
589
|
content: string
|
|
574
590
|
memoryType: MemoryType
|
|
575
591
|
metadata?: Record<string, unknown>
|
|
576
592
|
importance?: number
|
|
593
|
+
durability?: MemoryRecord['durability']
|
|
577
594
|
}): Promise<string> {
|
|
578
595
|
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
579
596
|
aiLogger.info`[MEMORY_DEBUG] createOrganizationMemory - orgId: "${orgId}", scopeId: "${orgScopeId}", content preview: "${content.slice(0, 50)}"`
|
|
@@ -583,6 +600,7 @@ class MemoryService {
|
|
|
583
600
|
scopeId: orgScopeId,
|
|
584
601
|
memoryType,
|
|
585
602
|
importance: importance ?? 1,
|
|
603
|
+
durability,
|
|
586
604
|
metadata: { orgId, ...metadata },
|
|
587
605
|
})
|
|
588
606
|
} catch (error) {
|
|
@@ -594,6 +612,23 @@ class MemoryService {
|
|
|
594
612
|
}
|
|
595
613
|
}
|
|
596
614
|
|
|
615
|
+
async addOrganizationMemoryRelation({
|
|
616
|
+
orgId,
|
|
617
|
+
fromMemoryId,
|
|
618
|
+
toMemoryId,
|
|
619
|
+
relationType,
|
|
620
|
+
confidence,
|
|
621
|
+
}: {
|
|
622
|
+
orgId: string
|
|
623
|
+
fromMemoryId: string
|
|
624
|
+
toMemoryId: string
|
|
625
|
+
relationType: RelationType
|
|
626
|
+
confidence?: number
|
|
627
|
+
}): Promise<void> {
|
|
628
|
+
const memory = this.getOrgMemory(orgId)
|
|
629
|
+
await memory.addRelation(fromMemoryId, toMemoryId, relationType, confidence)
|
|
630
|
+
}
|
|
631
|
+
|
|
597
632
|
async createAgentMemory({
|
|
598
633
|
orgId,
|
|
599
634
|
agentName,
|
|
@@ -24,6 +24,7 @@ import type { RecordIdRef } from '../db/record-id'
|
|
|
24
24
|
import { recordIdToString } from '../db/record-id'
|
|
25
25
|
import { TABLES } from '../db/tables'
|
|
26
26
|
import { enqueueContextCompaction } from '../queues/context-compaction.queue'
|
|
27
|
+
import { enqueueMemoryConsolidation } from '../queues/memory-consolidation.queue'
|
|
27
28
|
import { enqueuePostChatMemory } from '../queues/post-chat-memory.queue'
|
|
28
29
|
import { enqueueRecentActivityTitleRefinement } from '../queues/recent-activity-title-refinement.queue'
|
|
29
30
|
import { enqueueRegularChatMemoryDigest } from '../queues/regular-chat-memory-digest.queue'
|
|
@@ -44,6 +45,8 @@ import { CONTEXT_SIZE } from '../runtime/context-compaction-constants'
|
|
|
44
45
|
import { createExecutionPlanInstructionSectionCache } from '../runtime/execution-plan'
|
|
45
46
|
import { mergeInstructionSections } from '../runtime/instruction-sections'
|
|
46
47
|
import {
|
|
48
|
+
shouldEnqueueMemoryConsolidation,
|
|
49
|
+
shouldEnqueueMemoryExtraction,
|
|
47
50
|
shouldEnqueueOnboardingPostChatMemory,
|
|
48
51
|
shouldEnqueueRegularDigestForWorkstream,
|
|
49
52
|
} from '../runtime/memory-digest-policy'
|
|
@@ -1211,6 +1214,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1211
1214
|
}
|
|
1212
1215
|
|
|
1213
1216
|
if (allAssistantMessages.length > 0 && shouldProcessPostRunSideEffects) {
|
|
1217
|
+
const turnCount = await workstreamService.incrementTurnCount(workstreamRef)
|
|
1214
1218
|
const agentMessages = buildAgentHistoryMessages(allAssistantMessages)
|
|
1215
1219
|
const historyMessagesForMemory = appendCompactionContextToHistoryMessages(
|
|
1216
1220
|
toHistoryMessages(recentHistory),
|
|
@@ -1221,14 +1225,16 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1221
1225
|
const readableUploads = listReadableUploads()
|
|
1222
1226
|
const attachmentMetadataContext = buildReadableUploadMetadataContext(readableUploads)
|
|
1223
1227
|
const hasAttachmentContext = Boolean(attachmentMetadataContext)
|
|
1224
|
-
|
|
1225
|
-
shouldEnqueueOnboardingPostChatMemory({
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1228
|
+
const shouldExtractMemory = onboardingActive
|
|
1229
|
+
? shouldEnqueueOnboardingPostChatMemory({
|
|
1230
|
+
onboardingActive,
|
|
1231
|
+
userMessageText,
|
|
1232
|
+
hasAttachmentContext,
|
|
1233
|
+
agentMessageCount: agentMessages.length,
|
|
1234
|
+
})
|
|
1235
|
+
: shouldEnqueueMemoryExtraction({ onboardingActive, turnCount }) && userMessageText.length > 0
|
|
1236
|
+
|
|
1237
|
+
if (shouldExtractMemory) {
|
|
1232
1238
|
const memoryUserMessage = userMessageText || 'User uploaded attachment(s).'
|
|
1233
1239
|
await safeEnqueue(
|
|
1234
1240
|
() =>
|
|
@@ -1315,17 +1321,23 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1315
1321
|
}
|
|
1316
1322
|
}
|
|
1317
1323
|
|
|
1318
|
-
if (shouldEnqueueRegularDigestForWorkstream({ onboardingActive })) {
|
|
1324
|
+
if (shouldEnqueueRegularDigestForWorkstream({ onboardingActive, turnCount })) {
|
|
1319
1325
|
await safeEnqueue(() => enqueueRegularChatMemoryDigest({ orgId: orgIdString }), {
|
|
1320
1326
|
operationName: 'regular chat memory digest enqueue',
|
|
1321
1327
|
})
|
|
1322
1328
|
}
|
|
1323
1329
|
|
|
1324
|
-
if (shouldEnqueueSkillExtraction({ onboardingActive })) {
|
|
1330
|
+
if (shouldEnqueueSkillExtraction({ onboardingActive, turnCount })) {
|
|
1325
1331
|
await safeEnqueue(() => enqueueSkillExtraction({ orgId: orgIdString }), {
|
|
1326
1332
|
operationName: 'skill extraction enqueue',
|
|
1327
1333
|
})
|
|
1328
1334
|
}
|
|
1335
|
+
|
|
1336
|
+
if (shouldEnqueueMemoryConsolidation({ onboardingActive, turnCount })) {
|
|
1337
|
+
await safeEnqueue(() => enqueueMemoryConsolidation({ scopeId: orgIdString }), {
|
|
1338
|
+
operationName: 'memory consolidation enqueue',
|
|
1339
|
+
})
|
|
1340
|
+
}
|
|
1329
1341
|
}
|
|
1330
1342
|
|
|
1331
1343
|
if (allAssistantMessages.length > 0) {
|
|
@@ -2,10 +2,12 @@ import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
|
2
2
|
import { createUIMessageStream } from 'ai'
|
|
3
3
|
|
|
4
4
|
import { hasApprovalRespondedParts, isApprovalContinuationRequest } from '../runtime/approval-continuation'
|
|
5
|
+
import { wrapResponseWithKeepalive } from '../utils/sse-keepalive'
|
|
5
6
|
import { prepareWorkstreamRunCore } from './workstream-turn-preparation'
|
|
6
7
|
import type { WorkstreamTurnParams, WorkstreamApprovalContinuationParams } from './workstream-turn-preparation'
|
|
7
8
|
|
|
8
9
|
export { hasApprovalRespondedParts, isApprovalContinuationRequest }
|
|
10
|
+
export { wrapResponseWithKeepalive }
|
|
9
11
|
|
|
10
12
|
export async function createWorkstreamApprovalContinuationStream(params: WorkstreamApprovalContinuationParams) {
|
|
11
13
|
const prepared = await prepareWorkstreamRunCore({ ...params, kind: 'approvalContinuation' })
|
|
@@ -568,6 +568,48 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
568
568
|
return { workstreams: sliced.map((workstream) => this.normalizeWorkstream(workstream)), hasMore }
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
+
async listOrganizationWorkstreams(params: {
|
|
572
|
+
orgId: RecordIdRef
|
|
573
|
+
mode?: 'direct' | 'group'
|
|
574
|
+
agentId?: string
|
|
575
|
+
core?: boolean
|
|
576
|
+
includeArchived?: boolean
|
|
577
|
+
}): Promise<NormalizedWorkstream[]> {
|
|
578
|
+
const whereClauses = ['organizationId = $orgId']
|
|
579
|
+
const variables: Record<string, unknown> = { orgId: params.orgId }
|
|
580
|
+
|
|
581
|
+
if (params.mode) {
|
|
582
|
+
whereClauses.push('mode = $mode')
|
|
583
|
+
variables.mode = params.mode
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (typeof params.core === 'boolean') {
|
|
587
|
+
whereClauses.push('core = $core')
|
|
588
|
+
variables.core = params.core
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (params.agentId) {
|
|
592
|
+
whereClauses.push('agentId = $agentId')
|
|
593
|
+
variables.agentId = params.agentId
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (params.includeArchived !== true) {
|
|
597
|
+
whereClauses.push('status = "regular"')
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
|
|
601
|
+
new BoundQuery(
|
|
602
|
+
`SELECT * FROM ${TABLES.WORKSTREAM}
|
|
603
|
+
WHERE ${whereClauses.join('\n AND ')}
|
|
604
|
+
ORDER BY createdAt ASC, id ASC`,
|
|
605
|
+
variables,
|
|
606
|
+
),
|
|
607
|
+
WorkstreamSchema,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
return workstreams.map((workstream) => this.normalizeWorkstream(workstream))
|
|
611
|
+
}
|
|
612
|
+
|
|
571
613
|
async getWorkstream(workstreamId: RecordIdRef): Promise<NormalizedWorkstream> {
|
|
572
614
|
const workstream = await this.getById(workstreamId)
|
|
573
615
|
return this.normalizeWorkstream(workstream)
|
|
@@ -842,6 +884,16 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
842
884
|
return { ...publicWorkstream, workstreamState }
|
|
843
885
|
}
|
|
844
886
|
|
|
887
|
+
async incrementTurnCount(workstreamId: RecordIdRef): Promise<number> {
|
|
888
|
+
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
889
|
+
const result = await databaseService.query<{ turnCount: number }>(surql`
|
|
890
|
+
UPDATE ONLY ${workstreamRef}
|
|
891
|
+
SET turnCount += 1
|
|
892
|
+
RETURN turnCount
|
|
893
|
+
`)
|
|
894
|
+
return result[0]?.turnCount ?? 0
|
|
895
|
+
}
|
|
896
|
+
|
|
845
897
|
async persistGeneratedTitle(workstreamId: RecordIdRef, title: string): Promise<void> {
|
|
846
898
|
await this.update(workstreamId, { title, nameGenerated: true })
|
|
847
899
|
}
|
|
@@ -111,6 +111,7 @@ export const WorkstreamSchema = z.object({
|
|
|
111
111
|
nameGenerated: z.boolean().optional().default(false),
|
|
112
112
|
isCompacting: z.boolean().optional(),
|
|
113
113
|
state: z.unknown().optional(),
|
|
114
|
+
turnCount: z.number().int().optional().default(0),
|
|
114
115
|
createdAt: z.coerce.date(),
|
|
115
116
|
updatedAt: z.coerce.date(),
|
|
116
117
|
userId: z.any(), // RecordId
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { bifrostChatModel } from '../bifrost/bifrost'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS,
|
|
4
|
+
OPENROUTER_WEB_RESEARCH_MODEL_ID,
|
|
5
|
+
} from '../config/model-constants'
|
|
3
6
|
import { createDelegatedAgentTool } from '../system-agents/delegated-agent-factory'
|
|
4
7
|
import { RESEARCHER_PROMPT } from '../system-agents/researcher.agent'
|
|
5
8
|
import { fetchWebpageTool } from './fetch-webpage.tool'
|
|
@@ -10,6 +13,7 @@ export const researchTopicTool = createDelegatedAgentTool({
|
|
|
10
13
|
description:
|
|
11
14
|
'Delegate a research task to a dedicated research agent that searches the web, fetches pages, and returns a synthesized markdown report. Call multiple instances in parallel for broad research across different topics.',
|
|
12
15
|
model: bifrostChatModel(OPENROUTER_WEB_RESEARCH_MODEL_ID),
|
|
16
|
+
providerOptions: OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS,
|
|
13
17
|
instructions: RESEARCHER_PROMPT,
|
|
14
18
|
tools: { searchWeb: searchWebTool.create(), fetchWebpage: fetchWebpageTool.create() },
|
|
15
19
|
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const KEEPALIVE_COMMENT = new TextEncoder().encode(': keepalive\n\n')
|
|
2
|
+
const DEFAULT_KEEPALIVE_INTERVAL_MS = 20_000
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps an SSE Response body with periodic keepalive comments.
|
|
6
|
+
* SSE comments (`: keepalive\n\n`) are ignored by standard SSE parsers,
|
|
7
|
+
* so no client changes are needed.
|
|
8
|
+
*/
|
|
9
|
+
export function wrapResponseWithKeepalive(response: Response, intervalMs = DEFAULT_KEEPALIVE_INTERVAL_MS): Response {
|
|
10
|
+
const body = response.body
|
|
11
|
+
if (!body) return response
|
|
12
|
+
|
|
13
|
+
let intervalHandle: ReturnType<typeof setInterval> | null = null
|
|
14
|
+
|
|
15
|
+
const transformed = body.pipeThrough(
|
|
16
|
+
new TransformStream<Uint8Array, Uint8Array>({
|
|
17
|
+
start(controller) {
|
|
18
|
+
intervalHandle = setInterval(() => {
|
|
19
|
+
try {
|
|
20
|
+
controller.enqueue(KEEPALIVE_COMMENT)
|
|
21
|
+
} catch {
|
|
22
|
+
if (intervalHandle) clearInterval(intervalHandle)
|
|
23
|
+
}
|
|
24
|
+
}, intervalMs)
|
|
25
|
+
},
|
|
26
|
+
transform(chunk, controller) {
|
|
27
|
+
controller.enqueue(chunk)
|
|
28
|
+
},
|
|
29
|
+
flush() {
|
|
30
|
+
if (intervalHandle) clearInterval(intervalHandle)
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return new Response(transformed, {
|
|
36
|
+
headers: response.headers,
|
|
37
|
+
status: response.status,
|
|
38
|
+
statusText: response.statusText,
|
|
39
|
+
})
|
|
40
|
+
}
|