@lota-sdk/core 0.4.7 → 0.4.9
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 +94 -22
- package/src/ai-gateway/ai-gateway.ts +738 -223
- package/src/config/agent-defaults.ts +176 -75
- package/src/config/agent-types.ts +54 -4
- package/src/config/constants.ts +8 -2
- package/src/config/logger.ts +286 -19
- package/src/config/model-constants.ts +1 -0
- package/src/config/thread-defaults.ts +33 -21
- package/src/create-runtime.ts +725 -383
- package/src/db/base.service.ts +52 -28
- package/src/db/cursor-pagination.ts +71 -30
- package/src/db/memory-store.helpers.ts +4 -7
- package/src/db/memory-store.ts +856 -598
- package/src/db/memory.ts +398 -275
- package/src/db/record-id.ts +32 -10
- package/src/db/schema-fingerprint.ts +30 -12
- package/src/db/service-normalization.ts +255 -0
- package/src/db/service.ts +726 -761
- package/src/db/startup.ts +140 -66
- package/src/db/transaction-conflict.ts +15 -0
- package/src/effect/awaitable-effect.ts +87 -0
- package/src/effect/errors.ts +121 -0
- package/src/effect/helpers.ts +98 -0
- package/src/effect/index.ts +22 -0
- package/src/effect/layers.ts +228 -0
- package/src/effect/runtime-ref.ts +25 -0
- package/src/effect/runtime.ts +31 -0
- package/src/effect/services.ts +57 -0
- package/src/effect/zod.ts +43 -0
- package/src/embeddings/provider.ts +122 -71
- package/src/index.ts +46 -1
- package/src/openrouter/direct-provider.ts +29 -0
- package/src/queues/autonomous-job.queue.ts +130 -74
- package/src/queues/context-compaction.queue.ts +60 -15
- package/src/queues/delayed-node-promotion.queue.ts +52 -15
- 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 +13 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
- package/src/queues/plan-scheduler.queue.ts +107 -31
- package/src/queues/post-chat-memory.queue.ts +66 -24
- package/src/queues/queue-factory.ts +142 -52
- package/src/queues/standalone-worker.ts +39 -0
- package/src/queues/title-generation.queue.ts +54 -9
- package/src/redis/connection.ts +84 -32
- 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 +10 -0
- package/src/redis/stream-context.ts +84 -46
- package/src/runtime/agent-identity-overrides.ts +2 -2
- package/src/runtime/agent-runtime-policy.ts +4 -1
- package/src/runtime/agent-stream-helpers.ts +20 -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} +114 -91
- package/src/runtime/execution-plan-visibility.ts +2 -2
- package/src/runtime/execution-plan.ts +42 -15
- package/src/runtime/graph-designer.ts +11 -7
- package/src/runtime/helper-model.ts +135 -48
- package/src/runtime/index.ts +7 -7
- package/src/runtime/indexed-repositories-policy.ts +3 -3
- package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -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} +1 -1
- package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
- package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
- package/src/runtime/plugin-resolution.ts +144 -24
- package/src/runtime/plugin-types.ts +9 -1
- package/src/runtime/post-turn-side-effects.ts +197 -130
- package/src/runtime/retrieval-adapters.ts +38 -4
- package/src/runtime/runtime-config.ts +165 -61
- package/src/runtime/runtime-extensions.ts +21 -34
- package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
- package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
- package/src/runtime/social-chat/social-chat.ts +594 -0
- package/src/runtime/specialist-runner.ts +36 -10
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -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 +172 -94
- package/src/runtime/turn-lifecycle.ts +93 -27
- package/src/services/agent-activity.service.ts +287 -203
- package/src/services/agent-executor.service.ts +329 -217
- package/src/services/artifact.service.ts +225 -148
- package/src/services/attachment.service.ts +137 -115
- package/src/services/autonomous-job.service.ts +888 -491
- package/src/services/chat-run-registry.service.ts +11 -1
- package/src/services/context-compaction.service.ts +136 -86
- package/src/services/document-chunk.service.ts +162 -90
- 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 +256 -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 +80 -170
- package/src/services/graph-full-routing.ts +182 -0
- package/src/services/index.ts +18 -20
- package/src/services/institutional-memory.service.ts +220 -123
- package/src/services/learned-skill.service.ts +364 -259
- package/src/services/memory/memory-conversation.ts +95 -0
- package/src/services/memory/memory-org-memory.ts +39 -0
- package/src/services/memory/memory-preseeded.ts +80 -0
- package/src/services/memory/memory-rerank.ts +297 -0
- package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
- package/src/services/memory/memory.service.ts +692 -0
- package/src/services/memory/rerank.service.ts +209 -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 +17 -16
- package/src/services/organization-member.service.ts +120 -66
- package/src/services/organization.service.ts +144 -51
- package/src/services/ownership-dispatcher.service.ts +415 -264
- 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/plan-approval.service.ts +102 -0
- 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 +175 -0
- package/src/services/plan/plan-coordination.service.ts +181 -0
- package/src/services/plan/plan-cycle.service.ts +398 -0
- package/src/services/plan/plan-deadline.service.ts +547 -0
- package/src/services/plan/plan-event-delivery.service.ts +261 -0
- package/src/services/plan/plan-executor-context.ts +35 -0
- package/src/services/plan/plan-executor-graph.ts +475 -0
- package/src/services/plan/plan-executor-helpers.ts +322 -0
- package/src/services/plan/plan-executor-persistence.ts +209 -0
- package/src/services/plan/plan-executor.service.ts +1654 -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 +644 -0
- package/src/services/plan/plan-scheduler.service.ts +385 -0
- package/src/services/plan/plan-template.service.ts +224 -0
- package/src/services/plan/plan-transaction-events.ts +33 -0
- package/src/services/plan/plan-validator.service.ts +907 -0
- package/src/services/plan/plan-workspace.service.ts +125 -0
- package/src/services/plugin-executor.service.ts +97 -68
- package/src/services/quality-metrics.service.ts +112 -94
- package/src/services/queue-job.service.ts +296 -230
- package/src/services/recent-activity-title.service.ts +65 -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 +176 -125
- package/src/services/system-executor.service.ts +91 -61
- package/src/services/thread/thread-active-run.ts +203 -0
- package/src/services/thread/thread-bootstrap.ts +369 -0
- package/src/services/thread/thread-listing.ts +198 -0
- package/src/services/thread/thread-memory-block.ts +117 -0
- package/src/services/thread/thread-message.service.ts +363 -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 +1146 -0
- package/src/services/thread/thread-turn-streaming.ts +402 -0
- package/src/services/thread/thread-turn-tracing.ts +35 -0
- package/src/services/thread/thread-turn.ts +343 -0
- package/src/services/thread/thread.service.ts +335 -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 +331 -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 +2 -2
- package/src/system-agents/delegated-agent-factory.ts +159 -90
- package/src/system-agents/memory-reranker.agent.ts +2 -2
- package/src/system-agents/memory.agent.ts +2 -2
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
- package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
- package/src/system-agents/skill-extractor.agent.ts +2 -2
- package/src/system-agents/skill-manager.agent.ts +2 -2
- package/src/system-agents/thread-router.agent.ts +157 -113
- package/src/system-agents/title-generator.agent.ts +2 -2
- package/src/tools/execution-plan.tool.ts +220 -161
- package/src/tools/fetch-webpage.tool.ts +21 -17
- package/src/tools/firecrawl-client.ts +16 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-block.tool.ts +14 -6
- package/src/tools/plan-approval.tool.ts +49 -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 +26 -22
- package/src/tools/search.tool.ts +41 -29
- package/src/tools/team-think.tool.ts +124 -83
- package/src/tools/user-questions.tool.ts +4 -3
- package/src/tools/web-tool-shared.ts +6 -0
- package/src/utils/async.ts +17 -23
- package/src/utils/crypto.ts +21 -0
- package/src/utils/date-time.ts +40 -1
- package/src/utils/errors.ts +95 -16
- 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 +186 -51
- 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 +175 -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 +56 -31
- package/src/config/debug-logger.ts +0 -47
- package/src/redis/connection-accessor.ts +0 -26
- package/src/runtime/context-compaction-runtime.ts +0 -87
- 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 -844
- package/src/services/plan-agent-heartbeat.service.ts +0 -136
- package/src/services/plan-agent-query.service.ts +0 -267
- package/src/services/plan-approval.service.ts +0 -83
- 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/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,198 @@
|
|
|
1
|
+
import { THREAD } from '@lota-sdk/shared'
|
|
2
|
+
import { Schema, Effect } from 'effect'
|
|
3
|
+
import { BoundQuery } from 'surrealdb'
|
|
4
|
+
|
|
5
|
+
import type { RecordIdRef } from '../../db/record-id'
|
|
6
|
+
import type { SurrealDBService } from '../../db/service'
|
|
7
|
+
import { TABLES } from '../../db/tables'
|
|
8
|
+
import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
|
|
9
|
+
import { ThreadSchema } from './thread.types'
|
|
10
|
+
import type { NormalizedThread, ThreadRecord } from './thread.types'
|
|
11
|
+
|
|
12
|
+
function buildListThreadsQuery(options: {
|
|
13
|
+
includeArchived: boolean
|
|
14
|
+
paginate: boolean
|
|
15
|
+
type?: string
|
|
16
|
+
types?: string[]
|
|
17
|
+
}): string {
|
|
18
|
+
const clauses = [`SELECT * FROM ${TABLES.THREAD}`, 'WHERE userId = $userId', ' AND organizationId = $orgId']
|
|
19
|
+
|
|
20
|
+
if (options.types) {
|
|
21
|
+
clauses.push(' AND type IN $types')
|
|
22
|
+
} else if (options.type) {
|
|
23
|
+
clauses.push(' AND type = $type')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!options.includeArchived) {
|
|
27
|
+
clauses.push(' AND status = "active"')
|
|
28
|
+
}
|
|
29
|
+
clauses.push('ORDER BY updatedAt DESC')
|
|
30
|
+
if (options.paginate) {
|
|
31
|
+
clauses.push('LIMIT $limit START $offset')
|
|
32
|
+
}
|
|
33
|
+
return clauses.join('\n')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createThreadListingHelpers(deps: {
|
|
37
|
+
db: SurrealDBService
|
|
38
|
+
normalizeThreads(
|
|
39
|
+
threads: ThreadRecord[],
|
|
40
|
+
options?: { checkLease?: boolean },
|
|
41
|
+
): Effect.Effect<NormalizedThread[], unknown>
|
|
42
|
+
}) {
|
|
43
|
+
class ThreadListingError extends Schema.TaggedErrorClass<ThreadListingError>()('ThreadListingError', {
|
|
44
|
+
message: Schema.String,
|
|
45
|
+
cause: Schema.optional(Schema.Defect),
|
|
46
|
+
}) {}
|
|
47
|
+
|
|
48
|
+
const effectTryPromise = makeEffectTryPromiseWithMessage(
|
|
49
|
+
(message, cause) => new ThreadListingError({ message, cause }),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
function listThreads(
|
|
53
|
+
userId: RecordIdRef,
|
|
54
|
+
orgId: RecordIdRef,
|
|
55
|
+
options: { type?: string; types?: string[]; take?: number; page?: number; includeArchived?: boolean },
|
|
56
|
+
): Effect.Effect<{ threads: NormalizedThread[]; hasMore: boolean }, ThreadListingError> {
|
|
57
|
+
const includeArchived = options.includeArchived ?? false
|
|
58
|
+
const type = options.type
|
|
59
|
+
const types = options.types
|
|
60
|
+
|
|
61
|
+
if (type === 'default' || type === 'thread') {
|
|
62
|
+
const vars: Record<string, unknown> = { userId, orgId, type }
|
|
63
|
+
return Effect.gen(function* () {
|
|
64
|
+
const threads = yield* effectTryPromise(
|
|
65
|
+
() =>
|
|
66
|
+
deps.db.queryMany(
|
|
67
|
+
new BoundQuery(buildListThreadsQuery({ includeArchived, paginate: false, type }), vars),
|
|
68
|
+
ThreadSchema,
|
|
69
|
+
),
|
|
70
|
+
'Failed to list threads.',
|
|
71
|
+
)
|
|
72
|
+
const normalizedThreads = yield* effectTryPromise(
|
|
73
|
+
() => deps.normalizeThreads(threads, { checkLease: false }),
|
|
74
|
+
'Failed to normalize listed threads.',
|
|
75
|
+
)
|
|
76
|
+
return { threads: normalizedThreads, hasMore: false }
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const take = options.take ?? THREAD.DEFAULT_PAGE_LIMIT
|
|
81
|
+
const page = options.page ?? 1
|
|
82
|
+
const vars: Record<string, unknown> = { userId, orgId, limit: take + 1, offset: (page - 1) * take }
|
|
83
|
+
|
|
84
|
+
if (types) {
|
|
85
|
+
vars.types = types
|
|
86
|
+
} else if (type) {
|
|
87
|
+
vars.type = type
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Effect.gen(function* () {
|
|
91
|
+
const threads = yield* effectTryPromise(
|
|
92
|
+
() =>
|
|
93
|
+
deps.db.queryMany(
|
|
94
|
+
new BoundQuery(buildListThreadsQuery({ includeArchived, paginate: true, type, types }), vars),
|
|
95
|
+
ThreadSchema,
|
|
96
|
+
),
|
|
97
|
+
'Failed to list paginated threads.',
|
|
98
|
+
)
|
|
99
|
+
const hasMore = threads.length > take
|
|
100
|
+
const sliced = hasMore ? threads.slice(0, take) : threads
|
|
101
|
+
const normalizedThreads = yield* effectTryPromise(
|
|
102
|
+
() => deps.normalizeThreads(sliced, { checkLease: false }),
|
|
103
|
+
'Failed to normalize paginated threads.',
|
|
104
|
+
)
|
|
105
|
+
return { threads: normalizedThreads, hasMore }
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function listOrganizationThreads(params: {
|
|
110
|
+
orgId: RecordIdRef
|
|
111
|
+
type?: string
|
|
112
|
+
agentId?: string
|
|
113
|
+
includeArchived?: boolean
|
|
114
|
+
}): Effect.Effect<NormalizedThread[], ThreadListingError> {
|
|
115
|
+
const whereClauses = ['organizationId = $orgId']
|
|
116
|
+
const variables: Record<string, unknown> = { orgId: params.orgId }
|
|
117
|
+
|
|
118
|
+
if (params.type) {
|
|
119
|
+
whereClauses.push('type = $type')
|
|
120
|
+
variables.type = params.type
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (params.agentId) {
|
|
124
|
+
whereClauses.push('agentId = $agentId')
|
|
125
|
+
variables.agentId = params.agentId
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (params.includeArchived !== true) {
|
|
129
|
+
whereClauses.push('status = "active"')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Effect.gen(function* () {
|
|
133
|
+
const threads = yield* effectTryPromise(
|
|
134
|
+
() =>
|
|
135
|
+
deps.db.queryMany(
|
|
136
|
+
new BoundQuery(
|
|
137
|
+
`SELECT * FROM ${TABLES.THREAD}
|
|
138
|
+
WHERE ${whereClauses.join('\n AND ')}
|
|
139
|
+
ORDER BY createdAt ASC, id ASC`,
|
|
140
|
+
variables,
|
|
141
|
+
),
|
|
142
|
+
ThreadSchema,
|
|
143
|
+
),
|
|
144
|
+
'Failed to list organization threads.',
|
|
145
|
+
)
|
|
146
|
+
return yield* effectTryPromise(
|
|
147
|
+
() => deps.normalizeThreads(threads, { checkLease: false }),
|
|
148
|
+
'Failed to normalize organization threads.',
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function listRecentThreads({
|
|
154
|
+
userId,
|
|
155
|
+
orgId,
|
|
156
|
+
excludeThreadId,
|
|
157
|
+
limit,
|
|
158
|
+
}: {
|
|
159
|
+
userId: RecordIdRef
|
|
160
|
+
orgId: RecordIdRef
|
|
161
|
+
excludeThreadId?: RecordIdRef
|
|
162
|
+
limit: number
|
|
163
|
+
}): Effect.Effect<NormalizedThread[], ThreadListingError> {
|
|
164
|
+
let excludeCondition = ''
|
|
165
|
+
const vars: Record<string, unknown> = { userId, orgId, limit }
|
|
166
|
+
|
|
167
|
+
if (excludeThreadId) {
|
|
168
|
+
excludeCondition = 'AND id != $excludeThreadId'
|
|
169
|
+
vars.excludeThreadId = excludeThreadId
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return Effect.gen(function* () {
|
|
173
|
+
const threads = yield* effectTryPromise(
|
|
174
|
+
() =>
|
|
175
|
+
deps.db.queryMany(
|
|
176
|
+
new BoundQuery(
|
|
177
|
+
`SELECT * FROM ${TABLES.THREAD}
|
|
178
|
+
WHERE userId = $userId
|
|
179
|
+
AND organizationId = $orgId
|
|
180
|
+
${excludeCondition}
|
|
181
|
+
AND status != "archived"
|
|
182
|
+
ORDER BY updatedAt DESC
|
|
183
|
+
LIMIT $limit`,
|
|
184
|
+
vars,
|
|
185
|
+
),
|
|
186
|
+
ThreadSchema,
|
|
187
|
+
),
|
|
188
|
+
'Failed to list recent threads.',
|
|
189
|
+
)
|
|
190
|
+
return yield* effectTryPromise(
|
|
191
|
+
() => deps.normalizeThreads(threads, { checkLease: false }),
|
|
192
|
+
'Failed to normalize recent threads.',
|
|
193
|
+
)
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { listThreads, listOrganizationThreads, listRecentThreads }
|
|
198
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Schema, Effect } from 'effect'
|
|
2
|
+
|
|
3
|
+
import { serverLogger } from '../../config/logger'
|
|
4
|
+
import type { RecordIdRef } from '../../db/record-id'
|
|
5
|
+
import { ensureRecordId, recordIdToString } from '../../db/record-id'
|
|
6
|
+
import { TABLES } from '../../db/tables'
|
|
7
|
+
import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
|
|
8
|
+
import {
|
|
9
|
+
appendToMemoryBlock,
|
|
10
|
+
compactMemoryBlockEntries,
|
|
11
|
+
formatPersistedMemoryBlockForPrompt,
|
|
12
|
+
parseMemoryBlock,
|
|
13
|
+
serializeMemoryBlock,
|
|
14
|
+
} from '../../runtime/memory/memory-block'
|
|
15
|
+
import { MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES, MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES } from './thread-constants'
|
|
16
|
+
import type { ThreadRecordStore } from './thread-record-store'
|
|
17
|
+
import type { ThreadRecord } from './thread.types'
|
|
18
|
+
|
|
19
|
+
type ContextCompactionServiceLike = {
|
|
20
|
+
compactMemoryBlock(params: { previousSummary: string; newEntriesText: string }): Effect.Effect<string, unknown>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class ThreadMemoryBlockError extends Schema.TaggedErrorClass<ThreadMemoryBlockError>()('ThreadMemoryBlockError', {
|
|
24
|
+
message: Schema.String,
|
|
25
|
+
cause: Schema.optional(Schema.Defect),
|
|
26
|
+
}) {}
|
|
27
|
+
|
|
28
|
+
const tryThreadMemoryBlockEffect = makeEffectTryPromiseWithMessage(
|
|
29
|
+
(message, cause) => new ThreadMemoryBlockError({ message, cause }),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
export function formatMemoryBlockForPrompt(thread: Pick<ThreadRecord, 'memoryBlock' | 'memoryBlockSummary'>): string {
|
|
33
|
+
return formatPersistedMemoryBlockForPrompt({
|
|
34
|
+
summary: thread.memoryBlockSummary,
|
|
35
|
+
entries: parseMemoryBlock(thread.memoryBlock),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createThreadMemoryBlockHelpers(deps: {
|
|
40
|
+
threadStore: ThreadRecordStore
|
|
41
|
+
contextCompactionService: ContextCompactionServiceLike
|
|
42
|
+
}) {
|
|
43
|
+
function appendMemoryBlock(threadId: RecordIdRef, entry: string): Effect.Effect<string, ThreadMemoryBlockError> {
|
|
44
|
+
return Effect.gen(function* () {
|
|
45
|
+
const threadRef = ensureRecordId(threadId, TABLES.THREAD)
|
|
46
|
+
const threadIdString = recordIdToString(threadRef, TABLES.THREAD)
|
|
47
|
+
const thread = yield* tryThreadMemoryBlockEffect(
|
|
48
|
+
() => deps.threadStore.getById(threadRef),
|
|
49
|
+
`Failed to load thread ${threadIdString} for memory block append`,
|
|
50
|
+
)
|
|
51
|
+
const entries = parseMemoryBlock(thread.memoryBlock)
|
|
52
|
+
|
|
53
|
+
const labelMatch = entry.match(/^(\w+):\s*/i)
|
|
54
|
+
const role = labelMatch ? labelMatch[1].toLowerCase() : 'system'
|
|
55
|
+
const content = labelMatch ? entry.slice(labelMatch[0].length).trim() : entry.trim()
|
|
56
|
+
|
|
57
|
+
const updatedEntries = appendToMemoryBlock(entries, role, content)
|
|
58
|
+
const serialized = serializeMemoryBlock(updatedEntries)
|
|
59
|
+
|
|
60
|
+
yield* tryThreadMemoryBlockEffect(
|
|
61
|
+
() => deps.threadStore.update(threadRef, { memoryBlock: serialized }),
|
|
62
|
+
`Failed to persist memory block for thread ${threadIdString}`,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if (updatedEntries.length >= MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES) {
|
|
66
|
+
yield* Effect.forkDetach(
|
|
67
|
+
compactMemoryBlock(threadRef).pipe(
|
|
68
|
+
Effect.catch((error: unknown) =>
|
|
69
|
+
Effect.sync(() => {
|
|
70
|
+
serverLogger.warn`Memory block compaction failed for ${threadIdString}: ${error}`
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return formatMemoryBlockForPrompt({ memoryBlock: serialized, memoryBlockSummary: thread.memoryBlockSummary })
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function compactMemoryBlock(threadId: RecordIdRef): Effect.Effect<boolean, ThreadMemoryBlockError> {
|
|
82
|
+
return Effect.gen(function* () {
|
|
83
|
+
const threadRef = ensureRecordId(threadId, TABLES.THREAD)
|
|
84
|
+
const threadIdString = recordIdToString(threadRef, TABLES.THREAD)
|
|
85
|
+
const thread = yield* tryThreadMemoryBlockEffect(
|
|
86
|
+
() => deps.threadStore.getById(threadRef),
|
|
87
|
+
`Failed to load thread ${threadIdString} for memory block compaction`,
|
|
88
|
+
)
|
|
89
|
+
const result = yield* tryThreadMemoryBlockEffect(
|
|
90
|
+
() =>
|
|
91
|
+
compactMemoryBlockEntries({
|
|
92
|
+
previousSummary: thread.memoryBlockSummary,
|
|
93
|
+
entries: parseMemoryBlock(thread.memoryBlock),
|
|
94
|
+
triggerEntries: MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES,
|
|
95
|
+
chunkEntries: MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES,
|
|
96
|
+
compact: (params) => deps.contextCompactionService.compactMemoryBlock(params),
|
|
97
|
+
}),
|
|
98
|
+
`Failed to compact memory block for thread ${threadIdString}`,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if (!result.compacted) return false
|
|
102
|
+
|
|
103
|
+
yield* tryThreadMemoryBlockEffect(
|
|
104
|
+
() =>
|
|
105
|
+
deps.threadStore.update(threadRef, {
|
|
106
|
+
memoryBlockSummary: result.summary || '',
|
|
107
|
+
memoryBlock: serializeMemoryBlock(result.entries),
|
|
108
|
+
}),
|
|
109
|
+
`Failed to persist compacted memory block for thread ${threadIdString}`,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return true
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { appendMemoryBlock, compactMemoryBlock }
|
|
117
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
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
|
+
|
|
7
|
+
import { getAgentDisplayNames } from '../../config/agent-defaults'
|
|
8
|
+
import { CursorRowSchema, listMessageHistoryPageEffect } from '../../db/cursor-pagination'
|
|
9
|
+
import type { CursorPaginationConfig } from '../../db/cursor-pagination'
|
|
10
|
+
import { ensureRecordId, recordIdToString } from '../../db/record-id'
|
|
11
|
+
import type { RecordIdRef } from '../../db/record-id'
|
|
12
|
+
import type { SurrealDBService } from '../../db/service'
|
|
13
|
+
import { TABLES } from '../../db/tables'
|
|
14
|
+
import { ThreadMessageRowSchema } from '../../db/thread-message-row'
|
|
15
|
+
import type { ThreadMessageRow } from '../../db/thread-message-row'
|
|
16
|
+
import { ServiceError } from '../../effect/errors'
|
|
17
|
+
import { effectTryServicePromise } from '../../effect/helpers'
|
|
18
|
+
import { DatabaseServiceTag } from '../../effect/services'
|
|
19
|
+
import { sha256Hex } from '../../utils/crypto'
|
|
20
|
+
import { nowEpochMillis, unsafeDateFrom } from '../../utils/date-time'
|
|
21
|
+
|
|
22
|
+
const ThreadMessageExistingRowSchema = z.object({ id: recordIdSchema, createdAt: z.coerce.date() })
|
|
23
|
+
|
|
24
|
+
function toMessageId(value: string | RecordIdRef): string {
|
|
25
|
+
return recordIdToString(value, TABLES.THREAD_MESSAGE)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toThreadMessageRowId(threadId: RecordIdRef, messageId: string): RecordId {
|
|
29
|
+
const threadStr = recordIdToString(threadId, TABLES.THREAD)
|
|
30
|
+
const digest = sha256Hex(`${threadStr}\0${messageId}`).slice(0, 32)
|
|
31
|
+
return new RecordId(TABLES.THREAD_MESSAGE, digest)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toThreadRef(threadId: RecordIdRef): RecordId {
|
|
35
|
+
return ensureRecordId(threadId, TABLES.THREAD)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeMessageValue(value: unknown): unknown {
|
|
39
|
+
if (value instanceof Date) {
|
|
40
|
+
return value.toISOString()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return value.map((item) => normalizeMessageValue(item))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (value && typeof value === 'object') {
|
|
48
|
+
return Object.fromEntries(
|
|
49
|
+
Object.entries(value as Record<string, unknown>).map(([key, entry]) => [key, normalizeMessageValue(entry)]),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return value
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readPersistedMessageParts(parts: ThreadMessageRow['parts']): ChatMessage['parts'] {
|
|
57
|
+
return (Array.isArray(parts) ? normalizeMessageValue(parts) : []) as ChatMessage['parts']
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toChatMessage(row: ThreadMessageRow): ChatMessage {
|
|
61
|
+
const rowCreatedAt = requireTimestamp(row.createdAt)
|
|
62
|
+
const metadata = withCreatedAtMetadata(parseRowMetadata(row.metadata), rowCreatedAt)
|
|
63
|
+
|
|
64
|
+
return { id: row.messageId, role: row.role, parts: readPersistedMessageParts(row.parts), metadata }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cloneMessageParts(parts: ChatMessage['parts'] | undefined): NonNullable<ThreadMessageRow['parts']> {
|
|
68
|
+
return Array.isArray(parts)
|
|
69
|
+
? (normalizeMessageValue(structuredClone(parts)) as NonNullable<ThreadMessageRow['parts']>)
|
|
70
|
+
: []
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const threadPaginationConfig: CursorPaginationConfig = {
|
|
74
|
+
table: TABLES.THREAD_MESSAGE,
|
|
75
|
+
parentFilterField: 'threadId',
|
|
76
|
+
toRowId: toThreadMessageRowId,
|
|
77
|
+
parseRow: (row: unknown) => ThreadMessageRowSchema.parse(row),
|
|
78
|
+
toMessage: (row: unknown) => toChatMessage(ThreadMessageRowSchema.parse(row)),
|
|
79
|
+
queryLatest: (parentId, limit) => surql`
|
|
80
|
+
SELECT * FROM threadMessage
|
|
81
|
+
WHERE threadId = ${parentId}
|
|
82
|
+
ORDER BY createdAt DESC, id DESC
|
|
83
|
+
LIMIT ${limit}
|
|
84
|
+
`,
|
|
85
|
+
queryBefore: (parentId, cursorCreatedAt, cursorId, limit) => surql`
|
|
86
|
+
SELECT * FROM threadMessage
|
|
87
|
+
WHERE threadId = ${parentId}
|
|
88
|
+
AND (
|
|
89
|
+
createdAt < ${cursorCreatedAt}
|
|
90
|
+
OR (createdAt = ${cursorCreatedAt} AND id < ${cursorId})
|
|
91
|
+
)
|
|
92
|
+
ORDER BY createdAt DESC, id DESC
|
|
93
|
+
LIMIT ${limit}
|
|
94
|
+
`,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function makeThreadMessageService(db: SurrealDBService) {
|
|
98
|
+
function upsertMessages(params: { threadId: RecordIdRef; messages: ChatMessage[] }) {
|
|
99
|
+
const threadId = toThreadRef(params.threadId)
|
|
100
|
+
return Effect.forEach(params.messages, (message) =>
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
const messageId = message.id.trim()
|
|
103
|
+
if (!messageId) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const role = message.role
|
|
108
|
+
const parts = cloneMessageParts(message.parts)
|
|
109
|
+
if (parts.length === 0) {
|
|
110
|
+
if (role === 'assistant') {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
return yield* new ServiceError({
|
|
114
|
+
message: `Refusing to persist thread message "${messageId}" with empty parts`,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const rowId = toThreadMessageRowId(threadId, messageId)
|
|
119
|
+
const existingRow = yield* effectTryServicePromise(
|
|
120
|
+
() => db.findOne(TABLES.THREAD_MESSAGE, { threadId, messageId }, ThreadMessageExistingRowSchema),
|
|
121
|
+
`Failed to load existing thread message "${messageId}".`,
|
|
122
|
+
)
|
|
123
|
+
const persistedCreatedAt =
|
|
124
|
+
existingRow === null ? requireTimestamp(message.metadata?.createdAt) : requireTimestamp(existingRow.createdAt)
|
|
125
|
+
const metadata = withCreatedAtMetadata({ ...message.metadata, createdAt: persistedCreatedAt })
|
|
126
|
+
|
|
127
|
+
yield* effectTryServicePromise(
|
|
128
|
+
() =>
|
|
129
|
+
db.upsert(
|
|
130
|
+
TABLES.THREAD_MESSAGE,
|
|
131
|
+
rowId,
|
|
132
|
+
{
|
|
133
|
+
threadId,
|
|
134
|
+
messageId,
|
|
135
|
+
role,
|
|
136
|
+
parts,
|
|
137
|
+
metadata,
|
|
138
|
+
createdAt: existingRow ? existingRow.createdAt : unsafeDateFrom(persistedCreatedAt),
|
|
139
|
+
},
|
|
140
|
+
ThreadMessageRowSchema,
|
|
141
|
+
{ mutation: 'content' },
|
|
142
|
+
),
|
|
143
|
+
`Failed to upsert thread message "${messageId}".`,
|
|
144
|
+
)
|
|
145
|
+
}),
|
|
146
|
+
).pipe(Effect.asVoid)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const service = {
|
|
150
|
+
upsertMessages,
|
|
151
|
+
upsertMessagesEffect: upsertMessages,
|
|
152
|
+
listMessages(threadId: RecordIdRef) {
|
|
153
|
+
const threadRef = toThreadRef(threadId)
|
|
154
|
+
return effectTryServicePromise(
|
|
155
|
+
() =>
|
|
156
|
+
db.query<unknown>(surql`
|
|
157
|
+
SELECT * FROM threadMessage
|
|
158
|
+
WHERE threadId = ${threadRef}
|
|
159
|
+
ORDER BY createdAt ASC, id ASC
|
|
160
|
+
`),
|
|
161
|
+
'Failed to list thread messages.',
|
|
162
|
+
).pipe(
|
|
163
|
+
Effect.map((rows) => rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))),
|
|
164
|
+
)
|
|
165
|
+
},
|
|
166
|
+
listMessagesEffect(threadId: RecordIdRef) {
|
|
167
|
+
return service.listMessages(threadId)
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
listMessageHistoryPage(params: { threadId: RecordIdRef; take: number; beforeMessageId?: string }) {
|
|
171
|
+
const threadRef = toThreadRef(params.threadId)
|
|
172
|
+
return listMessageHistoryPageEffect(db, threadPaginationConfig, {
|
|
173
|
+
parentId: threadRef,
|
|
174
|
+
take: params.take,
|
|
175
|
+
beforeMessageId: params.beforeMessageId,
|
|
176
|
+
})
|
|
177
|
+
},
|
|
178
|
+
listMessageHistoryPageEffect(params: { threadId: RecordIdRef; take: number; beforeMessageId?: string }) {
|
|
179
|
+
return service.listMessageHistoryPage(params)
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
listMessagesAfterCursor(threadId: RecordIdRef, afterMessageId?: string) {
|
|
183
|
+
const threadRef = toThreadRef(threadId)
|
|
184
|
+
const cursorMessageId = afterMessageId?.trim()
|
|
185
|
+
if (!cursorMessageId) {
|
|
186
|
+
return service.listMessages(threadRef)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Effect.gen(function* () {
|
|
190
|
+
const cursorRow = yield* effectTryServicePromise(
|
|
191
|
+
() => db.findOne(TABLES.THREAD_MESSAGE, { threadId: threadRef, messageId: cursorMessageId }, CursorRowSchema),
|
|
192
|
+
`Failed to load cursor message "${cursorMessageId}".`,
|
|
193
|
+
)
|
|
194
|
+
if (!cursorRow) {
|
|
195
|
+
return yield* new ServiceError({ message: `Thread cursor message not found: ${cursorMessageId}` })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const cursorCreatedAt = cursorRow.createdAt
|
|
199
|
+
const cursorId = toThreadMessageRowId(threadRef, cursorMessageId)
|
|
200
|
+
const rows = yield* effectTryServicePromise(
|
|
201
|
+
() =>
|
|
202
|
+
db.query<unknown>(surql`
|
|
203
|
+
SELECT * FROM threadMessage
|
|
204
|
+
WHERE threadId = ${threadRef}
|
|
205
|
+
AND (
|
|
206
|
+
createdAt > ${cursorCreatedAt}
|
|
207
|
+
OR (createdAt = ${cursorCreatedAt} AND id > ${cursorId})
|
|
208
|
+
)
|
|
209
|
+
ORDER BY createdAt ASC, id ASC
|
|
210
|
+
`),
|
|
211
|
+
'Failed to list thread messages after cursor.',
|
|
212
|
+
)
|
|
213
|
+
return rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
|
|
214
|
+
})
|
|
215
|
+
},
|
|
216
|
+
listMessagesAfterCursorEffect(threadId: RecordIdRef, afterMessageId?: string) {
|
|
217
|
+
return service.listMessagesAfterCursor(threadId, afterMessageId)
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
listRecentMessages(threadId: RecordIdRef, limit: number) {
|
|
221
|
+
const threadRef = toThreadRef(threadId)
|
|
222
|
+
return effectTryServicePromise(
|
|
223
|
+
() =>
|
|
224
|
+
db.query<unknown>(surql`
|
|
225
|
+
SELECT * FROM threadMessage
|
|
226
|
+
WHERE threadId = ${threadRef}
|
|
227
|
+
ORDER BY createdAt DESC, id DESC
|
|
228
|
+
LIMIT ${Math.max(1, limit)}
|
|
229
|
+
`),
|
|
230
|
+
'Failed to list recent thread messages.',
|
|
231
|
+
).pipe(
|
|
232
|
+
Effect.map((rows) =>
|
|
233
|
+
rows
|
|
234
|
+
.map((row) => ThreadMessageRowSchema.parse(row))
|
|
235
|
+
.reverse()
|
|
236
|
+
.map((row) => toChatMessage(row)),
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
},
|
|
240
|
+
listRecentMessagesEffect(threadId: RecordIdRef, limit: number) {
|
|
241
|
+
return service.listRecentMessages(threadId, limit)
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
searchMessages(params: { threadId: RecordIdRef; role: 'user' | 'assistant'; query: string; limit: number }) {
|
|
245
|
+
const normalizedQuery = params.query.trim().toLowerCase()
|
|
246
|
+
if (!normalizedQuery) return Effect.succeed([])
|
|
247
|
+
|
|
248
|
+
return Effect.gen(function* () {
|
|
249
|
+
const messages = yield* service.listMessages(toThreadRef(params.threadId))
|
|
250
|
+
return messages
|
|
251
|
+
.filter((message) => message.role === params.role)
|
|
252
|
+
.map((message) => ({
|
|
253
|
+
id: message.id,
|
|
254
|
+
role: message.role as 'user' | 'assistant',
|
|
255
|
+
createdAt: unsafeDateFrom(requireTimestamp(message.metadata?.createdAt)).toISOString(),
|
|
256
|
+
content: message.parts
|
|
257
|
+
.flatMap((part) => (part.type === 'text' && typeof part.text === 'string' ? [part.text] : []))
|
|
258
|
+
.join('\n')
|
|
259
|
+
.trim(),
|
|
260
|
+
}))
|
|
261
|
+
.filter((item) => item.content.length > 0 && item.content.toLowerCase().includes(normalizedQuery))
|
|
262
|
+
.slice(-Math.max(1, params.limit))
|
|
263
|
+
})
|
|
264
|
+
},
|
|
265
|
+
searchMessagesEffect(params: { threadId: RecordIdRef; role: 'user' | 'assistant'; query: string; limit: number }) {
|
|
266
|
+
return service.searchMessages(params)
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
addUserMessage(params: { messageId: RecordIdRef; threadId: RecordIdRef; content: string }) {
|
|
270
|
+
const threadRef = toThreadRef(params.threadId)
|
|
271
|
+
const message: ChatMessage = {
|
|
272
|
+
id: toMessageId(params.messageId),
|
|
273
|
+
role: 'user',
|
|
274
|
+
parts: [{ type: 'text', text: params.content }],
|
|
275
|
+
metadata: { createdAt: nowEpochMillis() },
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return Effect.gen(function* () {
|
|
279
|
+
yield* upsertMessages({ threadId: threadRef, messages: [message] })
|
|
280
|
+
return message
|
|
281
|
+
})
|
|
282
|
+
},
|
|
283
|
+
addUserMessageEffect(params: { messageId: RecordIdRef; threadId: RecordIdRef; content: string }) {
|
|
284
|
+
return service.addUserMessage(params)
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
addAgentMessage(params: {
|
|
288
|
+
messageId: RecordIdRef
|
|
289
|
+
threadId: RecordIdRef
|
|
290
|
+
parts: ChatMessage['parts']
|
|
291
|
+
metadata?: ChatMessage['metadata']
|
|
292
|
+
}) {
|
|
293
|
+
const threadRef = toThreadRef(params.threadId)
|
|
294
|
+
const message: ChatMessage = {
|
|
295
|
+
id: toMessageId(params.messageId),
|
|
296
|
+
role: 'assistant',
|
|
297
|
+
parts: params.parts,
|
|
298
|
+
metadata: withCreatedAtMetadata(params.metadata, nowEpochMillis()),
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return Effect.gen(function* () {
|
|
302
|
+
yield* upsertMessages({ threadId: threadRef, messages: [message] })
|
|
303
|
+
return message
|
|
304
|
+
})
|
|
305
|
+
},
|
|
306
|
+
addAgentMessageEffect(params: {
|
|
307
|
+
messageId: RecordIdRef
|
|
308
|
+
threadId: RecordIdRef
|
|
309
|
+
parts: ChatMessage['parts']
|
|
310
|
+
metadata?: ChatMessage['metadata']
|
|
311
|
+
}) {
|
|
312
|
+
return service.addAgentMessage(params)
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
ensureBootstrapWelcomeMessage(params: { threadId: RecordIdRef; agentId: string; text: string }) {
|
|
316
|
+
const threadRef = toThreadRef(params.threadId)
|
|
317
|
+
return Effect.gen(function* () {
|
|
318
|
+
const existingRow = yield* effectTryServicePromise(
|
|
319
|
+
() => db.findOne(TABLES.THREAD_MESSAGE, { threadId: threadRef }, ThreadMessageExistingRowSchema),
|
|
320
|
+
'Failed to check for existing bootstrap welcome message.',
|
|
321
|
+
)
|
|
322
|
+
if (existingRow) return
|
|
323
|
+
|
|
324
|
+
const messageText = params.text.trim()
|
|
325
|
+
if (!messageText) return
|
|
326
|
+
|
|
327
|
+
yield* upsertMessages({
|
|
328
|
+
threadId: threadRef,
|
|
329
|
+
messages: [
|
|
330
|
+
{
|
|
331
|
+
id: Bun.randomUUIDv7(),
|
|
332
|
+
role: 'assistant',
|
|
333
|
+
parts: [{ type: 'text', text: messageText }],
|
|
334
|
+
metadata: {
|
|
335
|
+
agentId: params.agentId,
|
|
336
|
+
agentName: getAgentDisplayNames()[params.agentId] ?? params.agentId,
|
|
337
|
+
createdAt: nowEpochMillis(),
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
},
|
|
344
|
+
ensureBootstrapWelcomeMessageEffect(params: { threadId: RecordIdRef; agentId: string; text: string }) {
|
|
345
|
+
return service.ensureBootstrapWelcomeMessage(params)
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return service
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export class ThreadMessageServiceTag extends Context.Service<
|
|
353
|
+
ThreadMessageServiceTag,
|
|
354
|
+
ReturnType<typeof makeThreadMessageService>
|
|
355
|
+
>()('ThreadMessageService') {}
|
|
356
|
+
|
|
357
|
+
export const ThreadMessageServiceLive = Layer.effect(
|
|
358
|
+
ThreadMessageServiceTag,
|
|
359
|
+
Effect.gen(function* () {
|
|
360
|
+
const db = yield* DatabaseServiceTag
|
|
361
|
+
return makeThreadMessageService(db)
|
|
362
|
+
}),
|
|
363
|
+
)
|