@lota-sdk/core 0.1.5

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 (153) hide show
  1. package/infrastructure/schema/00_workstream.surql +55 -0
  2. package/infrastructure/schema/01_memory.surql +47 -0
  3. package/infrastructure/schema/02_execution_plan.surql +62 -0
  4. package/infrastructure/schema/03_learned_skill.surql +32 -0
  5. package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
  6. package/package.json +128 -0
  7. package/src/ai/definitions.ts +308 -0
  8. package/src/bifrost/bifrost.ts +256 -0
  9. package/src/config/agent-defaults.ts +99 -0
  10. package/src/config/constants.ts +33 -0
  11. package/src/config/env-shapes.ts +122 -0
  12. package/src/config/logger.ts +29 -0
  13. package/src/config/model-constants.ts +31 -0
  14. package/src/config/search.ts +17 -0
  15. package/src/config/workstream-defaults.ts +68 -0
  16. package/src/db/base.service.ts +55 -0
  17. package/src/db/cursor-pagination.ts +73 -0
  18. package/src/db/memory-query-builder.ts +207 -0
  19. package/src/db/memory-store.helpers.ts +118 -0
  20. package/src/db/memory-store.rows.ts +29 -0
  21. package/src/db/memory-store.ts +974 -0
  22. package/src/db/memory-types.ts +193 -0
  23. package/src/db/memory.ts +505 -0
  24. package/src/db/record-id.ts +78 -0
  25. package/src/db/service.ts +932 -0
  26. package/src/db/startup.ts +152 -0
  27. package/src/db/tables.ts +20 -0
  28. package/src/document/org-document-chunking.ts +224 -0
  29. package/src/document/parsing.ts +40 -0
  30. package/src/embeddings/provider.ts +76 -0
  31. package/src/index.ts +302 -0
  32. package/src/queues/context-compaction.queue.ts +82 -0
  33. package/src/queues/document-processor.queue.ts +118 -0
  34. package/src/queues/memory-consolidation.queue.ts +65 -0
  35. package/src/queues/post-chat-memory.queue.ts +128 -0
  36. package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
  37. package/src/queues/regular-chat-memory-digest.config.ts +12 -0
  38. package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
  39. package/src/queues/skill-extraction.config.ts +9 -0
  40. package/src/queues/skill-extraction.queue.ts +62 -0
  41. package/src/redis/connection.ts +176 -0
  42. package/src/redis/index.ts +30 -0
  43. package/src/redis/org-memory-lock.ts +43 -0
  44. package/src/redis/redis-lease-lock.ts +158 -0
  45. package/src/runtime/agent-contract.ts +1 -0
  46. package/src/runtime/agent-prompt-context.ts +119 -0
  47. package/src/runtime/agent-runtime-policy.ts +192 -0
  48. package/src/runtime/agent-stream-helpers.ts +117 -0
  49. package/src/runtime/agent-types.ts +22 -0
  50. package/src/runtime/approval-continuation.ts +16 -0
  51. package/src/runtime/chat-attachments.ts +46 -0
  52. package/src/runtime/chat-message.ts +10 -0
  53. package/src/runtime/chat-request-routing.ts +21 -0
  54. package/src/runtime/chat-run-orchestration.ts +25 -0
  55. package/src/runtime/chat-run-registry.ts +20 -0
  56. package/src/runtime/chat-types.ts +18 -0
  57. package/src/runtime/context-compaction-constants.ts +11 -0
  58. package/src/runtime/context-compaction-runtime.ts +86 -0
  59. package/src/runtime/context-compaction.ts +909 -0
  60. package/src/runtime/execution-plan.ts +59 -0
  61. package/src/runtime/helper-model.ts +405 -0
  62. package/src/runtime/indexed-repositories-policy.ts +28 -0
  63. package/src/runtime/instruction-sections.ts +8 -0
  64. package/src/runtime/llm-content.ts +71 -0
  65. package/src/runtime/memory-block.ts +264 -0
  66. package/src/runtime/memory-digest-policy.ts +14 -0
  67. package/src/runtime/memory-format.ts +8 -0
  68. package/src/runtime/memory-pipeline.ts +570 -0
  69. package/src/runtime/memory-prompts-fact.ts +47 -0
  70. package/src/runtime/memory-prompts-parse.ts +3 -0
  71. package/src/runtime/memory-prompts-update.ts +37 -0
  72. package/src/runtime/memory-scope.ts +43 -0
  73. package/src/runtime/plugin-types.ts +10 -0
  74. package/src/runtime/retrieval-adapters.ts +25 -0
  75. package/src/runtime/retrieval-pipeline.ts +3 -0
  76. package/src/runtime/runtime-extensions.ts +154 -0
  77. package/src/runtime/skill-extraction-policy.ts +3 -0
  78. package/src/runtime/team-consultation-orchestrator.ts +245 -0
  79. package/src/runtime/team-consultation-prompts.ts +32 -0
  80. package/src/runtime/title-helpers.ts +12 -0
  81. package/src/runtime/turn-lifecycle.ts +28 -0
  82. package/src/runtime/workstream-chat-helpers.ts +187 -0
  83. package/src/runtime/workstream-routing-policy.ts +301 -0
  84. package/src/runtime/workstream-state.ts +261 -0
  85. package/src/services/attachment.service.ts +159 -0
  86. package/src/services/chat-attachments.service.ts +17 -0
  87. package/src/services/chat-run-registry.service.ts +3 -0
  88. package/src/services/context-compaction-runtime.ts +13 -0
  89. package/src/services/context-compaction.service.ts +115 -0
  90. package/src/services/document-chunk.service.ts +141 -0
  91. package/src/services/execution-plan.service.ts +890 -0
  92. package/src/services/learned-skill.service.ts +328 -0
  93. package/src/services/memory-assessment.service.ts +43 -0
  94. package/src/services/memory.service.ts +807 -0
  95. package/src/services/memory.utils.ts +84 -0
  96. package/src/services/mutating-approval.service.ts +110 -0
  97. package/src/services/recent-activity-title.service.ts +74 -0
  98. package/src/services/recent-activity.service.ts +397 -0
  99. package/src/services/workstream-change-tracker.service.ts +313 -0
  100. package/src/services/workstream-message.service.ts +283 -0
  101. package/src/services/workstream-title.service.ts +58 -0
  102. package/src/services/workstream-turn-preparation.ts +1340 -0
  103. package/src/services/workstream-turn.ts +37 -0
  104. package/src/services/workstream.service.ts +854 -0
  105. package/src/services/workstream.types.ts +118 -0
  106. package/src/storage/attachment-parser.ts +101 -0
  107. package/src/storage/attachment-storage.service.ts +391 -0
  108. package/src/storage/attachments.types.ts +11 -0
  109. package/src/storage/attachments.utils.ts +58 -0
  110. package/src/storage/generated-document-storage.service.ts +55 -0
  111. package/src/system-agents/agent-result.ts +27 -0
  112. package/src/system-agents/context-compacter.agent.ts +46 -0
  113. package/src/system-agents/delegated-agent-factory.ts +177 -0
  114. package/src/system-agents/helper-agent-options.ts +20 -0
  115. package/src/system-agents/memory-reranker.agent.ts +38 -0
  116. package/src/system-agents/memory.agent.ts +58 -0
  117. package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
  118. package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
  119. package/src/system-agents/researcher.agent.ts +34 -0
  120. package/src/system-agents/skill-extractor.agent.ts +88 -0
  121. package/src/system-agents/skill-manager.agent.ts +80 -0
  122. package/src/system-agents/title-generator.agent.ts +42 -0
  123. package/src/system-agents/workstream-tracker.agent.ts +58 -0
  124. package/src/tools/execution-plan.tool.ts +163 -0
  125. package/src/tools/fetch-webpage.tool.ts +132 -0
  126. package/src/tools/firecrawl-client.ts +12 -0
  127. package/src/tools/memory-block.tool.ts +55 -0
  128. package/src/tools/read-file-parts.tool.ts +80 -0
  129. package/src/tools/remember-memory.tool.ts +85 -0
  130. package/src/tools/research-topic.tool.ts +15 -0
  131. package/src/tools/search-tools.ts +55 -0
  132. package/src/tools/search-web.tool.ts +175 -0
  133. package/src/tools/team-think.tool.ts +125 -0
  134. package/src/tools/tool-contract.ts +21 -0
  135. package/src/tools/user-questions.tool.ts +18 -0
  136. package/src/utils/async.ts +50 -0
  137. package/src/utils/date-time.ts +34 -0
  138. package/src/utils/error.ts +10 -0
  139. package/src/utils/errors.ts +28 -0
  140. package/src/utils/hono-error-handler.ts +71 -0
  141. package/src/utils/string.ts +51 -0
  142. package/src/workers/bootstrap.ts +44 -0
  143. package/src/workers/memory-consolidation.worker.ts +318 -0
  144. package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
  145. package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
  146. package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
  147. package/src/workers/skill-extraction.runner.ts +331 -0
  148. package/src/workers/skill-extraction.worker.ts +22 -0
  149. package/src/workers/utils/repo-indexer-chunker.ts +331 -0
  150. package/src/workers/utils/repo-structure-extractor.ts +645 -0
  151. package/src/workers/utils/repomix-process-concurrency.ts +65 -0
  152. package/src/workers/utils/sandbox-error.ts +5 -0
  153. package/src/workers/worker-utils.ts +182 -0
