@lota-sdk/core 0.2.3 → 0.3.1

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.
Files changed (106) hide show
  1. package/infrastructure/schema/00_identity.surql +2 -2
  2. package/infrastructure/schema/00_thread.surql +73 -0
  3. package/infrastructure/schema/02_execution_plan.surql +10 -11
  4. package/infrastructure/schema/04_runtime_bootstrap.surql +1 -0
  5. package/infrastructure/schema/10_autonomous_job.surql +3 -3
  6. package/package.json +2 -2
  7. package/src/ai/definitions.ts +1 -1
  8. package/src/config/agent-defaults.ts +5 -5
  9. package/src/config/index.ts +1 -1
  10. package/src/config/thread-defaults.ts +72 -0
  11. package/src/create-runtime.ts +90 -94
  12. package/src/db/record-id.ts +21 -21
  13. package/src/db/service.ts +44 -40
  14. package/src/db/tables.ts +3 -3
  15. package/src/db/{workstream-message-row.ts → thread-message-row.ts} +3 -3
  16. package/src/queues/context-compaction.queue.ts +6 -6
  17. package/src/queues/plan-agent-heartbeat.queue.ts +3 -3
  18. package/src/queues/post-chat-memory.queue.ts +1 -1
  19. package/src/queues/title-generation.queue.ts +10 -13
  20. package/src/redis/index.ts +1 -1
  21. package/src/redis/stream-context.ts +1 -1
  22. package/src/runtime/agent-identity-overrides.ts +1 -1
  23. package/src/runtime/agent-runtime-policy.ts +19 -21
  24. package/src/runtime/chat-request-routing.ts +1 -1
  25. package/src/runtime/context-compaction-constants.ts +1 -1
  26. package/src/runtime/context-compaction.ts +1 -1
  27. package/src/runtime/execution-plan.ts +1 -1
  28. package/src/runtime/index.ts +1 -1
  29. package/src/runtime/memory-digest-policy.ts +1 -1
  30. package/src/runtime/plugin-types.ts +1 -1
  31. package/src/runtime/post-turn-side-effects.ts +35 -35
  32. package/src/runtime/runtime-config.ts +24 -21
  33. package/src/runtime/runtime-extensions.ts +11 -11
  34. package/src/runtime/social-chat-agent-runner.ts +3 -3
  35. package/src/runtime/social-chat-history.ts +1 -1
  36. package/src/runtime/social-chat.ts +6 -6
  37. package/src/runtime/team-consultation-orchestrator.ts +1 -1
  38. package/src/runtime/{workstream-chat-helpers.ts → thread-chat-helpers.ts} +7 -7
  39. package/src/runtime/{workstream-plan-turn.ts → thread-plan-turn.ts} +11 -17
  40. package/src/runtime/{workstream-turn-context.ts → thread-turn-context.ts} +10 -10
  41. package/src/services/agent-activity.service.ts +39 -44
  42. package/src/services/agent-executor.service.ts +17 -19
  43. package/src/services/attachment.service.ts +4 -8
  44. package/src/services/autonomous-job.service.ts +29 -28
  45. package/src/services/context-compaction.service.ts +19 -29
  46. package/src/services/execution-plan.service.ts +58 -70
  47. package/src/services/global-orchestrator.service.ts +5 -5
  48. package/src/services/index.ts +6 -6
  49. package/src/services/memory.service.ts +1 -1
  50. package/src/services/monitoring-window.service.ts +2 -2
  51. package/src/services/mutating-approval.service.ts +7 -10
  52. package/src/services/node-workspace.service.ts +8 -7
  53. package/src/services/notification.service.ts +1 -1
  54. package/src/services/organization.service.ts +9 -9
  55. package/src/services/ownership-dispatcher.service.ts +13 -19
  56. package/src/services/plan-agent-heartbeat.service.ts +13 -13
  57. package/src/services/plan-agent-query.service.ts +7 -7
  58. package/src/services/plan-artifact.service.ts +1 -2
  59. package/src/services/plan-coordination.service.ts +4 -4
  60. package/src/services/plan-cycle.service.ts +7 -7
  61. package/src/services/plan-deadline.service.ts +4 -4
  62. package/src/services/plan-event-delivery.service.ts +8 -12
  63. package/src/services/plan-executor.service.ts +25 -39
  64. package/src/services/plan-run-data.ts +27 -8
  65. package/src/services/plan-run.service.ts +7 -9
  66. package/src/services/plan-scheduler.service.ts +4 -4
  67. package/src/services/plan-template.service.ts +2 -2
  68. package/src/services/plan-validator.service.ts +0 -11
  69. package/src/services/plugin-executor.service.ts +1 -1
  70. package/src/services/queue-job.service.ts +1 -1
  71. package/src/services/recent-activity-title.service.ts +1 -1
  72. package/src/services/recent-activity.service.ts +4 -4
  73. package/src/services/system-executor.service.ts +2 -2
  74. package/src/services/{workstream-message.service.ts → thread-message.service.ts} +72 -76
  75. package/src/services/thread-plan-registry.service.ts +22 -0
  76. package/src/services/thread-title.service.ts +39 -0
  77. package/src/services/{workstream-turn-preparation.service.ts → thread-turn-preparation.service.ts} +148 -171
  78. package/src/services/{workstream-turn.ts → thread-turn.ts} +27 -31
  79. package/src/services/thread.service.ts +853 -0
  80. package/src/services/thread.types.ts +17 -0
  81. package/src/storage/attachment-storage.service.ts +4 -4
  82. package/src/system-agents/index.ts +1 -1
  83. package/src/system-agents/memory.agent.ts +1 -1
  84. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  85. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  86. package/src/system-agents/researcher.agent.ts +3 -3
  87. package/src/system-agents/{workstream-router.agent.ts → thread-router.agent.ts} +68 -135
  88. package/src/system-agents/title-generator.agent.ts +8 -8
  89. package/src/tools/execution-plan.tool.ts +39 -40
  90. package/src/tools/memory-block.tool.ts +4 -4
  91. package/src/tools/research-topic.tool.ts +1 -0
  92. package/src/tools/search-web.tool.ts +1 -1
  93. package/src/tools/search.tool.ts +4 -4
  94. package/src/tools/team-think.tool.ts +9 -9
  95. package/src/utils/async.ts +6 -7
  96. package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
  97. package/src/workers/regular-chat-memory-digest.runner.ts +43 -43
  98. package/src/workers/skill-extraction.runner.ts +9 -13
  99. package/src/workers/utils/{workstream-message-query.ts → thread-message-query.ts} +21 -21
  100. package/infrastructure/schema/00_workstream.surql +0 -64
  101. package/src/config/workstream-defaults.ts +0 -72
  102. package/src/services/workstream-plan-registry.service.ts +0 -22
  103. package/src/services/workstream-title.service.ts +0 -42
  104. package/src/services/workstream.service.ts +0 -803
  105. package/src/services/workstream.types.ts +0 -17
  106. /package/src/services/{workstream-constants.ts → thread-constants.ts} +0 -0
