@lota-sdk/core 0.1.5 → 0.1.6
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 +26 -0
- package/infrastructure/schema/00_workstream.surql +7 -0
- package/infrastructure/schema/05_recent_activity.surql +48 -0
- package/package.json +4 -3
- package/src/config/env-shapes.ts +0 -1
- package/src/db/schema-fingerprint.ts +21 -0
- package/src/db/sdk-database.ts +1 -0
- package/src/db/service.ts +0 -4
- package/src/db/tables.ts +1 -1
- package/src/index.ts +179 -8
- package/src/queues/workstream-title-generation.queue.ts +69 -0
- package/src/runtime/helper-model.ts +9 -2
- package/src/services/organization-member.service.ts +114 -0
- package/src/services/organization.service.ts +117 -0
- package/src/services/user.service.ts +56 -0
- package/src/services/workstream-title.service.ts +25 -35
- package/src/services/workstream-turn-preparation.ts +15 -0
- package/src/services/workstream.service.ts +9 -1
- package/src/services/workstream.types.ts +2 -0
- package/src/system-agents/title-generator.agent.ts +5 -5
- package/src/workers/bootstrap.ts +26 -1
- package/src/workers/memory-consolidation.worker.ts +1 -9
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# SDK identity tables.
|
|
2
|
+
DEFINE TABLE IF NOT EXISTS organization SCHEMAFULL;
|
|
3
|
+
DEFINE FIELD IF NOT EXISTS name ON TABLE organization TYPE string;
|
|
4
|
+
DEFINE FIELD IF NOT EXISTS regularChatDigestLastWorkstreamCursorCreatedAt ON TABLE organization TYPE option<datetime>;
|
|
5
|
+
DEFINE FIELD IF NOT EXISTS regularChatDigestLastWorkstreamCursorId ON TABLE organization TYPE option<string>;
|
|
6
|
+
DEFINE FIELD IF NOT EXISTS skillExtractionLastCursorId ON TABLE organization TYPE option<string>;
|
|
7
|
+
DEFINE FIELD IF NOT EXISTS skillExtractionLastCursorCreatedAt ON TABLE organization TYPE option<datetime>;
|
|
8
|
+
DEFINE FIELD IF NOT EXISTS createdAt ON TABLE organization TYPE datetime DEFAULT time::now() READONLY;
|
|
9
|
+
DEFINE FIELD IF NOT EXISTS updatedAt ON TABLE organization TYPE datetime VALUE time::now();
|
|
10
|
+
|
|
11
|
+
DEFINE TABLE IF NOT EXISTS user SCHEMAFULL;
|
|
12
|
+
DEFINE FIELD IF NOT EXISTS name ON TABLE user TYPE string;
|
|
13
|
+
DEFINE FIELD IF NOT EXISTS email ON TABLE user TYPE string;
|
|
14
|
+
DEFINE FIELD IF NOT EXISTS createdAt ON TABLE user TYPE datetime DEFAULT time::now() READONLY;
|
|
15
|
+
DEFINE FIELD IF NOT EXISTS updatedAt ON TABLE user TYPE datetime VALUE time::now();
|
|
16
|
+
|
|
17
|
+
DEFINE INDEX IF NOT EXISTS sdkUserEmailIdx ON TABLE user COLUMNS email;
|
|
18
|
+
|
|
19
|
+
DEFINE TABLE IF NOT EXISTS organizationMember SCHEMAFULL TYPE RELATION IN user OUT organization;
|
|
20
|
+
DEFINE FIELD IF NOT EXISTS in ON TABLE organizationMember TYPE record<user>;
|
|
21
|
+
DEFINE FIELD IF NOT EXISTS out ON TABLE organizationMember TYPE record<organization>;
|
|
22
|
+
DEFINE FIELD IF NOT EXISTS role ON TABLE organizationMember TYPE string;
|
|
23
|
+
DEFINE FIELD IF NOT EXISTS createdAt ON TABLE organizationMember TYPE datetime DEFAULT time::now() READONLY;
|
|
24
|
+
|
|
25
|
+
DEFINE INDEX IF NOT EXISTS organizationMemberUniqueIdx ON TABLE organizationMember COLUMNS in, out UNIQUE;
|
|
26
|
+
DEFINE INDEX IF NOT EXISTS organizationMemberOutIdx ON TABLE organizationMember COLUMNS out, createdAt;
|
|
@@ -15,10 +15,16 @@ DEFINE FIELD IF NOT EXISTS memoryBlockSummary ON TABLE workstream TYPE option<st
|
|
|
15
15
|
DEFINE FIELD IF NOT EXISTS activeRunId ON TABLE workstream TYPE option<string>;
|
|
16
16
|
DEFINE FIELD IF NOT EXISTS chatSummary ON TABLE workstream TYPE option<string>;
|
|
17
17
|
DEFINE FIELD IF NOT EXISTS lastCompactedMessageId ON TABLE workstream TYPE option<string>;
|
|
18
|
+
DEFINE FIELD IF NOT EXISTS nameGenerated ON TABLE workstream TYPE bool DEFAULT false;
|
|
18
19
|
DEFINE FIELD IF NOT EXISTS isCompacting ON TABLE workstream TYPE bool DEFAULT false;
|
|
19
20
|
DEFINE FIELD IF NOT EXISTS state ON TABLE workstream TYPE option<object> FLEXIBLE;
|
|
20
21
|
|
|
21
22
|
DEFINE INDEX IF NOT EXISTS workstreamOrgIdx ON TABLE workstream COLUMNS organizationId;
|
|
23
|
+
DEFINE INDEX IF NOT EXISTS workstreamUserIdx ON TABLE workstream COLUMNS userId;
|
|
24
|
+
DEFINE INDEX IF NOT EXISTS workstreamUserOrgModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, mode, updatedAt;
|
|
25
|
+
DEFINE INDEX IF NOT EXISTS workstreamUserOrgStatusModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, status, mode, updatedAt;
|
|
26
|
+
DEFINE INDEX IF NOT EXISTS workstreamUserOrgCoreModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, core, mode, updatedAt;
|
|
27
|
+
DEFINE INDEX IF NOT EXISTS workstreamUserOrgStatusCoreModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, status, core, mode, updatedAt;
|
|
22
28
|
|
|
23
29
|
# Workstream Message table (AI SDK UIMessage persistence).
|
|
24
30
|
# parts uses OVERWRITE on the wildcard to override the implicit non-FLEXIBLE
|
|
@@ -53,3 +59,4 @@ DEFINE FIELD IF NOT EXISTS updatedAt ON TABLE workstreamAttachment TYPE datetime
|
|
|
53
59
|
|
|
54
60
|
DEFINE INDEX IF NOT EXISTS workstreamAttachmentWorkstreamIdx ON TABLE workstreamAttachment COLUMNS workstreamId;
|
|
55
61
|
DEFINE INDEX IF NOT EXISTS workstreamAttachmentMessageIdx ON TABLE workstreamAttachment COLUMNS messageId;
|
|
62
|
+
DEFINE INDEX IF NOT EXISTS workstreamAttachmentWorkstreamMessageIdx ON TABLE workstreamAttachment COLUMNS workstreamId, messageId;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# SDK recent activity tables.
|
|
2
|
+
DEFINE TABLE IF NOT EXISTS recentActivityEvent SCHEMAFULL;
|
|
3
|
+
DEFINE FIELD IF NOT EXISTS organizationId ON TABLE recentActivityEvent TYPE record<organization>;
|
|
4
|
+
DEFINE FIELD IF NOT EXISTS userId ON TABLE recentActivityEvent TYPE record<user>;
|
|
5
|
+
DEFINE FIELD IF NOT EXISTS sourceEventId ON TABLE recentActivityEvent TYPE string;
|
|
6
|
+
DEFINE FIELD IF NOT EXISTS source ON TABLE recentActivityEvent TYPE string;
|
|
7
|
+
DEFINE FIELD IF NOT EXISTS kind ON TABLE recentActivityEvent TYPE string;
|
|
8
|
+
DEFINE FIELD IF NOT EXISTS targetKind ON TABLE recentActivityEvent TYPE string;
|
|
9
|
+
DEFINE FIELD IF NOT EXISTS targetId ON TABLE recentActivityEvent TYPE option<string>;
|
|
10
|
+
DEFINE FIELD IF NOT EXISTS mergeKey ON TABLE recentActivityEvent TYPE string;
|
|
11
|
+
DEFINE FIELD IF NOT EXISTS title ON TABLE recentActivityEvent TYPE string;
|
|
12
|
+
DEFINE FIELD IF NOT EXISTS sourceLabel ON TABLE recentActivityEvent TYPE string;
|
|
13
|
+
DEFINE FIELD IF NOT EXISTS deepLink ON TABLE recentActivityEvent TYPE object FLEXIBLE;
|
|
14
|
+
DEFINE FIELD IF NOT EXISTS metadata ON TABLE recentActivityEvent TYPE option<object> FLEXIBLE;
|
|
15
|
+
DEFINE FIELD IF NOT EXISTS occurredAt ON TABLE recentActivityEvent TYPE datetime;
|
|
16
|
+
DEFINE FIELD IF NOT EXISTS createdAt ON TABLE recentActivityEvent TYPE datetime DEFAULT time::now() READONLY;
|
|
17
|
+
|
|
18
|
+
DEFINE INDEX IF NOT EXISTS recentActivityEventSourceUniqueIdx
|
|
19
|
+
ON TABLE recentActivityEvent COLUMNS organizationId, userId, sourceEventId UNIQUE;
|
|
20
|
+
DEFINE INDEX IF NOT EXISTS recentActivityEventRecentIdx
|
|
21
|
+
ON TABLE recentActivityEvent COLUMNS organizationId, userId, occurredAt;
|
|
22
|
+
DEFINE INDEX IF NOT EXISTS recentActivityEventMergeIdx
|
|
23
|
+
ON TABLE recentActivityEvent COLUMNS organizationId, userId, mergeKey, occurredAt;
|
|
24
|
+
|
|
25
|
+
DEFINE TABLE IF NOT EXISTS recentActivity SCHEMAFULL;
|
|
26
|
+
DEFINE FIELD IF NOT EXISTS organizationId ON TABLE recentActivity TYPE record<organization>;
|
|
27
|
+
DEFINE FIELD IF NOT EXISTS userId ON TABLE recentActivity TYPE record<user>;
|
|
28
|
+
DEFINE FIELD IF NOT EXISTS mergeKey ON TABLE recentActivity TYPE string;
|
|
29
|
+
DEFINE FIELD IF NOT EXISTS kind ON TABLE recentActivity TYPE string;
|
|
30
|
+
DEFINE FIELD IF NOT EXISTS targetKind ON TABLE recentActivity TYPE string;
|
|
31
|
+
DEFINE FIELD IF NOT EXISTS targetId ON TABLE recentActivity TYPE option<string>;
|
|
32
|
+
DEFINE FIELD IF NOT EXISTS title ON TABLE recentActivity TYPE string;
|
|
33
|
+
DEFINE FIELD IF NOT EXISTS systemTitle ON TABLE recentActivity TYPE string;
|
|
34
|
+
DEFINE FIELD IF NOT EXISTS titleSource ON TABLE recentActivity TYPE string;
|
|
35
|
+
DEFINE FIELD IF NOT EXISTS sourceLabel ON TABLE recentActivity TYPE string;
|
|
36
|
+
DEFINE FIELD IF NOT EXISTS deepLink ON TABLE recentActivity TYPE object FLEXIBLE;
|
|
37
|
+
DEFINE FIELD IF NOT EXISTS metadata ON TABLE recentActivity TYPE option<object> FLEXIBLE;
|
|
38
|
+
DEFINE FIELD IF NOT EXISTS latestEventId ON TABLE recentActivity TYPE option<record<recentActivityEvent>>;
|
|
39
|
+
DEFINE FIELD IF NOT EXISTS latestSourceEventId ON TABLE recentActivity TYPE option<string>;
|
|
40
|
+
DEFINE FIELD IF NOT EXISTS latestEventAt ON TABLE recentActivity TYPE datetime;
|
|
41
|
+
DEFINE FIELD IF NOT EXISTS titleRefinedAt ON TABLE recentActivity TYPE option<datetime>;
|
|
42
|
+
DEFINE FIELD IF NOT EXISTS createdAt ON TABLE recentActivity TYPE datetime DEFAULT time::now() READONLY;
|
|
43
|
+
DEFINE FIELD IF NOT EXISTS updatedAt ON TABLE recentActivity TYPE datetime VALUE time::now();
|
|
44
|
+
|
|
45
|
+
DEFINE INDEX IF NOT EXISTS recentActivityMergeUniqueIdx
|
|
46
|
+
ON TABLE recentActivity COLUMNS organizationId, userId, mergeKey UNIQUE;
|
|
47
|
+
DEFINE INDEX IF NOT EXISTS recentActivityRecentIdx
|
|
48
|
+
ON TABLE recentActivity COLUMNS organizationId, userId, latestEventAt;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -104,7 +104,8 @@
|
|
|
104
104
|
"lint": "node ../node_modules/oxlint/bin/oxlint --fix -c ../oxlint.config.ts src",
|
|
105
105
|
"format": "bunx oxfmt src",
|
|
106
106
|
"typecheck": "bunx tsgo --noEmit",
|
|
107
|
-
"test:unit": "bun test ../tests/unit/core"
|
|
107
|
+
"test:unit": "bun test ../tests/unit/core",
|
|
108
|
+
"test:coverage": "bun test --coverage ../tests/unit/core"
|
|
108
109
|
},
|
|
109
110
|
"publishConfig": {
|
|
110
111
|
"access": "public",
|
|
@@ -113,7 +114,7 @@
|
|
|
113
114
|
"dependencies": {
|
|
114
115
|
"@ai-sdk/openai": "^3.0.41",
|
|
115
116
|
"@logtape/logtape": "^2.0.4",
|
|
116
|
-
"@lota-sdk/shared": "
|
|
117
|
+
"@lota-sdk/shared": "file:../shared",
|
|
117
118
|
"@mendable/firecrawl-js": "^4.16.0",
|
|
118
119
|
"@surrealdb/node": "^3.0.3",
|
|
119
120
|
"ai": "^6.0.116",
|
package/src/config/env-shapes.ts
CHANGED
|
@@ -14,7 +14,6 @@ export const surrealDbEnvShape = {
|
|
|
14
14
|
SURREALDB_USER: z.string().min(1, 'SurrealDB user is required'),
|
|
15
15
|
SURREALDB_PASSWORD: z.string().min(1, 'SurrealDB password is required'),
|
|
16
16
|
SURREALDB_NAMESPACE: z.string().min(1, 'SurrealDB namespace is required'),
|
|
17
|
-
SURREALDB_DATABASE: z.string().min(1, 'SurrealDB database is required'),
|
|
18
17
|
} as const satisfies Record<string, ZodTypeAny>
|
|
19
18
|
|
|
20
19
|
export const betterAuthEnvShape = {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
function toSchemaFilePath(value: string | URL): string {
|
|
4
|
+
return value instanceof URL ? value.pathname : value
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function computeSchemaFingerprint(schemaFiles: readonly (string | URL)[]): Promise<string> {
|
|
8
|
+
const hash = createHash('sha256')
|
|
9
|
+
|
|
10
|
+
for (const schemaFile of schemaFiles) {
|
|
11
|
+
const sortKey = toSchemaFilePath(schemaFile)
|
|
12
|
+
const file = schemaFile instanceof URL ? Bun.file(schemaFile.pathname) : Bun.file(schemaFile)
|
|
13
|
+
|
|
14
|
+
hash.update(sortKey)
|
|
15
|
+
hash.update('\0')
|
|
16
|
+
hash.update((await file.text()).trim())
|
|
17
|
+
hash.update('\0')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return hash.digest('hex')
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const LOTA_SDK_DATABASE_NAME = 'lotasdk'
|
package/src/db/service.ts
CHANGED
package/src/db/tables.ts
CHANGED
|
@@ -11,8 +11,8 @@ export const TABLES = {
|
|
|
11
11
|
PLAN_TASK: 'planTask',
|
|
12
12
|
PLAN_EVENT: 'planEvent',
|
|
13
13
|
ORGANIZATION: 'organization',
|
|
14
|
+
ORGANIZATION_MEMBER: 'organizationMember',
|
|
14
15
|
USER: 'user',
|
|
15
|
-
ORG_ACTION: 'orgAction',
|
|
16
16
|
RECENT_ACTIVITY_EVENT: 'recentActivityEvent',
|
|
17
17
|
RECENT_ACTIVITY: 'recentActivity',
|
|
18
18
|
} as const
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CoreWorkstreamProfile } from './config/agent-defaults'
|
|
2
2
|
import type { LotaWorkstreamConfig } from './config/workstream-defaults'
|
|
3
|
+
import { ensureRecordId } from './db/record-id'
|
|
3
4
|
import type { SurrealDBService } from './db/service'
|
|
5
|
+
import { TABLES } from './db/tables'
|
|
4
6
|
import type { startContextCompactionWorker } from './queues/context-compaction.queue'
|
|
5
7
|
import type {
|
|
6
8
|
scheduleRecurringConsolidation,
|
|
@@ -10,16 +12,21 @@ import type { startPostChatMemoryWorker } from './queues/post-chat-memory.queue'
|
|
|
10
12
|
import type { startRecentActivityTitleRefinementWorker } from './queues/recent-activity-title-refinement.queue'
|
|
11
13
|
import type { startRegularChatMemoryDigestWorker } from './queues/regular-chat-memory-digest.queue'
|
|
12
14
|
import type { startSkillExtractionWorker } from './queues/skill-extraction.queue'
|
|
15
|
+
import type { startWorkstreamTitleGenerationWorker } from './queues/workstream-title-generation.queue'
|
|
13
16
|
import type { RedisConnectionManager } from './redis/connection'
|
|
14
17
|
import type { isApprovalContinuationRequest } from './runtime/approval-continuation'
|
|
18
|
+
import type { routeWorkstreamChatMessages } from './runtime/chat-request-routing'
|
|
15
19
|
import type { LotaPlugin } from './runtime/plugin-types'
|
|
16
20
|
import type { LotaRuntimeAdapters, LotaRuntimeTurnHooks } from './runtime/runtime-extensions'
|
|
17
21
|
import type { attachmentService } from './services/attachment.service'
|
|
18
22
|
import type { executionPlanService } from './services/execution-plan.service'
|
|
19
23
|
import type { memoryService } from './services/memory.service'
|
|
20
24
|
import type { verifyMutatingApproval } from './services/mutating-approval.service'
|
|
25
|
+
import type { organizationMemberService } from './services/organization-member.service'
|
|
26
|
+
import type { organizationService } from './services/organization.service'
|
|
21
27
|
import type { recentActivityTitleService } from './services/recent-activity-title.service'
|
|
22
28
|
import type { recentActivityService } from './services/recent-activity.service'
|
|
29
|
+
import type { userService } from './services/user.service'
|
|
23
30
|
import type { workstreamMessageService } from './services/workstream-message.service'
|
|
24
31
|
import type { workstreamTitleService } from './services/workstream-title.service'
|
|
25
32
|
import type {
|
|
@@ -39,11 +46,22 @@ interface LotaRuntimeBuiltInWorkers {
|
|
|
39
46
|
startRecentActivityTitleRefinementWorker: typeof startRecentActivityTitleRefinementWorker
|
|
40
47
|
startRegularChatMemoryDigestWorker: typeof startRegularChatMemoryDigestWorker
|
|
41
48
|
startSkillExtractionWorker: typeof startSkillExtractionWorker
|
|
49
|
+
startWorkstreamTitleGenerationWorker: typeof startWorkstreamTitleGenerationWorker
|
|
42
50
|
scheduleRecurringConsolidation: typeof scheduleRecurringConsolidation
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
type ArchiveSdkWorkstream = (
|
|
54
|
+
workstreamId: Parameters<typeof workstreamService.updateStatus>[0],
|
|
55
|
+
status?: 'archived',
|
|
56
|
+
) => ReturnType<typeof workstreamService.updateStatus>
|
|
57
|
+
|
|
58
|
+
type UnarchiveSdkWorkstream = (
|
|
59
|
+
workstreamId: Parameters<typeof workstreamService.updateStatus>[0],
|
|
60
|
+
status?: 'regular',
|
|
61
|
+
) => ReturnType<typeof workstreamService.updateStatus>
|
|
62
|
+
|
|
45
63
|
export interface LotaRuntimeConfig {
|
|
46
|
-
database: { url: string; namespace: string;
|
|
64
|
+
database: { url: string; namespace: string; username: string; password: string }
|
|
47
65
|
redis: { url: string }
|
|
48
66
|
aiGateway: { url: string; key: string; admin?: string; pass?: string; embeddingModel?: string }
|
|
49
67
|
s3: {
|
|
@@ -88,6 +106,9 @@ export interface LotaRuntime {
|
|
|
88
106
|
generatedDocumentStorageService: typeof generatedDocumentStorageService
|
|
89
107
|
memoryService: typeof memoryService
|
|
90
108
|
verifyMutatingApproval: typeof verifyMutatingApproval
|
|
109
|
+
organizationService: typeof organizationService
|
|
110
|
+
organizationMemberService: typeof organizationMemberService
|
|
111
|
+
userService: typeof userService
|
|
91
112
|
recentActivityService: typeof recentActivityService
|
|
92
113
|
recentActivityTitleService: typeof recentActivityTitleService
|
|
93
114
|
executionPlanService: typeof executionPlanService
|
|
@@ -99,6 +120,57 @@ export interface LotaRuntime {
|
|
|
99
120
|
isApprovalContinuationRequest: typeof isApprovalContinuationRequest
|
|
100
121
|
runWorkstreamTurnInBackground: typeof runWorkstreamTurnInBackground
|
|
101
122
|
}
|
|
123
|
+
lota: {
|
|
124
|
+
organizations: {
|
|
125
|
+
create: typeof organizationService.createOrganization
|
|
126
|
+
upsert: typeof organizationService.upsertOrganization
|
|
127
|
+
get: typeof organizationService.getOrganization
|
|
128
|
+
list: typeof organizationService.listOrganizations
|
|
129
|
+
update: typeof organizationService.updateOrganization
|
|
130
|
+
delete: typeof organizationService.deleteOrganization
|
|
131
|
+
}
|
|
132
|
+
users: {
|
|
133
|
+
upsert: typeof userService.upsertUser
|
|
134
|
+
get: typeof userService.getUser
|
|
135
|
+
list: typeof userService.listUsers
|
|
136
|
+
update: typeof userService.updateUser
|
|
137
|
+
delete: typeof userService.deleteUser
|
|
138
|
+
}
|
|
139
|
+
memberships: {
|
|
140
|
+
add: typeof organizationMemberService.addMembership
|
|
141
|
+
listForOrganization: typeof organizationMemberService.listMembershipsForOrganization
|
|
142
|
+
listForUser: typeof organizationMemberService.listMembershipsForUser
|
|
143
|
+
remove: typeof organizationMemberService.removeMembership
|
|
144
|
+
isMember: typeof organizationMemberService.isMember
|
|
145
|
+
}
|
|
146
|
+
workstreams: {
|
|
147
|
+
create: typeof workstreamService.createWorkstream
|
|
148
|
+
list: typeof workstreamService.listWorkstreams
|
|
149
|
+
get: typeof workstreamService.getWorkstream
|
|
150
|
+
update: typeof workstreamService.updateTitle
|
|
151
|
+
archive: ArchiveSdkWorkstream
|
|
152
|
+
unarchive: UnarchiveSdkWorkstream
|
|
153
|
+
delete: typeof workstreamService.deleteWorkstream
|
|
154
|
+
stop: typeof workstreamService.stopActiveRun
|
|
155
|
+
listMessages: typeof workstreamMessageService.listMessageHistoryPage
|
|
156
|
+
getMessage: (params: { workstreamId: string; messageId: string }) => Promise<unknown>
|
|
157
|
+
sendMessage: (params: {
|
|
158
|
+
workstreamId: string
|
|
159
|
+
organizationId: string
|
|
160
|
+
userId: string
|
|
161
|
+
userName: string
|
|
162
|
+
messages: Parameters<typeof routeWorkstreamChatMessages>[0]
|
|
163
|
+
}) => Promise<Awaited<ReturnType<typeof createWorkstreamTurnStream>>>
|
|
164
|
+
continueApproval: (params: {
|
|
165
|
+
workstreamId: string
|
|
166
|
+
organizationId: string
|
|
167
|
+
userId: string
|
|
168
|
+
userName: string
|
|
169
|
+
messages: Parameters<typeof routeWorkstreamChatMessages>[0]
|
|
170
|
+
}) => Promise<Awaited<ReturnType<typeof createWorkstreamApprovalContinuationStream>>>
|
|
171
|
+
uploadAttachment: typeof attachmentService.uploadWorkstreamAttachment
|
|
172
|
+
}
|
|
173
|
+
}
|
|
102
174
|
redis: {
|
|
103
175
|
manager: RedisConnectionManager
|
|
104
176
|
getConnection: () => ReturnType<RedisConnectionManager['getConnection']>
|
|
@@ -118,12 +190,16 @@ export interface LotaRuntime {
|
|
|
118
190
|
export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<LotaRuntime> {
|
|
119
191
|
const { lotaSdkEnvKeys, setEnv } = await import('./config/env-shapes')
|
|
120
192
|
const { configureLogger } = await import('./config/logger')
|
|
193
|
+
const { publishDatabaseBootstrap } = await import('./db/startup')
|
|
194
|
+
const { computeSchemaFingerprint } = await import('./db/schema-fingerprint')
|
|
195
|
+
const { LOTA_SDK_DATABASE_NAME } = await import('./db/sdk-database')
|
|
121
196
|
const { SurrealDBService: SurrealDBServiceClass, setDatabaseService } = await import('./db/service')
|
|
122
197
|
const { createRedisConnectionManager } = await import('./redis/connection')
|
|
123
198
|
const { setRedisConnectionManager } = await import('./redis/index')
|
|
124
199
|
const { configureAgents, configureAgentFactory } = await import('./config/agent-defaults')
|
|
125
200
|
const { configureWorkstreams } = await import('./config/workstream-defaults')
|
|
126
201
|
const { configureRuntimeExtensions } = await import('./runtime/runtime-extensions')
|
|
202
|
+
const { routeWorkstreamChatMessages } = await import('./runtime/chat-request-routing')
|
|
127
203
|
|
|
128
204
|
setEnv({
|
|
129
205
|
AI_GATEWAY_URL: config.aiGateway.url,
|
|
@@ -149,7 +225,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
149
225
|
const db = new SurrealDBServiceClass({
|
|
150
226
|
url: config.database.url,
|
|
151
227
|
namespace: config.database.namespace,
|
|
152
|
-
database:
|
|
228
|
+
database: LOTA_SDK_DATABASE_NAME,
|
|
153
229
|
username: config.database.username,
|
|
154
230
|
password: config.database.password,
|
|
155
231
|
})
|
|
@@ -182,6 +258,9 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
182
258
|
const { executionPlanService } = await import('./services/execution-plan.service')
|
|
183
259
|
const { memoryService } = await import('./services/memory.service')
|
|
184
260
|
const { verifyMutatingApproval } = await import('./services/mutating-approval.service')
|
|
261
|
+
const { organizationMemberService } = await import('./services/organization-member.service')
|
|
262
|
+
const { organizationService } = await import('./services/organization.service')
|
|
263
|
+
const { userService } = await import('./services/user.service')
|
|
185
264
|
const { workstreamMessageService } = await import('./services/workstream-message.service')
|
|
186
265
|
const { workstreamService } = await import('./services/workstream.service')
|
|
187
266
|
const { workstreamTitleService } = await import('./services/workstream-title.service')
|
|
@@ -199,6 +278,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
199
278
|
const { startRecentActivityTitleRefinementWorker } = await import('./queues/recent-activity-title-refinement.queue')
|
|
200
279
|
const { startRegularChatMemoryDigestWorker } = await import('./queues/regular-chat-memory-digest.queue')
|
|
201
280
|
const { startSkillExtractionWorker } = await import('./queues/skill-extraction.queue')
|
|
281
|
+
const { startWorkstreamTitleGenerationWorker } = await import('./queues/workstream-title-generation.queue')
|
|
202
282
|
|
|
203
283
|
configureRuntimeExtensions({
|
|
204
284
|
adapters: config.runtimeAdapters,
|
|
@@ -209,11 +289,8 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
209
289
|
|
|
210
290
|
const pluginRuntime = config.pluginRuntime ?? {}
|
|
211
291
|
const pluginContributions = Object.values(pluginRuntime).map((plugin) => plugin.contributions)
|
|
212
|
-
const schemaFiles = [
|
|
213
|
-
|
|
214
|
-
...pluginContributions.flatMap((plugin) => plugin.schemaFiles),
|
|
215
|
-
...(config.extraSchemaFiles ?? []),
|
|
216
|
-
]
|
|
292
|
+
const schemaFiles = [...getBuiltInSchemaFiles(), ...(config.extraSchemaFiles ?? [])]
|
|
293
|
+
const hostContributionSchemaFiles = pluginContributions.flatMap((plugin) => plugin.schemaFiles)
|
|
217
294
|
const contributionEnvKeys = [...lotaSdkEnvKeys, ...pluginContributions.flatMap((plugin) => plugin.envKeys)]
|
|
218
295
|
const connectPluginDatabases = createPluginDatabaseConnector(pluginRuntime)
|
|
219
296
|
const builtInWorkers = {
|
|
@@ -223,9 +300,91 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
223
300
|
startRecentActivityTitleRefinementWorker,
|
|
224
301
|
startRegularChatMemoryDigestWorker,
|
|
225
302
|
startSkillExtractionWorker,
|
|
303
|
+
startWorkstreamTitleGenerationWorker,
|
|
226
304
|
scheduleRecurringConsolidation,
|
|
227
305
|
} satisfies LotaRuntimeBuiltInWorkers
|
|
228
306
|
|
|
307
|
+
const lota = {
|
|
308
|
+
organizations: {
|
|
309
|
+
create: organizationService.createOrganization.bind(organizationService),
|
|
310
|
+
upsert: organizationService.upsertOrganization.bind(organizationService),
|
|
311
|
+
get: organizationService.getOrganization.bind(organizationService),
|
|
312
|
+
list: organizationService.listOrganizations.bind(organizationService),
|
|
313
|
+
update: organizationService.updateOrganization.bind(organizationService),
|
|
314
|
+
delete: organizationService.deleteOrganization.bind(organizationService),
|
|
315
|
+
},
|
|
316
|
+
users: {
|
|
317
|
+
upsert: userService.upsertUser.bind(userService),
|
|
318
|
+
get: userService.getUser.bind(userService),
|
|
319
|
+
list: userService.listUsers.bind(userService),
|
|
320
|
+
update: userService.updateUser.bind(userService),
|
|
321
|
+
delete: userService.deleteUser.bind(userService),
|
|
322
|
+
},
|
|
323
|
+
memberships: {
|
|
324
|
+
add: organizationMemberService.addMembership.bind(organizationMemberService),
|
|
325
|
+
listForOrganization: organizationMemberService.listMembershipsForOrganization.bind(organizationMemberService),
|
|
326
|
+
listForUser: organizationMemberService.listMembershipsForUser.bind(organizationMemberService),
|
|
327
|
+
remove: organizationMemberService.removeMembership.bind(organizationMemberService),
|
|
328
|
+
isMember: organizationMemberService.isMember.bind(organizationMemberService),
|
|
329
|
+
},
|
|
330
|
+
workstreams: {
|
|
331
|
+
create: workstreamService.createWorkstream.bind(workstreamService),
|
|
332
|
+
list: workstreamService.listWorkstreams.bind(workstreamService),
|
|
333
|
+
get: workstreamService.getWorkstream.bind(workstreamService),
|
|
334
|
+
update: workstreamService.updateTitle.bind(workstreamService),
|
|
335
|
+
archive: async (workstreamId, status = 'archived') => await workstreamService.updateStatus(workstreamId, status),
|
|
336
|
+
unarchive: async (workstreamId, status = 'regular') => await workstreamService.updateStatus(workstreamId, status),
|
|
337
|
+
delete: workstreamService.deleteWorkstream.bind(workstreamService),
|
|
338
|
+
stop: workstreamService.stopActiveRun.bind(workstreamService),
|
|
339
|
+
listMessages: workstreamMessageService.listMessageHistoryPage.bind(workstreamMessageService),
|
|
340
|
+
getMessage: async ({ workstreamId, messageId }) => {
|
|
341
|
+
const messages = await workstreamMessageService.listMessages(ensureRecordId(workstreamId, TABLES.WORKSTREAM))
|
|
342
|
+
const message = messages.find((candidate) => candidate.id === messageId)
|
|
343
|
+
if (!message) {
|
|
344
|
+
throw new Error(`Workstream message not found: ${messageId}`)
|
|
345
|
+
}
|
|
346
|
+
return message
|
|
347
|
+
},
|
|
348
|
+
sendMessage: async ({ workstreamId, organizationId, userId, userName, messages }) => {
|
|
349
|
+
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
350
|
+
const workstream = await workstreamService.getWorkstream(workstreamRef)
|
|
351
|
+
const routed = routeWorkstreamChatMessages(messages)
|
|
352
|
+
if (routed.kind !== 'turn') {
|
|
353
|
+
throw new Error(routed.kind === 'invalid' ? routed.message : 'Expected a user turn payload.')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return await createWorkstreamTurnStream({
|
|
357
|
+
workstream,
|
|
358
|
+
workstreamRef,
|
|
359
|
+
orgRef: ensureRecordId(organizationId, TABLES.ORGANIZATION),
|
|
360
|
+
userRef: ensureRecordId(userId, TABLES.USER),
|
|
361
|
+
userName,
|
|
362
|
+
inputMessage: routed.inputMessage,
|
|
363
|
+
})
|
|
364
|
+
},
|
|
365
|
+
continueApproval: async ({ workstreamId, organizationId, userId, userName, messages }) => {
|
|
366
|
+
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
367
|
+
const workstream = await workstreamService.getWorkstream(workstreamRef)
|
|
368
|
+
const routed = routeWorkstreamChatMessages(messages)
|
|
369
|
+
if (routed.kind !== 'approval-continuation') {
|
|
370
|
+
throw new Error(
|
|
371
|
+
routed.kind === 'invalid' ? routed.message : 'Expected approval continuation messages payload.',
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return await createWorkstreamApprovalContinuationStream({
|
|
376
|
+
workstream,
|
|
377
|
+
workstreamRef,
|
|
378
|
+
orgRef: ensureRecordId(organizationId, TABLES.ORGANIZATION),
|
|
379
|
+
userRef: ensureRecordId(userId, TABLES.USER),
|
|
380
|
+
userName,
|
|
381
|
+
approvalMessages: routed.approvalMessages,
|
|
382
|
+
})
|
|
383
|
+
},
|
|
384
|
+
uploadAttachment: attachmentService.uploadWorkstreamAttachment.bind(attachmentService),
|
|
385
|
+
},
|
|
386
|
+
} satisfies LotaRuntime['lota']
|
|
387
|
+
|
|
229
388
|
return {
|
|
230
389
|
services: {
|
|
231
390
|
database: db,
|
|
@@ -236,6 +395,9 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
236
395
|
generatedDocumentStorageService,
|
|
237
396
|
memoryService,
|
|
238
397
|
verifyMutatingApproval,
|
|
398
|
+
organizationService,
|
|
399
|
+
organizationMemberService,
|
|
400
|
+
userService,
|
|
239
401
|
recentActivityService,
|
|
240
402
|
recentActivityTitleService,
|
|
241
403
|
executionPlanService,
|
|
@@ -247,6 +409,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
247
409
|
isApprovalContinuationRequest,
|
|
248
410
|
runWorkstreamTurnInBackground,
|
|
249
411
|
},
|
|
412
|
+
lota,
|
|
250
413
|
redis: {
|
|
251
414
|
manager: redisManager,
|
|
252
415
|
getConnection: () => redisManager.getConnection(),
|
|
@@ -255,7 +418,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
255
418
|
},
|
|
256
419
|
workers: { ...builtInWorkers, ...config.extraWorkers },
|
|
257
420
|
schemaFiles,
|
|
258
|
-
contributions: { envKeys: [...new Set(contributionEnvKeys)], schemaFiles },
|
|
421
|
+
contributions: { envKeys: [...new Set(contributionEnvKeys)], schemaFiles: hostContributionSchemaFiles },
|
|
259
422
|
config,
|
|
260
423
|
plugins: pluginRuntime,
|
|
261
424
|
async connectPluginDatabases() {
|
|
@@ -263,6 +426,12 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
263
426
|
},
|
|
264
427
|
async connect() {
|
|
265
428
|
await db.connect()
|
|
429
|
+
const bunFiles = schemaFiles.map((schemaFile) =>
|
|
430
|
+
schemaFile instanceof URL ? Bun.file(schemaFile.pathname) : Bun.file(schemaFile),
|
|
431
|
+
)
|
|
432
|
+
await db.applySchemaAndMigrations(bunFiles)
|
|
433
|
+
const schemaFingerprint = await computeSchemaFingerprint(schemaFiles)
|
|
434
|
+
await publishDatabaseBootstrap({ databaseService: db, schemaFingerprint })
|
|
266
435
|
},
|
|
267
436
|
async disconnect() {
|
|
268
437
|
await db.disconnect()
|
|
@@ -273,10 +442,12 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
273
442
|
|
|
274
443
|
function getBuiltInSchemaFiles(): URL[] {
|
|
275
444
|
return [
|
|
445
|
+
new URL('../infrastructure/schema/00_identity.surql', import.meta.url),
|
|
276
446
|
new URL('../infrastructure/schema/00_workstream.surql', import.meta.url),
|
|
277
447
|
new URL('../infrastructure/schema/01_memory.surql', import.meta.url),
|
|
278
448
|
new URL('../infrastructure/schema/02_execution_plan.surql', import.meta.url),
|
|
279
449
|
new URL('../infrastructure/schema/03_learned_skill.surql', import.meta.url),
|
|
450
|
+
new URL('../infrastructure/schema/05_recent_activity.surql', import.meta.url),
|
|
280
451
|
new URL('../infrastructure/schema/04_runtime_bootstrap.surql', import.meta.url),
|
|
281
452
|
]
|
|
282
453
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Queue, Worker } from 'bullmq'
|
|
2
|
+
import type { Job } from 'bullmq'
|
|
3
|
+
|
|
4
|
+
import { serverLogger } from '../config/logger'
|
|
5
|
+
import { ensureRecordId } from '../db/record-id'
|
|
6
|
+
import { databaseService } from '../db/service'
|
|
7
|
+
import { getRedisConnectionForBullMQ } from '../redis'
|
|
8
|
+
import { workstreamTitleService } from '../services/workstream-title.service'
|
|
9
|
+
import {
|
|
10
|
+
attachWorkerEvents,
|
|
11
|
+
createTracedWorkerProcessor,
|
|
12
|
+
createWorkerShutdown,
|
|
13
|
+
registerShutdownSignals,
|
|
14
|
+
} from '../workers/worker-utils'
|
|
15
|
+
import type { WorkerHandle } from '../workers/worker-utils'
|
|
16
|
+
|
|
17
|
+
interface WorkstreamTitleGenerationJob {
|
|
18
|
+
workstreamId: string
|
|
19
|
+
sourceText: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const WORKSTREAM_TITLE_GENERATION_QUEUE = 'workstream-title-generation'
|
|
23
|
+
|
|
24
|
+
let _workstreamTitleGenerationQueue: Queue<WorkstreamTitleGenerationJob> | null = null
|
|
25
|
+
function getWorkstreamTitleGenerationQueue(): Queue<WorkstreamTitleGenerationJob> {
|
|
26
|
+
if (!_workstreamTitleGenerationQueue) {
|
|
27
|
+
_workstreamTitleGenerationQueue = new Queue<WorkstreamTitleGenerationJob>(WORKSTREAM_TITLE_GENERATION_QUEUE, {
|
|
28
|
+
connection: getRedisConnectionForBullMQ(),
|
|
29
|
+
defaultJobOptions: {
|
|
30
|
+
removeOnComplete: 200,
|
|
31
|
+
removeOnFail: 200,
|
|
32
|
+
attempts: 2,
|
|
33
|
+
backoff: { type: 'exponential', delay: 2_000 },
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
return _workstreamTitleGenerationQueue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function enqueueWorkstreamTitleGeneration(job: WorkstreamTitleGenerationJob) {
|
|
41
|
+
return await getWorkstreamTitleGenerationQueue().add('generate-workstream-title', job, {
|
|
42
|
+
jobId: `workstream-title:${job.workstreamId}`,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function processWorkstreamTitleGenerationJob(job: Job<WorkstreamTitleGenerationJob>): Promise<void> {
|
|
47
|
+
await databaseService.connect()
|
|
48
|
+
const workstreamRef = ensureRecordId(job.data.workstreamId)
|
|
49
|
+
await workstreamTitleService.generateAndPersistTitle(workstreamRef, job.data.sourceText)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function startWorkstreamTitleGenerationWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
|
|
53
|
+
const { registerSignals = import.meta.main } = options
|
|
54
|
+
const worker = new Worker(
|
|
55
|
+
WORKSTREAM_TITLE_GENERATION_QUEUE,
|
|
56
|
+
createTracedWorkerProcessor(WORKSTREAM_TITLE_GENERATION_QUEUE, processWorkstreamTitleGenerationJob),
|
|
57
|
+
{ connection: getRedisConnectionForBullMQ(), concurrency: 2, lockDuration: 60_000 },
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
attachWorkerEvents(worker, 'Workstream title generation', serverLogger)
|
|
61
|
+
|
|
62
|
+
const shutdown = createWorkerShutdown(worker, 'Workstream title generation', serverLogger)
|
|
63
|
+
|
|
64
|
+
if (registerSignals) {
|
|
65
|
+
registerShutdownSignals({ name: 'Workstream title generation', shutdown, logger: serverLogger })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { worker, shutdown }
|
|
69
|
+
}
|
|
@@ -48,6 +48,7 @@ export interface GenerateHelperStructuredParams<T> extends Omit<GenerateHelperTe
|
|
|
48
48
|
tag: string
|
|
49
49
|
schema: ZodSchema<T>
|
|
50
50
|
textFallbackParser?: (text: string) => T | null
|
|
51
|
+
normalizeCandidate?: (candidate: unknown) => unknown
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -358,6 +359,14 @@ export function createHelperModelRuntime() {
|
|
|
358
359
|
return parsed.data
|
|
359
360
|
}
|
|
360
361
|
|
|
362
|
+
if (params.normalizeCandidate && isObject(result.output)) {
|
|
363
|
+
const normalized = params.normalizeCandidate(result.output)
|
|
364
|
+
const normalizedParsed = parseStructuredCandidate({ schema: params.schema, candidate: normalized })
|
|
365
|
+
if (normalizedParsed) {
|
|
366
|
+
return normalizedParsed.data
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
361
370
|
if (typeof result.text === 'string' && result.text.trim().length > 0) {
|
|
362
371
|
return parseStructuredTextWithFallbacks({
|
|
363
372
|
schema: params.schema,
|
|
@@ -401,5 +410,3 @@ export function createHelperModelRuntime() {
|
|
|
401
410
|
|
|
402
411
|
return { generateHelperText, generateHelperStructured }
|
|
403
412
|
}
|
|
404
|
-
|
|
405
|
-
export const llmHelperService = createHelperModelRuntime()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { recordIdStringSchema } from '@lota-sdk/shared/schemas/common'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { BaseService } from '../db/base.service'
|
|
5
|
+
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
6
|
+
import type { RecordIdInput, RecordIdRef } from '../db/record-id'
|
|
7
|
+
import { TABLES } from '../db/tables'
|
|
8
|
+
import { toIsoDateTimeString } from '../utils/date-time'
|
|
9
|
+
|
|
10
|
+
const organizationMemberRecordSchema = z.object({
|
|
11
|
+
id: z.unknown(),
|
|
12
|
+
in: z.unknown(),
|
|
13
|
+
out: z.unknown(),
|
|
14
|
+
role: z.string(),
|
|
15
|
+
createdAt: z.coerce.date(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const sdkOrganizationMemberSchema = z.object({
|
|
19
|
+
id: recordIdStringSchema,
|
|
20
|
+
userId: recordIdStringSchema,
|
|
21
|
+
organizationId: recordIdStringSchema,
|
|
22
|
+
role: z.string(),
|
|
23
|
+
createdAt: z.iso.datetime(),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export type SdkOrganizationMemberRecord = z.infer<typeof organizationMemberRecordSchema>
|
|
27
|
+
export type SdkOrganizationMember = z.infer<typeof sdkOrganizationMemberSchema>
|
|
28
|
+
|
|
29
|
+
class OrganizationMemberService extends BaseService<typeof organizationMemberRecordSchema> {
|
|
30
|
+
constructor() {
|
|
31
|
+
super(TABLES.ORGANIZATION_MEMBER, organizationMemberRecordSchema)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toPublic(record: SdkOrganizationMemberRecord): SdkOrganizationMember {
|
|
35
|
+
return sdkOrganizationMemberSchema.parse({
|
|
36
|
+
id: recordIdToString(
|
|
37
|
+
ensureRecordId(record.id as RecordIdInput, TABLES.ORGANIZATION_MEMBER),
|
|
38
|
+
TABLES.ORGANIZATION_MEMBER,
|
|
39
|
+
),
|
|
40
|
+
userId: recordIdToString(ensureRecordId(record.in as RecordIdInput, TABLES.USER), TABLES.USER),
|
|
41
|
+
organizationId: recordIdToString(
|
|
42
|
+
ensureRecordId(record.out as RecordIdInput, TABLES.ORGANIZATION),
|
|
43
|
+
TABLES.ORGANIZATION,
|
|
44
|
+
),
|
|
45
|
+
role: record.role,
|
|
46
|
+
createdAt: toIsoDateTimeString(record.createdAt),
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async addMembership(params: {
|
|
51
|
+
userId: RecordIdInput
|
|
52
|
+
organizationId: RecordIdInput
|
|
53
|
+
role: 'owner' | 'member'
|
|
54
|
+
}): Promise<SdkOrganizationMember> {
|
|
55
|
+
const userRef = ensureRecordId(params.userId, TABLES.USER)
|
|
56
|
+
const organizationRef = ensureRecordId(params.organizationId, TABLES.ORGANIZATION)
|
|
57
|
+
const existing = (await this.findAll({ in: userRef, out: organizationRef }, { limit: 1 })).at(0)
|
|
58
|
+
|
|
59
|
+
if (existing) {
|
|
60
|
+
return this.toPublic(await this.update(existing.id, { role: params.role }))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const related = await this.databaseService.relate(userRef, TABLES.ORGANIZATION_MEMBER, organizationRef, {
|
|
64
|
+
role: params.role,
|
|
65
|
+
})
|
|
66
|
+
if (!related) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Failed to create membership for ${recordIdToString(userRef, TABLES.USER)} in ${recordIdToString(
|
|
69
|
+
organizationRef,
|
|
70
|
+
TABLES.ORGANIZATION,
|
|
71
|
+
)}`,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const record = organizationMemberRecordSchema.parse(related)
|
|
76
|
+
|
|
77
|
+
return this.toPublic(record)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async listMembershipsForOrganization(organizationId: RecordIdRef): Promise<SdkOrganizationMember[]> {
|
|
81
|
+
const organizationRef = ensureRecordId(organizationId, TABLES.ORGANIZATION)
|
|
82
|
+
return (await this.findAll({ out: organizationRef }, { orderBy: 'createdAt', orderDir: 'ASC' })).map((record) =>
|
|
83
|
+
this.toPublic(record),
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async listMembershipsForUser(userId: RecordIdRef): Promise<SdkOrganizationMember[]> {
|
|
88
|
+
const userRef = ensureRecordId(userId, TABLES.USER)
|
|
89
|
+
return (await this.findAll({ in: userRef }, { orderBy: 'createdAt', orderDir: 'ASC' })).map((record) =>
|
|
90
|
+
this.toPublic(record),
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async isMember(userId: RecordIdInput, organizationId: RecordIdInput): Promise<boolean> {
|
|
95
|
+
const userRef = ensureRecordId(userId, TABLES.USER)
|
|
96
|
+
const organizationRef = ensureRecordId(organizationId, TABLES.ORGANIZATION)
|
|
97
|
+
const existing = await this.findAll({ in: userRef, out: organizationRef }, { limit: 1 })
|
|
98
|
+
return existing.length > 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async removeMembership(params: { userId: RecordIdInput; organizationId: RecordIdInput }): Promise<void> {
|
|
102
|
+
const userRef = ensureRecordId(params.userId, TABLES.USER)
|
|
103
|
+
const organizationRef = ensureRecordId(params.organizationId, TABLES.ORGANIZATION)
|
|
104
|
+
const existing = await this.findAll({ in: userRef, out: organizationRef }, { limit: 1 })
|
|
105
|
+
const record = existing.at(0)
|
|
106
|
+
if (!record) {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await this.delete(record.id)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const organizationMemberService = new OrganizationMemberService()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { recordIdStringSchema } from '@lota-sdk/shared/schemas/common'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { BaseService } from '../db/base.service'
|
|
5
|
+
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
6
|
+
import type { RecordIdInput, RecordIdRef } from '../db/record-id'
|
|
7
|
+
import { databaseService } from '../db/service'
|
|
8
|
+
import { TABLES } from '../db/tables'
|
|
9
|
+
import { toIsoDateTimeString, toOptionalIsoDateTimeString } from '../utils/date-time'
|
|
10
|
+
|
|
11
|
+
const organizationRecordSchema = z.object({
|
|
12
|
+
id: z.unknown(),
|
|
13
|
+
name: z.string(),
|
|
14
|
+
regularChatDigestLastWorkstreamCursorCreatedAt: z.union([z.date(), z.string(), z.number()]).optional(),
|
|
15
|
+
regularChatDigestLastWorkstreamCursorId: z.string().optional(),
|
|
16
|
+
skillExtractionLastCursorId: z.string().optional(),
|
|
17
|
+
skillExtractionLastCursorCreatedAt: z.union([z.date(), z.string(), z.number()]).optional(),
|
|
18
|
+
createdAt: z.coerce.date(),
|
|
19
|
+
updatedAt: z.coerce.date(),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const sdkOrganizationSchema = z.object({
|
|
23
|
+
id: recordIdStringSchema,
|
|
24
|
+
name: z.string(),
|
|
25
|
+
regularChatDigestLastWorkstreamCursorCreatedAt: z.iso.datetime().nullable().optional(),
|
|
26
|
+
regularChatDigestLastWorkstreamCursorId: z.string().nullable().optional(),
|
|
27
|
+
skillExtractionLastCursorId: z.string().nullable().optional(),
|
|
28
|
+
skillExtractionLastCursorCreatedAt: z.iso.datetime().nullable().optional(),
|
|
29
|
+
createdAt: z.iso.datetime(),
|
|
30
|
+
updatedAt: z.iso.datetime(),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export type SdkOrganizationRecord = z.infer<typeof organizationRecordSchema>
|
|
34
|
+
export type SdkOrganization = z.infer<typeof sdkOrganizationSchema>
|
|
35
|
+
|
|
36
|
+
interface BackgroundCursor {
|
|
37
|
+
createdAt: Date
|
|
38
|
+
id: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toOptionalCursorTimestamp(value: unknown): string | null {
|
|
42
|
+
return toOptionalIsoDateTimeString(value) ?? null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class OrganizationService extends BaseService<typeof organizationRecordSchema> {
|
|
46
|
+
constructor() {
|
|
47
|
+
super(TABLES.ORGANIZATION, organizationRecordSchema)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
toPublic(record: SdkOrganizationRecord): SdkOrganization {
|
|
51
|
+
return sdkOrganizationSchema.parse({
|
|
52
|
+
id: recordIdToString(ensureRecordId(record.id as RecordIdInput, TABLES.ORGANIZATION), TABLES.ORGANIZATION),
|
|
53
|
+
name: record.name,
|
|
54
|
+
regularChatDigestLastWorkstreamCursorCreatedAt: toOptionalCursorTimestamp(
|
|
55
|
+
record.regularChatDigestLastWorkstreamCursorCreatedAt,
|
|
56
|
+
),
|
|
57
|
+
regularChatDigestLastWorkstreamCursorId: record.regularChatDigestLastWorkstreamCursorId ?? null,
|
|
58
|
+
skillExtractionLastCursorId: record.skillExtractionLastCursorId ?? null,
|
|
59
|
+
skillExtractionLastCursorCreatedAt: toOptionalCursorTimestamp(record.skillExtractionLastCursorCreatedAt),
|
|
60
|
+
createdAt: toIsoDateTimeString(record.createdAt),
|
|
61
|
+
updatedAt: toIsoDateTimeString(record.updatedAt),
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async createOrganization(params: { name: string }): Promise<SdkOrganization> {
|
|
66
|
+
return this.toPublic(await this.create({ name: params.name }))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async upsertOrganization(params: { id: RecordIdInput; name: string }): Promise<SdkOrganization> {
|
|
70
|
+
const organizationRef = ensureRecordId(params.id, TABLES.ORGANIZATION)
|
|
71
|
+
const record = await databaseService.upsert(
|
|
72
|
+
TABLES.ORGANIZATION,
|
|
73
|
+
organizationRef,
|
|
74
|
+
{ name: params.name },
|
|
75
|
+
organizationRecordSchema,
|
|
76
|
+
)
|
|
77
|
+
return this.toPublic(record)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async listOrganizations(): Promise<SdkOrganization[]> {
|
|
81
|
+
return (await this.findAll({}, { orderBy: 'createdAt', orderDir: 'ASC' })).map((record) => this.toPublic(record))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getOrganization(organizationId: RecordIdInput): Promise<SdkOrganization> {
|
|
85
|
+
return this.toPublic(await this.getById(ensureRecordId(organizationId, TABLES.ORGANIZATION)))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getOrganizationRecord(organizationId: RecordIdRef): Promise<SdkOrganizationRecord> {
|
|
89
|
+
return await this.getById(ensureRecordId(organizationId, TABLES.ORGANIZATION))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async updateOrganization(organizationId: RecordIdInput, params: { name: string }): Promise<SdkOrganization> {
|
|
93
|
+
return this.toPublic(await this.update(ensureRecordId(organizationId, TABLES.ORGANIZATION), { name: params.name }))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async deleteOrganization(organizationId: RecordIdInput): Promise<void> {
|
|
97
|
+
const organizationRef = ensureRecordId(organizationId, TABLES.ORGANIZATION)
|
|
98
|
+
await databaseService.deleteWhere(TABLES.ORGANIZATION_MEMBER, { out: organizationRef })
|
|
99
|
+
await this.delete(organizationRef)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async updateRegularChatDigestCursor(organizationId: RecordIdRef, cursor: BackgroundCursor): Promise<void> {
|
|
103
|
+
await this.update(organizationId, {
|
|
104
|
+
regularChatDigestLastWorkstreamCursorCreatedAt: cursor.createdAt,
|
|
105
|
+
regularChatDigestLastWorkstreamCursorId: cursor.id,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async updateSkillExtractionCursor(organizationId: RecordIdRef, cursor: BackgroundCursor): Promise<void> {
|
|
110
|
+
await this.update(organizationId, {
|
|
111
|
+
skillExtractionLastCursorCreatedAt: cursor.createdAt,
|
|
112
|
+
skillExtractionLastCursorId: cursor.id,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const organizationService = new OrganizationService()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SdkUser, SdkUserRecord } from '@lota-sdk/shared/schemas/user'
|
|
2
|
+
import { sdkUserRecordSchema, sdkUserSchema } from '@lota-sdk/shared/schemas/user'
|
|
3
|
+
|
|
4
|
+
import { BaseService } from '../db/base.service'
|
|
5
|
+
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
6
|
+
import type { RecordIdInput } from '../db/record-id'
|
|
7
|
+
import { databaseService } from '../db/service'
|
|
8
|
+
import { TABLES } from '../db/tables'
|
|
9
|
+
import { toIsoDateTimeString } from '../utils/date-time'
|
|
10
|
+
|
|
11
|
+
class UserService extends BaseService<typeof sdkUserRecordSchema> {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(TABLES.USER, sdkUserRecordSchema)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
toPublic(record: SdkUserRecord): SdkUser {
|
|
17
|
+
return sdkUserSchema.parse({
|
|
18
|
+
id: recordIdToString(ensureRecordId(record.id as RecordIdInput, TABLES.USER), TABLES.USER),
|
|
19
|
+
name: record.name,
|
|
20
|
+
email: record.email,
|
|
21
|
+
createdAt: toIsoDateTimeString(record.createdAt),
|
|
22
|
+
updatedAt: toIsoDateTimeString(record.updatedAt),
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async upsertUser(params: { id: RecordIdInput; name: string; email: string }): Promise<SdkUser> {
|
|
27
|
+
const userRef = ensureRecordId(params.id, TABLES.USER)
|
|
28
|
+
const record = await databaseService.upsert(
|
|
29
|
+
TABLES.USER,
|
|
30
|
+
userRef,
|
|
31
|
+
{ name: params.name, email: params.email },
|
|
32
|
+
sdkUserRecordSchema,
|
|
33
|
+
)
|
|
34
|
+
return this.toPublic(record)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getUser(userId: RecordIdInput): Promise<SdkUser> {
|
|
38
|
+
return this.toPublic(await this.getById(ensureRecordId(userId, TABLES.USER)))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async listUsers(): Promise<SdkUser[]> {
|
|
42
|
+
return (await this.findAll({}, { orderBy: 'createdAt', orderDir: 'ASC' })).map((record) => this.toPublic(record))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async updateUser(userId: RecordIdInput, params: { name?: string; email?: string }): Promise<SdkUser> {
|
|
46
|
+
return this.toPublic(await this.update(ensureRecordId(userId, TABLES.USER), params))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async deleteUser(userId: RecordIdInput): Promise<void> {
|
|
50
|
+
const userRef = ensureRecordId(userId, TABLES.USER)
|
|
51
|
+
await databaseService.deleteWhere(TABLES.ORGANIZATION_MEMBER, { in: userRef })
|
|
52
|
+
await this.delete(userRef)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const userService = new UserService()
|
|
@@ -2,56 +2,46 @@ import { WORKSTREAM } from '@lota-sdk/shared/constants/workstream'
|
|
|
2
2
|
|
|
3
3
|
import { chatLogger } from '../config/logger'
|
|
4
4
|
import type { RecordIdRef } from '../db/record-id'
|
|
5
|
-
import {
|
|
6
|
-
import { TABLES } from '../db/tables'
|
|
7
|
-
import type { HelperAgent } from '../runtime/helper-model'
|
|
8
|
-
import { llmHelperService } from '../runtime/helper-model'
|
|
5
|
+
import { createHelperModelRuntime } from '../runtime/helper-model'
|
|
9
6
|
import { deriveTitle, limitTitleWords } from '../runtime/title-helpers'
|
|
7
|
+
import {
|
|
8
|
+
createWorkstreamTitleGeneratorAgent,
|
|
9
|
+
WORKSTREAM_TITLE_GENERATOR_PROMPT,
|
|
10
|
+
} from '../system-agents/title-generator.agent'
|
|
11
|
+
import { compactWhitespace } from '../utils/string'
|
|
10
12
|
import { workstreamService } from './workstream.service'
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
function normalizeTitle(value: string): string {
|
|
15
|
+
const normalized = compactWhitespace(value)
|
|
16
|
+
.replace(/^["'`]+|["'`]+$/g, '')
|
|
17
|
+
.replace(/[.!?,;:]+$/g, '')
|
|
18
|
+
return normalized.length <= 80 ? normalized : normalized.slice(0, 80).trim()
|
|
19
|
+
}
|
|
13
20
|
|
|
14
21
|
class WorkstreamTitleService {
|
|
15
|
-
|
|
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
|
-
}
|
|
22
|
+
helperRuntime = createHelperModelRuntime()
|
|
35
23
|
|
|
36
|
-
|
|
24
|
+
async generateAndPersistTitle(workstreamId: RecordIdRef, sourceText: string): Promise<void> {
|
|
37
25
|
let title = ''
|
|
38
26
|
try {
|
|
39
|
-
title =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
title = normalizeTitle(
|
|
28
|
+
await this.helperRuntime.generateHelperText({
|
|
29
|
+
tag: 'workstream-title',
|
|
30
|
+
createAgent: createWorkstreamTitleGeneratorAgent,
|
|
31
|
+
defaultSystemPrompt: WORKSTREAM_TITLE_GENERATOR_PROMPT,
|
|
32
|
+
timeoutMs: 30_000,
|
|
33
|
+
messages: [{ role: 'user', content: sourceText }],
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
44
36
|
} catch (error) {
|
|
45
|
-
chatLogger.warn`Failed to generate workstream title (non-fatal): ${error}`
|
|
46
|
-
title = ''
|
|
37
|
+
chatLogger.warn`Failed to generate workstream title via LLM (non-fatal): ${error}`
|
|
47
38
|
}
|
|
48
39
|
|
|
49
40
|
if (!title) {
|
|
50
41
|
title = limitTitleWords(deriveTitle(sourceText || WORKSTREAM.DEFAULT_TITLE))
|
|
51
42
|
}
|
|
52
43
|
|
|
53
|
-
await workstreamService.
|
|
54
|
-
return title
|
|
44
|
+
await workstreamService.persistGeneratedTitle(workstreamId, title)
|
|
55
45
|
}
|
|
56
46
|
}
|
|
57
47
|
|
|
@@ -28,6 +28,7 @@ import { enqueuePostChatMemory } from '../queues/post-chat-memory.queue'
|
|
|
28
28
|
import { enqueueRecentActivityTitleRefinement } from '../queues/recent-activity-title-refinement.queue'
|
|
29
29
|
import { enqueueRegularChatMemoryDigest } from '../queues/regular-chat-memory-digest.queue'
|
|
30
30
|
import { enqueueSkillExtraction } from '../queues/skill-extraction.queue'
|
|
31
|
+
import { enqueueWorkstreamTitleGeneration } from '../queues/workstream-title-generation.queue'
|
|
31
32
|
import { buildAgentPromptContext } from '../runtime/agent-prompt-context'
|
|
32
33
|
import {
|
|
33
34
|
buildSpecialistTaskMessage,
|
|
@@ -445,6 +446,20 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
445
446
|
? [...liveHistory].reverse().find((m) => m.role === 'user')
|
|
446
447
|
: (userMessage ?? [...liveHistory].reverse().find((m) => m.role === 'user'))
|
|
447
448
|
const messageText = referenceUserMessage ? extractMessageText(referenceUserMessage).trim() : ''
|
|
449
|
+
|
|
450
|
+
if (
|
|
451
|
+
params.kind === 'userTurn' &&
|
|
452
|
+
workstream.mode === 'group' &&
|
|
453
|
+
!workstream.core &&
|
|
454
|
+
workstreamRecord.nameGenerated !== true &&
|
|
455
|
+
messageText.length > 0
|
|
456
|
+
) {
|
|
457
|
+
void safeEnqueue(
|
|
458
|
+
() => enqueueWorkstreamTitleGeneration({ workstreamId: workstreamIdString, sourceText: messageText }),
|
|
459
|
+
{ operationName: 'workstream-title-generation' },
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
|
|
448
463
|
const onboardingActive = workspaceLifecycleState?.bootstrapActive ?? false
|
|
449
464
|
if (workstream.core && !workstream.coreType) {
|
|
450
465
|
throw new WorkstreamTurnError('Core workstreams require a core type.', 400)
|
|
@@ -356,7 +356,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
356
356
|
.createWithId(
|
|
357
357
|
TABLES.WORKSTREAM,
|
|
358
358
|
directWorkstreamId,
|
|
359
|
-
{ userId, organizationId: orgId, mode, core: false, agentId, title, status: 'regular' },
|
|
359
|
+
{ userId, organizationId: orgId, mode, core: false, agentId, title, status: 'regular', nameGenerated: true },
|
|
360
360
|
WorkstreamSchema,
|
|
361
361
|
)
|
|
362
362
|
.catch((error) => {
|
|
@@ -402,6 +402,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
402
402
|
agentId: coreProfile.config.agentId,
|
|
403
403
|
title,
|
|
404
404
|
status: 'regular',
|
|
405
|
+
nameGenerated: true,
|
|
405
406
|
},
|
|
406
407
|
WorkstreamSchema,
|
|
407
408
|
)
|
|
@@ -781,6 +782,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
781
782
|
mode,
|
|
782
783
|
core,
|
|
783
784
|
...(coreType ? { coreType } : {}),
|
|
785
|
+
nameGenerated: workstream.nameGenerated === true,
|
|
784
786
|
isRunning: activeRunId !== null,
|
|
785
787
|
isCompacting,
|
|
786
788
|
...(isAgentName(workstream.agentId) ? { agentId: workstream.agentId } : {}),
|
|
@@ -807,6 +809,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
807
809
|
const mode = typeof workstream.mode === 'string' ? workstream.mode : 'group'
|
|
808
810
|
const core = workstream.core === true
|
|
809
811
|
const coreType = core && typeof workstream.coreType === 'string' ? workstream.coreType : undefined
|
|
812
|
+
const nameGenerated = 'nameGenerated' in workstream ? workstream.nameGenerated === true : false
|
|
810
813
|
return {
|
|
811
814
|
id,
|
|
812
815
|
mode,
|
|
@@ -815,6 +818,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
815
818
|
...(isAgentName(workstream.agentId) ? { agentId: workstream.agentId } : {}),
|
|
816
819
|
title: workstream.title ?? this.getDefaultTitle(workstream),
|
|
817
820
|
status: workstream.status ?? 'regular',
|
|
821
|
+
nameGenerated,
|
|
818
822
|
isRunning,
|
|
819
823
|
isCompacting,
|
|
820
824
|
createdAt,
|
|
@@ -838,6 +842,10 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
838
842
|
return { ...publicWorkstream, workstreamState }
|
|
839
843
|
}
|
|
840
844
|
|
|
845
|
+
async persistGeneratedTitle(workstreamId: RecordIdRef, title: string): Promise<void> {
|
|
846
|
+
await this.update(workstreamId, { title, nameGenerated: true })
|
|
847
|
+
}
|
|
848
|
+
|
|
841
849
|
private assertMutableWorkstream(
|
|
842
850
|
workstream: WorkstreamRecord,
|
|
843
851
|
action: 'rename' | 'archive' | 'unarchive' | 'delete',
|
|
@@ -22,6 +22,7 @@ export interface NormalizedWorkstream {
|
|
|
22
22
|
mode: 'direct' | 'group'
|
|
23
23
|
core: boolean
|
|
24
24
|
coreType?: string
|
|
25
|
+
nameGenerated: boolean
|
|
25
26
|
isRunning: boolean
|
|
26
27
|
isCompacting: boolean
|
|
27
28
|
agentId?: string | null
|
|
@@ -107,6 +108,7 @@ export const WorkstreamSchema = z.object({
|
|
|
107
108
|
activeRunId: z.string().nullish(),
|
|
108
109
|
chatSummary: z.string().nullish(),
|
|
109
110
|
lastCompactedMessageId: z.string().nullish(),
|
|
111
|
+
nameGenerated: z.boolean().optional().default(false),
|
|
110
112
|
isCompacting: z.boolean().optional(),
|
|
111
113
|
state: z.unknown().optional(),
|
|
112
114
|
createdAt: z.coerce.date(),
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
|
|
9
9
|
import { resolveHelperAgentOptions } from './helper-agent-options'
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const WORKSTREAM_TITLE_MAX_TOKENS = 64
|
|
12
12
|
|
|
13
|
-
const WORKSTREAM_TITLE_GENERATOR_PROMPT = `<agent-instructions>
|
|
13
|
+
export const WORKSTREAM_TITLE_GENERATOR_PROMPT = `<agent-instructions>
|
|
14
14
|
You are a **Title Generator** that creates concise chat titles.
|
|
15
15
|
|
|
16
16
|
<task>
|
|
@@ -29,14 +29,14 @@ Return only the title text. No quotes, no labels, no explanation.
|
|
|
29
29
|
</output-format>
|
|
30
30
|
</agent-instructions>`
|
|
31
31
|
|
|
32
|
-
export function
|
|
32
|
+
export function createWorkstreamTitleGeneratorAgent(options: CreateHelperToolLoopAgentOptions) {
|
|
33
33
|
return new ToolLoopAgent({
|
|
34
|
-
id: 'title-generator',
|
|
34
|
+
id: 'workstream-title-generator',
|
|
35
35
|
model: bifrostModel(OPENROUTER_FAST_REASONING_MODEL_ID),
|
|
36
36
|
providerOptions: OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
|
|
37
37
|
...resolveHelperAgentOptions(options, {
|
|
38
38
|
instructions: WORKSTREAM_TITLE_GENERATOR_PROMPT,
|
|
39
|
-
maxOutputTokens:
|
|
39
|
+
maxOutputTokens: WORKSTREAM_TITLE_MAX_TOKENS,
|
|
40
40
|
}),
|
|
41
41
|
})
|
|
42
42
|
}
|
package/src/workers/bootstrap.ts
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import { configureLogger, serverLogger } from '../config/logger'
|
|
2
|
-
import {
|
|
2
|
+
import { LOTA_SDK_DATABASE_NAME } from '../db/sdk-database'
|
|
3
|
+
import { SurrealDBService, databaseService, setDatabaseService } from '../db/service'
|
|
3
4
|
import { connectWithStartupRetry, waitForDatabaseBootstrap } from '../db/startup'
|
|
4
5
|
import { getConfiguredPluginDatabaseConnector } from '../runtime/runtime-extensions'
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Sandboxed BullMQ workers run in separate child processes where createLotaRuntime
|
|
9
|
+
* was never called, so the global databaseService proxy has no backing instance.
|
|
10
|
+
* Create one from env vars so the proxy resolves.
|
|
11
|
+
*/
|
|
12
|
+
function getRequiredEnv(key: string): string {
|
|
13
|
+
const value = process.env[key]
|
|
14
|
+
if (!value) throw new Error(`Missing required env var: ${key}`)
|
|
15
|
+
return value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureDatabaseServiceConfigured(): void {
|
|
19
|
+
const db = new SurrealDBService({
|
|
20
|
+
url: getRequiredEnv('SURREALDB_URL'),
|
|
21
|
+
namespace: getRequiredEnv('SURREALDB_NAMESPACE'),
|
|
22
|
+
database: LOTA_SDK_DATABASE_NAME,
|
|
23
|
+
username: process.env.SURREALDB_USER,
|
|
24
|
+
password: process.env.SURREALDB_PASSWORD,
|
|
25
|
+
})
|
|
26
|
+
setDatabaseService(db)
|
|
27
|
+
}
|
|
28
|
+
|
|
6
29
|
let sandboxedWorkerRuntimePromise: Promise<void> | null = null
|
|
7
30
|
|
|
8
31
|
export async function initializeSandboxedWorkerRuntime(): Promise<void> {
|
|
@@ -14,6 +37,8 @@ export async function initializeSandboxedWorkerRuntime(): Promise<void> {
|
|
|
14
37
|
sandboxedWorkerRuntimePromise = (async () => {
|
|
15
38
|
await configureLogger()
|
|
16
39
|
|
|
40
|
+
ensureDatabaseServiceConfigured()
|
|
41
|
+
|
|
17
42
|
await connectWithStartupRetry({
|
|
18
43
|
connect: () => databaseService.connect(),
|
|
19
44
|
label: 'sandboxed worker AI database runtime',
|
|
@@ -276,13 +276,6 @@ async function cleanupOrphanedRelations(): Promise<number> {
|
|
|
276
276
|
return result[0]?.count ?? 0
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
async function pruneOldOrgActions(): Promise<number> {
|
|
280
|
-
const result = await databaseService.query<{ count: number }>(
|
|
281
|
-
new BoundQuery(`DELETE ${TABLES.ORG_ACTION} WHERE createdAt < time::now() - 90d RETURN count() AS count`),
|
|
282
|
-
)
|
|
283
|
-
return result[0]?.count ?? 0
|
|
284
|
-
}
|
|
285
|
-
|
|
286
279
|
const handler = async (job: SandboxedJob<MemoryConsolidationJob>) => {
|
|
287
280
|
const targetScope = job.data.scopeId
|
|
288
281
|
|
|
@@ -305,9 +298,8 @@ const handler = async (job: SandboxedJob<MemoryConsolidationJob>) => {
|
|
|
305
298
|
const collapsed = await collapseSupersedeCh()
|
|
306
299
|
const decayed = await decayImportance()
|
|
307
300
|
const orphaned = await cleanupOrphanedRelations()
|
|
308
|
-
const prunedActions = await pruneOldOrgActions()
|
|
309
301
|
|
|
310
|
-
serverLogger.info`Memory consolidation complete (merged: ${totalMerged}, pruned: ${pruned}, collapsed: ${collapsed}, decayed: ${decayed}, orphaned relations: ${orphaned}
|
|
302
|
+
serverLogger.info`Memory consolidation complete (merged: ${totalMerged}, pruned: ${pruned}, collapsed: ${collapsed}, decayed: ${decayed}, orphaned relations: ${orphaned})`
|
|
311
303
|
} catch (error) {
|
|
312
304
|
const serialized = toSandboxedWorkerError(error, 'Memory consolidation job failed')
|
|
313
305
|
serverLogger.error`${serialized.message}`
|