@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,313 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { aiLogger } from '../config/logger'
|
|
4
|
+
import type { RecordIdRef } from '../db/record-id'
|
|
5
|
+
import { mergeStateDelta } from '../runtime/context-compaction'
|
|
6
|
+
import { createHelperModelRuntime, extractJsonObjectCandidates } from '../runtime/helper-model'
|
|
7
|
+
import { toOptionalTrimmedString } from '../runtime/workstream-chat-helpers'
|
|
8
|
+
import {
|
|
9
|
+
StructuredWorkstreamStateDeltaSchema,
|
|
10
|
+
createEmptyStructuredWorkstreamStateDelta,
|
|
11
|
+
createEmptyWorkstreamState,
|
|
12
|
+
parseStructuredWorkstreamStateDelta,
|
|
13
|
+
} from '../runtime/workstream-state'
|
|
14
|
+
import type { WorkstreamState, WorkstreamStateDelta } from '../runtime/workstream-state'
|
|
15
|
+
import { createWorkstreamTrackerAgent } from '../system-agents/workstream-tracker.agent'
|
|
16
|
+
import { compactWhitespace, isRecord, truncateText } from '../utils/string'
|
|
17
|
+
import { workstreamService } from './workstream.service'
|
|
18
|
+
|
|
19
|
+
const helperModelRuntime = createHelperModelRuntime()
|
|
20
|
+
|
|
21
|
+
const TrackerOutputSchema = z.object({
|
|
22
|
+
summary: z.string().trim().min(1).max(1_200),
|
|
23
|
+
stateDelta: StructuredWorkstreamStateDeltaSchema,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
type TrackerMessage = { label: string; text: string }
|
|
27
|
+
|
|
28
|
+
const TRACKER_JSON_WRAPPER_KEYS = ['output', 'result', 'data'] as const
|
|
29
|
+
const TRACKER_SUMMARY_KEYS = ['summary', 'chatSummary', 'summaryText', 'sidebarSummary', 'message'] as const
|
|
30
|
+
const TRACKER_STATE_DELTA_KEYS = ['stateDelta', 'delta', 'workstreamStateDelta', 'trackerStateDelta', 'state'] as const
|
|
31
|
+
const TRACKER_FALLBACK_SECTION_PREFIX =
|
|
32
|
+
/^(?:#{1,6}\s*)?(?:\*\*)?(?:state delta|current plan|new decisions|resolved questions|new questions|new constraints|new risks|task updates|artifacts|agent note|conflicts|approved by|approved at|approval message id|approval note)(?:\*\*)?\s*:?\s*/i
|
|
33
|
+
|
|
34
|
+
function renderMessages(messages: TrackerMessage[]): string {
|
|
35
|
+
if (messages.length === 0) return '- None'
|
|
36
|
+
return messages
|
|
37
|
+
.map((message, index) => `### Message ${index + 1}\n- Actor: ${message.label}\n- Content: ${message.text}`)
|
|
38
|
+
.join('\n\n')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getRecordStringField(record: Record<string, unknown>, keys: readonly string[]): string | null {
|
|
42
|
+
for (const key of keys) {
|
|
43
|
+
const field = toOptionalTrimmedString(typeof record[key] === 'string' ? record[key] : null)
|
|
44
|
+
if (field) return field
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getTrackerJsonObjects(value: unknown): Record<string, unknown>[] {
|
|
51
|
+
if (!isRecord(value)) return []
|
|
52
|
+
|
|
53
|
+
const candidates: Record<string, unknown>[] = [value]
|
|
54
|
+
for (const key of TRACKER_JSON_WRAPPER_KEYS) {
|
|
55
|
+
const nested = value[key]
|
|
56
|
+
if (isRecord(nested)) {
|
|
57
|
+
candidates.push(nested)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return candidates
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseTrackerJsonFallback(text: string): z.infer<typeof TrackerOutputSchema> | null {
|
|
65
|
+
for (const candidateText of extractJsonObjectCandidates(text)) {
|
|
66
|
+
let parsedJson: unknown
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
parsedJson = JSON.parse(candidateText) as unknown
|
|
70
|
+
} catch {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const candidate of getTrackerJsonObjects(parsedJson)) {
|
|
75
|
+
const summary = getRecordStringField(candidate, TRACKER_SUMMARY_KEYS)
|
|
76
|
+
if (!summary) continue
|
|
77
|
+
|
|
78
|
+
const stateDeltaValue = TRACKER_STATE_DELTA_KEYS.map((key) => candidate[key]).find((value) => value !== undefined)
|
|
79
|
+
const parsedStateDelta = StructuredWorkstreamStateDeltaSchema.safeParse(stateDeltaValue)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
summary: truncateText(summary, 1_200),
|
|
83
|
+
stateDelta: parsedStateDelta.success ? parsedStateDelta.data : createEmptyStructuredWorkstreamStateDelta(),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function extractTrackerFallbackSummary(text: string): string | null {
|
|
92
|
+
const normalized = text.replace(/\r/g, '').trim()
|
|
93
|
+
if (!normalized) return null
|
|
94
|
+
|
|
95
|
+
const summaryLineMatch = normalized.match(/(?:^|\n)(?:#{1,6}\s*)?(?:\*\*)?summary(?:\*\*)?\s*:?\s*(.+?)(?=\n|$)/i)
|
|
96
|
+
const summaryLine = summaryLineMatch?.[1]?.trim()
|
|
97
|
+
if (summaryLine && !summaryLine.startsWith('{') && !summaryLine.startsWith('```')) {
|
|
98
|
+
return truncateText(summaryLine, 1_200)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const summarySectionMatch = normalized.match(
|
|
102
|
+
/(?:^|\n)(?:#{1,6}\s*)?(?:\*\*)?summary(?:\*\*)?\s*:?\s*\n([\s\S]*?)(?=\n(?:#{1,6}\s*)?(?:\*\*)?(?:state delta|summary)(?:\*\*)?\s*:?\s*\n|$)/i,
|
|
103
|
+
)
|
|
104
|
+
const summarySource = summarySectionMatch?.[1]?.trim() || normalized
|
|
105
|
+
const paragraphs = summarySource
|
|
106
|
+
.split(/\n\s*\n/)
|
|
107
|
+
.map((paragraph) => paragraph.trim())
|
|
108
|
+
.filter((paragraph) => paragraph.length > 0)
|
|
109
|
+
|
|
110
|
+
for (const paragraph of paragraphs) {
|
|
111
|
+
const candidate = paragraph.replace(/^(?:#{1,6}\s*)?(?:\*\*)?summary(?:\*\*)?\s*:?\s*/i, '').trim()
|
|
112
|
+
if (
|
|
113
|
+
!candidate ||
|
|
114
|
+
candidate.startsWith('{') ||
|
|
115
|
+
candidate.startsWith('```') ||
|
|
116
|
+
TRACKER_FALLBACK_SECTION_PREFIX.test(candidate)
|
|
117
|
+
) {
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return truncateText(candidate, 1_200)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function parseTrackerTextFallback(text: string): z.infer<typeof TrackerOutputSchema> | null {
|
|
128
|
+
const jsonFallback = parseTrackerJsonFallback(text)
|
|
129
|
+
if (jsonFallback) {
|
|
130
|
+
return jsonFallback
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const summary = extractTrackerFallbackSummary(text)
|
|
134
|
+
if (!summary) return null
|
|
135
|
+
|
|
136
|
+
return { summary, stateDelta: createEmptyStructuredWorkstreamStateDelta() }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function extractHeuristicSummaryCandidate(text: string | null | undefined): string | null {
|
|
140
|
+
const normalized = compactWhitespace(toOptionalTrimmedString(text) ?? '')
|
|
141
|
+
if (!normalized) return null
|
|
142
|
+
|
|
143
|
+
const match = /^(.+?[.!?])(?:\s|$)/.exec(normalized)
|
|
144
|
+
const candidate = match ? String(match[1]) : normalized
|
|
145
|
+
return truncateText(candidate, 1_200)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function buildHeuristicTrackerSummary(params: {
|
|
149
|
+
previousSummary: string | null
|
|
150
|
+
userMessageText: string | null
|
|
151
|
+
assistantMessages: TrackerMessage[]
|
|
152
|
+
}): string | null {
|
|
153
|
+
for (const message of params.assistantMessages) {
|
|
154
|
+
const summary = extractHeuristicSummaryCandidate(message.text)
|
|
155
|
+
if (summary) {
|
|
156
|
+
return summary
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const userSummary = extractHeuristicSummaryCandidate(params.userMessageText)
|
|
161
|
+
if (userSummary) {
|
|
162
|
+
return truncateText(`User request: ${userSummary}`, 1_200)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return extractHeuristicSummaryCandidate(params.previousSummary)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function formatTrackerPrompt(params: {
|
|
169
|
+
title: string
|
|
170
|
+
mode: 'direct' | 'group'
|
|
171
|
+
coreType?: string
|
|
172
|
+
visibleAgentId?: string
|
|
173
|
+
hasActiveExecutionPlan: boolean
|
|
174
|
+
previousSummary: string | null
|
|
175
|
+
existingState: WorkstreamState
|
|
176
|
+
userMessageText: string | null
|
|
177
|
+
assistantMessages: TrackerMessage[]
|
|
178
|
+
}): string {
|
|
179
|
+
return [
|
|
180
|
+
'# Workstream Turn',
|
|
181
|
+
'',
|
|
182
|
+
`- Title: ${params.title}`,
|
|
183
|
+
`- Mode: ${params.mode}`,
|
|
184
|
+
`- Visible agent: ${params.visibleAgentId ?? 'none'}`,
|
|
185
|
+
`- Active execution plan: ${params.hasActiveExecutionPlan ? 'yes' : 'no'}`,
|
|
186
|
+
...(params.coreType ? [`- Core type: ${params.coreType}`] : []),
|
|
187
|
+
'',
|
|
188
|
+
'## Previous Summary',
|
|
189
|
+
params.previousSummary ?? 'None',
|
|
190
|
+
'',
|
|
191
|
+
'## Existing State',
|
|
192
|
+
JSON.stringify(params.existingState),
|
|
193
|
+
'',
|
|
194
|
+
'## User Message',
|
|
195
|
+
params.userMessageText ?? 'None',
|
|
196
|
+
'',
|
|
197
|
+
'## Assistant Messages',
|
|
198
|
+
renderMessages(params.assistantMessages),
|
|
199
|
+
...(params.hasActiveExecutionPlan
|
|
200
|
+
? [
|
|
201
|
+
'',
|
|
202
|
+
'## Tracker Constraint',
|
|
203
|
+
'An active execution plan exists. Do not update currentPlan or taskUpdates. Track only decisions, questions, risks, artifacts, approvals, and agent notes.',
|
|
204
|
+
]
|
|
205
|
+
: []),
|
|
206
|
+
].join('\n')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function applyTrackedStateDelta(params: {
|
|
210
|
+
existingState: WorkstreamState
|
|
211
|
+
delta: WorkstreamStateDelta
|
|
212
|
+
hasActiveExecutionPlan: boolean
|
|
213
|
+
now: () => number
|
|
214
|
+
}): WorkstreamState {
|
|
215
|
+
const mergedState = mergeStateDelta(params.existingState, params.delta, params.now)
|
|
216
|
+
if (!params.hasActiveExecutionPlan) {
|
|
217
|
+
return mergedState
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { ...mergedState, currentPlan: null, tasks: [] }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function updateWorkstreamChangeTracker(params: {
|
|
224
|
+
workstreamId: RecordIdRef
|
|
225
|
+
title: string
|
|
226
|
+
mode: 'direct' | 'group'
|
|
227
|
+
coreType?: string
|
|
228
|
+
visibleAgentId?: string
|
|
229
|
+
hasActiveExecutionPlan: boolean
|
|
230
|
+
previousSummary: string | null
|
|
231
|
+
existingState: WorkstreamState | null
|
|
232
|
+
userMessageText: string | null
|
|
233
|
+
assistantMessages: TrackerMessage[]
|
|
234
|
+
}): Promise<boolean> {
|
|
235
|
+
const assistantMessages = params.assistantMessages
|
|
236
|
+
.map((message) => ({
|
|
237
|
+
label: toOptionalTrimmedString(message.label) ?? 'Assistant',
|
|
238
|
+
text: truncateText(message.text.trim(), 2_400),
|
|
239
|
+
}))
|
|
240
|
+
.filter((message) => message.text.length > 0)
|
|
241
|
+
.slice(0, 6)
|
|
242
|
+
|
|
243
|
+
if (!toOptionalTrimmedString(params.userMessageText) && assistantMessages.length === 0) {
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const existingState = params.existingState ?? createEmptyWorkstreamState()
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const output = await helperModelRuntime.generateHelperStructured({
|
|
251
|
+
tag: 'workstream-change-tracker',
|
|
252
|
+
createAgent: createWorkstreamTrackerAgent,
|
|
253
|
+
schema: TrackerOutputSchema,
|
|
254
|
+
maxOutputTokens: 1_400,
|
|
255
|
+
textFallbackParser: parseTrackerTextFallback,
|
|
256
|
+
messages: [
|
|
257
|
+
{
|
|
258
|
+
role: 'user',
|
|
259
|
+
content: formatTrackerPrompt({
|
|
260
|
+
title: params.title,
|
|
261
|
+
mode: params.mode,
|
|
262
|
+
...(params.coreType ? { coreType: params.coreType } : {}),
|
|
263
|
+
...(params.visibleAgentId ? { visibleAgentId: params.visibleAgentId } : {}),
|
|
264
|
+
hasActiveExecutionPlan: params.hasActiveExecutionPlan,
|
|
265
|
+
previousSummary: params.previousSummary,
|
|
266
|
+
existingState,
|
|
267
|
+
userMessageText: toOptionalTrimmedString(params.userMessageText),
|
|
268
|
+
assistantMessages,
|
|
269
|
+
}),
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const sparseStateDelta = parseStructuredWorkstreamStateDelta(output.stateDelta)
|
|
275
|
+
const nextState = applyTrackedStateDelta({
|
|
276
|
+
existingState,
|
|
277
|
+
delta: sparseStateDelta,
|
|
278
|
+
hasActiveExecutionPlan: params.hasActiveExecutionPlan,
|
|
279
|
+
now: () => Date.now(),
|
|
280
|
+
})
|
|
281
|
+
await workstreamService.persistChangeTracker(params.workstreamId, { chatSummary: output.summary, state: nextState })
|
|
282
|
+
return true
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const fallbackSummary = buildHeuristicTrackerSummary({
|
|
285
|
+
previousSummary: params.previousSummary,
|
|
286
|
+
userMessageText: toOptionalTrimmedString(params.userMessageText),
|
|
287
|
+
assistantMessages,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
if (!fallbackSummary) {
|
|
291
|
+
aiLogger.warn`Workstream change tracker update failed: ${error}`
|
|
292
|
+
return false
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const nextState = applyTrackedStateDelta({
|
|
297
|
+
existingState,
|
|
298
|
+
delta: {},
|
|
299
|
+
hasActiveExecutionPlan: params.hasActiveExecutionPlan,
|
|
300
|
+
now: () => Date.now(),
|
|
301
|
+
})
|
|
302
|
+
await workstreamService.persistChangeTracker(params.workstreamId, {
|
|
303
|
+
chatSummary: fallbackSummary,
|
|
304
|
+
state: nextState,
|
|
305
|
+
})
|
|
306
|
+
aiLogger.info`Workstream change tracker used heuristic fallback after helper failure`
|
|
307
|
+
return true
|
|
308
|
+
} catch (persistError) {
|
|
309
|
+
aiLogger.warn`Workstream change tracker update failed: ${error}; fallback_persist=${persistError}`
|
|
310
|
+
return false
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { toTimestamp, withCreatedAtMetadata } from '@lota-sdk/shared/runtime/chat-message-metadata'
|
|
2
|
+
import { parseRowMetadata } from '@lota-sdk/shared/schemas/chat-message'
|
|
3
|
+
import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
4
|
+
import { RecordId, surql } from 'surrealdb'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
import { agentDisplayNames } from '../config/agent-defaults'
|
|
8
|
+
import { CursorRowSchema, listMessageHistoryPage } from '../db/cursor-pagination'
|
|
9
|
+
import type { CursorPaginationConfig, MessageHistoryPage } from '../db/cursor-pagination'
|
|
10
|
+
import { recordIdToString } from '../db/record-id'
|
|
11
|
+
import type { RecordIdRef } from '../db/record-id'
|
|
12
|
+
import { databaseService } from '../db/service'
|
|
13
|
+
import { TABLES } from '../db/tables'
|
|
14
|
+
|
|
15
|
+
const WorkstreamMessageRowSchema = z.object({
|
|
16
|
+
id: z.unknown(),
|
|
17
|
+
workstreamId: z.unknown(),
|
|
18
|
+
messageId: z.string(),
|
|
19
|
+
role: z.enum(['system', 'user', 'assistant']),
|
|
20
|
+
parts: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
21
|
+
metadata: z.record(z.string(), z.unknown()).nullish(),
|
|
22
|
+
createdAt: z.union([z.date(), z.string(), z.number()]),
|
|
23
|
+
updatedAt: z.union([z.date(), z.string(), z.number()]).optional(),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
type WorkstreamMessageRow = z.infer<typeof WorkstreamMessageRowSchema>
|
|
27
|
+
|
|
28
|
+
const WorkstreamMessageExistingRowSchema = z.object({
|
|
29
|
+
id: z.unknown(),
|
|
30
|
+
createdAt: z.union([z.date(), z.string(), z.number()]),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
function toMessageId(value: string | RecordIdRef): string {
|
|
34
|
+
return recordIdToString(value, TABLES.WORKSTREAM_MESSAGE)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toWorkstreamMessageRowId(workstreamId: RecordIdRef, messageId: string): RecordId {
|
|
38
|
+
const workstreamPart = recordIdToString(workstreamId, TABLES.WORKSTREAM).replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
39
|
+
const messagePart = messageId.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
40
|
+
return new RecordId(TABLES.WORKSTREAM_MESSAGE, `${workstreamPart}__${messagePart}`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toChatMessage(row: WorkstreamMessageRow): ChatMessage {
|
|
44
|
+
const rowCreatedAt = toTimestamp(row.createdAt)
|
|
45
|
+
const metadata = withCreatedAtMetadata(parseRowMetadata(row.metadata), rowCreatedAt)
|
|
46
|
+
|
|
47
|
+
return { id: row.messageId, role: row.role, parts: (row.parts ?? []) as ChatMessage['parts'], metadata }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const workstreamPaginationConfig: CursorPaginationConfig = {
|
|
51
|
+
table: TABLES.WORKSTREAM_MESSAGE,
|
|
52
|
+
parentFilterField: 'workstreamId',
|
|
53
|
+
toRowId: toWorkstreamMessageRowId,
|
|
54
|
+
parseRow: (row: unknown) => WorkstreamMessageRowSchema.parse(row),
|
|
55
|
+
toMessage: (row: unknown) => toChatMessage(WorkstreamMessageRowSchema.parse(row)),
|
|
56
|
+
queryLatest: (parentId, limit) => surql`
|
|
57
|
+
SELECT * FROM workstreamMessage
|
|
58
|
+
WHERE workstreamId = ${parentId}
|
|
59
|
+
ORDER BY createdAt DESC, id DESC
|
|
60
|
+
LIMIT ${limit}
|
|
61
|
+
`,
|
|
62
|
+
queryBefore: (parentId, cursorCreatedAt, cursorId, limit) => surql`
|
|
63
|
+
SELECT * FROM workstreamMessage
|
|
64
|
+
WHERE workstreamId = ${parentId}
|
|
65
|
+
AND (
|
|
66
|
+
createdAt < ${cursorCreatedAt}
|
|
67
|
+
OR (createdAt = ${cursorCreatedAt} AND id < ${cursorId})
|
|
68
|
+
)
|
|
69
|
+
ORDER BY createdAt DESC, id DESC
|
|
70
|
+
LIMIT ${limit}
|
|
71
|
+
`,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class WorkstreamMessageService {
|
|
75
|
+
async upsertMessages(params: { workstreamId: RecordIdRef; messages: ChatMessage[] }): Promise<void> {
|
|
76
|
+
const workstreamId = params.workstreamId
|
|
77
|
+
|
|
78
|
+
for (const message of params.messages) {
|
|
79
|
+
const messageId = message.id.trim()
|
|
80
|
+
if (!messageId) continue
|
|
81
|
+
|
|
82
|
+
const role = message.role
|
|
83
|
+
const parts = Array.isArray(message.parts)
|
|
84
|
+
? message.parts.map((part) => structuredClone(part) as Record<string, unknown>)
|
|
85
|
+
: []
|
|
86
|
+
if (parts.length === 0) {
|
|
87
|
+
if (role === 'assistant') continue
|
|
88
|
+
throw new Error(`Refusing to persist workstream message "${messageId}" with empty parts`)
|
|
89
|
+
}
|
|
90
|
+
const rowId = toWorkstreamMessageRowId(workstreamId, messageId)
|
|
91
|
+
const existingRow = await databaseService.findOne(
|
|
92
|
+
TABLES.WORKSTREAM_MESSAGE,
|
|
93
|
+
{ workstreamId, messageId },
|
|
94
|
+
WorkstreamMessageExistingRowSchema,
|
|
95
|
+
)
|
|
96
|
+
const persistedCreatedAt =
|
|
97
|
+
existingRow === null ? toTimestamp(message.metadata?.createdAt) : toTimestamp(existingRow.createdAt)
|
|
98
|
+
const metadata = withCreatedAtMetadata({ ...message.metadata, createdAt: persistedCreatedAt })
|
|
99
|
+
|
|
100
|
+
await databaseService.upsert(
|
|
101
|
+
TABLES.WORKSTREAM_MESSAGE,
|
|
102
|
+
rowId,
|
|
103
|
+
{
|
|
104
|
+
workstreamId,
|
|
105
|
+
messageId,
|
|
106
|
+
role,
|
|
107
|
+
parts,
|
|
108
|
+
metadata,
|
|
109
|
+
createdAt: existingRow ? new Date(toTimestamp(existingRow.createdAt)) : new Date(persistedCreatedAt),
|
|
110
|
+
},
|
|
111
|
+
WorkstreamMessageRowSchema,
|
|
112
|
+
{ mutation: 'content' },
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async listMessages(workstreamId: RecordIdRef): Promise<ChatMessage[]> {
|
|
118
|
+
const rows = await databaseService.query<unknown>(surql`
|
|
119
|
+
SELECT * FROM workstreamMessage
|
|
120
|
+
WHERE workstreamId = ${workstreamId}
|
|
121
|
+
ORDER BY createdAt ASC, id ASC
|
|
122
|
+
`)
|
|
123
|
+
|
|
124
|
+
return rows.map((row) => WorkstreamMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async listMessageHistoryPage(params: {
|
|
128
|
+
workstreamId: RecordIdRef
|
|
129
|
+
take: number
|
|
130
|
+
beforeMessageId?: string
|
|
131
|
+
}): Promise<MessageHistoryPage> {
|
|
132
|
+
return listMessageHistoryPage(workstreamPaginationConfig, {
|
|
133
|
+
parentId: params.workstreamId,
|
|
134
|
+
take: params.take,
|
|
135
|
+
beforeMessageId: params.beforeMessageId,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async listMessagesAfterCursor(workstreamId: RecordIdRef, afterMessageId?: string): Promise<ChatMessage[]> {
|
|
140
|
+
const cursorMessageId = afterMessageId?.trim()
|
|
141
|
+
if (!cursorMessageId) {
|
|
142
|
+
return await this.listMessages(workstreamId)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const cursorRow = await databaseService.findOne(
|
|
146
|
+
TABLES.WORKSTREAM_MESSAGE,
|
|
147
|
+
{ workstreamId, messageId: cursorMessageId },
|
|
148
|
+
CursorRowSchema,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if (!cursorRow) {
|
|
152
|
+
throw new Error(`Workstream cursor message not found: ${cursorMessageId}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const cursorCreatedAt = new Date(toTimestamp(cursorRow.createdAt))
|
|
156
|
+
const cursorId = toWorkstreamMessageRowId(workstreamId, cursorMessageId)
|
|
157
|
+
const rows = await databaseService.query<unknown>(surql`
|
|
158
|
+
SELECT * FROM workstreamMessage
|
|
159
|
+
WHERE workstreamId = ${workstreamId}
|
|
160
|
+
AND (
|
|
161
|
+
createdAt > ${cursorCreatedAt}
|
|
162
|
+
OR (createdAt = ${cursorCreatedAt} AND id > ${cursorId})
|
|
163
|
+
)
|
|
164
|
+
ORDER BY createdAt ASC, id ASC
|
|
165
|
+
`)
|
|
166
|
+
|
|
167
|
+
return rows.map((row) => WorkstreamMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async listRecentMessages(workstreamId: RecordIdRef, limit: number): Promise<ChatMessage[]> {
|
|
171
|
+
const rows = await databaseService.query<unknown>(surql`
|
|
172
|
+
SELECT * FROM workstreamMessage
|
|
173
|
+
WHERE workstreamId = ${workstreamId}
|
|
174
|
+
ORDER BY createdAt DESC, id DESC
|
|
175
|
+
LIMIT ${Math.max(1, limit)}
|
|
176
|
+
`)
|
|
177
|
+
|
|
178
|
+
return rows
|
|
179
|
+
.map((row) => WorkstreamMessageRowSchema.parse(row))
|
|
180
|
+
.reverse()
|
|
181
|
+
.map((row) => toChatMessage(row))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async searchMessages(params: {
|
|
185
|
+
workstreamId: RecordIdRef
|
|
186
|
+
role: 'user' | 'assistant'
|
|
187
|
+
query: string
|
|
188
|
+
limit: number
|
|
189
|
+
}): Promise<Array<{ id: string; role: 'user' | 'assistant'; createdAt: string; content: string }>> {
|
|
190
|
+
const normalizedQuery = params.query.trim().toLowerCase()
|
|
191
|
+
if (!normalizedQuery) return []
|
|
192
|
+
|
|
193
|
+
const messages = await this.listMessages(params.workstreamId)
|
|
194
|
+
return messages
|
|
195
|
+
.filter((message) => message.role === params.role)
|
|
196
|
+
.map((message) => ({
|
|
197
|
+
id: message.id,
|
|
198
|
+
role: message.role as 'user' | 'assistant',
|
|
199
|
+
createdAt: new Date(toTimestamp(message.metadata?.createdAt)).toISOString(),
|
|
200
|
+
content: message.parts
|
|
201
|
+
.flatMap((part) => (part.type === 'text' && typeof part.text === 'string' ? [part.text] : []))
|
|
202
|
+
.join('\n')
|
|
203
|
+
.trim(),
|
|
204
|
+
}))
|
|
205
|
+
.filter((item) => item.content.length > 0 && item.content.toLowerCase().includes(normalizedQuery))
|
|
206
|
+
.slice(-Math.max(1, params.limit))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async addUserMessage(params: {
|
|
210
|
+
messageId: RecordIdRef
|
|
211
|
+
workstreamId: RecordIdRef
|
|
212
|
+
content: string
|
|
213
|
+
}): Promise<ChatMessage> {
|
|
214
|
+
const message: ChatMessage = {
|
|
215
|
+
id: toMessageId(params.messageId),
|
|
216
|
+
role: 'user',
|
|
217
|
+
parts: [{ type: 'text', text: params.content }],
|
|
218
|
+
metadata: { createdAt: Date.now() },
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await this.upsertMessages({ workstreamId: params.workstreamId, messages: [message] })
|
|
222
|
+
return message
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async addAgentMessage(params: {
|
|
226
|
+
messageId: RecordIdRef
|
|
227
|
+
workstreamId: RecordIdRef
|
|
228
|
+
parts: ChatMessage['parts']
|
|
229
|
+
metadata?: ChatMessage['metadata']
|
|
230
|
+
}): Promise<ChatMessage> {
|
|
231
|
+
const message: ChatMessage = {
|
|
232
|
+
id: toMessageId(params.messageId),
|
|
233
|
+
role: 'assistant',
|
|
234
|
+
parts: params.parts,
|
|
235
|
+
metadata: withCreatedAtMetadata(params.metadata),
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await this.upsertMessages({ workstreamId: params.workstreamId, messages: [message] })
|
|
239
|
+
return message
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async ensureBootstrapWelcomeMessage(params: {
|
|
243
|
+
workstreamId: RecordIdRef
|
|
244
|
+
agentId: string
|
|
245
|
+
text: string
|
|
246
|
+
}): Promise<void> {
|
|
247
|
+
const existingRow = await databaseService.findOne(
|
|
248
|
+
TABLES.WORKSTREAM_MESSAGE,
|
|
249
|
+
{ workstreamId: params.workstreamId },
|
|
250
|
+
WorkstreamMessageExistingRowSchema,
|
|
251
|
+
)
|
|
252
|
+
if (existingRow) return
|
|
253
|
+
|
|
254
|
+
const messageText = params.text.trim()
|
|
255
|
+
if (!messageText) return
|
|
256
|
+
|
|
257
|
+
await this.upsertMessages({
|
|
258
|
+
workstreamId: params.workstreamId,
|
|
259
|
+
messages: [
|
|
260
|
+
{
|
|
261
|
+
id: Bun.randomUUIDv7(),
|
|
262
|
+
role: 'assistant',
|
|
263
|
+
parts: [{ type: 'text', text: messageText }],
|
|
264
|
+
metadata: {
|
|
265
|
+
agentId: params.agentId,
|
|
266
|
+
agentName: agentDisplayNames[params.agentId] ?? params.agentId,
|
|
267
|
+
createdAt: Date.now(),
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async listAllMessages(workstreamId: RecordIdRef): Promise<ChatMessage[]> {
|
|
275
|
+
return await this.listMessages(workstreamId)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async addAttachments(): Promise<void> {
|
|
279
|
+
// Attachments are no longer persisted via workstreamMessage service in AI SDK mode.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export const workstreamMessageService = new WorkstreamMessageService()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { WORKSTREAM } from '@lota-sdk/shared/constants/workstream'
|
|
2
|
+
|
|
3
|
+
import { chatLogger } from '../config/logger'
|
|
4
|
+
import type { RecordIdRef } from '../db/record-id'
|
|
5
|
+
import { recordIdToString } from '../db/record-id'
|
|
6
|
+
import { TABLES } from '../db/tables'
|
|
7
|
+
import type { HelperAgent } from '../runtime/helper-model'
|
|
8
|
+
import { llmHelperService } from '../runtime/helper-model'
|
|
9
|
+
import { deriveTitle, limitTitleWords } from '../runtime/title-helpers'
|
|
10
|
+
import { workstreamService } from './workstream.service'
|
|
11
|
+
|
|
12
|
+
const titlePromises = new Map<string, Promise<string>>()
|
|
13
|
+
|
|
14
|
+
class WorkstreamTitleService {
|
|
15
|
+
async ensureTitle(workstreamId: RecordIdRef, existingTitle: string, sourceText: string): Promise<string> {
|
|
16
|
+
const trimmedSource = sourceText.trim()
|
|
17
|
+
if (!trimmedSource) return existingTitle
|
|
18
|
+
if (existingTitle && existingTitle.trim() !== WORKSTREAM.DEFAULT_TITLE) {
|
|
19
|
+
return existingTitle
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const key = recordIdToString(workstreamId, TABLES.WORKSTREAM)
|
|
23
|
+
const existingPromise = titlePromises.get(key)
|
|
24
|
+
if (existingPromise) {
|
|
25
|
+
return existingPromise
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const promise = this.generateAndPersistTitle(workstreamId, trimmedSource).finally(() => {
|
|
29
|
+
titlePromises.delete(key)
|
|
30
|
+
})
|
|
31
|
+
titlePromises.set(key, promise)
|
|
32
|
+
|
|
33
|
+
return promise
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async generateAndPersistTitle(workstreamId: RecordIdRef, sourceText: string): Promise<string> {
|
|
37
|
+
let title = ''
|
|
38
|
+
try {
|
|
39
|
+
title = await llmHelperService.generateHelperText({
|
|
40
|
+
tag: 'workstream-title',
|
|
41
|
+
createAgent: (_opts: unknown) => ({ generate: async () => ({ text: '' }) }) as unknown as HelperAgent,
|
|
42
|
+
messages: [{ role: 'user' as const, content: `Generate a concise 4-5 word title for: ${sourceText}` }],
|
|
43
|
+
})
|
|
44
|
+
} catch (error) {
|
|
45
|
+
chatLogger.warn`Failed to generate workstream title (non-fatal): ${error}`
|
|
46
|
+
title = ''
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!title) {
|
|
50
|
+
title = limitTitleWords(deriveTitle(sourceText || WORKSTREAM.DEFAULT_TITLE))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await workstreamService.updateTitle(workstreamId, title)
|
|
54
|
+
return title
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const workstreamTitleService = new WorkstreamTitleService()
|