@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.
- package/infrastructure/schema/00_workstream.surql +55 -0
- package/infrastructure/schema/01_memory.surql +47 -0
- package/infrastructure/schema/02_execution_plan.surql +62 -0
- package/infrastructure/schema/03_learned_skill.surql +32 -0
- package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
- package/package.json +128 -0
- package/src/ai/definitions.ts +308 -0
- package/src/bifrost/bifrost.ts +256 -0
- package/src/config/agent-defaults.ts +99 -0
- package/src/config/constants.ts +33 -0
- package/src/config/env-shapes.ts +122 -0
- package/src/config/logger.ts +29 -0
- package/src/config/model-constants.ts +31 -0
- package/src/config/search.ts +17 -0
- package/src/config/workstream-defaults.ts +68 -0
- package/src/db/base.service.ts +55 -0
- package/src/db/cursor-pagination.ts +73 -0
- package/src/db/memory-query-builder.ts +207 -0
- package/src/db/memory-store.helpers.ts +118 -0
- package/src/db/memory-store.rows.ts +29 -0
- package/src/db/memory-store.ts +974 -0
- package/src/db/memory-types.ts +193 -0
- package/src/db/memory.ts +505 -0
- package/src/db/record-id.ts +78 -0
- package/src/db/service.ts +932 -0
- package/src/db/startup.ts +152 -0
- package/src/db/tables.ts +20 -0
- package/src/document/org-document-chunking.ts +224 -0
- package/src/document/parsing.ts +40 -0
- package/src/embeddings/provider.ts +76 -0
- package/src/index.ts +302 -0
- package/src/queues/context-compaction.queue.ts +82 -0
- package/src/queues/document-processor.queue.ts +118 -0
- package/src/queues/memory-consolidation.queue.ts +65 -0
- package/src/queues/post-chat-memory.queue.ts +128 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
- package/src/queues/regular-chat-memory-digest.config.ts +12 -0
- package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
- package/src/queues/skill-extraction.config.ts +9 -0
- package/src/queues/skill-extraction.queue.ts +62 -0
- package/src/redis/connection.ts +176 -0
- package/src/redis/index.ts +30 -0
- package/src/redis/org-memory-lock.ts +43 -0
- package/src/redis/redis-lease-lock.ts +158 -0
- package/src/runtime/agent-contract.ts +1 -0
- package/src/runtime/agent-prompt-context.ts +119 -0
- package/src/runtime/agent-runtime-policy.ts +192 -0
- package/src/runtime/agent-stream-helpers.ts +117 -0
- package/src/runtime/agent-types.ts +22 -0
- package/src/runtime/approval-continuation.ts +16 -0
- package/src/runtime/chat-attachments.ts +46 -0
- package/src/runtime/chat-message.ts +10 -0
- package/src/runtime/chat-request-routing.ts +21 -0
- package/src/runtime/chat-run-orchestration.ts +25 -0
- package/src/runtime/chat-run-registry.ts +20 -0
- package/src/runtime/chat-types.ts +18 -0
- package/src/runtime/context-compaction-constants.ts +11 -0
- package/src/runtime/context-compaction-runtime.ts +86 -0
- package/src/runtime/context-compaction.ts +909 -0
- package/src/runtime/execution-plan.ts +59 -0
- package/src/runtime/helper-model.ts +405 -0
- package/src/runtime/indexed-repositories-policy.ts +28 -0
- package/src/runtime/instruction-sections.ts +8 -0
- package/src/runtime/llm-content.ts +71 -0
- package/src/runtime/memory-block.ts +264 -0
- package/src/runtime/memory-digest-policy.ts +14 -0
- package/src/runtime/memory-format.ts +8 -0
- package/src/runtime/memory-pipeline.ts +570 -0
- package/src/runtime/memory-prompts-fact.ts +47 -0
- package/src/runtime/memory-prompts-parse.ts +3 -0
- package/src/runtime/memory-prompts-update.ts +37 -0
- package/src/runtime/memory-scope.ts +43 -0
- package/src/runtime/plugin-types.ts +10 -0
- package/src/runtime/retrieval-adapters.ts +25 -0
- package/src/runtime/retrieval-pipeline.ts +3 -0
- package/src/runtime/runtime-extensions.ts +154 -0
- package/src/runtime/skill-extraction-policy.ts +3 -0
- package/src/runtime/team-consultation-orchestrator.ts +245 -0
- package/src/runtime/team-consultation-prompts.ts +32 -0
- package/src/runtime/title-helpers.ts +12 -0
- package/src/runtime/turn-lifecycle.ts +28 -0
- package/src/runtime/workstream-chat-helpers.ts +187 -0
- package/src/runtime/workstream-routing-policy.ts +301 -0
- package/src/runtime/workstream-state.ts +261 -0
- package/src/services/attachment.service.ts +159 -0
- package/src/services/chat-attachments.service.ts +17 -0
- package/src/services/chat-run-registry.service.ts +3 -0
- package/src/services/context-compaction-runtime.ts +13 -0
- package/src/services/context-compaction.service.ts +115 -0
- package/src/services/document-chunk.service.ts +141 -0
- package/src/services/execution-plan.service.ts +890 -0
- package/src/services/learned-skill.service.ts +328 -0
- package/src/services/memory-assessment.service.ts +43 -0
- package/src/services/memory.service.ts +807 -0
- package/src/services/memory.utils.ts +84 -0
- package/src/services/mutating-approval.service.ts +110 -0
- package/src/services/recent-activity-title.service.ts +74 -0
- package/src/services/recent-activity.service.ts +397 -0
- package/src/services/workstream-change-tracker.service.ts +313 -0
- package/src/services/workstream-message.service.ts +283 -0
- package/src/services/workstream-title.service.ts +58 -0
- package/src/services/workstream-turn-preparation.ts +1340 -0
- package/src/services/workstream-turn.ts +37 -0
- package/src/services/workstream.service.ts +854 -0
- package/src/services/workstream.types.ts +118 -0
- package/src/storage/attachment-parser.ts +101 -0
- package/src/storage/attachment-storage.service.ts +391 -0
- package/src/storage/attachments.types.ts +11 -0
- package/src/storage/attachments.utils.ts +58 -0
- package/src/storage/generated-document-storage.service.ts +55 -0
- package/src/system-agents/agent-result.ts +27 -0
- package/src/system-agents/context-compacter.agent.ts +46 -0
- package/src/system-agents/delegated-agent-factory.ts +177 -0
- package/src/system-agents/helper-agent-options.ts +20 -0
- package/src/system-agents/memory-reranker.agent.ts +38 -0
- package/src/system-agents/memory.agent.ts +58 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
- package/src/system-agents/researcher.agent.ts +34 -0
- package/src/system-agents/skill-extractor.agent.ts +88 -0
- package/src/system-agents/skill-manager.agent.ts +80 -0
- package/src/system-agents/title-generator.agent.ts +42 -0
- package/src/system-agents/workstream-tracker.agent.ts +58 -0
- package/src/tools/execution-plan.tool.ts +163 -0
- package/src/tools/fetch-webpage.tool.ts +132 -0
- package/src/tools/firecrawl-client.ts +12 -0
- package/src/tools/memory-block.tool.ts +55 -0
- package/src/tools/read-file-parts.tool.ts +80 -0
- package/src/tools/remember-memory.tool.ts +85 -0
- package/src/tools/research-topic.tool.ts +15 -0
- package/src/tools/search-tools.ts +55 -0
- package/src/tools/search-web.tool.ts +175 -0
- package/src/tools/team-think.tool.ts +125 -0
- package/src/tools/tool-contract.ts +21 -0
- package/src/tools/user-questions.tool.ts +18 -0
- package/src/utils/async.ts +50 -0
- package/src/utils/date-time.ts +34 -0
- package/src/utils/error.ts +10 -0
- package/src/utils/errors.ts +28 -0
- package/src/utils/hono-error-handler.ts +71 -0
- package/src/utils/string.ts +51 -0
- package/src/workers/bootstrap.ts +44 -0
- package/src/workers/memory-consolidation.worker.ts +318 -0
- package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
- package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
- package/src/workers/skill-extraction.runner.ts +331 -0
- package/src/workers/skill-extraction.worker.ts +22 -0
- package/src/workers/utils/repo-indexer-chunker.ts +331 -0
- package/src/workers/utils/repo-structure-extractor.ts +645 -0
- package/src/workers/utils/repomix-process-concurrency.ts +65 -0
- package/src/workers/utils/sandbox-error.ts +5 -0
- 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()
|