@@ -0,0 +1,854 @@
1
+ import { WORKSTREAM } from '@lota-sdk/shared/constants/workstream'
2
+ import { BoundQuery, RecordId, StringRecordId, surql } from 'surrealdb'
3
+
4
+ import { agentDisplayNames, getCoreWorkstreamProfile, isAgentName } from '../config/agent-defaults'
5
+ import { getWorkstreamBootstrapConfig } from '../config/workstream-defaults'
6
+ import { BaseService } from '../db/base.service'
7
+ import { ensureRecordId, recordIdToString } from '../db/record-id'
8
+ import type { RecordIdInput, RecordIdRef } from '../db/record-id'
9
+ import { databaseService } from '../db/service'
10
+ import type { DatabaseTable } from '../db/tables'
11
+ import { TABLES } from '../db/tables'
12
+ import {
13
+ MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES,
14
+ MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES,
15
+ } from '../runtime/context-compaction-constants'
16
+ import {
17
+ appendToMemoryBlock,
18
+ compactMemoryBlockEntries,
19
+ formatPersistedMemoryBlockForPrompt,
20
+ parseMemoryBlock,
21
+ serializeMemoryBlock,
22
+ } from '../runtime/memory-block'
23
+ import { toOptionalTrimmedString } from '../runtime/workstream-chat-helpers'
24
+ import { WorkstreamStateSchema } from '../runtime/workstream-state'
25
+ import type { WorkstreamState } from '../runtime/workstream-state'
26
+ import { toIsoDateTimeString } from '../utils/date-time'
27
+ import { chatRunRegistry } from './chat-run-registry.service'
28
+ import { contextCompactionService } from './context-compaction.service'
29
+ import { workstreamMessageService } from './workstream-message.service'
30
+ import { WorkstreamSchema, WorkstreamStatusSchema } from './workstream.types'
31
+ import type {
32
+ NormalizedWorkstream,
33
+ PublicWorkstreamApprovalState,
34
+ PublicWorkstreamDetail,
35
+ PublicWorkstreamStateFocus,
36
+ PublicWorkstreamStatePayload,
37
+ PublicWorkstreamStateProgress,
38
+ WorkstreamRecord,
39
+ } from './workstream.types'
40
+
41
+ // Uses SurrealQL directly to keep pagination/order logic close to queries.
42
+
43
+ const LIST_WORKSTREAMS_QUERY = `SELECT * FROM ${TABLES.WORKSTREAM}
44
+ WHERE userId = $userId
45
+ AND organizationId = $orgId
46
+ AND mode = $mode
47
+ AND core = $core
48
+ ORDER BY updatedAt DESC
49
+ LIMIT $limit START $offset`
50
+
51
+ const LIST_WORKSTREAMS_EXCLUDE_ARCHIVED_QUERY = `SELECT * FROM ${TABLES.WORKSTREAM}
52
+ WHERE userId = $userId
53
+ AND organizationId = $orgId
54
+ AND mode = $mode
55
+ AND core = $core
56
+ AND status = "regular"
57
+ ORDER BY updatedAt DESC
58
+ LIMIT $limit START $offset`
59
+
60
+ const LIST_ALL_WORKSTREAMS_BY_MODE_QUERY = `SELECT * FROM ${TABLES.WORKSTREAM}
61
+ WHERE userId = $userId
62
+ AND organizationId = $orgId
63
+ AND mode = $mode
64
+ AND core = $core
65
+ ORDER BY updatedAt DESC`
66
+
67
+ const LIST_ALL_WORKSTREAMS_BY_MODE_EXCLUDE_ARCHIVED_QUERY = `SELECT * FROM ${TABLES.WORKSTREAM}
68
+ WHERE userId = $userId
69
+ AND organizationId = $orgId
70
+ AND mode = $mode
71
+ AND core = $core
72
+ AND status = "regular"
73
+ ORDER BY updatedAt DESC`
74
+
75
+ function toSafeDirectIdSegment(value: string): string {
76
+ return value.replace(/[^a-zA-Z0-9_-]/g, '_')
77
+ }
78
+
79
+ function toRecordIdValueString(value: RecordIdRef, fallbackTable: string): string {
80
+ const canonical = recordIdToString(ensureRecordId(value, fallbackTable), fallbackTable)
81
+ const prefix = `${fallbackTable}:`
82
+ const withoutTable = canonical.startsWith(prefix) ? canonical.slice(prefix.length) : canonical
83
+ const wrappedMatch = withoutTable.match(/^⟨(.+)⟩$/)
84
+ return wrappedMatch ? wrappedMatch[1] : withoutTable
85
+ }
86
+
87
+ function isRecordIdInput(value: unknown): value is RecordIdInput {
88
+ if (typeof value === 'string' || value instanceof RecordId || value instanceof StringRecordId) {
89
+ return true
90
+ }
91
+
92
+ if (!value || typeof value !== 'object') {
93
+ return false
94
+ }
95
+
96
+ const record = value as { tb?: unknown; id?: unknown }
97
+ return typeof record.tb === 'string' && record.id !== undefined
98
+ }
99
+
100
+ function getAgentDisplayName(agentId: string): string {
101
+ return agentDisplayNames[agentId] ?? agentId
102
+ }
103
+
104
+ function requireDirectAgentId(agentId: string | undefined): string {
105
+ if (!agentId) {
106
+ throw new Error('Direct workstreams require an agentId')
107
+ }
108
+
109
+ return agentId
110
+ }
111
+
112
+ function requirestring(coreType: string | undefined): string {
113
+ if (!coreType) {
114
+ throw new Error('Core workstreams require a coreType')
115
+ }
116
+
117
+ return coreType
118
+ }
119
+
120
+ function buildDirectWorkstreamId({
121
+ userId,
122
+ orgId,
123
+ agentId,
124
+ }: {
125
+ userId: RecordIdRef
126
+ orgId: RecordIdRef
127
+ agentId: string
128
+ }): RecordId {
129
+ const userValue = toSafeDirectIdSegment(toRecordIdValueString(userId, TABLES.USER))
130
+ const orgValue = toSafeDirectIdSegment(toRecordIdValueString(orgId, TABLES.ORGANIZATION))
131
+ return new RecordId(TABLES.WORKSTREAM, `direct_${agentId}_user_${userValue}_organization_${orgValue}`)
132
+ }
133
+
134
+ function buildCoreWorkstreamId({
135
+ userId,
136
+ orgId,
137
+ coreType,
138
+ }: {
139
+ userId: RecordIdRef
140
+ orgId: RecordIdRef
141
+ coreType: string
142
+ }): RecordId {
143
+ const userValue = toSafeDirectIdSegment(toRecordIdValueString(userId, TABLES.USER))
144
+ const orgValue = toSafeDirectIdSegment(toRecordIdValueString(orgId, TABLES.ORGANIZATION))
145
+ const typeValue = toSafeDirectIdSegment(coreType)
146
+ return new RecordId(TABLES.WORKSTREAM, `core_${typeValue}_user_${userValue}_organization_${orgValue}`)
147
+ }
148
+
149
+ function toOptionalIsoDateTimeString(value: number | null | undefined): string | null {
150
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return null
151
+ return toIsoDateTimeString(new Date(value))
152
+ }
153
+
154
+ function getCompactedSummaryFocus(chatSummary: string | null): string | null {
155
+ if (!chatSummary) return null
156
+
157
+ const lines = chatSummary
158
+ .split('\n')
159
+ .map((line) => line.trim())
160
+ .filter(Boolean)
161
+
162
+ for (const line of lines) {
163
+ if (line.endsWith(':')) continue
164
+ if (line.startsWith('- ')) {
165
+ return toOptionalTrimmedString(line.slice(2))
166
+ }
167
+ return toOptionalTrimmedString(line)
168
+ }
169
+
170
+ return null
171
+ }
172
+
173
+ function parsePersistedWorkstreamState(value: unknown): WorkstreamState | null {
174
+ const parsed = WorkstreamStateSchema.safeParse(value)
175
+ return parsed.success ? parsed.data : null
176
+ }
177
+
178
+ function buildEmptyWorkstreamStateProgress(): PublicWorkstreamStateProgress {
179
+ return {
180
+ hasState: false,
181
+ lastUpdated: null,
182
+ completionRatio: null,
183
+ tasks: { total: 0, open: 0, inProgress: 0, done: 0, blocked: 0 },
184
+ constraints: { total: 0, approved: 0, candidate: 0 },
185
+ keyDecisions: 0,
186
+ openQuestions: 0,
187
+ risks: 0,
188
+ artifacts: 0,
189
+ agentContributions: 0,
190
+ }
191
+ }
192
+
193
+ function buildWorkstreamStateProgress(state: WorkstreamState | null): PublicWorkstreamStateProgress {
194
+ if (!state) {
195
+ return buildEmptyWorkstreamStateProgress()
196
+ }
197
+
198
+ const tasks = {
199
+ total: state.tasks.length,
200
+ open: state.tasks.filter((task) => task.status === 'open').length,
201
+ inProgress: state.tasks.filter((task) => task.status === 'in-progress').length,
202
+ done: state.tasks.filter((task) => task.status === 'done').length,
203
+ blocked: state.tasks.filter((task) => task.status === 'blocked').length,
204
+ }
205
+ const constraintsApproved = state.activeConstraints.filter((constraint) => constraint.approved).length
206
+
207
+ return {
208
+ hasState:
209
+ state.currentPlan !== null ||
210
+ state.activeConstraints.length > 0 ||
211
+ state.keyDecisions.length > 0 ||
212
+ state.tasks.length > 0 ||
213
+ state.openQuestions.length > 0 ||
214
+ state.risks.length > 0 ||
215
+ state.artifacts.length > 0 ||
216
+ state.agentContributions.length > 0 ||
217
+ toOptionalTrimmedString(state.approvedBy) !== null ||
218
+ typeof state.approvedAt === 'number' ||
219
+ toOptionalTrimmedString(state.approvalMessageId) !== null ||
220
+ toOptionalTrimmedString(state.approvalNote) !== null,
221
+ lastUpdated: toOptionalIsoDateTimeString(state.lastUpdated),
222
+ completionRatio: tasks.total > 0 ? Number((tasks.done / tasks.total).toFixed(4)) : null,
223
+ tasks,
224
+ constraints: {
225
+ total: state.activeConstraints.length,
226
+ approved: constraintsApproved,
227
+ candidate: state.activeConstraints.length - constraintsApproved,
228
+ },
229
+ keyDecisions: state.keyDecisions.length,
230
+ openQuestions: state.openQuestions.length,
231
+ risks: state.risks.length,
232
+ artifacts: state.artifacts.length,
233
+ agentContributions: state.agentContributions.length,
234
+ }
235
+ }
236
+
237
+ function buildWorkstreamApprovalState(state: WorkstreamState | null): PublicWorkstreamApprovalState | null {
238
+ if (!state) return null
239
+
240
+ const approvedBy = toOptionalTrimmedString(state.approvedBy)
241
+ const approvedAt = toOptionalIsoDateTimeString(state.approvedAt)
242
+ const approvalMessageId = toOptionalTrimmedString(state.approvalMessageId)
243
+ const approvalNote = toOptionalTrimmedString(state.approvalNote)
244
+
245
+ if (!approvedBy && !approvedAt && !approvalMessageId && !approvalNote) {
246
+ return null
247
+ }
248
+
249
+ return { approvedBy, approvedAt, approvalMessageId, approvalNote }
250
+ }
251
+
252
+ function buildWorkstreamStateFocus(
253
+ state: WorkstreamState | null,
254
+ chatSummary: string | null,
255
+ ): PublicWorkstreamStateFocus | null {
256
+ if (!state) {
257
+ const compactedSummaryFocus = getCompactedSummaryFocus(chatSummary)
258
+ return compactedSummaryFocus ? { kind: 'chat-summary', text: compactedSummaryFocus } : null
259
+ }
260
+
261
+ const currentPlan = toOptionalTrimmedString(state.currentPlan?.text)
262
+ if (currentPlan) {
263
+ return { kind: 'plan', text: currentPlan }
264
+ }
265
+
266
+ const latestTask =
267
+ [...state.tasks].reverse().find((task) => task.status === 'blocked') ??
268
+ [...state.tasks].reverse().find((task) => task.status === 'in-progress') ??
269
+ [...state.tasks].reverse().find((task) => task.status === 'open')
270
+ const taskTitle = toOptionalTrimmedString(latestTask?.title)
271
+ if (taskTitle) {
272
+ return { kind: 'task', text: taskTitle }
273
+ }
274
+
275
+ const latestQuestion = toOptionalTrimmedString(state.openQuestions.at(-1)?.text)
276
+ if (latestQuestion) {
277
+ return { kind: 'question', text: latestQuestion }
278
+ }
279
+
280
+ const latestDecision = toOptionalTrimmedString(state.keyDecisions.at(-1)?.decision)
281
+ if (latestDecision) {
282
+ return { kind: 'decision', text: latestDecision }
283
+ }
284
+
285
+ const latestAgentNote = toOptionalTrimmedString(state.agentContributions.at(-1)?.summary)
286
+ if (latestAgentNote) {
287
+ return { kind: 'agent-note', text: latestAgentNote }
288
+ }
289
+
290
+ const compactedSummaryFocus = getCompactedSummaryFocus(chatSummary)
291
+ if (compactedSummaryFocus) {
292
+ return { kind: 'chat-summary', text: compactedSummaryFocus }
293
+ }
294
+
295
+ return null
296
+ }
297
+
298
+ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
299
+ constructor() {
300
+ super(TABLES.WORKSTREAM, WorkstreamSchema)
301
+ }
302
+
303
+ async createWorkstream(
304
+ userId: RecordIdRef,
305
+ orgId: RecordIdRef,
306
+ options?: { title?: string; mode?: string; agentId?: string; core?: boolean; coreType?: string },
307
+ ): Promise<NormalizedWorkstream> {
308
+ const mode = options?.mode ?? 'group'
309
+ const directAgentId = options?.agentId
310
+ const core = options?.core === true
311
+ const coreType = options?.coreType
312
+
313
+ if (mode === 'direct' && !directAgentId) {
314
+ throw new Error('Direct workstreams require an agentId')
315
+ }
316
+ if (mode === 'group' && directAgentId) {
317
+ throw new Error('Group workstreams cannot set agentId')
318
+ }
319
+ if (mode === 'direct' && core) {
320
+ throw new Error('Direct workstreams cannot be core workstreams')
321
+ }
322
+ if (core && mode !== 'group') {
323
+ throw new Error('Core workstreams must use group mode')
324
+ }
325
+ if (core && !coreType) {
326
+ throw new Error('Core workstreams require a coreType')
327
+ }
328
+ if (!core && coreType) {
329
+ throw new Error('Only core workstreams can set a coreType')
330
+ }
331
+ const title = (() => {
332
+ if (options?.title) {
333
+ return options.title
334
+ }
335
+ if (core) {
336
+ return getCoreWorkstreamProfile(requirestring(coreType)).config.title
337
+ }
338
+ if (mode === 'direct') {
339
+ return getAgentDisplayName(requireDirectAgentId(directAgentId))
340
+ }
341
+ return WORKSTREAM.DEFAULT_TITLE
342
+ })()
343
+
344
+ if (mode === 'direct') {
345
+ const agentId = requireDirectAgentId(directAgentId)
346
+ const directWorkstreamId = buildDirectWorkstreamId({ userId, orgId, agentId })
347
+
348
+ const existing = await this.findById(directWorkstreamId)
349
+ if (existing) {
350
+ return this.normalizeWorkstream(existing)
351
+ }
352
+
353
+ let createError: unknown = null
354
+
355
+ let workstream = await databaseService
356
+ .createWithId(
357
+ TABLES.WORKSTREAM,
358
+ directWorkstreamId,
359
+ { userId, organizationId: orgId, mode, core: false, agentId, title, status: 'regular' },
360
+ WorkstreamSchema,
361
+ )
362
+ .catch((error) => {
363
+ createError = error
364
+ return null
365
+ })
366
+
367
+ if (!workstream) {
368
+ workstream = await this.findById(directWorkstreamId)
369
+ }
370
+
371
+ if (!workstream) {
372
+ if (createError instanceof Error) {
373
+ throw createError
374
+ }
375
+ throw new Error('Failed to create or load direct workstream')
376
+ }
377
+
378
+ return this.normalizeWorkstream(workstream)
379
+ }
380
+
381
+ if (core) {
382
+ const resolvedCoreType = requirestring(coreType)
383
+ const coreProfile = getCoreWorkstreamProfile(resolvedCoreType)
384
+ const coreWorkstreamId = buildCoreWorkstreamId({ userId, orgId, coreType: resolvedCoreType })
385
+ const existing = await this.findById(coreWorkstreamId)
386
+ if (existing) {
387
+ return this.normalizeWorkstream(existing)
388
+ }
389
+
390
+ let createError: unknown = null
391
+
392
+ let workstream = await databaseService
393
+ .createWithId(
394
+ TABLES.WORKSTREAM,
395
+ coreWorkstreamId,
396
+ {
397
+ userId,
398
+ organizationId: orgId,
399
+ mode,
400
+ core: true,
401
+ coreType: resolvedCoreType,
402
+ agentId: coreProfile.config.agentId,
403
+ title,
404
+ status: 'regular',
405
+ },
406
+ WorkstreamSchema,
407
+ )
408
+ .catch((error) => {
409
+ createError = error
410
+ return null
411
+ })
412
+
413
+ if (!workstream) {
414
+ workstream = await this.findById(coreWorkstreamId)
415
+ }
416
+
417
+ if (!workstream) {
418
+ if (createError instanceof Error) {
419
+ throw createError
420
+ }
421
+ throw new Error('Failed to create or load core workstream')
422
+ }
423
+
424
+ return this.normalizeWorkstream(workstream)
425
+ }
426
+
427
+ const groupWorkstream = await this.create({
428
+ userId,
429
+ organizationId: orgId,
430
+ mode,
431
+ core: false,
432
+ title,
433
+ status: 'regular',
434
+ })
435
+
436
+ return this.normalizeWorkstream(groupWorkstream)
437
+ }
438
+
439
+ async ensureBootstrapWorkstreams(
440
+ userId: RecordIdRef,
441
+ orgId: RecordIdRef,
442
+ options?: { onboardStatus?: string; userName?: string | null },
443
+ ): Promise<void> {
444
+ const onboardStatus = options?.onboardStatus ?? 'completed'
445
+ const onboardingCompleted = onboardStatus === 'completed'
446
+ const bootstrapConfig = getWorkstreamBootstrapConfig()
447
+
448
+ const existingWorkstreams = await databaseService.findMany(
449
+ TABLES.WORKSTREAM,
450
+ { userId, organizationId: orgId },
451
+ WorkstreamSchema,
452
+ )
453
+
454
+ const hasStandardGroupWorkstream = existingWorkstreams.some(
455
+ (workstream) => workstream.mode === 'group' && workstream.core !== true,
456
+ )
457
+ const directWorkstreamsByAgent = new Map<string, WorkstreamRecord>()
458
+ const coreWorkstreamsByType = new Map<string, WorkstreamRecord>()
459
+ for (const workstream of existingWorkstreams) {
460
+ if (workstream.mode !== 'direct' || !workstream.agentId) continue
461
+ directWorkstreamsByAgent.set(workstream.agentId, workstream)
462
+ }
463
+ for (const workstream of existingWorkstreams) {
464
+ if (workstream.mode !== 'group' || workstream.core !== true) continue
465
+ if (typeof workstream.coreType !== 'string') continue
466
+ coreWorkstreamsByType.set(workstream.coreType, workstream)
467
+ }
468
+
469
+ const requiredDirectAgents = onboardingCompleted
470
+ ? bootstrapConfig.completedDirectAgents
471
+ : bootstrapConfig.onboardingDirectAgents
472
+ const creations: Promise<NormalizedWorkstream>[] = []
473
+ for (const agentId of requiredDirectAgents) {
474
+ if (directWorkstreamsByAgent.has(agentId)) continue
475
+ creations.push(
476
+ this.createWorkstream(userId, orgId, { mode: 'direct', agentId, title: getAgentDisplayName(agentId) }),
477
+ )
478
+ }
479
+
480
+ if (onboardingCompleted && bootstrapConfig.ensureDefaultGroupOnCompleted && !hasStandardGroupWorkstream) {
481
+ creations.push(
482
+ this.createWorkstream(userId, orgId, { mode: 'group', core: false, title: WORKSTREAM.DEFAULT_TITLE }),
483
+ )
484
+ }
485
+
486
+ if (onboardingCompleted) {
487
+ for (const coreType of bootstrapConfig.coreTypesAfterOnboarding) {
488
+ if (coreWorkstreamsByType.has(coreType)) continue
489
+ creations.push(
490
+ this.createWorkstream(userId, orgId, {
491
+ mode: 'group',
492
+ core: true,
493
+ coreType,
494
+ title: getCoreWorkstreamProfile(coreType).config.title,
495
+ }),
496
+ )
497
+ }
498
+ }
499
+
500
+ let createdWorkstreams: NormalizedWorkstream[] = []
501
+ if (creations.length > 0) {
502
+ createdWorkstreams = await Promise.all(creations)
503
+ }
504
+
505
+ const onboardingWelcome = bootstrapConfig.onboardingWelcome
506
+ if (!onboardingCompleted && onboardingWelcome) {
507
+ const createdChiefWorkstream = createdWorkstreams.find(
508
+ (workstream) => workstream.mode === 'direct' && workstream.agentId === onboardingWelcome.directAgentId,
509
+ )
510
+ const existingChiefWorkstream = directWorkstreamsByAgent.get(onboardingWelcome.directAgentId)
511
+
512
+ const chiefWorkstreamId =
513
+ createdChiefWorkstream?.id ??
514
+ (existingChiefWorkstream ? this.normalizeWorkstreamId(existingChiefWorkstream.id) : null)
515
+
516
+ if (chiefWorkstreamId) {
517
+ const chiefWorkstreamRef = ensureRecordId(chiefWorkstreamId, TABLES.WORKSTREAM)
518
+ await workstreamMessageService.ensureBootstrapWelcomeMessage({
519
+ workstreamId: chiefWorkstreamRef,
520
+ agentId: onboardingWelcome.directAgentId,
521
+ text: onboardingWelcome.buildMessageText({ userName: options?.userName }),
522
+ })
523
+ }
524
+ }
525
+ }
526
+
527
+ async listWorkstreams(
528
+ userId: RecordIdRef,
529
+ orgId: RecordIdRef,
530
+ options: { mode: string; core?: boolean; take?: number; page?: number; includeArchived: boolean },
531
+ ): Promise<{ workstreams: NormalizedWorkstream[]; hasMore: boolean }> {
532
+ const core = options.core === true
533
+ if (options.mode === 'direct' && core) {
534
+ throw new Error('Direct workstreams cannot be queried as core workstreams')
535
+ }
536
+
537
+ if (options.mode === 'direct' || core) {
538
+ const query = options.includeArchived
539
+ ? LIST_ALL_WORKSTREAMS_BY_MODE_QUERY
540
+ : LIST_ALL_WORKSTREAMS_BY_MODE_EXCLUDE_ARCHIVED_QUERY
541
+ const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
542
+ new BoundQuery(query, { userId, orgId, mode: options.mode, core }),
543
+ WorkstreamSchema,
544
+ )
545
+
546
+ return { workstreams: workstreams.map((workstream) => this.normalizeWorkstream(workstream)), hasMore: false }
547
+ }
548
+
549
+ const take = options.take ?? WORKSTREAM.DEFAULT_PAGE_LIMIT
550
+ const page = options.page ?? 1
551
+ const query = options.includeArchived ? LIST_WORKSTREAMS_QUERY : LIST_WORKSTREAMS_EXCLUDE_ARCHIVED_QUERY
552
+ const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
553
+ new BoundQuery(query, {
554
+ userId,
555
+ orgId,
556
+ mode: options.mode,
557
+ core: false,
558
+ limit: take + 1,
559
+ offset: (page - 1) * take,
560
+ }),
561
+ WorkstreamSchema,
562
+ )
563
+
564
+ const hasMore = workstreams.length > take
565
+ const sliced = hasMore ? workstreams.slice(0, take) : workstreams
566
+
567
+ return { workstreams: sliced.map((workstream) => this.normalizeWorkstream(workstream)), hasMore }
568
+ }
569
+
570
+ async getWorkstream(workstreamId: RecordIdRef): Promise<NormalizedWorkstream> {
571
+ const workstream = await this.getById(workstreamId)
572
+ return this.normalizeWorkstream(workstream)
573
+ }
574
+
575
+ async getWorkstreamRecord(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
576
+ return await this.getById(workstreamId)
577
+ }
578
+
579
+ async updateTitle(workstreamId: RecordIdRef, title: string): Promise<NormalizedWorkstream> {
580
+ const existing = await this.getById(workstreamId)
581
+ this.assertMutableWorkstream(existing, 'rename')
582
+ const workstream = await this.update(workstreamId, { title })
583
+ return this.normalizeWorkstream(workstream)
584
+ }
585
+
586
+ async updateStatus(workstreamId: RecordIdRef, status: string): Promise<NormalizedWorkstream> {
587
+ const validStatus = WorkstreamStatusSchema.parse(status)
588
+ const existing = await this.getById(workstreamId)
589
+ this.assertMutableWorkstream(existing, validStatus === 'archived' ? 'archive' : 'unarchive')
590
+ const workstream = await this.update(workstreamId, { status: validStatus })
591
+ return this.normalizeWorkstream(workstream)
592
+ }
593
+
594
+ async setActiveRunId(workstreamId: RecordIdRef, runId: string | null): Promise<void> {
595
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
596
+ if (runId === null) {
597
+ await databaseService.query<unknown>(surql`
598
+ UPDATE ONLY ${workstreamRef}
599
+ SET activeRunId = NONE
600
+ `)
601
+ return
602
+ }
603
+
604
+ await databaseService.query<unknown>(surql`
605
+ UPDATE ONLY ${workstreamRef}
606
+ SET activeRunId = ${runId}
607
+ `)
608
+ }
609
+
610
+ async getActiveRunId(workstreamId: RecordIdRef): Promise<string | null> {
611
+ const workstream = await this.getById(workstreamId)
612
+ const activeRunId = workstream.activeRunId
613
+ if (typeof activeRunId !== 'string') return null
614
+ const normalized = activeRunId.trim()
615
+ return normalized.length > 0 ? normalized : null
616
+ }
617
+
618
+ async clearActiveRunIdIfMatches(workstreamId: RecordIdRef, runId: string): Promise<void> {
619
+ const activeRunId = await this.getActiveRunId(workstreamId)
620
+ if (activeRunId !== runId) return
621
+ await this.setActiveRunId(workstreamId, null)
622
+ }
623
+
624
+ async persistChangeTracker(
625
+ workstreamId: RecordIdRef,
626
+ payload: { chatSummary: string; state: WorkstreamState },
627
+ ): Promise<void> {
628
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
629
+ await this.update(workstreamRef, { chatSummary: payload.chatSummary, state: payload.state })
630
+ }
631
+
632
+ async stopActiveRun(workstreamId: RecordIdRef): Promise<boolean> {
633
+ const activeRunId = await this.getActiveRunId(workstreamId)
634
+ if (!activeRunId) return false
635
+
636
+ const stopped = chatRunRegistry.stop(activeRunId, new DOMException('Run stopped by user.', 'AbortError'))
637
+ if (stopped) {
638
+ return true
639
+ }
640
+
641
+ await this.clearActiveRunIdIfMatches(workstreamId, activeRunId)
642
+ return false
643
+ }
644
+
645
+ async setCompacting(workstreamId: RecordIdRef, value: boolean): Promise<void> {
646
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
647
+ await databaseService.query<unknown>(surql`
648
+ UPDATE ONLY ${workstreamRef}
649
+ SET isCompacting = ${value}
650
+ `)
651
+ }
652
+
653
+ async appendMemoryBlock(workstreamId: RecordIdRef, entry: string): Promise<string> {
654
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
655
+ const workstream = await this.getById(workstreamRef)
656
+ const entries = parseMemoryBlock(workstream.memoryBlock)
657
+
658
+ const labelMatch = entry.match(/^(\w+):\s*/i)
659
+ const role = labelMatch ? labelMatch[1].toLowerCase() : 'system'
660
+ const content = labelMatch ? entry.slice(labelMatch[0].length).trim() : entry.trim()
661
+
662
+ const updatedEntries = appendToMemoryBlock(entries, role, content)
663
+ const serialized = serializeMemoryBlock(updatedEntries)
664
+
665
+ await this.update(workstreamRef, { memoryBlock: serialized })
666
+
667
+ if (updatedEntries.length >= MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES) {
668
+ void this.compactMemoryBlock(workstreamRef).catch(() => {})
669
+ }
670
+
671
+ return this.formatMemoryBlockForPrompt({
672
+ memoryBlock: serialized,
673
+ memoryBlockSummary: workstream.memoryBlockSummary,
674
+ })
675
+ }
676
+
677
+ async compactMemoryBlock(workstreamId: RecordIdRef): Promise<boolean> {
678
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
679
+ const workstream = await this.getById(workstreamRef)
680
+ const result = await compactMemoryBlockEntries({
681
+ previousSummary: workstream.memoryBlockSummary,
682
+ entries: parseMemoryBlock(workstream.memoryBlock),
683
+ triggerEntries: MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES,
684
+ chunkEntries: MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES,
685
+ compact: (params) => contextCompactionService.compactMemoryBlock(params),
686
+ })
687
+
688
+ if (!result.compacted) return false
689
+
690
+ await this.update(workstreamRef, {
691
+ memoryBlockSummary: result.summary || '',
692
+ memoryBlock: serializeMemoryBlock(result.entries),
693
+ })
694
+
695
+ return true
696
+ }
697
+
698
+ async deleteWorkstream(workstreamId: RecordIdRef): Promise<void> {
699
+ const existing = await this.getById(workstreamId)
700
+ this.assertMutableWorkstream(existing, 'delete')
701
+ await this.delete(workstreamId)
702
+ }
703
+
704
+ async listRecentWorkstreams({
705
+ userId,
706
+ orgId,
707
+ excludeWorkstreamId,
708
+ limit,
709
+ }: {
710
+ userId: RecordIdRef
711
+ orgId: RecordIdRef
712
+ excludeWorkstreamId?: RecordIdRef
713
+ limit: number
714
+ }) {
715
+ let excludeCondition = ''
716
+ const vars: Record<string, unknown> = { userId, orgId, limit }
717
+
718
+ if (excludeWorkstreamId) {
719
+ excludeCondition = 'AND id != $excludeWorkstreamId'
720
+ vars.excludeWorkstreamId = excludeWorkstreamId
721
+ }
722
+
723
+ const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
724
+ new BoundQuery(
725
+ `SELECT * FROM ${TABLES.WORKSTREAM}
726
+ WHERE userId = $userId
727
+ AND organizationId = $orgId
728
+ ${excludeCondition}
729
+ AND status != "archived"
730
+ ORDER BY updatedAt DESC
731
+ LIMIT $limit`,
732
+ vars,
733
+ ),
734
+ WorkstreamSchema,
735
+ )
736
+
737
+ return workstreams.map((workstream) => this.normalizeWorkstream(workstream))
738
+ }
739
+
740
+ private normalizeWorkstreamId(id: unknown): string {
741
+ return this.normalizeRecordIdString(id, TABLES.WORKSTREAM)
742
+ }
743
+
744
+ private normalizeRecordIdString(id: unknown, table: DatabaseTable): string {
745
+ if (!isRecordIdInput(id)) {
746
+ throw new Error(`Invalid record id for table ${table}`)
747
+ }
748
+
749
+ return recordIdToString(id, String(table))
750
+ }
751
+
752
+ formatMemoryBlockForPrompt(workstream: Pick<WorkstreamRecord, 'memoryBlock' | 'memoryBlockSummary'>): string {
753
+ return formatPersistedMemoryBlockForPrompt({
754
+ summary: workstream.memoryBlockSummary,
755
+ entries: parseMemoryBlock(workstream.memoryBlock),
756
+ })
757
+ }
758
+
759
+ private getDefaultTitle(workstream: Pick<WorkstreamRecord, 'core' | 'coreType'>): string {
760
+ if (workstream.core === true && typeof workstream.coreType === 'string') {
761
+ return getCoreWorkstreamProfile(workstream.coreType).config.title
762
+ }
763
+
764
+ return WORKSTREAM.DEFAULT_TITLE
765
+ }
766
+
767
+ normalizeWorkstream(workstream: WorkstreamRecord): NormalizedWorkstream {
768
+ const activeRunId =
769
+ typeof workstream.activeRunId === 'string' && workstream.activeRunId.trim().length > 0
770
+ ? workstream.activeRunId
771
+ : null
772
+ const isCompacting = workstream.isCompacting === true
773
+ const mode = typeof workstream.mode === 'string' ? workstream.mode : 'group'
774
+ const core = workstream.core === true
775
+ const coreType = core && typeof workstream.coreType === 'string' ? workstream.coreType : undefined
776
+ const status = typeof workstream.status === 'string' ? workstream.status : 'regular'
777
+ return {
778
+ id: this.normalizeWorkstreamId(workstream.id),
779
+ userId: this.normalizeRecordIdString(workstream.userId, TABLES.USER),
780
+ organizationId: this.normalizeRecordIdString(workstream.organizationId, TABLES.ORGANIZATION),
781
+ mode,
782
+ core,
783
+ ...(coreType ? { coreType } : {}),
784
+ isRunning: activeRunId !== null,
785
+ isCompacting,
786
+ ...(isAgentName(workstream.agentId) ? { agentId: workstream.agentId } : {}),
787
+ title: workstream.title ?? this.getDefaultTitle(workstream),
788
+ status,
789
+ memoryBlock: this.formatMemoryBlockForPrompt(workstream),
790
+ createdAt: toIsoDateTimeString(workstream.createdAt),
791
+ updatedAt: toIsoDateTimeString(workstream.updatedAt),
792
+ }
793
+ }
794
+
795
+ toPublicWorkstream(workstream: NormalizedWorkstream | WorkstreamRecord) {
796
+ const id = typeof workstream.id === 'string' ? workstream.id : this.normalizeWorkstreamId(workstream.id)
797
+ const createdAt = toIsoDateTimeString(workstream.createdAt)
798
+ const updatedAt = toIsoDateTimeString(workstream.updatedAt)
799
+ const activeRunId =
800
+ 'activeRunId' in workstream &&
801
+ typeof workstream.activeRunId === 'string' &&
802
+ workstream.activeRunId.trim().length > 0
803
+ ? workstream.activeRunId
804
+ : null
805
+ const isRunning = 'isRunning' in workstream ? workstream.isRunning : activeRunId !== null
806
+ const isCompacting = workstream.isCompacting === true
807
+ const mode = typeof workstream.mode === 'string' ? workstream.mode : 'group'
808
+ const core = workstream.core === true
809
+ const coreType = core && typeof workstream.coreType === 'string' ? workstream.coreType : undefined
810
+ return {
811
+ id,
812
+ mode,
813
+ core,
814
+ ...(coreType ? { coreType } : {}),
815
+ ...(isAgentName(workstream.agentId) ? { agentId: workstream.agentId } : {}),
816
+ title: workstream.title ?? this.getDefaultTitle(workstream),
817
+ status: workstream.status ?? 'regular',
818
+ isRunning,
819
+ isCompacting,
820
+ createdAt,
821
+ updatedAt,
822
+ }
823
+ }
824
+
825
+ toPublicWorkstreamDetail(workstream: WorkstreamRecord): PublicWorkstreamDetail {
826
+ const publicWorkstream = this.toPublicWorkstream(workstream)
827
+ const snapshot = parsePersistedWorkstreamState(workstream.state)
828
+ const chatSummary = toOptionalTrimmedString(workstream.chatSummary)
829
+ const progress = buildWorkstreamStateProgress(snapshot)
830
+ const workstreamState: PublicWorkstreamStatePayload = {
831
+ focus: buildWorkstreamStateFocus(snapshot, chatSummary),
832
+ chatSummary,
833
+ approval: buildWorkstreamApprovalState(snapshot),
834
+ progress,
835
+ snapshot,
836
+ }
837
+
838
+ return { ...publicWorkstream, workstreamState }
839
+ }
840
+
841
+ private assertMutableWorkstream(
842
+ workstream: WorkstreamRecord,
843
+ action: 'rename' | 'archive' | 'unarchive' | 'delete',
844
+ ): void {
845
+ if (workstream.mode === 'direct') {
846
+ throw new Error(`Direct workstreams cannot be ${action}d`)
847
+ }
848
+ if (workstream.core === true) {
849
+ throw new Error(`Core workstreams cannot be ${action}d`)
850
+ }
851
+ }
852
+ }
853
+
854
+ export const workstreamService = new WorkstreamService()