@lota-sdk/core 0.4.8 → 0.4.10
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 +11 -12
- package/src/ai/embedding-cache.ts +96 -22
- package/src/ai-gateway/ai-gateway.ts +766 -223
- package/src/config/agent-defaults.ts +189 -75
- package/src/config/agent-types.ts +54 -4
- package/src/config/background-processing.ts +1 -1
- package/src/config/constants.ts +8 -2
- package/src/config/index.ts +0 -1
- package/src/config/logger.ts +299 -19
- package/src/config/thread-defaults.ts +40 -20
- package/src/create-runtime.ts +200 -449
- package/src/db/base.service.ts +52 -28
- package/src/db/cursor-pagination.ts +71 -30
- package/src/db/memory-query-builder.ts +2 -1
- package/src/db/memory-store.helpers.ts +4 -7
- package/src/db/memory-store.ts +868 -601
- package/src/db/memory.ts +396 -280
- package/src/db/record-id.ts +32 -10
- package/src/db/schema-fingerprint.ts +30 -12
- package/src/db/service-normalization.ts +288 -0
- package/src/db/service.ts +912 -779
- package/src/db/startup.ts +153 -68
- package/src/db/transaction-conflict.ts +15 -0
- package/src/effect/awaitable-effect.ts +96 -0
- package/src/effect/errors.ts +121 -0
- package/src/effect/helpers.ts +123 -0
- package/src/effect/index.ts +24 -0
- package/src/effect/layers.ts +238 -0
- package/src/effect/runtime-ref.ts +25 -0
- package/src/effect/runtime.ts +46 -0
- package/src/effect/services.ts +61 -0
- package/src/effect/zod.ts +43 -0
- package/src/embeddings/provider.ts +128 -83
- package/src/index.ts +48 -1
- package/src/openrouter/direct-provider.ts +11 -35
- package/src/queues/autonomous-job.queue.ts +117 -73
- package/src/queues/context-compaction.queue.ts +50 -17
- package/src/queues/delayed-node-promotion.queue.ts +46 -17
- package/src/queues/document-processor.queue.ts +52 -77
- package/src/queues/memory-consolidation.queue.ts +47 -32
- package/src/queues/organization-learning.queue.ts +26 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
- package/src/queues/plan-scheduler.queue.ts +97 -33
- package/src/queues/post-chat-memory.queue.ts +56 -26
- package/src/queues/queue-factory.ts +227 -59
- package/src/queues/standalone-worker.ts +39 -0
- package/src/queues/title-generation.queue.ts +45 -11
- package/src/redis/connection.ts +182 -113
- package/src/redis/index.ts +6 -8
- package/src/redis/org-memory-lock.ts +60 -27
- package/src/redis/redis-lease-lock.ts +200 -121
- package/src/redis/runtime-connection.ts +20 -0
- package/src/redis/stream-context.ts +92 -46
- package/src/runtime/agent-identity-overrides.ts +2 -2
- package/src/runtime/agent-runtime-policy.ts +5 -2
- package/src/runtime/agent-stream-helpers.ts +24 -9
- package/src/runtime/chat-run-orchestration.ts +102 -19
- package/src/runtime/chat-run-registry.ts +36 -2
- package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
- package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
- package/src/runtime/domain-layer.ts +192 -0
- package/src/runtime/execution-plan-visibility.ts +2 -2
- package/src/runtime/execution-plan.ts +42 -15
- package/src/runtime/graph-designer.ts +16 -4
- package/src/runtime/helper-model.ts +139 -48
- package/src/runtime/index.ts +7 -8
- package/src/runtime/indexed-repositories-policy.ts +3 -3
- package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
- package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
- package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
- package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
- package/src/runtime/memory/memory-scope.ts +53 -0
- package/src/runtime/plugin-resolution.ts +124 -25
- package/src/runtime/plugin-types.ts +9 -1
- package/src/runtime/post-turn-side-effects.ts +177 -130
- package/src/runtime/retrieval-adapters.ts +40 -6
- package/src/runtime/runtime-accessors.ts +92 -0
- package/src/runtime/runtime-config.ts +150 -61
- package/src/runtime/runtime-extensions.ts +23 -25
- package/src/runtime/runtime-lifecycle.ts +124 -0
- package/src/runtime/runtime-services.ts +386 -0
- package/src/runtime/runtime-token.ts +47 -0
- package/src/runtime/social-chat/social-chat-agent-runner.ts +159 -0
- package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
- package/src/runtime/social-chat/social-chat.ts +630 -0
- package/src/runtime/specialist-runner.ts +36 -10
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
- package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
- package/src/runtime/thread-chat-helpers.ts +2 -2
- package/src/runtime/thread-plan-turn.ts +2 -1
- package/src/runtime/thread-turn-context.ts +183 -111
- package/src/runtime/turn-lifecycle.ts +93 -27
- package/src/services/agent-activity.service.ts +287 -203
- package/src/services/agent-executor.service.ts +253 -149
- package/src/services/artifact.service.ts +231 -149
- package/src/services/attachment.service.ts +171 -115
- package/src/services/autonomous-job.service.ts +890 -491
- package/src/services/background-work.service.ts +54 -0
- package/src/services/chat-run-registry.service.ts +13 -1
- package/src/services/context-compaction.service.ts +136 -86
- package/src/services/document-chunk.service.ts +151 -88
- package/src/services/execution-plan/execution-plan-approval.ts +26 -0
- package/src/services/execution-plan/execution-plan-context.ts +29 -0
- package/src/services/execution-plan/execution-plan-graph.ts +278 -0
- package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
- package/src/services/execution-plan/execution-plan-spec.ts +75 -0
- package/src/services/execution-plan/execution-plan.service.ts +1041 -0
- package/src/services/feedback-loop.service.ts +132 -76
- package/src/services/global-orchestrator.service.ts +101 -168
- package/src/services/graph-full-routing.ts +193 -0
- package/src/services/index.ts +19 -21
- package/src/services/institutional-memory.service.ts +213 -125
- package/src/services/learned-skill.service.ts +368 -260
- package/src/services/memory/memory-conversation.ts +95 -0
- package/src/services/memory/memory-errors.ts +27 -0
- package/src/services/memory/memory-org-memory.ts +50 -0
- package/src/services/memory/memory-preseeded.ts +86 -0
- package/src/services/memory/memory-rerank.ts +297 -0
- package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
- package/src/services/memory/memory.service.ts +674 -0
- package/src/services/memory/rerank.service.ts +201 -0
- package/src/services/monitoring-window.service.ts +92 -70
- package/src/services/mutating-approval.service.ts +62 -53
- package/src/services/node-workspace.service.ts +141 -98
- package/src/services/notification.service.ts +29 -16
- package/src/services/organization-member.service.ts +120 -66
- package/src/services/organization.service.ts +153 -77
- package/src/services/ownership-dispatcher.service.ts +456 -263
- package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
- package/src/services/plan/plan-agent-query.service.ts +322 -0
- package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
- package/src/services/plan/plan-artifact.service.ts +60 -0
- package/src/services/plan/plan-builder.service.ts +76 -0
- package/src/services/plan/plan-checkpoint.service.ts +103 -0
- package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
- package/src/services/plan/plan-completion-side-effects.ts +169 -0
- package/src/services/plan/plan-coordination.service.ts +181 -0
- package/src/services/plan/plan-cycle.service.ts +405 -0
- package/src/services/plan/plan-deadline.service.ts +533 -0
- package/src/services/plan/plan-event-delivery.service.ts +266 -0
- package/src/services/plan/plan-executor-context.ts +35 -0
- package/src/services/plan/plan-executor-graph.ts +522 -0
- package/src/services/plan/plan-executor-helpers.ts +307 -0
- package/src/services/plan/plan-executor-persistence.ts +209 -0
- package/src/services/plan/plan-executor.service.ts +1737 -0
- package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
- package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
- package/src/services/plan/plan-run-serialization.ts +15 -0
- package/src/services/plan/plan-run.service.ts +637 -0
- package/src/services/plan/plan-scheduler.service.ts +379 -0
- package/src/services/plan/plan-template.service.ts +224 -0
- package/src/services/plan/plan-transaction-events.ts +36 -0
- package/src/services/plan/plan-validator.service.ts +907 -0
- package/src/services/plan/plan-workspace.service.ts +131 -0
- package/src/services/plugin-executor.service.ts +102 -68
- package/src/services/quality-metrics.service.ts +112 -94
- package/src/services/queue-job.service.ts +288 -231
- package/src/services/recent-activity-title.service.ts +73 -36
- package/src/services/recent-activity.service.ts +274 -259
- package/src/services/skill-resolver.service.ts +38 -12
- package/src/services/social-chat-history.service.ts +190 -122
- package/src/services/system-executor.service.ts +96 -61
- package/src/services/thread/thread-active-run.ts +203 -0
- package/src/services/thread/thread-bootstrap.ts +385 -0
- package/src/services/thread/thread-listing.ts +199 -0
- package/src/services/thread/thread-memory-block.ts +130 -0
- package/src/services/thread/thread-message.service.ts +379 -0
- package/src/services/thread/thread-record-store.ts +155 -0
- package/src/services/thread/thread-title.service.ts +74 -0
- package/src/services/thread/thread-turn-execution.ts +280 -0
- package/src/services/thread/thread-turn-message-context.ts +73 -0
- package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
- package/src/services/thread/thread-turn-streaming.ts +403 -0
- package/src/services/thread/thread-turn-tracing.ts +35 -0
- package/src/services/thread/thread-turn.ts +376 -0
- package/src/services/thread/thread.service.ts +344 -0
- package/src/services/user.service.ts +82 -32
- package/src/services/write-intent-validator.service.ts +63 -51
- package/src/storage/attachment-parser.ts +69 -27
- package/src/storage/attachment-storage.service.ts +334 -275
- package/src/storage/generated-document-storage.service.ts +66 -34
- package/src/system-agents/agent-result.ts +3 -1
- package/src/system-agents/context-compaction.agent.ts +3 -3
- package/src/system-agents/delegated-agent-factory.ts +159 -90
- package/src/system-agents/helper-agent-options.ts +1 -1
- package/src/system-agents/memory-reranker.agent.ts +3 -3
- package/src/system-agents/memory.agent.ts +3 -3
- package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
- package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
- package/src/system-agents/skill-extractor.agent.ts +3 -3
- package/src/system-agents/skill-manager.agent.ts +3 -3
- package/src/system-agents/thread-router.agent.ts +157 -113
- package/src/system-agents/title-generator.agent.ts +3 -3
- package/src/tools/execution-plan.tool.ts +241 -171
- package/src/tools/fetch-webpage.tool.ts +29 -18
- package/src/tools/firecrawl-client.ts +26 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-block.tool.ts +14 -6
- package/src/tools/plan-approval.tool.ts +57 -47
- package/src/tools/read-file-parts.tool.ts +44 -33
- package/src/tools/remember-memory.tool.ts +65 -45
- package/src/tools/search-web.tool.ts +33 -22
- package/src/tools/search.tool.ts +41 -29
- package/src/tools/team-think.tool.ts +125 -84
- package/src/tools/user-questions.tool.ts +4 -3
- package/src/tools/web-tool-shared.ts +6 -0
- package/src/utils/async.ts +25 -22
- package/src/utils/crypto.ts +21 -0
- package/src/utils/date-time.ts +40 -1
- package/src/utils/errors.ts +111 -20
- package/src/utils/hono-error-handler.ts +24 -39
- package/src/utils/index.ts +2 -1
- package/src/utils/null-proto-record.ts +41 -0
- package/src/utils/sse-keepalive.ts +124 -21
- package/src/workers/bootstrap.ts +164 -52
- package/src/workers/memory-consolidation.worker.ts +325 -237
- package/src/workers/organization-learning.worker.ts +50 -16
- package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
- package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
- package/src/workers/skill-extraction.runner.ts +176 -93
- package/src/workers/utils/file-section-chunker.ts +8 -10
- package/src/workers/utils/repo-structure-extractor.ts +349 -260
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/thread-message-query.ts +97 -38
- package/src/workers/worker-utils.ts +74 -31
- package/src/config/debug-logger.ts +0 -47
- package/src/config/search.ts +0 -3
- package/src/redis/connection-accessor.ts +0 -26
- package/src/runtime/agent-types.ts +0 -1
- package/src/runtime/context-compaction-runtime.ts +0 -87
- package/src/runtime/memory-scope.ts +0 -43
- package/src/runtime/social-chat-agent-runner.ts +0 -118
- package/src/runtime/social-chat.ts +0 -516
- package/src/runtime/team-consultation-orchestrator.ts +0 -272
- package/src/services/adaptive-playbook.service.ts +0 -152
- package/src/services/artifact-provenance.service.ts +0 -172
- package/src/services/chat-attachments.service.ts +0 -17
- package/src/services/context-compaction-runtime.singleton.ts +0 -13
- package/src/services/execution-plan.service.ts +0 -1118
- package/src/services/memory.service.ts +0 -914
- package/src/services/plan-agent-heartbeat.service.ts +0 -136
- package/src/services/plan-agent-query.service.ts +0 -267
- package/src/services/plan-artifact.service.ts +0 -50
- package/src/services/plan-builder.service.ts +0 -67
- package/src/services/plan-checkpoint.service.ts +0 -81
- package/src/services/plan-completion-side-effects.ts +0 -80
- package/src/services/plan-coordination.service.ts +0 -157
- package/src/services/plan-cycle.service.ts +0 -284
- package/src/services/plan-deadline.service.ts +0 -430
- package/src/services/plan-event-delivery.service.ts +0 -166
- package/src/services/plan-executor.service.ts +0 -1950
- package/src/services/plan-run.service.ts +0 -515
- package/src/services/plan-scheduler.service.ts +0 -240
- package/src/services/plan-template.service.ts +0 -177
- package/src/services/plan-validator.service.ts +0 -818
- package/src/services/plan-workspace.service.ts +0 -83
- package/src/services/rerank.service.ts +0 -156
- package/src/services/thread-message.service.ts +0 -275
- package/src/services/thread-plan-registry.service.ts +0 -22
- package/src/services/thread-title.service.ts +0 -39
- package/src/services/thread-turn-preparation.service.ts +0 -1147
- package/src/services/thread-turn.ts +0 -172
- package/src/services/thread.service.ts +0 -869
- package/src/utils/env.ts +0 -8
- /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
- /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
- /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
- /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
- /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
- /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
- /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
- /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
|
@@ -1,869 +0,0 @@
|
|
|
1
|
-
import { THREAD, sdkThreadStatusSchema } from '@lota-sdk/shared'
|
|
2
|
-
import { BoundQuery, RecordId, StringRecordId, surql } from 'surrealdb'
|
|
3
|
-
|
|
4
|
-
import { agentDisplayNames, agentRoster, getCoreThreadProfile, isAgentName } from '../config/agent-defaults'
|
|
5
|
-
import { serverLogger } from '../config/logger'
|
|
6
|
-
import { getThreadBootstrapConfig } from '../config/thread-defaults'
|
|
7
|
-
import { BaseService } from '../db/base.service'
|
|
8
|
-
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
9
|
-
import type { RecordIdInput, RecordIdRef } from '../db/record-id'
|
|
10
|
-
import { databaseService } from '../db/service'
|
|
11
|
-
import type { DatabaseTable } from '../db/tables'
|
|
12
|
-
import { TABLES } from '../db/tables'
|
|
13
|
-
import { getRedisConnection, withRedisLeaseLock } from '../redis'
|
|
14
|
-
import {
|
|
15
|
-
appendToMemoryBlock,
|
|
16
|
-
compactMemoryBlockEntries,
|
|
17
|
-
formatPersistedMemoryBlockForPrompt,
|
|
18
|
-
parseMemoryBlock,
|
|
19
|
-
serializeMemoryBlock,
|
|
20
|
-
} from '../runtime/memory-block'
|
|
21
|
-
import { toIsoDateTimeString } from '../utils/date-time'
|
|
22
|
-
import { chatRunRegistry } from './chat-run-registry.service'
|
|
23
|
-
import { contextCompactionService } from './context-compaction.service'
|
|
24
|
-
import { MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES, MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES } from './thread-constants'
|
|
25
|
-
import { threadMessageService } from './thread-message.service'
|
|
26
|
-
import { NormalizedThreadSchema, PublicThreadSchema, ThreadSchema } from './thread.types'
|
|
27
|
-
import type { NormalizedThread, PublicThread, ThreadRecord } from './thread.types'
|
|
28
|
-
|
|
29
|
-
const THREAD_ACTIVE_RUN_LOCK_TTL_MS = 90_000
|
|
30
|
-
const THREAD_ACTIVE_RUN_LOCK_MAX_WAIT_MS = 750
|
|
31
|
-
const THREAD_ACTIVE_RUN_LOCK_RETRY_DELAY_MS = 75
|
|
32
|
-
const THREAD_BOOTSTRAP_LOCK_TTL_MS = 15_000
|
|
33
|
-
const THREAD_BOOTSTRAP_LOCK_REFRESH_INTERVAL_MS = 5_000
|
|
34
|
-
const THREAD_BOOTSTRAP_LOCK_MAX_WAIT_MS = 5_000
|
|
35
|
-
const THREAD_BOOTSTRAP_LOCK_RETRY_DELAY_MS = 100
|
|
36
|
-
const THREAD_UNIQUE_LOOKUP_MAX_ATTEMPTS = 20
|
|
37
|
-
const THREAD_UNIQUE_LOOKUP_RETRY_DELAY_MS = 100
|
|
38
|
-
|
|
39
|
-
function isRecordIdInput(value: unknown): value is RecordIdInput {
|
|
40
|
-
if (typeof value === 'string' || value instanceof RecordId || value instanceof StringRecordId) {
|
|
41
|
-
return true
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (!value || typeof value !== 'object') {
|
|
45
|
-
return false
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const record = value as { tb?: unknown; id?: unknown }
|
|
49
|
-
return typeof record.tb === 'string' && record.id !== undefined
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function getAgentDisplayName(agentId: string): string {
|
|
53
|
-
return agentDisplayNames[agentId] ?? agentId
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function buildActiveRunLockKey(threadId: RecordIdRef): string {
|
|
57
|
-
return `thread-active-run:${recordIdToString(ensureRecordId(threadId, TABLES.THREAD), TABLES.THREAD)}`
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function buildBootstrapThreadsLockKey(userId: RecordIdRef, orgId: RecordIdRef): string {
|
|
61
|
-
return `thread-bootstrap:${recordIdToString(ensureRecordId(userId, TABLES.USER), TABLES.USER)}:${recordIdToString(ensureRecordId(orgId, TABLES.ORGANIZATION), TABLES.ORGANIZATION)}`
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function buildListThreadsQuery(options: {
|
|
65
|
-
includeArchived: boolean
|
|
66
|
-
paginate: boolean
|
|
67
|
-
type?: string
|
|
68
|
-
types?: string[]
|
|
69
|
-
}): string {
|
|
70
|
-
const clauses = [`SELECT * FROM ${TABLES.THREAD}`, 'WHERE userId = $userId', ' AND organizationId = $orgId']
|
|
71
|
-
|
|
72
|
-
if (options.types) {
|
|
73
|
-
clauses.push(' AND type IN $types')
|
|
74
|
-
} else if (options.type) {
|
|
75
|
-
clauses.push(' AND type = $type')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!options.includeArchived) {
|
|
79
|
-
clauses.push(' AND status = "active"')
|
|
80
|
-
}
|
|
81
|
-
clauses.push('ORDER BY updatedAt DESC')
|
|
82
|
-
if (options.paginate) {
|
|
83
|
-
clauses.push('LIMIT $limit START $offset')
|
|
84
|
-
}
|
|
85
|
-
return clauses.join('\n')
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function normalizeActiveTurnValue(value: unknown): string | null {
|
|
89
|
-
if (typeof value !== 'string') {
|
|
90
|
-
return null
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const normalized = value.trim()
|
|
94
|
-
return normalized.length > 0 ? normalized : null
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function assertMutableThread(thread: ThreadRecord): void {
|
|
98
|
-
if (thread.type === 'default') throw new Error('Default threads cannot be modified')
|
|
99
|
-
if (thread.type === 'thread') throw new Error('Thread threads cannot be modified')
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function isUniqueConstraintError(error: unknown): boolean {
|
|
103
|
-
if (!(error instanceof Error)) return false
|
|
104
|
-
return error.message.includes('already contains')
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function requireExistingThread(
|
|
108
|
-
record: ThreadRecord | null,
|
|
109
|
-
params: {
|
|
110
|
-
type: 'default' | 'thread'
|
|
111
|
-
organizationId: RecordIdRef
|
|
112
|
-
userId: RecordIdRef
|
|
113
|
-
agentId?: string
|
|
114
|
-
threadType?: string
|
|
115
|
-
},
|
|
116
|
-
): ThreadRecord {
|
|
117
|
-
if (record) {
|
|
118
|
-
return record
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const scope = {
|
|
122
|
-
organizationId: recordIdToString(params.organizationId, TABLES.ORGANIZATION),
|
|
123
|
-
userId: recordIdToString(params.userId, TABLES.USER),
|
|
124
|
-
...(params.agentId ? { agentId: params.agentId } : {}),
|
|
125
|
-
...(params.threadType ? { threadType: params.threadType } : {}),
|
|
126
|
-
}
|
|
127
|
-
throw new Error(`Thread lookup failed after duplicate ${params.type} thread create: ${JSON.stringify(scope)}`)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
type UniqueThreadLookupParams = Parameters<typeof requireExistingThread>[1]
|
|
131
|
-
|
|
132
|
-
function haveSameMembers(left: string[], right: string[]): boolean {
|
|
133
|
-
return left.length === right.length && left.every((value, index) => value === right[index])
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export class ActiveThreadRunConflictError extends Error {
|
|
137
|
-
constructor() {
|
|
138
|
-
super('A chat run is already active.')
|
|
139
|
-
this.name = 'ActiveThreadRunConflictError'
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
class ThreadService extends BaseService<typeof ThreadSchema> {
|
|
144
|
-
constructor() {
|
|
145
|
-
super(TABLES.THREAD, ThreadSchema)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async createThread(input: {
|
|
149
|
-
userId: RecordIdRef
|
|
150
|
-
organizationId: RecordIdRef
|
|
151
|
-
type: string
|
|
152
|
-
agentId?: string
|
|
153
|
-
threadType?: string
|
|
154
|
-
members?: string[]
|
|
155
|
-
title?: string
|
|
156
|
-
}): Promise<NormalizedThread> {
|
|
157
|
-
switch (input.type) {
|
|
158
|
-
case 'default':
|
|
159
|
-
if (!input.agentId) throw new Error('Default threads require agentId')
|
|
160
|
-
if (input.threadType) throw new Error('Default threads cannot have threadType')
|
|
161
|
-
break
|
|
162
|
-
case 'topic':
|
|
163
|
-
if (!input.agentId) throw new Error('Topic threads require agentId')
|
|
164
|
-
if (input.threadType) throw new Error('Topic threads cannot have threadType')
|
|
165
|
-
break
|
|
166
|
-
case 'thread':
|
|
167
|
-
if (!input.threadType) throw new Error('Thread threads require threadType')
|
|
168
|
-
if (input.agentId) throw new Error('Thread threads cannot have agentId')
|
|
169
|
-
break
|
|
170
|
-
case 'group':
|
|
171
|
-
if (input.agentId) throw new Error('Group threads cannot have agentId')
|
|
172
|
-
if (input.threadType) throw new Error('Group threads cannot have threadType')
|
|
173
|
-
break
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const title = input.title ?? THREAD.DEFAULT_TITLE
|
|
177
|
-
const nameGenerated = input.title !== undefined && input.title !== THREAD.DEFAULT_TITLE
|
|
178
|
-
|
|
179
|
-
if (input.type === 'default') {
|
|
180
|
-
const agentId = input.agentId
|
|
181
|
-
if (!agentId) {
|
|
182
|
-
throw new Error('Default threads require agentId')
|
|
183
|
-
}
|
|
184
|
-
const { record } = await this.getOrCreateDefault(input.organizationId, input.userId, agentId)
|
|
185
|
-
return await this.toNormalizedThread(record)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (input.type === 'thread') {
|
|
189
|
-
const threadType = input.threadType
|
|
190
|
-
if (!threadType) {
|
|
191
|
-
throw new Error('Thread threads require threadType')
|
|
192
|
-
}
|
|
193
|
-
const { record } = await this.getOrCreateThread(input.organizationId, input.userId, threadType, {
|
|
194
|
-
members: input.members ?? [...agentRoster],
|
|
195
|
-
title,
|
|
196
|
-
nameGenerated,
|
|
197
|
-
})
|
|
198
|
-
return await this.toNormalizedThread(record)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const thread = await this.create({
|
|
202
|
-
userId: input.userId,
|
|
203
|
-
organizationId: input.organizationId,
|
|
204
|
-
type: input.type,
|
|
205
|
-
agentId: input.agentId,
|
|
206
|
-
threadType: input.threadType,
|
|
207
|
-
members: input.members ?? [...agentRoster],
|
|
208
|
-
title,
|
|
209
|
-
status: 'active',
|
|
210
|
-
nameGenerated,
|
|
211
|
-
isCompacting: false,
|
|
212
|
-
turnCount: 0,
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
return await this.toNormalizedThread(thread)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async getOrCreateDefault(
|
|
219
|
-
orgId: RecordIdRef,
|
|
220
|
-
userId: RecordIdRef,
|
|
221
|
-
agentId: string,
|
|
222
|
-
config?: { title?: string; nameGenerated?: boolean },
|
|
223
|
-
): Promise<{ record: ThreadRecord; created: boolean }> {
|
|
224
|
-
const lookup = { type: 'default' as const, organizationId: orgId, userId, agentId }
|
|
225
|
-
const existing = await this.findThreadByUniqueLookup(lookup)
|
|
226
|
-
if (existing) {
|
|
227
|
-
return { record: await this.syncThreadConfig(existing, config), created: false }
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
const record = await this.create({
|
|
232
|
-
type: 'default',
|
|
233
|
-
organizationId: orgId,
|
|
234
|
-
userId,
|
|
235
|
-
agentId,
|
|
236
|
-
members: [agentId],
|
|
237
|
-
title: config?.title ?? getAgentDisplayName(agentId),
|
|
238
|
-
status: 'active',
|
|
239
|
-
nameGenerated: config?.nameGenerated ?? false,
|
|
240
|
-
isCompacting: false,
|
|
241
|
-
turnCount: 0,
|
|
242
|
-
})
|
|
243
|
-
return { record, created: true }
|
|
244
|
-
} catch (e) {
|
|
245
|
-
if (isUniqueConstraintError(e)) {
|
|
246
|
-
return { record: await this.syncThreadConfig(await this.waitForExistingThread(lookup), config), created: false }
|
|
247
|
-
}
|
|
248
|
-
throw e
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async getOrCreateThread(
|
|
253
|
-
orgId: RecordIdRef,
|
|
254
|
-
userId: RecordIdRef,
|
|
255
|
-
threadType: string,
|
|
256
|
-
config: { members: string[]; title: string; nameGenerated?: boolean },
|
|
257
|
-
): Promise<{ record: ThreadRecord; created: boolean }> {
|
|
258
|
-
const lookup = { type: 'thread' as const, organizationId: orgId, userId, threadType }
|
|
259
|
-
const existing = await this.findThreadByUniqueLookup(lookup)
|
|
260
|
-
if (existing) {
|
|
261
|
-
return { record: await this.syncThreadConfig(existing, config), created: false }
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
const record = await this.create({
|
|
266
|
-
type: 'thread',
|
|
267
|
-
organizationId: orgId,
|
|
268
|
-
userId,
|
|
269
|
-
threadType,
|
|
270
|
-
members: config.members,
|
|
271
|
-
title: config.title,
|
|
272
|
-
status: 'active',
|
|
273
|
-
nameGenerated: config.nameGenerated ?? false,
|
|
274
|
-
isCompacting: false,
|
|
275
|
-
turnCount: 0,
|
|
276
|
-
})
|
|
277
|
-
return { record, created: true }
|
|
278
|
-
} catch (e) {
|
|
279
|
-
if (isUniqueConstraintError(e)) {
|
|
280
|
-
return { record: await this.syncThreadConfig(await this.waitForExistingThread(lookup), config), created: false }
|
|
281
|
-
}
|
|
282
|
-
throw e
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
private async syncThreadConfig(
|
|
287
|
-
record: ThreadRecord,
|
|
288
|
-
config?: { title?: string; nameGenerated?: boolean; members?: string[] },
|
|
289
|
-
): Promise<ThreadRecord> {
|
|
290
|
-
if (!config) {
|
|
291
|
-
return record
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const updates: Partial<ThreadRecord> = {}
|
|
295
|
-
|
|
296
|
-
if (config.title !== undefined && record.title !== config.title) {
|
|
297
|
-
updates.title = config.title
|
|
298
|
-
}
|
|
299
|
-
if (config.nameGenerated !== undefined && record.nameGenerated !== config.nameGenerated) {
|
|
300
|
-
updates.nameGenerated = config.nameGenerated
|
|
301
|
-
}
|
|
302
|
-
if (config.members !== undefined && !haveSameMembers(record.members, config.members)) {
|
|
303
|
-
updates.members = config.members
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (Object.keys(updates).length === 0) {
|
|
307
|
-
return record
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return await this.update(record.id, updates)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
private async findThreadByUniqueLookup(params: UniqueThreadLookupParams): Promise<ThreadRecord | null> {
|
|
314
|
-
return await this.databaseService.findOne(
|
|
315
|
-
this.table,
|
|
316
|
-
{
|
|
317
|
-
type: params.type,
|
|
318
|
-
organizationId: params.organizationId,
|
|
319
|
-
userId: params.userId,
|
|
320
|
-
...(params.agentId ? { agentId: params.agentId } : {}),
|
|
321
|
-
...(params.threadType ? { threadType: params.threadType } : {}),
|
|
322
|
-
},
|
|
323
|
-
ThreadSchema,
|
|
324
|
-
)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
private async waitForExistingThread(params: UniqueThreadLookupParams): Promise<ThreadRecord> {
|
|
328
|
-
for (let attempt = 0; attempt < THREAD_UNIQUE_LOOKUP_MAX_ATTEMPTS; attempt += 1) {
|
|
329
|
-
const record = await this.findThreadByUniqueLookup(params)
|
|
330
|
-
if (record) {
|
|
331
|
-
return record
|
|
332
|
-
}
|
|
333
|
-
if (attempt < THREAD_UNIQUE_LOOKUP_MAX_ATTEMPTS - 1) {
|
|
334
|
-
await Bun.sleep(THREAD_UNIQUE_LOOKUP_RETRY_DELAY_MS)
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return requireExistingThread(null, params)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async ensureBootstrapThreads(
|
|
342
|
-
userId: RecordIdRef,
|
|
343
|
-
orgId: RecordIdRef,
|
|
344
|
-
options?: { onboardStatus?: string; userName?: string | null },
|
|
345
|
-
): Promise<void> {
|
|
346
|
-
await withRedisLeaseLock(
|
|
347
|
-
{
|
|
348
|
-
redis: getRedisConnection(),
|
|
349
|
-
lockKey: buildBootstrapThreadsLockKey(userId, orgId),
|
|
350
|
-
lockTtlMs: THREAD_BOOTSTRAP_LOCK_TTL_MS,
|
|
351
|
-
refreshIntervalMs: THREAD_BOOTSTRAP_LOCK_REFRESH_INTERVAL_MS,
|
|
352
|
-
retryDelayMs: THREAD_BOOTSTRAP_LOCK_RETRY_DELAY_MS,
|
|
353
|
-
maxWaitMs: THREAD_BOOTSTRAP_LOCK_MAX_WAIT_MS,
|
|
354
|
-
label: 'thread bootstrap',
|
|
355
|
-
logger: serverLogger,
|
|
356
|
-
},
|
|
357
|
-
async (signal) => {
|
|
358
|
-
const throwIfAborted = () => {
|
|
359
|
-
if (signal.aborted) {
|
|
360
|
-
throw signal.reason instanceof Error ? signal.reason : new Error('Thread bootstrap lease was aborted.')
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
throwIfAborted()
|
|
365
|
-
const onboardStatus = options?.onboardStatus ?? 'completed'
|
|
366
|
-
const onboardingCompleted = onboardStatus === 'completed'
|
|
367
|
-
const bootstrapConfig = getThreadBootstrapConfig()
|
|
368
|
-
|
|
369
|
-
const existingThreads = await databaseService.findMany(
|
|
370
|
-
TABLES.THREAD,
|
|
371
|
-
{ userId, organizationId: orgId },
|
|
372
|
-
ThreadSchema,
|
|
373
|
-
)
|
|
374
|
-
throwIfAborted()
|
|
375
|
-
|
|
376
|
-
const hasGroupThread = existingThreads.some((t) => t.type === 'group')
|
|
377
|
-
const defaultThreadsByAgent = new Map<string, ThreadRecord>()
|
|
378
|
-
const threadThreadsByType = new Map<string, ThreadRecord>()
|
|
379
|
-
|
|
380
|
-
for (const thread of existingThreads) {
|
|
381
|
-
if (thread.type === 'default' && thread.agentId) {
|
|
382
|
-
defaultThreadsByAgent.set(thread.agentId, thread)
|
|
383
|
-
}
|
|
384
|
-
if (thread.type === 'thread' && typeof thread.threadType === 'string') {
|
|
385
|
-
threadThreadsByType.set(thread.threadType, thread)
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const requiredDefaultAgents = onboardingCompleted
|
|
390
|
-
? bootstrapConfig.completedDefaultAgents
|
|
391
|
-
: bootstrapConfig.onboardingDefaultAgents
|
|
392
|
-
|
|
393
|
-
const creationTasks: Array<() => Promise<{ record: ThreadRecord; created: boolean }>> = []
|
|
394
|
-
|
|
395
|
-
for (const agentId of requiredDefaultAgents) {
|
|
396
|
-
if (defaultThreadsByAgent.has(agentId)) continue
|
|
397
|
-
creationTasks.push(async () => await this.getOrCreateDefault(orgId, userId, agentId))
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (onboardingCompleted && bootstrapConfig.ensureDefaultGroupOnCompleted && !hasGroupThread) {
|
|
401
|
-
creationTasks.push(
|
|
402
|
-
async () =>
|
|
403
|
-
await this.createThread({
|
|
404
|
-
userId,
|
|
405
|
-
organizationId: orgId,
|
|
406
|
-
type: 'group',
|
|
407
|
-
title: THREAD.DEFAULT_TITLE,
|
|
408
|
-
}).then(async (normalized) => ({
|
|
409
|
-
record: await this.getById(ensureRecordId(normalized.id, TABLES.THREAD)),
|
|
410
|
-
created: true,
|
|
411
|
-
})),
|
|
412
|
-
)
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (onboardingCompleted) {
|
|
416
|
-
for (const wsType of bootstrapConfig.threadTypesAfterOnboarding) {
|
|
417
|
-
if (threadThreadsByType.has(wsType)) continue
|
|
418
|
-
const profile = getCoreThreadProfile(wsType)
|
|
419
|
-
creationTasks.push(
|
|
420
|
-
async () =>
|
|
421
|
-
await this.getOrCreateThread(orgId, userId, wsType, {
|
|
422
|
-
members: [...profile.members],
|
|
423
|
-
title: profile.config.title,
|
|
424
|
-
}),
|
|
425
|
-
)
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
let createdResults: { record: ThreadRecord; created: boolean }[] = []
|
|
430
|
-
if (creationTasks.length > 0) {
|
|
431
|
-
for (const runCreation of creationTasks) {
|
|
432
|
-
throwIfAborted()
|
|
433
|
-
createdResults.push(await runCreation())
|
|
434
|
-
throwIfAborted()
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const onboardingWelcome = bootstrapConfig.onboardingWelcome
|
|
439
|
-
if (!onboardingCompleted && onboardingWelcome) {
|
|
440
|
-
const createdOwnerThread = createdResults.find(
|
|
441
|
-
(r) => r.created && r.record.type === 'default' && r.record.agentId === onboardingWelcome.defaultAgentId,
|
|
442
|
-
)
|
|
443
|
-
const existingOwnerThread = defaultThreadsByAgent.get(onboardingWelcome.defaultAgentId)
|
|
444
|
-
|
|
445
|
-
const ownerThreadId = createdOwnerThread?.record.id ?? existingOwnerThread?.id
|
|
446
|
-
|
|
447
|
-
if (ownerThreadId) {
|
|
448
|
-
throwIfAborted()
|
|
449
|
-
const ownerThreadRef = ensureRecordId(ownerThreadId, TABLES.THREAD)
|
|
450
|
-
await threadMessageService.ensureBootstrapWelcomeMessage({
|
|
451
|
-
threadId: ownerThreadRef,
|
|
452
|
-
agentId: onboardingWelcome.defaultAgentId,
|
|
453
|
-
text: onboardingWelcome.buildMessageText({ userName: options?.userName }),
|
|
454
|
-
})
|
|
455
|
-
throwIfAborted()
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
},
|
|
459
|
-
)
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
async listThreads(
|
|
463
|
-
userId: RecordIdRef,
|
|
464
|
-
orgId: RecordIdRef,
|
|
465
|
-
options: { type?: string; types?: string[]; take?: number; page?: number; includeArchived?: boolean },
|
|
466
|
-
): Promise<{ threads: NormalizedThread[]; hasMore: boolean }> {
|
|
467
|
-
const includeArchived = options.includeArchived ?? false
|
|
468
|
-
const type = options.type
|
|
469
|
-
const types = options.types
|
|
470
|
-
|
|
471
|
-
if (type === 'default' || type === 'thread') {
|
|
472
|
-
const vars: Record<string, unknown> = { userId, orgId, type }
|
|
473
|
-
const threads = await databaseService.queryMany<typeof ThreadSchema>(
|
|
474
|
-
new BoundQuery(buildListThreadsQuery({ includeArchived, paginate: false, type }), vars),
|
|
475
|
-
ThreadSchema,
|
|
476
|
-
)
|
|
477
|
-
return { threads: await this.toNormalizedThreads(threads, { checkLease: false }), hasMore: false }
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const take = options.take ?? THREAD.DEFAULT_PAGE_LIMIT
|
|
481
|
-
const page = options.page ?? 1
|
|
482
|
-
const vars: Record<string, unknown> = { userId, orgId, limit: take + 1, offset: (page - 1) * take }
|
|
483
|
-
|
|
484
|
-
if (types) {
|
|
485
|
-
vars.types = types
|
|
486
|
-
} else if (type) {
|
|
487
|
-
vars.type = type
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const threads = await databaseService.queryMany<typeof ThreadSchema>(
|
|
491
|
-
new BoundQuery(buildListThreadsQuery({ includeArchived, paginate: true, type, types }), vars),
|
|
492
|
-
ThreadSchema,
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
const hasMore = threads.length > take
|
|
496
|
-
const sliced = hasMore ? threads.slice(0, take) : threads
|
|
497
|
-
|
|
498
|
-
return { threads: await this.toNormalizedThreads(sliced, { checkLease: false }), hasMore }
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
async listOrganizationThreads(params: {
|
|
502
|
-
orgId: RecordIdRef
|
|
503
|
-
type?: string
|
|
504
|
-
agentId?: string
|
|
505
|
-
includeArchived?: boolean
|
|
506
|
-
}): Promise<NormalizedThread[]> {
|
|
507
|
-
const whereClauses = ['organizationId = $orgId']
|
|
508
|
-
const variables: Record<string, unknown> = { orgId: params.orgId }
|
|
509
|
-
|
|
510
|
-
if (params.type) {
|
|
511
|
-
whereClauses.push('type = $type')
|
|
512
|
-
variables.type = params.type
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (params.agentId) {
|
|
516
|
-
whereClauses.push('agentId = $agentId')
|
|
517
|
-
variables.agentId = params.agentId
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (params.includeArchived !== true) {
|
|
521
|
-
whereClauses.push('status = "active"')
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const threads = await databaseService.queryMany<typeof ThreadSchema>(
|
|
525
|
-
new BoundQuery(
|
|
526
|
-
`SELECT * FROM ${TABLES.THREAD}
|
|
527
|
-
WHERE ${whereClauses.join('\n AND ')}
|
|
528
|
-
ORDER BY createdAt ASC, id ASC`,
|
|
529
|
-
variables,
|
|
530
|
-
),
|
|
531
|
-
ThreadSchema,
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
return await this.toNormalizedThreads(threads, { checkLease: false })
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
async getThread(threadId: RecordIdRef): Promise<NormalizedThread> {
|
|
538
|
-
const thread = await this.getById(threadId)
|
|
539
|
-
return await this.toNormalizedThread(thread)
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
async updateTitle(threadId: RecordIdRef, title: string): Promise<NormalizedThread> {
|
|
543
|
-
const existing = await this.getById(threadId)
|
|
544
|
-
assertMutableThread(existing)
|
|
545
|
-
const thread = await this.update(threadId, { title, nameGenerated: true })
|
|
546
|
-
return await this.toNormalizedThread(thread)
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
async updateStatus(threadId: RecordIdRef, status: string): Promise<NormalizedThread> {
|
|
550
|
-
const validStatus = sdkThreadStatusSchema.parse(status)
|
|
551
|
-
const existing = await this.getById(threadId)
|
|
552
|
-
assertMutableThread(existing)
|
|
553
|
-
const thread = await this.update(threadId, { status: validStatus })
|
|
554
|
-
return await this.toNormalizedThread(thread)
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
async setActiveTurn(threadId: RecordIdRef, runId: string, streamId?: string | null): Promise<void> {
|
|
558
|
-
const threadRef = ensureRecordId(threadId, TABLES.THREAD)
|
|
559
|
-
if (streamId === null || streamId === undefined) {
|
|
560
|
-
await databaseService.query<unknown>(surql`
|
|
561
|
-
UPDATE ONLY ${threadRef}
|
|
562
|
-
SET activeRunId = ${runId},
|
|
563
|
-
activeStreamId = NONE
|
|
564
|
-
`)
|
|
565
|
-
return
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
await databaseService.query<unknown>(surql`
|
|
569
|
-
UPDATE ONLY ${threadRef}
|
|
570
|
-
SET activeRunId = ${runId},
|
|
571
|
-
activeStreamId = ${streamId}
|
|
572
|
-
`)
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
async getActiveTurn(threadId: RecordIdRef): Promise<{ runId: string | null; streamId: string | null }> {
|
|
576
|
-
const thread = await this.getById(threadId)
|
|
577
|
-
return {
|
|
578
|
-
runId: normalizeActiveTurnValue(thread.activeRunId),
|
|
579
|
-
streamId: normalizeActiveTurnValue(thread.activeStreamId),
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
async getActiveRunId(threadId: RecordIdRef): Promise<string | null> {
|
|
584
|
-
const { runId } = await this.getActiveTurn(threadId)
|
|
585
|
-
return runId
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
async hasActiveRunLease(threadId: RecordIdRef): Promise<boolean> {
|
|
589
|
-
const count = await getRedisConnection().exists(buildActiveRunLockKey(threadId))
|
|
590
|
-
return count > 0
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
async withActiveRunLease<T>(threadId: RecordIdRef, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
594
|
-
try {
|
|
595
|
-
return await withRedisLeaseLock(
|
|
596
|
-
{
|
|
597
|
-
redis: getRedisConnection(),
|
|
598
|
-
lockKey: buildActiveRunLockKey(threadId),
|
|
599
|
-
lockTtlMs: THREAD_ACTIVE_RUN_LOCK_TTL_MS,
|
|
600
|
-
retryDelayMs: THREAD_ACTIVE_RUN_LOCK_RETRY_DELAY_MS,
|
|
601
|
-
maxWaitMs: THREAD_ACTIVE_RUN_LOCK_MAX_WAIT_MS,
|
|
602
|
-
label: 'thread active run',
|
|
603
|
-
logger: serverLogger,
|
|
604
|
-
},
|
|
605
|
-
fn,
|
|
606
|
-
)
|
|
607
|
-
} catch (error) {
|
|
608
|
-
if (error instanceof Error && error.message.startsWith('Timed out waiting for thread active run')) {
|
|
609
|
-
throw new ActiveThreadRunConflictError()
|
|
610
|
-
}
|
|
611
|
-
throw error
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
async getActiveStreamId(threadId: RecordIdRef): Promise<string | null> {
|
|
616
|
-
const { streamId } = await this.getActiveTurn(threadId)
|
|
617
|
-
return streamId
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
async clearActiveTurn(threadId: RecordIdRef, params: { runId: string; streamId?: string | null }): Promise<void> {
|
|
621
|
-
const threadRef = ensureRecordId(threadId, TABLES.THREAD)
|
|
622
|
-
const currentStreamId = params.streamId ?? null
|
|
623
|
-
if (currentStreamId === null) {
|
|
624
|
-
await databaseService.query(
|
|
625
|
-
surql`UPDATE ONLY ${threadRef} SET activeRunId = NONE, activeStreamId = NONE WHERE activeRunId = ${params.runId}`,
|
|
626
|
-
)
|
|
627
|
-
return
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
await databaseService.query(surql`
|
|
631
|
-
UPDATE ONLY ${threadRef}
|
|
632
|
-
SET activeRunId = NONE,
|
|
633
|
-
activeStreamId = NONE
|
|
634
|
-
WHERE activeRunId = ${params.runId} AND activeStreamId = ${currentStreamId}
|
|
635
|
-
`)
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async clearStaleActiveRunIfMissingFromRegistry(threadId: RecordIdRef): Promise<boolean> {
|
|
639
|
-
const { runId: activeRunId, streamId: activeStreamId } = await this.getActiveTurn(threadId)
|
|
640
|
-
if (!activeRunId || (await this.hasActiveRunLease(threadId))) {
|
|
641
|
-
return false
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
await this.clearActiveTurn(threadId, { runId: activeRunId, streamId: activeStreamId })
|
|
645
|
-
|
|
646
|
-
serverLogger.warn`Cleared stale thread run after lease expired: thread=${recordIdToString(ensureRecordId(threadId, TABLES.THREAD), TABLES.THREAD)} run=${activeRunId}`
|
|
647
|
-
return true
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
async stopActiveRun(threadId: RecordIdRef): Promise<boolean> {
|
|
651
|
-
const { runId: activeRunId } = await this.getActiveTurn(threadId)
|
|
652
|
-
if (!activeRunId) return false
|
|
653
|
-
|
|
654
|
-
const stopped = chatRunRegistry.stop(activeRunId, new DOMException('Run stopped by user.', 'AbortError'))
|
|
655
|
-
if (stopped) {
|
|
656
|
-
return true
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
await this.clearStaleActiveRunIfMissingFromRegistry(threadId)
|
|
660
|
-
return false
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
async setCompacting(threadId: RecordIdRef, value: boolean): Promise<void> {
|
|
664
|
-
const threadRef = ensureRecordId(threadId, TABLES.THREAD)
|
|
665
|
-
await databaseService.query<unknown>(surql`
|
|
666
|
-
UPDATE ONLY ${threadRef}
|
|
667
|
-
SET isCompacting = ${value}
|
|
668
|
-
`)
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
async appendMemoryBlock(threadId: RecordIdRef, entry: string): Promise<string> {
|
|
672
|
-
const threadRef = ensureRecordId(threadId, TABLES.THREAD)
|
|
673
|
-
const thread = await this.getById(threadRef)
|
|
674
|
-
const entries = parseMemoryBlock(thread.memoryBlock)
|
|
675
|
-
|
|
676
|
-
const labelMatch = entry.match(/^(\w+):\s*/i)
|
|
677
|
-
const role = labelMatch ? labelMatch[1].toLowerCase() : 'system'
|
|
678
|
-
const content = labelMatch ? entry.slice(labelMatch[0].length).trim() : entry.trim()
|
|
679
|
-
|
|
680
|
-
const updatedEntries = appendToMemoryBlock(entries, role, content)
|
|
681
|
-
const serialized = serializeMemoryBlock(updatedEntries)
|
|
682
|
-
|
|
683
|
-
await this.update(threadRef, { memoryBlock: serialized })
|
|
684
|
-
|
|
685
|
-
if (updatedEntries.length >= MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES) {
|
|
686
|
-
void this.compactMemoryBlock(threadRef).catch((err: unknown) => {
|
|
687
|
-
serverLogger.warn`Memory block compaction failed for ${threadRef}: ${err}`
|
|
688
|
-
})
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
return this.formatMemoryBlockForPrompt({ memoryBlock: serialized, memoryBlockSummary: thread.memoryBlockSummary })
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
async compactMemoryBlock(threadId: RecordIdRef): Promise<boolean> {
|
|
695
|
-
const threadRef = ensureRecordId(threadId, TABLES.THREAD)
|
|
696
|
-
const thread = await this.getById(threadRef)
|
|
697
|
-
const result = await compactMemoryBlockEntries({
|
|
698
|
-
previousSummary: thread.memoryBlockSummary,
|
|
699
|
-
entries: parseMemoryBlock(thread.memoryBlock),
|
|
700
|
-
triggerEntries: MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES,
|
|
701
|
-
chunkEntries: MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES,
|
|
702
|
-
compact: (params) => contextCompactionService.compactMemoryBlock(params),
|
|
703
|
-
})
|
|
704
|
-
|
|
705
|
-
if (!result.compacted) return false
|
|
706
|
-
|
|
707
|
-
await this.update(threadRef, {
|
|
708
|
-
memoryBlockSummary: result.summary || '',
|
|
709
|
-
memoryBlock: serializeMemoryBlock(result.entries),
|
|
710
|
-
})
|
|
711
|
-
|
|
712
|
-
return true
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
async clearThread(threadId: RecordIdRef): Promise<void> {
|
|
716
|
-
const existing = await this.getById(threadId)
|
|
717
|
-
assertMutableThread(existing)
|
|
718
|
-
const threadRef = ensureRecordId(threadId, TABLES.THREAD)
|
|
719
|
-
await databaseService.deleteWhere(TABLES.THREAD_MESSAGE, { threadId: threadRef })
|
|
720
|
-
await databaseService.query<unknown>(surql`
|
|
721
|
-
UPDATE ONLY ${threadRef}
|
|
722
|
-
SET turnCount = 0,
|
|
723
|
-
compactionSummary = NONE,
|
|
724
|
-
lastCompactedMessageId = NONE,
|
|
725
|
-
activeRunId = NONE,
|
|
726
|
-
activeStreamId = NONE,
|
|
727
|
-
isCompacting = false
|
|
728
|
-
`)
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
async deleteThread(threadId: RecordIdRef): Promise<void> {
|
|
732
|
-
const existing = await this.getById(threadId)
|
|
733
|
-
assertMutableThread(existing)
|
|
734
|
-
await this.delete(threadId)
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
async listRecentThreads({
|
|
738
|
-
userId,
|
|
739
|
-
orgId,
|
|
740
|
-
excludeThreadId,
|
|
741
|
-
limit,
|
|
742
|
-
}: {
|
|
743
|
-
userId: RecordIdRef
|
|
744
|
-
orgId: RecordIdRef
|
|
745
|
-
excludeThreadId?: RecordIdRef
|
|
746
|
-
limit: number
|
|
747
|
-
}): Promise<NormalizedThread[]> {
|
|
748
|
-
let excludeCondition = ''
|
|
749
|
-
const vars: Record<string, unknown> = { userId, orgId, limit }
|
|
750
|
-
|
|
751
|
-
if (excludeThreadId) {
|
|
752
|
-
excludeCondition = 'AND id != $excludeThreadId'
|
|
753
|
-
vars.excludeThreadId = excludeThreadId
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const threads = await databaseService.queryMany<typeof ThreadSchema>(
|
|
757
|
-
new BoundQuery(
|
|
758
|
-
`SELECT * FROM ${TABLES.THREAD}
|
|
759
|
-
WHERE userId = $userId
|
|
760
|
-
AND organizationId = $orgId
|
|
761
|
-
${excludeCondition}
|
|
762
|
-
AND status != "archived"
|
|
763
|
-
ORDER BY updatedAt DESC
|
|
764
|
-
LIMIT $limit`,
|
|
765
|
-
vars,
|
|
766
|
-
),
|
|
767
|
-
ThreadSchema,
|
|
768
|
-
)
|
|
769
|
-
|
|
770
|
-
return await this.toNormalizedThreads(threads, { checkLease: false })
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
private normalizeRecordIdString(id: unknown, table: DatabaseTable): string {
|
|
774
|
-
if (!isRecordIdInput(id)) {
|
|
775
|
-
throw new Error(`Invalid record id for table ${table}`)
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
return recordIdToString(id, table)
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
formatMemoryBlockForPrompt(thread: Pick<ThreadRecord, 'memoryBlock' | 'memoryBlockSummary'>): string {
|
|
782
|
-
return formatPersistedMemoryBlockForPrompt({
|
|
783
|
-
summary: thread.memoryBlockSummary,
|
|
784
|
-
entries: parseMemoryBlock(thread.memoryBlock),
|
|
785
|
-
})
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
private getDefaultTitle(thread: Pick<ThreadRecord, 'type' | 'threadType'>): string {
|
|
789
|
-
if (thread.type === 'thread' && typeof thread.threadType === 'string') {
|
|
790
|
-
return getCoreThreadProfile(thread.threadType).config.title
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
return THREAD.DEFAULT_TITLE
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
private async computeIsRunning(
|
|
797
|
-
thread: Pick<ThreadRecord, 'id' | 'activeRunId'>,
|
|
798
|
-
options: { checkLease: boolean },
|
|
799
|
-
): Promise<boolean> {
|
|
800
|
-
const activeRunId =
|
|
801
|
-
typeof thread.activeRunId === 'string' && thread.activeRunId.trim().length > 0 ? thread.activeRunId : null
|
|
802
|
-
|
|
803
|
-
if (activeRunId === null) {
|
|
804
|
-
return false
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
if (chatRunRegistry.has(activeRunId)) {
|
|
808
|
-
return true
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
if (!options.checkLease) {
|
|
812
|
-
return true
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
return await this.hasActiveRunLease(ensureRecordId(thread.id, TABLES.THREAD))
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
private async toNormalizedThread(
|
|
819
|
-
thread: ThreadRecord,
|
|
820
|
-
options: { checkLease?: boolean } = {},
|
|
821
|
-
): Promise<NormalizedThread> {
|
|
822
|
-
const isRunning = await this.computeIsRunning(thread, { checkLease: options.checkLease ?? true })
|
|
823
|
-
const isCompacting = thread.isCompacting === true
|
|
824
|
-
const type = thread.type
|
|
825
|
-
const threadType = type === 'thread' && typeof thread.threadType === 'string' ? thread.threadType : undefined
|
|
826
|
-
const status = thread.status
|
|
827
|
-
return NormalizedThreadSchema.parse({
|
|
828
|
-
id: this.normalizeRecordIdString(thread.id, TABLES.THREAD),
|
|
829
|
-
userId: this.normalizeRecordIdString(thread.userId, TABLES.USER),
|
|
830
|
-
organizationId: this.normalizeRecordIdString(thread.organizationId, TABLES.ORGANIZATION),
|
|
831
|
-
type,
|
|
832
|
-
...(threadType ? { threadType } : {}),
|
|
833
|
-
nameGenerated: thread.nameGenerated,
|
|
834
|
-
isRunning,
|
|
835
|
-
isCompacting,
|
|
836
|
-
...(isAgentName(thread.agentId) ? { agentId: thread.agentId } : {}),
|
|
837
|
-
title: thread.title ?? this.getDefaultTitle(thread),
|
|
838
|
-
status,
|
|
839
|
-
memoryBlock: this.formatMemoryBlockForPrompt(thread),
|
|
840
|
-
members: thread.members,
|
|
841
|
-
createdAt: toIsoDateTimeString(thread.createdAt),
|
|
842
|
-
updatedAt: toIsoDateTimeString(thread.updatedAt),
|
|
843
|
-
})
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
private async toNormalizedThreads(
|
|
847
|
-
threads: ThreadRecord[],
|
|
848
|
-
options: { checkLease?: boolean } = {},
|
|
849
|
-
): Promise<NormalizedThread[]> {
|
|
850
|
-
return await Promise.all(threads.map(async (thread) => await this.toNormalizedThread(thread, options)))
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
toPublicThread(thread: NormalizedThread): PublicThread {
|
|
854
|
-
const { organizationId: _organizationId, userId: _userId, memoryBlock: _memoryBlock, ...publicThread } = thread
|
|
855
|
-
return PublicThreadSchema.parse(publicThread)
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
async incrementTurnCount(threadId: RecordIdRef): Promise<number> {
|
|
859
|
-
const threadRef = ensureRecordId(threadId, TABLES.THREAD)
|
|
860
|
-
const result = await databaseService.query<{ turnCount: number }>(surql`
|
|
861
|
-
UPDATE ONLY ${threadRef}
|
|
862
|
-
SET turnCount += 1
|
|
863
|
-
RETURN turnCount
|
|
864
|
-
`)
|
|
865
|
-
return result[0].turnCount
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
export const threadService = new ThreadService()
|