@@ -10,54 +10,54 @@ import { ensureRecordId, recordIdToString } from '../db/record-id'
10
10
  import type { RecordIdRef } from '../db/record-id'
11
11
  import { databaseService } from '../db/service'
12
12
  import { TABLES } from '../db/tables'
13
- import { WorkstreamMessageRowSchema } from '../db/workstream-message-row'
14
- import type { WorkstreamMessageRow } from '../db/workstream-message-row'
13
+ import { ThreadMessageRowSchema } from '../db/thread-message-row'
14
+ import type { ThreadMessageRow } from '../db/thread-message-row'
15
15
 
16
- const WorkstreamMessageExistingRowSchema = z.object({ id: recordIdSchema, createdAt: z.coerce.date() })
16
+ const ThreadMessageExistingRowSchema = z.object({ id: recordIdSchema, createdAt: z.coerce.date() })
17
17
 
18
18
  function toMessageId(value: string | RecordIdRef): string {
19
- return recordIdToString(value, TABLES.WORKSTREAM_MESSAGE)
19
+ return recordIdToString(value, TABLES.THREAD_MESSAGE)
20
20
  }
21
21
 
22
22
  /**
23
- * Builds a collision-free row id by hashing the workstream + message id pair.
23
+ * Builds a collision-free row id by hashing the thread + message id pair.
24
24
  * Previous implementation replaced non-alphanumeric chars with '_', which was
25
25
  * lossy (e.g. "msg:foo" and "msg_foo" mapped to the same row id).
26
26
  * Now uses a 32-char SHA-256 hex prefix -- short enough for ergonomic ids,
27
27
  * long enough (128 bits) to make collisions negligible.
28
28
  */
