@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
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { parseRowMetadata, recordIdSchema, requireTimestamp, withCreatedAtMetadata } from '@lota-sdk/shared'
|
|
2
|
+
import type { ChatMessage } from '@lota-sdk/shared'
|
|
3
|
+
import { Context, Effect, Layer } from 'effect'
|
|
4
|
+
import { RecordId, surql } from 'surrealdb'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import type { ZodTypeAny } from 'zod'
|
|
7
|
+
|
|
8
|
+
import { getAgentDisplayNames } from '../../config/agent-defaults'
|
|
9
|
+
import { CursorRowSchema, listMessageHistoryPageEffect } from '../../db/cursor-pagination'
|
|
10
|
+
import type { CursorPaginationConfig } from '../../db/cursor-pagination'
|
|
11
|
+
import { ensureRecordId, recordIdToString } from '../../db/record-id'
|
|
12
|
+
import type { RecordIdRef } from '../../db/record-id'
|
|
13
|
+
import type { SurrealDBService } from '../../db/service'
|
|
14
|
+
import { TABLES } from '../../db/tables'
|
|
15
|
+
import { ThreadMessageRowSchema } from '../../db/thread-message-row'
|
|
16
|
+
import type { ThreadMessageRow } from '../../db/thread-message-row'
|
|
17
|
+
import { ServiceError } from '../../effect/errors'
|
|
18
|
+
import { effectTryServicePromise } from '../../effect/helpers'
|
|
19
|
+
import { DatabaseServiceTag } from '../../effect/services'
|
|
20
|
+
import { sha256Hex } from '../../utils/crypto'
|
|
21
|
+
import { nowEpochMillis, unsafeDateFrom } from '../../utils/date-time'
|
|
22
|
+
|
|
23
|
+
const ThreadMessageExistingRowSchema = z.object({ id: recordIdSchema, createdAt: z.coerce.date() })
|
|
24
|
+
|
|
25
|
+
function parseRowOrFail<TSchema extends ZodTypeAny>(
|
|
26
|
+
schema: TSchema,
|
|
27
|
+
value: unknown,
|
|
28
|
+
operation: string,
|
|
29
|
+
): Effect.Effect<z.infer<TSchema>, ServiceError> {
|
|
30
|
+
return Effect.try({
|
|
31
|
+
try: () => schema.parse(value) as z.infer<TSchema>,
|
|
32
|
+
catch: (cause) => new ServiceError({ message: `Failed to parse row for ${operation}.`, cause }),
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toMessageId(value: string | RecordIdRef): string {
|
|
37
|
+
return recordIdToString(value, TABLES.THREAD_MESSAGE)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toThreadMessageRowId(threadId: RecordIdRef, messageId: string): RecordId {
|
|
41
|
+
const threadStr = recordIdToString(threadId, TABLES.THREAD)
|
|
42
|
+
const digest = sha256Hex(`${threadStr}\0${messageId}`).slice(0, 32)
|
|
43
|
+
return new RecordId(TABLES.THREAD_MESSAGE, digest)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toThreadRef(threadId: RecordIdRef): RecordId {
|
|
47
|
+
return ensureRecordId(threadId, TABLES.THREAD)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeMessageValue(value: unknown): unknown {
|
|
51
|
+
if (value instanceof Date) {
|
|
52
|
+
return value.toISOString()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
return value.map((item) => normalizeMessageValue(item))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (value && typeof value === 'object') {
|
|
60
|
+
return Object.fromEntries(
|
|
61
|
+
Object.entries(value as Record<string, unknown>).map(([key, entry]) => [key, normalizeMessageValue(entry)]),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readPersistedMessageParts(parts: ThreadMessageRow['parts']): ChatMessage['parts'] {
|
|
69
|
+
return (Array.isArray(parts) ? normalizeMessageValue(parts) : []) as ChatMessage['parts']
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function toChatMessage(row: ThreadMessageRow): ChatMessage {
|
|
73
|
+
const rowCreatedAt = requireTimestamp(row.createdAt)
|
|
74
|
+
const metadata = withCreatedAtMetadata(parseRowMetadata(row.metadata), rowCreatedAt)
|
|
75
|
+
|
|
76
|
+
return { id: row.messageId, role: row.role, parts: readPersistedMessageParts(row.parts), metadata }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function cloneMessageParts(parts: ChatMessage['parts'] | undefined): NonNullable<ThreadMessageRow['parts']> {
|
|
80
|
+
return Array.isArray(parts)
|
|
81
|
+
? (normalizeMessageValue(structuredClone(parts)) as NonNullable<ThreadMessageRow['parts']>)
|
|
82
|
+
: []
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const threadPaginationConfig: CursorPaginationConfig = {
|
|
86
|
+
table: TABLES.THREAD_MESSAGE,
|
|
87
|
+
parentFilterField: 'threadId',
|
|
88
|
+
toRowId: toThreadMessageRowId,
|
|
89
|
+
parseRow: (row: unknown) => ThreadMessageRowSchema.parse(row),
|
|
90
|
+
toMessage: (row: unknown) => toChatMessage(ThreadMessageRowSchema.parse(row)),
|
|
91
|
+
queryLatest: (parentId, limit) => surql`
|
|
92
|
+
SELECT * FROM threadMessage
|
|
93
|
+
WHERE threadId = ${parentId}
|
|
94
|
+
ORDER BY createdAt DESC, id DESC
|
|
95
|
+
LIMIT ${limit}
|
|
96
|
+
`,
|
|
97
|
+
queryBefore: (parentId, cursorCreatedAt, cursorId, limit) => surql`
|
|
98
|
+
SELECT * FROM threadMessage
|
|
99
|
+
WHERE threadId = ${parentId}
|
|
100
|
+
AND (
|
|
101
|
+
createdAt < ${cursorCreatedAt}
|
|
102
|
+
OR (createdAt = ${cursorCreatedAt} AND id < ${cursorId})
|
|
103
|
+
)
|
|
104
|
+
ORDER BY createdAt DESC, id DESC
|
|
105
|
+
LIMIT ${limit}
|
|
106
|
+
`,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function makeThreadMessageService(db: SurrealDBService) {
|
|
110
|
+
function upsertMessages(params: { threadId: RecordIdRef; messages: ChatMessage[] }) {
|
|
111
|
+
const threadId = toThreadRef(params.threadId)
|
|
112
|
+
return Effect.forEach(params.messages, (message) =>
|
|
113
|
+
Effect.gen(function* () {
|
|
114
|
+
const messageId = message.id.trim()
|
|
115
|
+
if (!messageId) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const role = message.role
|
|
120
|
+
const parts = cloneMessageParts(message.parts)
|
|
121
|
+
if (parts.length === 0) {
|
|
122
|
+
if (role === 'assistant') {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
return yield* new ServiceError({
|
|
126
|
+
message: `Refusing to persist thread message "${messageId}" with empty parts`,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const rowId = toThreadMessageRowId(threadId, messageId)
|
|
131
|
+
const existingRow = yield* effectTryServicePromise(
|
|
132
|
+
() => db.findOne(TABLES.THREAD_MESSAGE, { threadId, messageId }, ThreadMessageExistingRowSchema),
|
|
133
|
+
`Failed to load existing thread message "${messageId}".`,
|
|
134
|
+
)
|
|
135
|
+
const persistedCreatedAt =
|
|
136
|
+
existingRow === null ? requireTimestamp(message.metadata?.createdAt) : requireTimestamp(existingRow.createdAt)
|
|
137
|
+
const metadata = withCreatedAtMetadata({ ...message.metadata, createdAt: persistedCreatedAt })
|
|
138
|
+
|
|
139
|
+
yield* effectTryServicePromise(
|
|
140
|
+
() =>
|
|
141
|
+
db.upsert(
|
|
142
|
+
TABLES.THREAD_MESSAGE,
|
|
143
|
+
rowId,
|
|
144
|
+
{
|
|
145
|
+
threadId,
|
|
146
|
+
messageId,
|
|
147
|
+
role,
|
|
148
|
+
parts,
|
|
149
|
+
metadata,
|
|
150
|
+
createdAt: existingRow ? existingRow.createdAt : unsafeDateFrom(persistedCreatedAt),
|
|
151
|
+
},
|
|
152
|
+
ThreadMessageRowSchema,
|
|
153
|
+
{ mutation: 'content' },
|
|
154
|
+
),
|
|
155
|
+
`Failed to upsert thread message "${messageId}".`,
|
|
156
|
+
)
|
|
157
|
+
}),
|
|
158
|
+
).pipe(Effect.asVoid)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const service = {
|
|
162
|
+
upsertMessages,
|
|
163
|
+
upsertMessagesEffect: upsertMessages,
|
|
164
|
+
listMessages(threadId: RecordIdRef) {
|
|
165
|
+
const threadRef = toThreadRef(threadId)
|
|
166
|
+
return effectTryServicePromise(
|
|
167
|
+
() =>
|
|
168
|
+
db.query<unknown>(surql`
|
|
169
|
+
SELECT * FROM threadMessage
|
|
170
|
+
WHERE threadId = ${threadRef}
|
|
171
|
+
ORDER BY createdAt ASC, id ASC
|
|
172
|
+
`),
|
|
173
|
+
'Failed to list thread messages.',
|
|
174
|
+
).pipe(
|
|
175
|
+
Effect.flatMap((rows) =>
|
|
176
|
+
Effect.forEach(rows, (row) => parseRowOrFail(ThreadMessageRowSchema, row, 'listMessages')),
|
|
177
|
+
),
|
|
178
|
+
Effect.map((rows) => rows.map((row) => toChatMessage(row))),
|
|
179
|
+
)
|
|
180
|
+
},
|
|
181
|
+
listMessagesEffect(threadId: RecordIdRef) {
|
|
182
|
+
return service.listMessages(threadId)
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
listMessageHistoryPage(params: { threadId: RecordIdRef; take: number; beforeMessageId?: string }) {
|
|
186
|
+
const threadRef = toThreadRef(params.threadId)
|
|
187
|
+
return listMessageHistoryPageEffect(db, threadPaginationConfig, {
|
|
188
|
+
parentId: threadRef,
|
|
189
|
+
take: params.take,
|
|
190
|
+
beforeMessageId: params.beforeMessageId,
|
|
191
|
+
})
|
|
192
|
+
},
|
|
193
|
+
listMessageHistoryPageEffect(params: { threadId: RecordIdRef; take: number; beforeMessageId?: string }) {
|
|
194
|
+
return service.listMessageHistoryPage(params)
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
listMessagesAfterCursor(threadId: RecordIdRef, afterMessageId?: string) {
|
|
198
|
+
const threadRef = toThreadRef(threadId)
|
|
199
|
+
const cursorMessageId = afterMessageId?.trim()
|
|
200
|
+
if (!cursorMessageId) {
|
|
201
|
+
return service.listMessages(threadRef)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return Effect.gen(function* () {
|
|
205
|
+
const cursorRow = yield* effectTryServicePromise(
|
|
206
|
+
() => db.findOne(TABLES.THREAD_MESSAGE, { threadId: threadRef, messageId: cursorMessageId }, CursorRowSchema),
|
|
207
|
+
`Failed to load cursor message "${cursorMessageId}".`,
|
|
208
|
+
)
|
|
209
|
+
if (!cursorRow) {
|
|
210
|
+
return yield* new ServiceError({ message: `Thread cursor message not found: ${cursorMessageId}` })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const cursorCreatedAt = cursorRow.createdAt
|
|
214
|
+
const cursorId = toThreadMessageRowId(threadRef, cursorMessageId)
|
|
215
|
+
const rows = yield* effectTryServicePromise(
|
|
216
|
+
() =>
|
|
217
|
+
db.query<unknown>(surql`
|
|
218
|
+
SELECT * FROM threadMessage
|
|
219
|
+
WHERE threadId = ${threadRef}
|
|
220
|
+
AND (
|
|
221
|
+
createdAt > ${cursorCreatedAt}
|
|
222
|
+
OR (createdAt = ${cursorCreatedAt} AND id > ${cursorId})
|
|
223
|
+
)
|
|
224
|
+
ORDER BY createdAt ASC, id ASC
|
|
225
|
+
`),
|
|
226
|
+
'Failed to list thread messages after cursor.',
|
|
227
|
+
)
|
|
228
|
+
const parsedRows = yield* Effect.forEach(rows, (row) =>
|
|
229
|
+
parseRowOrFail(ThreadMessageRowSchema, row, 'listMessagesAfterCursor'),
|
|
230
|
+
)
|
|
231
|
+
return parsedRows.map((row) => toChatMessage(row))
|
|
232
|
+
})
|
|
233
|
+
},
|
|
234
|
+
listMessagesAfterCursorEffect(threadId: RecordIdRef, afterMessageId?: string) {
|
|
235
|
+
return service.listMessagesAfterCursor(threadId, afterMessageId)
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
listRecentMessages(threadId: RecordIdRef, limit: number) {
|
|
239
|
+
const threadRef = toThreadRef(threadId)
|
|
240
|
+
return effectTryServicePromise(
|
|
241
|
+
() =>
|
|
242
|
+
db.query<unknown>(surql`
|
|
243
|
+
SELECT * FROM threadMessage
|
|
244
|
+
WHERE threadId = ${threadRef}
|
|
245
|
+
ORDER BY createdAt DESC, id DESC
|
|
246
|
+
LIMIT ${Math.max(1, limit)}
|
|
247
|
+
`),
|
|
248
|
+
'Failed to list recent thread messages.',
|
|
249
|
+
).pipe(
|
|
250
|
+
Effect.flatMap((rows) =>
|
|
251
|
+
Effect.forEach(rows, (row) => parseRowOrFail(ThreadMessageRowSchema, row, 'listRecentMessages')),
|
|
252
|
+
),
|
|
253
|
+
Effect.map((rows) => rows.reverse().map((row) => toChatMessage(row))),
|
|
254
|
+
)
|
|
255
|
+
},
|
|
256
|
+
listRecentMessagesEffect(threadId: RecordIdRef, limit: number) {
|
|
257
|
+
return service.listRecentMessages(threadId, limit)
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
searchMessages(params: { threadId: RecordIdRef; role: 'user' | 'assistant'; query: string; limit: number }) {
|
|
261
|
+
const normalizedQuery = params.query.trim().toLowerCase()
|
|
262
|
+
if (!normalizedQuery) return Effect.succeed([])
|
|
263
|
+
|
|
264
|
+
return Effect.gen(function* () {
|
|
265
|
+
const messages = yield* service.listMessages(toThreadRef(params.threadId))
|
|
266
|
+
return messages
|
|
267
|
+
.filter((message) => message.role === params.role)
|
|
268
|
+
.map((message) => ({
|
|
269
|
+
id: message.id,
|
|
270
|
+
role: message.role as 'user' | 'assistant',
|
|
271
|
+
createdAt: unsafeDateFrom(requireTimestamp(message.metadata?.createdAt)).toISOString(),
|
|
272
|
+
content: message.parts
|
|
273
|
+
.flatMap((part) => (part.type === 'text' && typeof part.text === 'string' ? [part.text] : []))
|
|
274
|
+
.join('\n')
|
|
275
|
+
.trim(),
|
|
276
|
+
}))
|
|
277
|
+
.filter((item) => item.content.length > 0 && item.content.toLowerCase().includes(normalizedQuery))
|
|
278
|
+
.slice(-Math.max(1, params.limit))
|
|
279
|
+
})
|
|
280
|
+
},
|
|
281
|
+
searchMessagesEffect(params: { threadId: RecordIdRef; role: 'user' | 'assistant'; query: string; limit: number }) {
|
|
282
|
+
return service.searchMessages(params)
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
addUserMessage(params: { messageId: RecordIdRef; threadId: RecordIdRef; content: string }) {
|
|
286
|
+
const threadRef = toThreadRef(params.threadId)
|
|
287
|
+
const message: ChatMessage = {
|
|
288
|
+
id: toMessageId(params.messageId),
|
|
289
|
+
role: 'user',
|
|
290
|
+
parts: [{ type: 'text', text: params.content }],
|
|
291
|
+
metadata: { createdAt: nowEpochMillis() },
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return Effect.gen(function* () {
|
|
295
|
+
yield* upsertMessages({ threadId: threadRef, messages: [message] })
|
|
296
|
+
return message
|
|
297
|
+
})
|
|
298
|
+
},
|
|
299
|
+
addUserMessageEffect(params: { messageId: RecordIdRef; threadId: RecordIdRef; content: string }) {
|
|
300
|
+
return service.addUserMessage(params)
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
addAgentMessage(params: {
|
|
304
|
+
messageId: RecordIdRef
|
|
305
|
+
threadId: RecordIdRef
|
|
306
|
+
parts: ChatMessage['parts']
|
|
307
|
+
metadata?: ChatMessage['metadata']
|
|
308
|
+
}) {
|
|
309
|
+
const threadRef = toThreadRef(params.threadId)
|
|
310
|
+
const message: ChatMessage = {
|
|
311
|
+
id: toMessageId(params.messageId),
|
|
312
|
+
role: 'assistant',
|
|
313
|
+
parts: params.parts,
|
|
314
|
+
metadata: withCreatedAtMetadata(params.metadata, nowEpochMillis()),
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return Effect.gen(function* () {
|
|
318
|
+
yield* upsertMessages({ threadId: threadRef, messages: [message] })
|
|
319
|
+
return message
|
|
320
|
+
})
|
|
321
|
+
},
|
|
322
|
+
addAgentMessageEffect(params: {
|
|
323
|
+
messageId: RecordIdRef
|
|
324
|
+
threadId: RecordIdRef
|
|
325
|
+
parts: ChatMessage['parts']
|
|
326
|
+
metadata?: ChatMessage['metadata']
|
|
327
|
+
}) {
|
|
328
|
+
return service.addAgentMessage(params)
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
ensureBootstrapWelcomeMessage(params: { threadId: RecordIdRef; agentId: string; text: string }) {
|
|
332
|
+
const threadRef = toThreadRef(params.threadId)
|
|
333
|
+
return Effect.gen(function* () {
|
|
334
|
+
const existingRow = yield* effectTryServicePromise(
|
|
335
|
+
() => db.findOne(TABLES.THREAD_MESSAGE, { threadId: threadRef }, ThreadMessageExistingRowSchema),
|
|
336
|
+
'Failed to check for existing bootstrap welcome message.',
|
|
337
|
+
)
|
|
338
|
+
if (existingRow) return
|
|
339
|
+
|
|
340
|
+
const messageText = params.text.trim()
|
|
341
|
+
if (!messageText) return
|
|
342
|
+
|
|
343
|
+
yield* upsertMessages({
|
|
344
|
+
threadId: threadRef,
|
|
345
|
+
messages: [
|
|
346
|
+
{
|
|
347
|
+
id: Bun.randomUUIDv7(),
|
|
348
|
+
role: 'assistant',
|
|
349
|
+
parts: [{ type: 'text', text: messageText }],
|
|
350
|
+
metadata: {
|
|
351
|
+
agentId: params.agentId,
|
|
352
|
+
agentName: getAgentDisplayNames()[params.agentId] ?? params.agentId,
|
|
353
|
+
createdAt: nowEpochMillis(),
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
},
|
|
360
|
+
ensureBootstrapWelcomeMessageEffect(params: { threadId: RecordIdRef; agentId: string; text: string }) {
|
|
361
|
+
return service.ensureBootstrapWelcomeMessage(params)
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return service
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export class ThreadMessageServiceTag extends Context.Service<
|
|
369
|
+
ThreadMessageServiceTag,
|
|
370
|
+
ReturnType<typeof makeThreadMessageService>
|
|
371
|
+
>()('@lota-sdk/core/ThreadMessageService') {}
|
|
372
|
+
|
|
373
|
+
export const ThreadMessageServiceLive = Layer.effect(
|
|
374
|
+
ThreadMessageServiceTag,
|
|
375
|
+
Effect.gen(function* () {
|
|
376
|
+
const db = yield* DatabaseServiceTag
|
|
377
|
+
return makeThreadMessageService(db)
|
|
378
|
+
}),
|
|
379
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Duration, Effect, Schedule } from 'effect'
|
|
2
|
+
|
|
3
|
+
import type { RecordIdInput, RecordIdRef } from '../../db/record-id'
|
|
4
|
+
import { ensureRecordId, isRecordIdInput, recordIdToString } from '../../db/record-id'
|
|
5
|
+
import type { SurrealDBService } from '../../db/service'
|
|
6
|
+
import { TABLES } from '../../db/tables'
|
|
7
|
+
import { BadRequestError, DatabaseError, NotFoundError } from '../../effect/errors'
|
|
8
|
+
import { ThreadSchema } from './thread.types'
|
|
9
|
+
import type { ThreadRecord } from './thread.types'
|
|
10
|
+
|
|
11
|
+
const THREAD_UNIQUE_LOOKUP_MAX_ATTEMPTS = 20
|
|
12
|
+
const THREAD_UNIQUE_LOOKUP_RETRY_DELAY_MS = 100
|
|
13
|
+
|
|
14
|
+
function requireThreadIdEffect(id: unknown): Effect.Effect<RecordIdInput, BadRequestError> {
|
|
15
|
+
return isRecordIdInput(id)
|
|
16
|
+
? Effect.succeed(id)
|
|
17
|
+
: Effect.fail(new BadRequestError({ message: `Invalid record id for table ${TABLES.THREAD}` }))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type UniqueThreadLookupParams = {
|
|
21
|
+
type: 'default' | 'thread'
|
|
22
|
+
organizationId: RecordIdRef
|
|
23
|
+
userId: RecordIdRef
|
|
24
|
+
agentId?: string
|
|
25
|
+
threadType?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildExistingThreadLookupMessage(params: UniqueThreadLookupParams): string {
|
|
29
|
+
const scope = {
|
|
30
|
+
organizationId: recordIdToString(params.organizationId, TABLES.ORGANIZATION),
|
|
31
|
+
userId: recordIdToString(params.userId, TABLES.USER),
|
|
32
|
+
...(params.agentId ? { agentId: params.agentId } : {}),
|
|
33
|
+
...(params.threadType ? { threadType: params.threadType } : {}),
|
|
34
|
+
}
|
|
35
|
+
return `Thread lookup failed after duplicate ${params.type} thread create: ${JSON.stringify(scope)}`
|
|
36
|
+
}
|
|
37
|
+
type ThreadOrderKey = Extract<keyof ThreadRecord, string>
|
|
38
|
+
|
|
39
|
+
function mapThreadStoreError(message: string) {
|
|
40
|
+
return Effect.mapError((cause: unknown) => new DatabaseError({ message, cause }))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ThreadRecordStore {
|
|
44
|
+
findById(id: unknown): Effect.Effect<ThreadRecord | null, DatabaseError | BadRequestError>
|
|
45
|
+
getById(id: unknown): Effect.Effect<ThreadRecord, DatabaseError | BadRequestError | NotFoundError>
|
|
46
|
+
findAll(
|
|
47
|
+
filter?: Record<string, unknown>,
|
|
48
|
+
options?: { limit?: number; offset?: number; orderBy?: ThreadOrderKey; orderDir?: 'ASC' | 'DESC' },
|
|
49
|
+
): Effect.Effect<ThreadRecord[], DatabaseError>
|
|
50
|
+
create(data: Record<string, unknown>): Effect.Effect<ThreadRecord, DatabaseError>
|
|
51
|
+
update(id: unknown, data: Record<string, unknown>): Effect.Effect<ThreadRecord, DatabaseError | NotFoundError>
|
|
52
|
+
deleteById(id: unknown): Effect.Effect<void, DatabaseError | NotFoundError>
|
|
53
|
+
findThreadByUniqueLookup(params: UniqueThreadLookupParams): Effect.Effect<ThreadRecord | null, DatabaseError>
|
|
54
|
+
waitForExistingThread(params: UniqueThreadLookupParams): Effect.Effect<ThreadRecord, NotFoundError | DatabaseError>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createThreadRecordStore(deps: { db: SurrealDBService }): ThreadRecordStore {
|
|
58
|
+
const { db } = deps
|
|
59
|
+
function findById(id: unknown): Effect.Effect<ThreadRecord | null, DatabaseError | BadRequestError> {
|
|
60
|
+
return Effect.gen(function* () {
|
|
61
|
+
const threadId = yield* requireThreadIdEffect(id)
|
|
62
|
+
return yield* db
|
|
63
|
+
.findOne(TABLES.THREAD, { id: ensureRecordId(threadId, TABLES.THREAD) }, ThreadSchema)
|
|
64
|
+
.pipe(mapThreadStoreError('Failed to load thread record.'))
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getById(id: unknown): Effect.Effect<ThreadRecord, DatabaseError | BadRequestError | NotFoundError> {
|
|
69
|
+
return Effect.gen(function* () {
|
|
70
|
+
const record = yield* findById(id)
|
|
71
|
+
if (!record) {
|
|
72
|
+
return yield* new NotFoundError({ resource: TABLES.THREAD, message: `${TABLES.THREAD} record not found` })
|
|
73
|
+
}
|
|
74
|
+
return record
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function findAll(
|
|
79
|
+
filter: Record<string, unknown> = {},
|
|
80
|
+
options?: { limit?: number; offset?: number; orderBy?: ThreadOrderKey; orderDir?: 'ASC' | 'DESC' },
|
|
81
|
+
): Effect.Effect<ThreadRecord[], DatabaseError> {
|
|
82
|
+
return db
|
|
83
|
+
.findMany(TABLES.THREAD, filter, ThreadSchema, options)
|
|
84
|
+
.pipe(mapThreadStoreError('Failed to list threads.'))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function create(data: Record<string, unknown>): Effect.Effect<ThreadRecord, DatabaseError> {
|
|
88
|
+
return db.create(TABLES.THREAD, data, ThreadSchema).pipe(mapThreadStoreError('Failed to create thread record.'))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function update(
|
|
92
|
+
id: unknown,
|
|
93
|
+
data: Record<string, unknown>,
|
|
94
|
+
): Effect.Effect<ThreadRecord, DatabaseError | NotFoundError> {
|
|
95
|
+
return Effect.gen(function* () {
|
|
96
|
+
const record = yield* db
|
|
97
|
+
.update(TABLES.THREAD, id, data, ThreadSchema)
|
|
98
|
+
.pipe(mapThreadStoreError('Failed to update thread record.'))
|
|
99
|
+
if (!record) {
|
|
100
|
+
return yield* new NotFoundError({ resource: TABLES.THREAD, message: `${TABLES.THREAD} record not found` })
|
|
101
|
+
}
|
|
102
|
+
return record
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function deleteById(id: unknown): Effect.Effect<void, DatabaseError | NotFoundError> {
|
|
107
|
+
return Effect.gen(function* () {
|
|
108
|
+
const deleted = yield* db
|
|
109
|
+
.deleteById(TABLES.THREAD, id)
|
|
110
|
+
.pipe(mapThreadStoreError('Failed to delete thread record.'))
|
|
111
|
+
if (!deleted) {
|
|
112
|
+
return yield* new NotFoundError({ resource: TABLES.THREAD, message: `${TABLES.THREAD} record not found` })
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function findThreadByUniqueLookup(
|
|
118
|
+
params: UniqueThreadLookupParams,
|
|
119
|
+
): Effect.Effect<ThreadRecord | null, DatabaseError> {
|
|
120
|
+
return db
|
|
121
|
+
.findOne(
|
|
122
|
+
TABLES.THREAD,
|
|
123
|
+
{
|
|
124
|
+
type: params.type,
|
|
125
|
+
organizationId: params.organizationId,
|
|
126
|
+
userId: params.userId,
|
|
127
|
+
...(params.agentId ? { agentId: params.agentId } : {}),
|
|
128
|
+
...(params.threadType ? { threadType: params.threadType } : {}),
|
|
129
|
+
},
|
|
130
|
+
ThreadSchema,
|
|
131
|
+
)
|
|
132
|
+
.pipe(mapThreadStoreError('Failed to find thread by unique lookup.'))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function waitForExistingThread(
|
|
136
|
+
params: UniqueThreadLookupParams,
|
|
137
|
+
): Effect.Effect<ThreadRecord, NotFoundError | DatabaseError> {
|
|
138
|
+
const poll = findThreadByUniqueLookup(params).pipe(
|
|
139
|
+
Effect.flatMap((result) => (result ? Effect.succeed(result) : Effect.fail('not_found' as const))),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return Effect.retry(poll, {
|
|
143
|
+
times: THREAD_UNIQUE_LOOKUP_MAX_ATTEMPTS - 1,
|
|
144
|
+
schedule: Schedule.fixed(Duration.millis(THREAD_UNIQUE_LOOKUP_RETRY_DELAY_MS)),
|
|
145
|
+
}).pipe(
|
|
146
|
+
Effect.mapError((error) =>
|
|
147
|
+
error === 'not_found'
|
|
148
|
+
? new NotFoundError({ resource: TABLES.THREAD, message: buildExistingThreadLookupMessage(params) })
|
|
149
|
+
: error,
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { findById, getById, findAll, create, update, deleteById, findThreadByUniqueLookup, waitForExistingThread }
|
|
155
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { THREAD } from '@lota-sdk/shared'
|
|
2
|
+
import { Context, Schema, Effect, Layer } from 'effect'
|
|
3
|
+
|
|
4
|
+
import { chatLogger } from '../../config/logger'
|
|
5
|
+
import type { RecordIdRef } from '../../db/record-id'
|
|
6
|
+
import type { HelperModelRuntime } from '../../runtime/helper-model'
|
|
7
|
+
import { HelperModelTag } from '../../runtime/helper-model'
|
|
8
|
+
import { deriveTitle, limitTitleWords, normalizeTitle } from '../../runtime/title-helpers'
|
|
9
|
+
import {
|
|
10
|
+
createThreadTitleGeneratorAgent,
|
|
11
|
+
THREAD_TITLE_GENERATOR_PROMPT,
|
|
12
|
+
} from '../../system-agents/title-generator.agent'
|
|
13
|
+
import type { makeThreadService } from './thread.service'
|
|
14
|
+
import { ThreadServiceTag } from './thread.service'
|
|
15
|
+
|
|
16
|
+
const THREAD_TITLE_TIMEOUT_MS = 30_000
|
|
17
|
+
|
|
18
|
+
class ThreadTitleError extends Schema.TaggedErrorClass<ThreadTitleError>()('ThreadTitleError', {
|
|
19
|
+
message: Schema.String,
|
|
20
|
+
cause: Schema.optional(Schema.Defect),
|
|
21
|
+
}) {}
|
|
22
|
+
|
|
23
|
+
export function makeThreadTitleService(
|
|
24
|
+
threadService: ReturnType<typeof makeThreadService>,
|
|
25
|
+
helperModelRuntime: HelperModelRuntime,
|
|
26
|
+
) {
|
|
27
|
+
return {
|
|
28
|
+
generateAndPersistTitle(threadId: RecordIdRef, sourceText: string) {
|
|
29
|
+
return Effect.gen(function* () {
|
|
30
|
+
const generatedTitle = yield* Effect.tryPromise({
|
|
31
|
+
try: () =>
|
|
32
|
+
helperModelRuntime.generateHelperText({
|
|
33
|
+
tag: 'thread-title',
|
|
34
|
+
createAgent: createThreadTitleGeneratorAgent,
|
|
35
|
+
defaultSystemPrompt: THREAD_TITLE_GENERATOR_PROMPT,
|
|
36
|
+
timeoutMs: THREAD_TITLE_TIMEOUT_MS,
|
|
37
|
+
messages: [{ role: 'user', content: sourceText }],
|
|
38
|
+
}),
|
|
39
|
+
catch: (error) => new ThreadTitleError({ message: 'Failed to generate thread title.', cause: error }),
|
|
40
|
+
}).pipe(
|
|
41
|
+
Effect.map((title) => normalizeTitle(title)),
|
|
42
|
+
Effect.catch((error) =>
|
|
43
|
+
Effect.sync(() => {
|
|
44
|
+
chatLogger.warn`Failed to generate thread title via LLM (non-fatal): ${error}`
|
|
45
|
+
return ''
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
const title = generatedTitle || limitTitleWords(deriveTitle(sourceText || THREAD.DEFAULT_TITLE))
|
|
50
|
+
yield* threadService
|
|
51
|
+
.update(threadId, { title, nameGenerated: true })
|
|
52
|
+
.pipe(
|
|
53
|
+
Effect.mapError(
|
|
54
|
+
(error) => new ThreadTitleError({ message: 'Failed to persist generated thread title.', cause: error }),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class ThreadTitleServiceTag extends Context.Service<
|
|
63
|
+
ThreadTitleServiceTag,
|
|
64
|
+
ReturnType<typeof makeThreadTitleService>
|
|
65
|
+
>()('@lota-sdk/core/ThreadTitleService') {}
|
|
66
|
+
|
|
67
|
+
export const ThreadTitleServiceLive = Layer.effect(
|
|
68
|
+
ThreadTitleServiceTag,
|
|
69
|
+
Effect.gen(function* () {
|
|
70
|
+
const threadService = yield* ThreadServiceTag
|
|
71
|
+
const helperModelRuntime = yield* HelperModelTag
|
|
72
|
+
return makeThreadTitleService(threadService, helperModelRuntime)
|
|
73
|
+
}),
|
|
74
|
+
)
|