@lota-sdk/core 0.2.2 → 0.3.0
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_identity.surql +2 -2
- package/infrastructure/schema/00_thread.surql +75 -0
- package/infrastructure/schema/02_execution_plan.surql +10 -11
- package/infrastructure/schema/10_autonomous_job.surql +3 -3
- package/package.json +2 -2
- package/src/ai/definitions.ts +1 -1
- package/src/config/agent-defaults.ts +5 -5
- package/src/config/index.ts +1 -1
- package/src/config/thread-defaults.ts +72 -0
- package/src/create-runtime.ts +89 -93
- package/src/db/tables.ts +3 -3
- package/src/db/{workstream-message-row.ts → thread-message-row.ts} +3 -3
- package/src/queues/context-compaction.queue.ts +6 -6
- package/src/queues/plan-agent-heartbeat.queue.ts +3 -3
- package/src/queues/post-chat-memory.queue.ts +1 -1
- package/src/queues/title-generation.queue.ts +10 -13
- package/src/redis/index.ts +1 -1
- package/src/redis/stream-context.ts +1 -1
- package/src/runtime/agent-identity-overrides.ts +1 -1
- package/src/runtime/agent-runtime-policy.ts +19 -21
- package/src/runtime/chat-request-routing.ts +1 -1
- package/src/runtime/context-compaction-constants.ts +1 -1
- package/src/runtime/context-compaction.ts +1 -1
- package/src/runtime/execution-plan.ts +1 -1
- package/src/runtime/index.ts +1 -1
- package/src/runtime/memory-digest-policy.ts +1 -1
- package/src/runtime/plugin-types.ts +1 -1
- package/src/runtime/post-turn-side-effects.ts +35 -35
- package/src/runtime/runtime-config.ts +12 -12
- package/src/runtime/runtime-extensions.ts +11 -11
- package/src/runtime/social-chat-agent-runner.ts +3 -3
- package/src/runtime/social-chat-history.ts +1 -1
- package/src/runtime/social-chat.ts +6 -6
- package/src/runtime/team-consultation-orchestrator.ts +1 -1
- package/src/runtime/{workstream-chat-helpers.ts → thread-chat-helpers.ts} +7 -7
- package/src/runtime/{workstream-plan-turn.ts → thread-plan-turn.ts} +11 -17
- package/src/runtime/{workstream-turn-context.ts → thread-turn-context.ts} +10 -10
- package/src/services/agent-activity.service.ts +39 -44
- package/src/services/agent-executor.service.ts +17 -19
- package/src/services/attachment.service.ts +4 -8
- package/src/services/autonomous-job.service.ts +29 -28
- package/src/services/context-compaction.service.ts +19 -29
- package/src/services/execution-plan.service.ts +58 -70
- package/src/services/global-orchestrator.service.ts +5 -5
- package/src/services/index.ts +6 -6
- package/src/services/memory.service.ts +1 -1
- package/src/services/monitoring-window.service.ts +2 -2
- package/src/services/mutating-approval.service.ts +7 -10
- package/src/services/node-workspace.service.ts +8 -7
- package/src/services/notification.service.ts +1 -1
- package/src/services/organization.service.ts +9 -9
- package/src/services/ownership-dispatcher.service.ts +13 -19
- package/src/services/plan-agent-heartbeat.service.ts +13 -13
- package/src/services/plan-agent-query.service.ts +7 -7
- package/src/services/plan-artifact.service.ts +1 -2
- package/src/services/plan-coordination.service.ts +4 -4
- package/src/services/plan-cycle.service.ts +7 -7
- package/src/services/plan-deadline.service.ts +4 -4
- package/src/services/plan-event-delivery.service.ts +8 -12
- package/src/services/plan-executor.service.ts +16 -37
- package/src/services/plan-run-data.ts +27 -8
- package/src/services/plan-run.service.ts +7 -9
- package/src/services/plan-scheduler.service.ts +4 -4
- package/src/services/plan-template.service.ts +2 -2
- package/src/services/plan-validator.service.ts +0 -11
- package/src/services/plugin-executor.service.ts +1 -1
- package/src/services/queue-job.service.ts +1 -1
- package/src/services/recent-activity-title.service.ts +1 -1
- package/src/services/recent-activity.service.ts +4 -4
- package/src/services/system-executor.service.ts +2 -2
- package/src/services/{workstream-message.service.ts → thread-message.service.ts} +72 -76
- package/src/services/thread-plan-registry.service.ts +22 -0
- package/src/services/thread-title.service.ts +39 -0
- package/src/services/{workstream-turn-preparation.service.ts → thread-turn-preparation.service.ts} +131 -143
- package/src/services/{workstream-turn.ts → thread-turn.ts} +27 -31
- package/src/services/thread.service.ts +707 -0
- package/src/services/thread.types.ts +17 -0
- package/src/storage/attachment-storage.service.ts +4 -4
- package/src/system-agents/index.ts +1 -1
- package/src/system-agents/memory.agent.ts +1 -1
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
- package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
- package/src/system-agents/researcher.agent.ts +3 -3
- package/src/system-agents/{workstream-router.agent.ts → thread-router.agent.ts} +21 -21
- package/src/system-agents/title-generator.agent.ts +8 -8
- package/src/tools/execution-plan.tool.ts +39 -40
- package/src/tools/memory-block.tool.ts +4 -4
- package/src/tools/research-topic.tool.ts +1 -0
- package/src/tools/search-web.tool.ts +1 -1
- package/src/tools/search.tool.ts +4 -4
- package/src/tools/team-think.tool.ts +9 -9
- package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
- package/src/workers/regular-chat-memory-digest.runner.ts +43 -43
- package/src/workers/skill-extraction.runner.ts +9 -13
- package/src/workers/utils/{workstream-message-query.ts → thread-message-query.ts} +21 -21
- package/infrastructure/schema/00_workstream.surql +0 -64
- package/src/config/workstream-defaults.ts +0 -72
- package/src/services/workstream-plan-registry.service.ts +0 -22
- package/src/services/workstream-title.service.ts +0 -42
- package/src/services/workstream.service.ts +0 -803
- package/src/services/workstream.types.ts +0 -17
- /package/src/services/{workstream-constants.ts → thread-constants.ts} +0 -0
|
@@ -20,11 +20,11 @@ import { compactWhitespace } from '../utils/string'
|
|
|
20
20
|
import { buildDigestTranscript, resolveWorkspaceBootstrapCutoff } from './regular-chat-memory-digest.helpers'
|
|
21
21
|
import {
|
|
22
22
|
compareDigestMessageOrder,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
listEligibleThreadMessages,
|
|
24
|
+
listThreadIdsForOrg,
|
|
25
25
|
normalizeBlock,
|
|
26
|
-
} from './utils/
|
|
27
|
-
import type { DigestCursor, DigestMessage } from './utils/
|
|
26
|
+
} from './utils/thread-message-query'
|
|
27
|
+
import type { DigestCursor, DigestMessage } from './utils/thread-message-query'
|
|
28
28
|
|
|
29
29
|
// Onboarding extracts memory immediately inside the turn flow. This delayed
|
|
30
30
|
// runner handles the regular-chat path after onboarding so longer transcripts
|
|
@@ -54,7 +54,7 @@ const helperModelRuntime = createHelperModelRuntime()
|
|
|
54
54
|
|
|
55
55
|
interface RegularChatDigestRunResult {
|
|
56
56
|
skipped: boolean
|
|
57
|
-
|
|
57
|
+
processedThreadMessages: number
|
|
58
58
|
processedSocialMessages: number
|
|
59
59
|
followUpScheduled: boolean
|
|
60
60
|
}
|
|
@@ -100,35 +100,35 @@ function getLastCursor(messages: DigestMessage[]): DigestCursor | null {
|
|
|
100
100
|
return messages.length > 0 ? messages[messages.length - 1].cursor : null
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
async function
|
|
104
|
-
|
|
103
|
+
async function hasNewEligibleThreadMessages(params: {
|
|
104
|
+
threadIds: RecordIdRef[]
|
|
105
105
|
cursor: DigestCursor | null
|
|
106
106
|
onboardingCutoff: Date | null
|
|
107
107
|
}): Promise<boolean> {
|
|
108
|
-
if (params.
|
|
108
|
+
if (params.threadIds.length === 0) return false
|
|
109
109
|
|
|
110
110
|
let query: BoundQuery | null = null
|
|
111
111
|
if (params.cursor) {
|
|
112
|
-
const cursorRowId = ensureRecordId(params.cursor.id, TABLES.
|
|
112
|
+
const cursorRowId = ensureRecordId(params.cursor.id, TABLES.THREAD_MESSAGE)
|
|
113
113
|
query = new BoundQuery(
|
|
114
|
-
`SELECT id, createdAt FROM ${TABLES.
|
|
115
|
-
WHERE
|
|
114
|
+
`SELECT id, createdAt FROM ${TABLES.THREAD_MESSAGE}
|
|
115
|
+
WHERE threadId IN $threadIds
|
|
116
116
|
AND (
|
|
117
117
|
createdAt > $cursorCreatedAt
|
|
118
118
|
OR (createdAt = $cursorCreatedAt AND id > $cursorRowId)
|
|
119
119
|
)
|
|
120
120
|
ORDER BY createdAt ASC, id ASC
|
|
121
121
|
LIMIT 1`,
|
|
122
|
-
{
|
|
122
|
+
{ threadIds: params.threadIds, cursorCreatedAt: params.cursor.createdAt, cursorRowId },
|
|
123
123
|
)
|
|
124
124
|
} else if (params.onboardingCutoff) {
|
|
125
125
|
query = new BoundQuery(
|
|
126
|
-
`SELECT id, createdAt FROM ${TABLES.
|
|
127
|
-
WHERE
|
|
126
|
+
`SELECT id, createdAt FROM ${TABLES.THREAD_MESSAGE}
|
|
127
|
+
WHERE threadId IN $threadIds
|
|
128
128
|
AND createdAt > $onboardingCutoff
|
|
129
129
|
ORDER BY createdAt ASC, id ASC
|
|
130
130
|
LIMIT 1`,
|
|
131
|
-
{
|
|
131
|
+
{ threadIds: params.threadIds, onboardingCutoff: params.onboardingCutoff },
|
|
132
132
|
)
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -160,7 +160,7 @@ export async function runRegularChatMemoryDigest(
|
|
|
160
160
|
const workspaceProvider = getRuntimeAdapters().workspaceProvider
|
|
161
161
|
if (!workspaceProvider) {
|
|
162
162
|
serverLogger.info`Skipping regular chat memory digest for ${orgId}: workspaceProvider is not configured`
|
|
163
|
-
return { skipped: true,
|
|
163
|
+
return { skipped: true, processedThreadMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
return withConfiguredWorkspaceMemoryLock(orgId, async () => {
|
|
@@ -170,28 +170,28 @@ export async function runRegularChatMemoryDigest(
|
|
|
170
170
|
!workspaceProvider.applyProfileProjection
|
|
171
171
|
) {
|
|
172
172
|
serverLogger.info`Skipping regular chat memory digest for ${orgId}: workspaceProvider background/profile methods are incomplete`
|
|
173
|
-
return { skipped: true,
|
|
173
|
+
return { skipped: true, processedThreadMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
const workspace = await workspaceProvider.getWorkspace(orgRef)
|
|
177
177
|
const lifecycleState = await workspaceProvider.getLifecycleState?.(workspace)
|
|
178
178
|
if (lifecycleState?.bootstrapActive ?? false) {
|
|
179
179
|
serverLogger.info`Skipping regular chat memory digest for ${orgId}: onboarding is not completed`
|
|
180
|
-
return { skipped: true,
|
|
180
|
+
return { skipped: true, processedThreadMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
|
|
181
181
|
}
|
|
182
182
|
const projectionState = await workspaceProvider.readProfileProjectionState?.(workspace)
|
|
183
183
|
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
hasExistingCursor:
|
|
184
|
+
const existingThreadCursor = await workspaceProvider.getBackgroundCursor('regular-chat-digest', orgRef)
|
|
185
|
+
const threadOnboardingCutoff = resolveWorkspaceBootstrapCutoff({
|
|
186
|
+
hasExistingCursor: existingThreadCursor !== null,
|
|
187
187
|
bootstrapCompletedAt: lifecycleState?.bootstrapCompletedAt,
|
|
188
188
|
})
|
|
189
189
|
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
cursor:
|
|
194
|
-
onboardingCutoff:
|
|
190
|
+
const threadIds = await listThreadIdsForOrg(orgRef)
|
|
191
|
+
const threadMessages = await listEligibleThreadMessages({
|
|
192
|
+
threadIds,
|
|
193
|
+
cursor: existingThreadCursor,
|
|
194
|
+
onboardingCutoff: threadOnboardingCutoff,
|
|
195
195
|
})
|
|
196
196
|
const existingSocialCursor = await socialChatHistoryService.getBackgroundCursor('regular-chat-digest', orgId)
|
|
197
197
|
const socialOnboardingCutoff = resolveWorkspaceBootstrapCutoff({
|
|
@@ -204,12 +204,12 @@ export async function runRegularChatMemoryDigest(
|
|
|
204
204
|
onboardingCutoff: socialOnboardingCutoff,
|
|
205
205
|
})
|
|
206
206
|
|
|
207
|
-
if (
|
|
207
|
+
if (threadMessages.length === 0 && socialMessages.length === 0) {
|
|
208
208
|
serverLogger.info`Skipping regular chat memory digest for ${orgId}: no eligible messages`
|
|
209
|
-
return { skipped: true,
|
|
209
|
+
return { skipped: true, processedThreadMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
const combinedMessages = [...
|
|
212
|
+
const combinedMessages = [...threadMessages, ...socialMessages].sort(compareDigestMessageOrder)
|
|
213
213
|
const { transcript, involvedAgentNames } = buildDigestTranscript({ messages: combinedMessages })
|
|
214
214
|
const existingMemories = await loadExistingOrganizationMemories(orgId)
|
|
215
215
|
|
|
@@ -237,7 +237,7 @@ export async function runRegularChatMemoryDigest(
|
|
|
237
237
|
throw new Error('Regular chat memory digest returned an empty summaryBlock')
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
const
|
|
240
|
+
const processedThreadCursor = getLastCursor(threadMessages)
|
|
241
241
|
const processedSocialCursor = getLastCursor(socialMessages)
|
|
242
242
|
const digestRunAt = new Date().toISOString()
|
|
243
243
|
|
|
@@ -248,10 +248,10 @@ export async function runRegularChatMemoryDigest(
|
|
|
248
248
|
source: 'regular_chat_digest',
|
|
249
249
|
sourceMetadata: {
|
|
250
250
|
digestRunAt,
|
|
251
|
-
...(
|
|
251
|
+
...(processedThreadCursor
|
|
252
252
|
? {
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
digestThreadCursorCreatedAt: processedThreadCursor.createdAt.toISOString(),
|
|
254
|
+
digestThreadCursorId: processedThreadCursor.id,
|
|
255
255
|
}
|
|
256
256
|
: {}),
|
|
257
257
|
...(processedSocialCursor
|
|
@@ -270,18 +270,18 @@ export async function runRegularChatMemoryDigest(
|
|
|
270
270
|
summaryBlock,
|
|
271
271
|
structuredPatch: synthesis.structuredProfilePatch,
|
|
272
272
|
})
|
|
273
|
-
if (
|
|
274
|
-
await workspaceProvider.setBackgroundCursor('regular-chat-digest', orgRef,
|
|
273
|
+
if (processedThreadCursor) {
|
|
274
|
+
await workspaceProvider.setBackgroundCursor('regular-chat-digest', orgRef, processedThreadCursor)
|
|
275
275
|
}
|
|
276
276
|
if (processedSocialCursor) {
|
|
277
277
|
await socialChatHistoryService.setBackgroundCursor('regular-chat-digest', orgId, processedSocialCursor)
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
cursor:
|
|
284
|
-
onboardingCutoff:
|
|
280
|
+
const threadBoundaryCursor = processedThreadCursor ?? existingThreadCursor
|
|
281
|
+
const hasMoreThreadMessages = await hasNewEligibleThreadMessages({
|
|
282
|
+
threadIds,
|
|
283
|
+
cursor: threadBoundaryCursor,
|
|
284
|
+
onboardingCutoff: threadBoundaryCursor ? null : threadOnboardingCutoff,
|
|
285
285
|
})
|
|
286
286
|
const socialBoundaryCursor = processedSocialCursor ?? existingSocialCursor
|
|
287
287
|
const hasMoreSocialMessages = await socialChatHistoryService.hasWorkspaceMessages({
|
|
@@ -290,17 +290,17 @@ export async function runRegularChatMemoryDigest(
|
|
|
290
290
|
onboardingCutoff: socialBoundaryCursor ? null : socialOnboardingCutoff,
|
|
291
291
|
})
|
|
292
292
|
|
|
293
|
-
const followUpScheduled =
|
|
293
|
+
const followUpScheduled = hasMoreThreadMessages || hasMoreSocialMessages
|
|
294
294
|
if (followUpScheduled) {
|
|
295
295
|
await clearRegularChatMemoryDigestDeduplicationKey(orgId)
|
|
296
296
|
await enqueueRegularChatMemoryDigest({ orgId })
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
serverLogger.info`Regular chat memory digest completed for ${orgId}:
|
|
299
|
+
serverLogger.info`Regular chat memory digest completed for ${orgId}: threadMessages=${threadMessages.length}, socialMessages=${socialMessages.length}, facts=${synthesis.facts.length}, followUpScheduled=${followUpScheduled}`
|
|
300
300
|
|
|
301
301
|
return {
|
|
302
302
|
skipped: false,
|
|
303
|
-
|
|
303
|
+
processedThreadMessages: threadMessages.length,
|
|
304
304
|
processedSocialMessages: socialMessages.length,
|
|
305
305
|
followUpScheduled,
|
|
306
306
|
}
|
|
@@ -13,9 +13,9 @@ import { createSkillManagerAgent, SkillManagerOutputSchema } from '../system-age
|
|
|
13
13
|
import { buildDigestTranscript, resolveWorkspaceBootstrapCutoff } from './regular-chat-memory-digest.helpers'
|
|
14
14
|
import {
|
|
15
15
|
compareDigestMessageOrder,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from './utils/
|
|
16
|
+
listEligibleThreadMessages,
|
|
17
|
+
listThreadIdsForOrg,
|
|
18
|
+
} from './utils/thread-message-query'
|
|
19
19
|
|
|
20
20
|
const SKILL_EXTRACTION_TIMEOUT_MS = 10 * 60 * 1000
|
|
21
21
|
const MIN_MESSAGE_THRESHOLD = 10
|
|
@@ -107,18 +107,14 @@ export async function runSkillExtraction(data: SkillExtractionJob): Promise<Skil
|
|
|
107
107
|
bootstrapCompletedAt: lifecycleState?.bootstrapCompletedAt,
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
workstreamIds,
|
|
113
|
-
cursor: existingCursor,
|
|
114
|
-
onboardingCutoff,
|
|
115
|
-
})
|
|
110
|
+
const threadIds = await listThreadIdsForOrg(orgRef)
|
|
111
|
+
const threadMessages = await listEligibleThreadMessages({ threadIds, cursor: existingCursor, onboardingCutoff })
|
|
116
112
|
const socialMessages = await socialChatHistoryService.listWorkspaceMessages({
|
|
117
113
|
workspaceId: orgId,
|
|
118
114
|
cursor: existingSocialCursor,
|
|
119
115
|
onboardingCutoff: socialOnboardingCutoff,
|
|
120
116
|
})
|
|
121
|
-
const messages = [...
|
|
117
|
+
const messages = [...threadMessages, ...socialMessages]
|
|
122
118
|
|
|
123
119
|
if (messages.length < MIN_MESSAGE_THRESHOLD) {
|
|
124
120
|
serverLogger.info`Skipping skill extraction for ${orgId}: only ${messages.length} messages (threshold: ${MIN_MESSAGE_THRESHOLD})`
|
|
@@ -241,10 +237,10 @@ export async function runSkillExtraction(data: SkillExtractionJob): Promise<Skil
|
|
|
241
237
|
}
|
|
242
238
|
}
|
|
243
239
|
|
|
244
|
-
const
|
|
240
|
+
const lastThreadMessage = threadMessages.at(-1)
|
|
245
241
|
const lastSocialMessage = socialMessages.at(-1)
|
|
246
|
-
if (
|
|
247
|
-
await cursorAwareWorkspaceProvider.setBackgroundCursor('skill-extraction', orgRef,
|
|
242
|
+
if (lastThreadMessage) {
|
|
243
|
+
await cursorAwareWorkspaceProvider.setBackgroundCursor('skill-extraction', orgRef, lastThreadMessage.cursor)
|
|
248
244
|
}
|
|
249
245
|
if (lastSocialMessage) {
|
|
250
246
|
await socialChatHistoryService.setBackgroundCursor('skill-extraction', orgId, lastSocialMessage.cursor)
|
|
@@ -6,15 +6,15 @@ import { ensureRecordId, recordIdToString } from '../../db/record-id'
|
|
|
6
6
|
import type { RecordIdRef } from '../../db/record-id'
|
|
7
7
|
import { databaseService } from '../../db/service'
|
|
8
8
|
import { TABLES } from '../../db/tables'
|
|
9
|
-
import {
|
|
10
|
-
import type {
|
|
9
|
+
import { ThreadMessageRowSchema } from '../../db/thread-message-row'
|
|
10
|
+
import type { ThreadMessageRow } from '../../db/thread-message-row'
|
|
11
11
|
import { normalizeTextBody } from '../../document/parsing'
|
|
12
12
|
import type { LotaRuntimeBackgroundCursor } from '../../runtime/runtime-extensions'
|
|
13
13
|
|
|
14
14
|
export type DigestCursor = LotaRuntimeBackgroundCursor
|
|
15
15
|
|
|
16
16
|
export interface DigestMessage {
|
|
17
|
-
source: '
|
|
17
|
+
source: 'thread' | 'social'
|
|
18
18
|
sourceId: string
|
|
19
19
|
role: 'system' | 'user' | 'assistant'
|
|
20
20
|
parts: Array<Record<string, unknown>>
|
|
@@ -22,16 +22,16 @@ export interface DigestMessage {
|
|
|
22
22
|
cursor: DigestCursor
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function
|
|
25
|
+
function mapThreadRow(row: ThreadMessageRow): DigestMessage {
|
|
26
26
|
return {
|
|
27
|
-
source: '
|
|
28
|
-
sourceId: recordIdToString(row.
|
|
27
|
+
source: 'thread',
|
|
28
|
+
sourceId: recordIdToString(row.threadId, TABLES.THREAD),
|
|
29
29
|
role: row.role,
|
|
30
30
|
parts: row.parts as Array<Record<string, unknown>>,
|
|
31
31
|
metadata: row.metadata ?? undefined,
|
|
32
32
|
cursor: {
|
|
33
33
|
createdAt: new Date(requireTimestamp(row.createdAt)),
|
|
34
|
-
id: recordIdToString(row.id, TABLES.
|
|
34
|
+
id: recordIdToString(row.id, TABLES.THREAD_MESSAGE),
|
|
35
35
|
},
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -42,52 +42,52 @@ export function compareDigestMessageOrder(left: DigestMessage, right: DigestMess
|
|
|
42
42
|
return left.cursor.id.localeCompare(right.cursor.id)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export async function
|
|
45
|
+
export async function listThreadIdsForOrg(orgRef: RecordIdRef): Promise<RecordIdRef[]> {
|
|
46
46
|
const EntityIdRowSchema = z.string().trim().min(1)
|
|
47
47
|
const ids = await databaseService.query<unknown>(
|
|
48
48
|
new BoundQuery(
|
|
49
|
-
`SELECT VALUE type::string(id) FROM ${TABLES.
|
|
49
|
+
`SELECT VALUE type::string(id) FROM ${TABLES.THREAD}
|
|
50
50
|
WHERE organizationId = $organizationId`,
|
|
51
51
|
{ organizationId: orgRef },
|
|
52
52
|
),
|
|
53
53
|
)
|
|
54
|
-
return ids.map((value) => ensureRecordId(EntityIdRowSchema.parse(value), TABLES.
|
|
54
|
+
return ids.map((value) => ensureRecordId(EntityIdRowSchema.parse(value), TABLES.THREAD))
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export async function
|
|
58
|
-
|
|
57
|
+
export async function listEligibleThreadMessages(params: {
|
|
58
|
+
threadIds: RecordIdRef[]
|
|
59
59
|
cursor: DigestCursor | null
|
|
60
60
|
onboardingCutoff: Date | null
|
|
61
61
|
}): Promise<DigestMessage[]> {
|
|
62
|
-
if (params.
|
|
62
|
+
if (params.threadIds.length === 0) return []
|
|
63
63
|
|
|
64
64
|
let query: BoundQuery | null = null
|
|
65
65
|
if (params.cursor) {
|
|
66
|
-
const cursorRowId = ensureRecordId(params.cursor.id, TABLES.
|
|
66
|
+
const cursorRowId = ensureRecordId(params.cursor.id, TABLES.THREAD_MESSAGE)
|
|
67
67
|
query = new BoundQuery(
|
|
68
|
-
`SELECT type::string(id) AS id, type::string(
|
|
69
|
-
WHERE
|
|
68
|
+
`SELECT type::string(id) AS id, type::string(threadId) AS threadId, role, parts, metadata, createdAt FROM ${TABLES.THREAD_MESSAGE}
|
|
69
|
+
WHERE threadId IN $threadIds
|
|
70
70
|
AND (
|
|
71
71
|
createdAt > $cursorCreatedAt
|
|
72
72
|
OR (createdAt = $cursorCreatedAt AND id > $cursorRowId)
|
|
73
73
|
)
|
|
74
74
|
ORDER BY createdAt ASC, id ASC`,
|
|
75
|
-
{
|
|
75
|
+
{ threadIds: params.threadIds, cursorCreatedAt: params.cursor.createdAt, cursorRowId },
|
|
76
76
|
)
|
|
77
77
|
} else if (params.onboardingCutoff) {
|
|
78
78
|
query = new BoundQuery(
|
|
79
|
-
`SELECT type::string(id) AS id, type::string(
|
|
80
|
-
WHERE
|
|
79
|
+
`SELECT type::string(id) AS id, type::string(threadId) AS threadId, role, parts, metadata, createdAt FROM ${TABLES.THREAD_MESSAGE}
|
|
80
|
+
WHERE threadId IN $threadIds
|
|
81
81
|
AND createdAt > $onboardingCutoff
|
|
82
82
|
ORDER BY createdAt ASC, id ASC`,
|
|
83
|
-
{
|
|
83
|
+
{ threadIds: params.threadIds, onboardingCutoff: params.onboardingCutoff },
|
|
84
84
|
)
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
if (!query) return []
|
|
88
88
|
|
|
89
89
|
const rows = await databaseService.query<unknown>(query)
|
|
90
|
-
return rows.map((row) =>
|
|
90
|
+
return rows.map((row) => mapThreadRow(ThreadMessageRowSchema.parse(row)))
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export function normalizeBlock(value: string): string {
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# Workstream table.
|
|
2
|
-
DEFINE TABLE IF NOT EXISTS workstream SCHEMAFULL;
|
|
3
|
-
DEFINE FIELD IF NOT EXISTS organizationId ON TABLE workstream TYPE record<organization>;
|
|
4
|
-
DEFINE FIELD IF NOT EXISTS userId ON TABLE workstream TYPE record<user>;
|
|
5
|
-
DEFINE FIELD IF NOT EXISTS agentId ON TABLE workstream TYPE option<string>;
|
|
6
|
-
DEFINE FIELD IF NOT EXISTS mode ON TABLE workstream TYPE string DEFAULT 'group';
|
|
7
|
-
DEFINE FIELD IF NOT EXISTS core ON TABLE workstream TYPE bool DEFAULT false;
|
|
8
|
-
DEFINE FIELD IF NOT EXISTS coreType ON TABLE workstream TYPE option<string>;
|
|
9
|
-
DEFINE FIELD IF NOT EXISTS title ON TABLE workstream TYPE option<string>;
|
|
10
|
-
DEFINE FIELD IF NOT EXISTS status ON TABLE workstream TYPE string DEFAULT 'regular';
|
|
11
|
-
DEFINE FIELD IF NOT EXISTS createdAt ON TABLE workstream TYPE datetime DEFAULT time::now() READONLY;
|
|
12
|
-
DEFINE FIELD IF NOT EXISTS updatedAt ON TABLE workstream TYPE datetime VALUE time::now();
|
|
13
|
-
DEFINE FIELD IF NOT EXISTS memoryBlock ON TABLE workstream TYPE option<string>;
|
|
14
|
-
DEFINE FIELD IF NOT EXISTS memoryBlockSummary ON TABLE workstream TYPE option<string>;
|
|
15
|
-
DEFINE FIELD IF NOT EXISTS activeRunId ON TABLE workstream TYPE option<string>;
|
|
16
|
-
DEFINE FIELD IF NOT EXISTS activeStreamId ON TABLE workstream TYPE option<string>;
|
|
17
|
-
DEFINE FIELD IF NOT EXISTS compactionSummary ON TABLE workstream TYPE option<string>;
|
|
18
|
-
DEFINE FIELD IF NOT EXISTS lastCompactedMessageId ON TABLE workstream TYPE option<string>;
|
|
19
|
-
DEFINE FIELD IF NOT EXISTS nameGenerated ON TABLE workstream TYPE bool DEFAULT false;
|
|
20
|
-
DEFINE FIELD IF NOT EXISTS isCompacting ON TABLE workstream TYPE bool DEFAULT false;
|
|
21
|
-
DEFINE FIELD IF NOT EXISTS members ON TABLE workstream TYPE option<array<string>> DEFAULT [];
|
|
22
|
-
DEFINE FIELD IF NOT EXISTS turnCount ON TABLE workstream TYPE int DEFAULT 0;
|
|
23
|
-
|
|
24
|
-
DEFINE INDEX IF NOT EXISTS workstreamOrgIdx ON TABLE workstream COLUMNS organizationId;
|
|
25
|
-
DEFINE INDEX IF NOT EXISTS workstreamUserIdx ON TABLE workstream COLUMNS userId;
|
|
26
|
-
DEFINE INDEX IF NOT EXISTS workstreamUserOrgModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, mode, updatedAt;
|
|
27
|
-
DEFINE INDEX IF NOT EXISTS workstreamUserOrgStatusModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, status, mode, updatedAt;
|
|
28
|
-
DEFINE INDEX IF NOT EXISTS workstreamUserOrgCoreModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, core, mode, updatedAt;
|
|
29
|
-
DEFINE INDEX IF NOT EXISTS workstreamUserOrgStatusCoreModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, status, core, mode, updatedAt;
|
|
30
|
-
|
|
31
|
-
# Workstream Message table (AI SDK UIMessage persistence).
|
|
32
|
-
# parts uses OVERWRITE on the wildcard to override the implicit non-FLEXIBLE
|
|
33
|
-
# definition that array<object> creates — this is the only way to allow
|
|
34
|
-
# arbitrary nested object shapes inside the array on SCHEMAFULL tables.
|
|
35
|
-
DEFINE TABLE IF NOT EXISTS workstreamMessage SCHEMAFULL;
|
|
36
|
-
DEFINE FIELD IF NOT EXISTS workstreamId ON TABLE workstreamMessage TYPE record<workstream> REFERENCE ON DELETE CASCADE;
|
|
37
|
-
DEFINE FIELD IF NOT EXISTS messageId ON TABLE workstreamMessage TYPE string;
|
|
38
|
-
DEFINE FIELD IF NOT EXISTS role ON TABLE workstreamMessage TYPE string;
|
|
39
|
-
DEFINE FIELD IF NOT EXISTS parts ON TABLE workstreamMessage TYPE array<object> FLEXIBLE;
|
|
40
|
-
DEFINE FIELD OVERWRITE parts.* ON TABLE workstreamMessage TYPE object FLEXIBLE;
|
|
41
|
-
DEFINE FIELD IF NOT EXISTS metadata ON TABLE workstreamMessage TYPE option<object> FLEXIBLE;
|
|
42
|
-
DEFINE FIELD IF NOT EXISTS createdAt ON TABLE workstreamMessage TYPE datetime DEFAULT time::now() READONLY;
|
|
43
|
-
DEFINE FIELD IF NOT EXISTS updatedAt ON TABLE workstreamMessage TYPE datetime VALUE time::now();
|
|
44
|
-
|
|
45
|
-
DEFINE INDEX IF NOT EXISTS workstreamMessageWorkstreamIdx ON TABLE workstreamMessage COLUMNS workstreamId;
|
|
46
|
-
DEFINE INDEX IF NOT EXISTS workstreamMessageWorkstreamCreatedIdx ON TABLE workstreamMessage COLUMNS workstreamId, createdAt;
|
|
47
|
-
DEFINE INDEX IF NOT EXISTS workstreamMessageWorkstreamMessageUniqueIdx ON TABLE workstreamMessage COLUMNS workstreamId, messageId UNIQUE;
|
|
48
|
-
|
|
49
|
-
# Workstream attachments.
|
|
50
|
-
DEFINE TABLE IF NOT EXISTS workstreamAttachment SCHEMAFULL;
|
|
51
|
-
DEFINE FIELD IF NOT EXISTS workstreamId ON TABLE workstreamAttachment TYPE record<workstream> REFERENCE ON DELETE CASCADE;
|
|
52
|
-
DEFINE FIELD IF NOT EXISTS messageId ON TABLE workstreamAttachment TYPE record<workstreamMessage> REFERENCE ON DELETE CASCADE;
|
|
53
|
-
DEFINE FIELD IF NOT EXISTS attachmentType ON TABLE workstreamAttachment TYPE string;
|
|
54
|
-
DEFINE FIELD IF NOT EXISTS name ON TABLE workstreamAttachment TYPE string;
|
|
55
|
-
DEFINE FIELD IF NOT EXISTS contentType ON TABLE workstreamAttachment TYPE string;
|
|
56
|
-
DEFINE FIELD IF NOT EXISTS storageKey ON TABLE workstreamAttachment TYPE option<string>;
|
|
57
|
-
DEFINE FIELD IF NOT EXISTS sizeBytes ON TABLE workstreamAttachment TYPE option<int>;
|
|
58
|
-
DEFINE FIELD IF NOT EXISTS url ON TABLE workstreamAttachment TYPE option<string>;
|
|
59
|
-
DEFINE FIELD IF NOT EXISTS createdAt ON TABLE workstreamAttachment TYPE datetime DEFAULT time::now() READONLY;
|
|
60
|
-
DEFINE FIELD IF NOT EXISTS updatedAt ON TABLE workstreamAttachment TYPE datetime VALUE time::now();
|
|
61
|
-
|
|
62
|
-
DEFINE INDEX IF NOT EXISTS workstreamAttachmentWorkstreamIdx ON TABLE workstreamAttachment COLUMNS workstreamId;
|
|
63
|
-
DEFINE INDEX IF NOT EXISTS workstreamAttachmentMessageIdx ON TABLE workstreamAttachment COLUMNS messageId;
|
|
64
|
-
DEFINE INDEX IF NOT EXISTS workstreamAttachmentWorkstreamMessageIdx ON TABLE workstreamAttachment COLUMNS workstreamId, messageId;
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
export interface WorkstreamBootstrapWelcomeConfig {
|
|
2
|
-
directAgentId: string
|
|
3
|
-
buildMessageText: (params: { userName?: string | null }) => string
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface LotaWorkstreamBootstrapConfig {
|
|
7
|
-
onboardingDirectAgents?: readonly string[]
|
|
8
|
-
completedDirectAgents?: readonly string[]
|
|
9
|
-
coreTypesAfterOnboarding?: readonly string[]
|
|
10
|
-
ensureDefaultGroupOnCompleted?: boolean
|
|
11
|
-
onboardingWelcome?: WorkstreamBootstrapWelcomeConfig
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface LotaWorkstreamConfig {
|
|
15
|
-
bootstrap?: LotaWorkstreamBootstrapConfig
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface ResolvedWorkstreamBootstrapConfig {
|
|
19
|
-
onboardingDirectAgents: readonly string[]
|
|
20
|
-
completedDirectAgents: readonly string[]
|
|
21
|
-
coreTypesAfterOnboarding: readonly string[]
|
|
22
|
-
ensureDefaultGroupOnCompleted: boolean
|
|
23
|
-
onboardingWelcome?: WorkstreamBootstrapWelcomeConfig
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const DEFAULT_WORKSTREAM_BOOTSTRAP_CONFIG: ResolvedWorkstreamBootstrapConfig = {
|
|
27
|
-
onboardingDirectAgents: [],
|
|
28
|
-
completedDirectAgents: [],
|
|
29
|
-
coreTypesAfterOnboarding: [],
|
|
30
|
-
ensureDefaultGroupOnCompleted: true,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let resolvedWorkstreamBootstrapConfig: ResolvedWorkstreamBootstrapConfig = DEFAULT_WORKSTREAM_BOOTSTRAP_CONFIG
|
|
34
|
-
|
|
35
|
-
function withDedupedStrings(values: readonly string[]): string[] {
|
|
36
|
-
const seen = new Set<string>()
|
|
37
|
-
const deduped: string[] = []
|
|
38
|
-
|
|
39
|
-
for (const value of values) {
|
|
40
|
-
const normalized = value.trim()
|
|
41
|
-
if (!normalized || seen.has(normalized)) continue
|
|
42
|
-
seen.add(normalized)
|
|
43
|
-
deduped.push(normalized)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return deduped
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function configureWorkstreams(params: { agentRoster: readonly string[]; config?: LotaWorkstreamConfig }): void {
|
|
50
|
-
const bootstrap = params.config?.bootstrap
|
|
51
|
-
const onboardingWelcome = bootstrap?.onboardingWelcome
|
|
52
|
-
const onboardingDirectAgents = withDedupedStrings([
|
|
53
|
-
...(bootstrap?.onboardingDirectAgents ?? params.agentRoster),
|
|
54
|
-
...(onboardingWelcome ? [onboardingWelcome.directAgentId] : []),
|
|
55
|
-
])
|
|
56
|
-
|
|
57
|
-
resolvedWorkstreamBootstrapConfig = {
|
|
58
|
-
onboardingDirectAgents,
|
|
59
|
-
completedDirectAgents: withDedupedStrings(bootstrap?.completedDirectAgents ?? params.agentRoster),
|
|
60
|
-
coreTypesAfterOnboarding: withDedupedStrings(bootstrap?.coreTypesAfterOnboarding ?? []),
|
|
61
|
-
ensureDefaultGroupOnCompleted: bootstrap?.ensureDefaultGroupOnCompleted ?? true,
|
|
62
|
-
...(onboardingWelcome ? { onboardingWelcome } : {}),
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function getWorkstreamBootstrapConfig(): ResolvedWorkstreamBootstrapConfig {
|
|
67
|
-
return resolvedWorkstreamBootstrapConfig
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function resolveOnboardingOwnerAgentId(defaultLeadAgentId: string): string {
|
|
71
|
-
return resolvedWorkstreamBootstrapConfig.onboardingWelcome?.directAgentId ?? defaultLeadAgentId
|
|
72
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { PlanRunRecord, SerializableExecutionPlan } from '@lota-sdk/shared'
|
|
2
|
-
|
|
3
|
-
import type { RecordIdInput } from '../db/record-id'
|
|
4
|
-
import { planRunService } from './plan-run.service'
|
|
5
|
-
|
|
6
|
-
class WorkstreamPlanRegistryService {
|
|
7
|
-
async listActiveRuns(workstreamId: RecordIdInput): Promise<PlanRunRecord[]> {
|
|
8
|
-
return planRunService.getActiveRunRecords(workstreamId)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async countActiveRuns(workstreamId: RecordIdInput): Promise<number> {
|
|
12
|
-
const runs = await this.listActiveRuns(workstreamId)
|
|
13
|
-
return runs.length
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async listActivePlans(workstreamId: RecordIdInput): Promise<SerializableExecutionPlan[]> {
|
|
17
|
-
const runs = await this.listActiveRuns(workstreamId)
|
|
18
|
-
return Promise.all(runs.map((run) => planRunService.toSerializablePlan(run)))
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const workstreamPlanRegistryService = new WorkstreamPlanRegistryService()
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { WORKSTREAM } from '@lota-sdk/shared'
|
|
2
|
-
|
|
3
|
-
import { chatLogger } from '../config/logger'
|
|
4
|
-
import type { RecordIdRef } from '../db/record-id'
|
|
5
|
-
import { createHelperModelRuntime } from '../runtime/helper-model'
|
|
6
|
-
import { deriveTitle, limitTitleWords, normalizeTitle } from '../runtime/title-helpers'
|
|
7
|
-
import {
|
|
8
|
-
createWorkstreamTitleGeneratorAgent,
|
|
9
|
-
WORKSTREAM_TITLE_GENERATOR_PROMPT,
|
|
10
|
-
} from '../system-agents/title-generator.agent'
|
|
11
|
-
import { workstreamService } from './workstream.service'
|
|
12
|
-
|
|
13
|
-
const WORKSTREAM_TITLE_TIMEOUT_MS = 30_000
|
|
14
|
-
|
|
15
|
-
class WorkstreamTitleService {
|
|
16
|
-
helperRuntime = createHelperModelRuntime()
|
|
17
|
-
|
|
18
|
-
async generateAndPersistTitle(workstreamId: RecordIdRef, sourceText: string): Promise<void> {
|
|
19
|
-
let title = ''
|
|
20
|
-
try {
|
|
21
|
-
title = normalizeTitle(
|
|
22
|
-
await this.helperRuntime.generateHelperText({
|
|
23
|
-
tag: 'workstream-title',
|
|
24
|
-
createAgent: createWorkstreamTitleGeneratorAgent,
|
|
25
|
-
defaultSystemPrompt: WORKSTREAM_TITLE_GENERATOR_PROMPT,
|
|
26
|
-
timeoutMs: WORKSTREAM_TITLE_TIMEOUT_MS,
|
|
27
|
-
messages: [{ role: 'user', content: sourceText }],
|
|
28
|
-
}),
|
|
29
|
-
)
|
|
30
|
-
} catch (error) {
|
|
31
|
-
chatLogger.warn`Failed to generate workstream title via LLM (non-fatal): ${error}`
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!title) {
|
|
35
|
-
title = limitTitleWords(deriveTitle(sourceText || WORKSTREAM.DEFAULT_TITLE))
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
await workstreamService.update(workstreamId, { title, nameGenerated: true })
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export const workstreamTitleService = new WorkstreamTitleService()
|