29
- function toWorkstreamMessageRowId(workstreamId: RecordIdRef, messageId: string): RecordId {
30
- const workstreamStr = recordIdToString(workstreamId, TABLES.WORKSTREAM)
31
- const digest = new Bun.CryptoHasher('sha256').update(`${workstreamStr}\0${messageId}`).digest('hex').slice(0, 32)
32
- return new RecordId(TABLES.WORKSTREAM_MESSAGE, digest)
29
+ function toThreadMessageRowId(threadId: RecordIdRef, messageId: string): RecordId {
30
+ const threadStr = recordIdToString(threadId, TABLES.THREAD)
31
+ const digest = new Bun.CryptoHasher('sha256').update(`${threadStr}\0${messageId}`).digest('hex').slice(0, 32)
32
+ return new RecordId(TABLES.THREAD_MESSAGE, digest)
33
33
  }
34
34
 
35
- function toWorkstreamRef(workstreamId: RecordIdRef): RecordId {
36
- return ensureRecordId(workstreamId, TABLES.WORKSTREAM)
35
+ function toThreadRef(threadId: RecordIdRef): RecordId {
36
+ return ensureRecordId(threadId, TABLES.THREAD)
37
37
  }
38
38
 
39
- function toChatMessage(row: WorkstreamMessageRow): ChatMessage {
39
+ function toChatMessage(row: ThreadMessageRow): ChatMessage {
40
40
  const rowCreatedAt = requireTimestamp(row.createdAt)
41
41
  const metadata = withCreatedAtMetadata(parseRowMetadata(row.metadata), rowCreatedAt)
42
42
 
43
43
  return { id: row.messageId, role: row.role, parts: row.parts as ChatMessage['parts'], metadata }
44
44
  }
45
45
 
46
- const workstreamPaginationConfig: CursorPaginationConfig = {
47
- table: TABLES.WORKSTREAM_MESSAGE,
48
- parentFilterField: 'workstreamId',
49
- toRowId: toWorkstreamMessageRowId,
50
- parseRow: (row: unknown) => WorkstreamMessageRowSchema.parse(row),
51
- toMessage: (row: unknown) => toChatMessage(WorkstreamMessageRowSchema.parse(row)),
46
+ const threadPaginationConfig: CursorPaginationConfig = {
47
+ table: TABLES.THREAD_MESSAGE,
48
+ parentFilterField: 'threadId',
49
+ toRowId: toThreadMessageRowId,
50
+ parseRow: (row: unknown) => ThreadMessageRowSchema.parse(row),
51
+ toMessage: (row: unknown) => toChatMessage(ThreadMessageRowSchema.parse(row)),
52
52
  queryLatest: (parentId, limit) => surql`
53
- SELECT * FROM workstreamMessage
54
- WHERE workstreamId = ${parentId}
53
+ SELECT * FROM threadMessage
54
+ WHERE threadId = ${parentId}
55
55
  ORDER BY createdAt DESC, id DESC
56
56
  LIMIT ${limit}
57
57
  `,
58
58
  queryBefore: (parentId, cursorCreatedAt, cursorId, limit) => surql`
59
- SELECT * FROM workstreamMessage
60
- WHERE workstreamId = ${parentId}
59
+ SELECT * FROM threadMessage
60
+ WHERE threadId = ${parentId}
61
61
  AND (
62
62
  createdAt < ${cursorCreatedAt}
63
63
  OR (createdAt = ${cursorCreatedAt} AND id < ${cursorId})
@@ -67,9 +67,9 @@ const workstreamPaginationConfig: CursorPaginationConfig = {
67
67
  `,
68
68
  }
69
69
 
70
- class WorkstreamMessageService {
71
- async upsertMessages(params: { workstreamId: RecordIdRef; messages: ChatMessage[] }): Promise<void> {
72
- const workstreamId = toWorkstreamRef(params.workstreamId)
70
+ class ThreadMessageService {
71
+ async upsertMessages(params: { threadId: RecordIdRef; messages: ChatMessage[] }): Promise<void> {
72
+ const threadId = toThreadRef(params.threadId)
73
73
 
74
74
  const upsertPromises = params.messages.map(async (message) => {
75
75
  const messageId = message.id.trim()
@@ -81,82 +81,82 @@ class WorkstreamMessageService {
81
81
  : []
82
82
  if (parts.length === 0) {
83
83
  if (role === 'assistant') return
84
- throw new Error(`Refusing to persist workstream message "${messageId}" with empty parts`)
84
+ throw new Error(`Refusing to persist thread message "${messageId}" with empty parts`)
85
85
  }
86
- const rowId = toWorkstreamMessageRowId(workstreamId, messageId)
86
+ const rowId = toThreadMessageRowId(threadId, messageId)
87
87
  const existingRow = await databaseService.findOne(
88
- TABLES.WORKSTREAM_MESSAGE,
89
- { workstreamId, messageId },
90
- WorkstreamMessageExistingRowSchema,
88
+ TABLES.THREAD_MESSAGE,
89
+ { threadId, messageId },
90
+ ThreadMessageExistingRowSchema,
91
91
  )
92
92
  const persistedCreatedAt =
93
93
  existingRow === null ? requireTimestamp(message.metadata?.createdAt) : requireTimestamp(existingRow.createdAt)
94
94
  const metadata = withCreatedAtMetadata({ ...message.metadata, createdAt: persistedCreatedAt })
95
95
 
96
96
  await databaseService.upsert(
97
- TABLES.WORKSTREAM_MESSAGE,
97
+ TABLES.THREAD_MESSAGE,
98
98
  rowId,
99
99
  {
100
- workstreamId,
100
+ threadId,
101
101
  messageId,
102
102
  role,
103
103
  parts,
104
104
  metadata,
105
105
  createdAt: existingRow ? existingRow.createdAt : new Date(persistedCreatedAt),
106
106
  },
107
- WorkstreamMessageRowSchema,
107
+ ThreadMessageRowSchema,
108
108
  { mutation: 'content' },
109
109
  )
110
110
  })
111
111
  await Promise.all(upsertPromises)
112
112
  }
113
113
 
114
- async listMessages(workstreamId: RecordIdRef): Promise<ChatMessage[]> {
115
- const workstreamRef = toWorkstreamRef(workstreamId)
114
+ async listMessages(threadId: RecordIdRef): Promise<ChatMessage[]> {
115
+ const threadRef = toThreadRef(threadId)
116
116
  const rows = await databaseService.query<unknown>(surql`
117
- SELECT * FROM workstreamMessage
118
- WHERE workstreamId = ${workstreamRef}
117
+ SELECT * FROM threadMessage
118
+ WHERE threadId = ${threadRef}
119
119
  ORDER BY createdAt ASC, id ASC
120
120
  `)
121
121
 
122
- return rows.map((row) => WorkstreamMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
122
+ return rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
123
123
  }
124
124
 
125
125
  async listMessageHistoryPage(params: {
126
- workstreamId: RecordIdRef
126
+ threadId: RecordIdRef
127
127
  take: number
128
128
  beforeMessageId?: string
129
129
  }): Promise<MessageHistoryPage> {
130
- const workstreamRef = toWorkstreamRef(params.workstreamId)
131
- return listMessageHistoryPage(workstreamPaginationConfig, {
132
- parentId: workstreamRef,
130
+ const threadRef = toThreadRef(params.threadId)
131
+ return listMessageHistoryPage(threadPaginationConfig, {
132
+ parentId: threadRef,
133
133
  take: params.take,
134
134
  beforeMessageId: params.beforeMessageId,
135
135
  })
136
136
  }
137
137
 
138
- async listMessagesAfterCursor(workstreamId: RecordIdRef, afterMessageId?: string): Promise<ChatMessage[]> {
139
- const workstreamRef = toWorkstreamRef(workstreamId)
138
+ async listMessagesAfterCursor(threadId: RecordIdRef, afterMessageId?: string): Promise<ChatMessage[]> {
139
+ const threadRef = toThreadRef(threadId)
140
140
  const cursorMessageId = afterMessageId?.trim()
141
141
  if (!cursorMessageId) {
142
- return this.listMessages(workstreamRef)
142
+ return this.listMessages(threadRef)
143
143
  }
144
144
 
145
145
  const cursorRow = await databaseService.findOne(
146
- TABLES.WORKSTREAM_MESSAGE,
147
- { workstreamId: workstreamRef, messageId: cursorMessageId },
146
+ TABLES.THREAD_MESSAGE,
147
+ { threadId: threadRef, messageId: cursorMessageId },
148
148
  CursorRowSchema,
149
149
  )
150
150
 
151
151
  if (!cursorRow) {
152
- throw new Error(`Workstream cursor message not found: ${cursorMessageId}`)
152
+ throw new Error(`Thread cursor message not found: ${cursorMessageId}`)
153
153
  }
154
154
 
155
155
  const cursorCreatedAt = cursorRow.createdAt
156
- const cursorId = toWorkstreamMessageRowId(workstreamRef, cursorMessageId)
156
+ const cursorId = toThreadMessageRowId(threadRef, cursorMessageId)
157
157
  const rows = await databaseService.query<unknown>(surql`
158
- SELECT * FROM workstreamMessage
159
- WHERE workstreamId = ${workstreamRef}
158
+ SELECT * FROM threadMessage
159
+ WHERE threadId = ${threadRef}
160
160
  AND (
161
161
  createdAt > ${cursorCreatedAt}
162
162
  OR (createdAt = ${cursorCreatedAt} AND id > ${cursorId})
@@ -164,26 +164,26 @@ class WorkstreamMessageService {
164
164
  ORDER BY createdAt ASC, id ASC
165
165
  `)
166
166
 
167
- return rows.map((row) => WorkstreamMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
167
+ return rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
168
168
  }
169
169
 
170
- async listRecentMessages(workstreamId: RecordIdRef, limit: number): Promise<ChatMessage[]> {
171
- const workstreamRef = toWorkstreamRef(workstreamId)
170
+ async listRecentMessages(threadId: RecordIdRef, limit: number): Promise<ChatMessage[]> {
171
+ const threadRef = toThreadRef(threadId)
172
172
  const rows = await databaseService.query<unknown>(surql`
173
- SELECT * FROM workstreamMessage
174
- WHERE workstreamId = ${workstreamRef}
173
+ SELECT * FROM threadMessage
174
+ WHERE threadId = ${threadRef}
175
175
  ORDER BY createdAt DESC, id DESC
176
176
  LIMIT ${Math.max(1, limit)}
177
177
  `)
178
178
 
179
179
  return rows
180
- .map((row) => WorkstreamMessageRowSchema.parse(row))
180
+ .map((row) => ThreadMessageRowSchema.parse(row))
181
181
  .reverse()
182
182
  .map((row) => toChatMessage(row))
183
183
  }
184
184
 
185
185
  async searchMessages(params: {
186
- workstreamId: RecordIdRef
186
+ threadId: RecordIdRef
187
187
  role: 'user' | 'assistant'
188
188
  query: string
189
189
  limit: number
@@ -191,7 +191,7 @@ class WorkstreamMessageService {
191
191
  const normalizedQuery = params.query.trim().toLowerCase()
192
192
  if (!normalizedQuery) return []
193
193
 
194
- const messages = await this.listMessages(toWorkstreamRef(params.workstreamId))
194
+ const messages = await this.listMessages(toThreadRef(params.threadId))
195
195
  return messages
196
196
  .filter((message) => message.role === params.role)
197
197
  .map((message) => ({
@@ -209,10 +209,10 @@ class WorkstreamMessageService {
209
209
 
210
210
  async addUserMessage(params: {
211
211
  messageId: RecordIdRef
212
- workstreamId: RecordIdRef
212
+ threadId: RecordIdRef
213
213
  content: string
214
214
  }): Promise<ChatMessage> {
215
- const workstreamRef = toWorkstreamRef(params.workstreamId)
215
+ const threadRef = toThreadRef(params.threadId)
216
216
  const message: ChatMessage = {
217
217
  id: toMessageId(params.messageId),
218
218
  role: 'user',
@@ -220,17 +220,17 @@ class WorkstreamMessageService {
220
220
  metadata: { createdAt: Date.now() },
221
221
  }
222
222
 
223
- await this.upsertMessages({ workstreamId: workstreamRef, messages: [message] })
223
+ await this.upsertMessages({ threadId: threadRef, messages: [message] })
224
224
  return message
225
225
  }
226
226
 
227
227
  async addAgentMessage(params: {
228
228
  messageId: RecordIdRef
229
- workstreamId: RecordIdRef
229
+ threadId: RecordIdRef
230
230
  parts: ChatMessage['parts']
231
231
  metadata?: ChatMessage['metadata']
232
232
  }): Promise<ChatMessage> {
233
- const workstreamRef = toWorkstreamRef(params.workstreamId)
233
+ const threadRef = toThreadRef(params.threadId)
234
234
  const message: ChatMessage = {
235
235
  id: toMessageId(params.messageId),
236
236
  role: 'assistant',
@@ -238,20 +238,16 @@ class WorkstreamMessageService {
238
238
  metadata: withCreatedAtMetadata(params.metadata, Date.now()),
239
239
  }
240
240
 
241
- await this.upsertMessages({ workstreamId: workstreamRef, messages: [message] })
241
+ await this.upsertMessages({ threadId: threadRef, messages: [message] })
242
242
  return message
243
243
  }
244
244
 
245
- async ensureBootstrapWelcomeMessage(params: {
246
- workstreamId: RecordIdRef
247
- agentId: string
248
- text: string
249
- }): Promise<void> {
250
- const workstreamRef = toWorkstreamRef(params.workstreamId)
245
+ async ensureBootstrapWelcomeMessage(params: { threadId: RecordIdRef; agentId: string; text: string }): Promise<void> {
246
+ const threadRef = toThreadRef(params.threadId)
251
247
  const existingRow = await databaseService.findOne(
252
- TABLES.WORKSTREAM_MESSAGE,
253
- { workstreamId: workstreamRef },
254
- WorkstreamMessageExistingRowSchema,
248
+ TABLES.THREAD_MESSAGE,
249
+ { threadId: threadRef },
250
+ ThreadMessageExistingRowSchema,
255
251
  )
256
252
  if (existingRow) return
257
253
 
@@ -259,7 +255,7 @@ class WorkstreamMessageService {
259
255
  if (!messageText) return
260
256
 
261
257
  await this.upsertMessages({
262
- workstreamId: workstreamRef,
258
+ threadId: threadRef,
263
259
  messages: [
264
260
  {
265
261
  id: Bun.randomUUIDv7(),
@@ -276,4 +272,4 @@ class WorkstreamMessageService {
276
272
  }
277
273
  }
278
274
 
279
- export const workstreamMessageService = new WorkstreamMessageService()
275
+ export const threadMessageService = new ThreadMessageService()
@@ -0,0 +1,22 @@
1
+ import type { PlanRunRecord, SerializableExecutionPlan } from '@lota-sdk/shared'
2
+
3
+ import type { RecordIdInput } from '../db/record-id'
4
+ import { planRunService } from './plan-run.service'
5
+
6
+ class ThreadPlanRegistryService {
7
+ async listActiveRuns(threadId: RecordIdInput): Promise<PlanRunRecord[]> {
8
+ return planRunService.getActiveRunRecords(threadId)
9
+ }
10
+
11
+ async countActiveRuns(threadId: RecordIdInput): Promise<number> {
12
+ const runs = await this.listActiveRuns(threadId)
13
+ return runs.length
14
+ }
15
+
16
+ async listActivePlans(threadId: RecordIdInput): Promise<SerializableExecutionPlan[]> {
17
+ const runs = await this.listActiveRuns(threadId)
18
+ return Promise.all(runs.map((run) => planRunService.toSerializablePlan(run)))
19
+ }
20
+ }
21
+
22
+ export const threadPlanRegistryService = new ThreadPlanRegistryService()
@@ -0,0 +1,39 @@
1
+ import { THREAD } from '@lota-sdk/shared'
2
+
3
+ import { chatLogger } from '../config/logger'
4
+ import type { RecordIdRef } from '../db/record-id'
5
+ import { createHelperModelRuntime } from '../runtime/helper-model'
6
+ import { deriveTitle, limitTitleWords, normalizeTitle } from '../runtime/title-helpers'
7
+ import { createThreadTitleGeneratorAgent, THREAD_TITLE_GENERATOR_PROMPT } from '../system-agents/title-generator.agent'
8
+ import { threadService } from './thread.service'
9
+
10
+ const THREAD_TITLE_TIMEOUT_MS = 30_000
11
+
12
+ class ThreadTitleService {
13
+ helperRuntime = createHelperModelRuntime()
14
+
15
+ async generateAndPersistTitle(threadId: RecordIdRef, sourceText: string): Promise<void> {
16
+ let title = ''
17
+ try {
18
+ title = normalizeTitle(
19
+ await this.helperRuntime.generateHelperText({
20
+ tag: 'thread-title',
21
+ createAgent: createThreadTitleGeneratorAgent,
22
+ defaultSystemPrompt: THREAD_TITLE_GENERATOR_PROMPT,
23
+ timeoutMs: THREAD_TITLE_TIMEOUT_MS,
24
+ messages: [{ role: 'user', content: sourceText }],
25
+ }),
26
+ )
27
+ } catch (error) {
28
+ chatLogger.warn`Failed to generate thread title via LLM (non-fatal): ${error}`
29
+ }
30
+
31
+ if (!title) {
32
+ title = limitTitleWords(deriveTitle(sourceText || THREAD.DEFAULT_TITLE))
33
+ }
34
+
35
+ await threadService.update(threadId, { title, nameGenerated: true })
36
+ }
37
+ }
38
+
39
+ export const threadTitleService = new ThreadTitleService()