@lota-sdk/core 0.1.9 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/infrastructure/schema/00_workstream.surql +1 -0
  2. package/infrastructure/schema/02_execution_plan.surql +202 -52
  3. package/package.json +4 -87
  4. package/src/ai/index.ts +3 -0
  5. package/src/bifrost/bifrost.ts +94 -25
  6. package/src/bifrost/index.ts +1 -0
  7. package/src/config/agent-defaults.ts +30 -7
  8. package/src/config/constants.ts +0 -9
  9. package/src/config/debug-logger.ts +43 -0
  10. package/src/config/index.ts +5 -0
  11. package/src/config/model-constants.ts +8 -9
  12. package/src/config/workstream-defaults.ts +4 -0
  13. package/src/db/cursor-pagination.ts +2 -2
  14. package/src/db/index.ts +10 -0
  15. package/src/db/memory-store.ts +3 -71
  16. package/src/db/memory.ts +9 -15
  17. package/src/db/service.ts +42 -2
  18. package/src/db/tables.ts +9 -2
  19. package/src/document/index.ts +2 -0
  20. package/src/document/parsing.ts +0 -25
  21. package/src/embeddings/provider.ts +102 -22
  22. package/src/index.ts +15 -499
  23. package/src/queues/index.ts +10 -0
  24. package/src/redis/connection-accessor.ts +26 -0
  25. package/src/redis/connection.ts +1 -1
  26. package/src/redis/index.ts +9 -25
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +1 -1
  29. package/src/redis/stream-context.ts +54 -0
  30. package/src/runtime/agent-runtime-policy.ts +9 -5
  31. package/src/runtime/agent-stream-helpers.ts +6 -3
  32. package/src/runtime/agent-types.ts +1 -5
  33. package/src/runtime/approval-continuation.ts +68 -1
  34. package/src/runtime/chat-attachments.ts +1 -1
  35. package/src/runtime/chat-request-routing.ts +6 -2
  36. package/src/runtime/context-compaction-runtime.ts +2 -2
  37. package/src/runtime/context-compaction.ts +1 -1
  38. package/src/runtime/execution-plan.ts +22 -15
  39. package/src/runtime/index.ts +26 -0
  40. package/src/runtime/indexed-repositories-policy.ts +10 -10
  41. package/src/runtime/memory-pipeline.ts +0 -2
  42. package/src/runtime/runtime-config.ts +238 -0
  43. package/src/runtime/runtime-extensions.ts +3 -2
  44. package/src/runtime/runtime-worker-registry.ts +47 -0
  45. package/src/runtime/team-consultation-orchestrator.ts +9 -6
  46. package/src/runtime/team-consultation-prompts.ts +3 -2
  47. package/src/runtime/turn-lifecycle.ts +13 -5
  48. package/src/runtime/workstream-chat-helpers.ts +0 -54
  49. package/src/runtime/workstream-routing-policy.ts +3 -7
  50. package/src/runtime.ts +387 -0
  51. package/src/services/chat-attachments.service.ts +1 -1
  52. package/src/services/context-compaction.service.ts +1 -1
  53. package/src/services/document-chunk.service.ts +2 -2
  54. package/src/services/execution-plan.service.ts +584 -793
  55. package/src/services/index.ts +14 -0
  56. package/src/services/learned-skill.service.ts +82 -39
  57. package/src/services/memory.service.ts +5 -4
  58. package/src/services/mutating-approval.service.ts +1 -1
  59. package/src/services/organization-member.service.ts +1 -1
  60. package/src/services/organization.service.ts +1 -1
  61. package/src/services/plan-approval.service.ts +83 -0
  62. package/src/services/plan-artifact.service.ts +44 -0
  63. package/src/services/plan-builder.service.ts +61 -0
  64. package/src/services/plan-checkpoint.service.ts +53 -0
  65. package/src/services/plan-compiler.service.ts +81 -0
  66. package/src/services/plan-executor.service.ts +1624 -0
  67. package/src/services/plan-run.service.ts +422 -0
  68. package/src/services/plan-validator.service.ts +760 -0
  69. package/src/services/recent-activity-title.service.ts +1 -1
  70. package/src/services/recent-activity.service.ts +14 -16
  71. package/src/services/user.service.ts +2 -2
  72. package/src/services/workstream-message.service.ts +2 -3
  73. package/src/services/workstream-title.service.ts +1 -1
  74. package/src/services/workstream-turn-preparation.ts +156 -59
  75. package/src/services/workstream-turn.ts +26 -1
  76. package/src/services/workstream.service.ts +35 -9
  77. package/src/services/workstream.types.ts +1 -0
  78. package/src/storage/attachment-parser.ts +1 -1
  79. package/src/storage/attachment-storage.service.ts +11 -10
  80. package/src/storage/generated-document-storage.service.ts +7 -6
  81. package/src/storage/index.ts +10 -0
  82. package/src/system-agents/delegated-agent-factory.ts +78 -29
  83. package/src/system-agents/index.ts +4 -0
  84. package/src/system-agents/recent-activity-title-refiner.agent.ts +38 -3
  85. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  86. package/src/system-agents/skill-extractor.agent.ts +1 -1
  87. package/src/system-agents/skill-manager.agent.ts +2 -4
  88. package/src/system-agents/title-generator.agent.ts +2 -2
  89. package/src/tools/execution-plan.tool.ts +22 -48
  90. package/src/tools/firecrawl-client.ts +2 -2
  91. package/src/tools/index.ts +12 -0
  92. package/src/tools/log-hello-world.tool.ts +17 -0
  93. package/src/tools/research-topic.tool.ts +1 -1
  94. package/src/tools/team-think.tool.ts +1 -1
  95. package/src/tools/user-questions.tool.ts +2 -2
  96. package/src/utils/index.ts +6 -0
  97. package/src/workers/bootstrap.ts +8 -16
  98. package/src/workers/index.ts +7 -0
  99. package/src/workers/regular-chat-memory-digest.runner.ts +1 -1
  100. package/src/workers/skill-extraction.runner.ts +3 -3
  101. package/src/workers/utils/{repo-indexer-chunker.ts → file-section-chunker.ts} +23 -52
  102. package/src/workers/utils/repo-structure-extractor.ts +2 -5
  103. package/src/workers/utils/repomix-file-sections.ts +42 -0
  104. package/src/config/env-shapes.ts +0 -121
  105. package/src/runtime/agent-contract.ts +0 -1
@@ -0,0 +1,14 @@
1
+ export * from './attachment.service'
2
+ export * from './document-chunk.service'
3
+ export * from './execution-plan.service'
4
+ export * from './memory.service'
5
+ export * from './organization-member.service'
6
+ export * from './organization.service'
7
+ export * from './plan-run.service'
8
+ export * from './recent-activity-title.service'
9
+ export * from './recent-activity.service'
10
+ export * from './user.service'
11
+ export * from './workstream-message.service'
12
+ export * from './workstream-title.service'
13
+ export * from './workstream-turn'
14
+ export * from './workstream.service'
@@ -1,19 +1,28 @@
1
- import { recordIdStringSchema } from '@lota-sdk/shared/schemas/common'
1
+ import { recordIdStringSchema } from '@lota-sdk/shared'
2
2
  import { BoundQuery } from 'surrealdb'
3
3
  import { z } from 'zod'
4
4
 
5
5
  import { renderLearnedSkillInstructions } from '../ai/definitions'
6
+ import { lotaDebugLogger } from '../config/debug-logger'
6
7
  import { serverLogger } from '../config/logger'
7
8
  import { ensureRecordId } from '../db/record-id'
8
9
  import { databaseService } from '../db/service'
9
10
  import { TABLES } from '../db/tables'
10
- import { createDefaultEmbeddings } from '../embeddings/provider'
11
+ import { getDefaultEmbeddings } from '../embeddings/provider'
12
+ import { getRedisConnection } from '../redis'
11
13
 
12
- const embeddings = createDefaultEmbeddings()
14
+ const embeddings = getDefaultEmbeddings()
13
15
 
14
16
  const PROMOTION_MIN_USES = 5
15
17
  const PROMOTION_MIN_SUCCESS_RATE = 0.6
16
18
 
19
+ const SKILL_EXISTS_TTL_SECONDS = 120
20
+ const SKILL_EXISTS_KEY_PREFIX = 'skill-exists'
21
+
22
+ function skillExistsKey(orgId: string, agentId: string): string {
23
+ return `${SKILL_EXISTS_KEY_PREFIX}:${orgId}:${agentId}`
24
+ }
25
+
17
26
  const LearnedSkillRowSchema = z.object({
18
27
  id: recordIdStringSchema,
19
28
  name: z.string(),
@@ -51,7 +60,7 @@ const SearchResultRowSchema = z.object({
51
60
 
52
61
  type SearchResultRow = z.infer<typeof SearchResultRowSchema>
53
62
 
54
- export interface CreateLearnedSkillInput {
63
+ interface CreateLearnedSkillInput {
55
64
  name: string
56
65
  description: string
57
66
  instructions: string
@@ -66,7 +75,7 @@ export interface CreateLearnedSkillInput {
66
75
  hash: string
67
76
  }
68
77
 
69
- export interface UpdateLearnedSkillInput {
78
+ interface UpdateLearnedSkillInput {
70
79
  name?: string
71
80
  description?: string
72
81
  instructions?: string
@@ -80,7 +89,7 @@ export interface UpdateLearnedSkillInput {
80
89
  supersedes?: string
81
90
  }
82
91
 
83
- export interface RetrieveForTurnParams {
92
+ interface RetrieveForTurnParams {
84
93
  orgId: string
85
94
  agentId: string
86
95
  query: string
@@ -107,7 +116,9 @@ class LearnedSkillService {
107
116
  hash: input.hash,
108
117
  }
109
118
 
110
- return databaseService.create(TABLES.LEARNED_SKILL, data, LearnedSkillRowSchema)
119
+ const result = await databaseService.create(TABLES.LEARNED_SKILL, data, LearnedSkillRowSchema)
120
+ await this.invalidateSkillExistsCache(input.organizationId, input.agentId)
121
+ return result
111
122
  }
112
123
 
113
124
  async update(skillId: string, input: UpdateLearnedSkillInput): Promise<LearnedSkillRow> {
@@ -133,12 +144,16 @@ class LearnedSkillService {
133
144
 
134
145
  async archive(skillId: string): Promise<void> {
135
146
  const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
147
+ const skill = await this.getById(skillId)
136
148
  await databaseService.update(
137
149
  TABLES.LEARNED_SKILL,
138
150
  ref,
139
151
  { status: 'archived', archivedAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
140
152
  LearnedSkillRowSchema,
141
153
  )
154
+ if (skill) {
155
+ await this.invalidateSkillExistsCache(String(skill.organizationId), skill.agentId ?? null)
156
+ }
142
157
  }
143
158
 
144
159
  async getById(skillId: string): Promise<LearnedSkillRow | null> {
@@ -149,27 +164,60 @@ class LearnedSkillService {
149
164
  )
150
165
  }
151
166
 
167
+ private async hasSkillsForAgent(orgId: string, agentId: string): Promise<boolean> {
168
+ const redis = getRedisConnection()
169
+ const key = skillExistsKey(orgId, agentId)
170
+
171
+ const cached = await redis.get(key)
172
+ if (cached !== null) return cached === '1'
173
+
174
+ const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
175
+ const rows = await databaseService.query<{ id: unknown }>(
176
+ new BoundQuery(
177
+ `SELECT id FROM ${TABLES.LEARNED_SKILL}
178
+ WHERE organizationId = $orgRef
179
+ AND status IN ['learned', 'verified', 'promoted']
180
+ AND archivedAt IS NONE
181
+ AND (agentId IS NONE OR agentId = $agentId)
182
+ LIMIT 1`,
183
+ { orgRef, agentId },
184
+ ),
185
+ )
186
+
187
+ const exists = rows.length > 0
188
+ await redis.set(key, exists ? '1' : '0', 'EX', SKILL_EXISTS_TTL_SECONDS)
189
+ return exists
190
+ }
191
+
192
+ private async invalidateSkillExistsCache(orgId: string, agentId: string | null): Promise<void> {
193
+ const redis = getRedisConnection()
194
+ const pattern = `${SKILL_EXISTS_KEY_PREFIX}:${orgId}:*`
195
+ const keys = await redis.keys(pattern)
196
+ if (keys.length > 0) {
197
+ await redis.del(...keys)
198
+ }
199
+ // Also set the specific key if we know a skill was just created
200
+ if (agentId) {
201
+ await redis.set(skillExistsKey(orgId, agentId), '1', 'EX', SKILL_EXISTS_TTL_SECONDS)
202
+ }
203
+ }
204
+
152
205
  async searchForTurn(params: RetrieveForTurnParams): Promise<SearchResultRow[]> {
153
- const orgRef = ensureRecordId(params.orgId, TABLES.ORGANIZATION)
206
+ const timer = lotaDebugLogger.timer('learned-skills')
207
+
208
+ const hasSkills = await this.hasSkillsForAgent(params.orgId, params.agentId)
209
+ if (!hasSkills) {
210
+ lotaDebugLogger.step('learned-skills: skipped — no skills for org+agent')
211
+ return []
212
+ }
213
+ timer.step('has-skills-check')
214
+
154
215
  const queryEmbedding = await embeddings.embedQuery(params.query)
216
+ timer.step('embed-query')
155
217
  if (queryEmbedding.length === 0) return []
156
218
 
157
- const candidateLimit = params.limit * 4
219
+ const orgRef = ensureRecordId(params.orgId, TABLES.ORGANIZATION)
158
220
  const sql = `
159
- LET $candidateRows = (
160
- SELECT
161
- id,
162
- vector::distance::knn() AS distance
163
- FROM ${TABLES.LEARNED_SKILL}
164
- WHERE organizationId = $organizationId
165
- AND status IN ['learned', 'verified', 'promoted']
166
- AND archivedAt IS NONE
167
- AND confidence >= $minConfidence
168
- AND (agentId IS NONE OR agentId = $agentId)
169
- AND embedding <|${candidateLimit}|> $embedding
170
- ORDER BY distance ASC
171
- );
172
-
173
221
  SELECT
174
222
  type::string(id) AS id,
175
223
  name,
@@ -178,9 +226,13 @@ class LearnedSkillService {
178
226
  confidence,
179
227
  vector::similarity::cosine(embedding, $embedding) AS similarity
180
228
  FROM ${TABLES.LEARNED_SKILL}
181
- WHERE id IN $candidateRows.id
229
+ WHERE organizationId = $organizationId
230
+ AND status IN ['learned', 'verified', 'promoted']
231
+ AND archivedAt IS NONE
232
+ AND confidence >= $minConfidence
233
+ AND (agentId IS NONE OR agentId = $agentId)
234
+ AND embedding <|${params.limit}|> $embedding
182
235
  ORDER BY similarity DESC
183
- LIMIT $limit
184
236
  `
185
237
 
186
238
  const rows = await databaseService.query<unknown>(
@@ -189,9 +241,9 @@ class LearnedSkillService {
189
241
  embedding: queryEmbedding,
190
242
  agentId: params.agentId,
191
243
  minConfidence: params.minConfidence,
192
- limit: params.limit,
193
244
  }),
194
245
  )
246
+ timer.step('knn-query')
195
247
 
196
248
  return rows.map((row) => SearchResultRowSchema.parse(row)).filter((row) => row.similarity >= 0.3)
197
249
  }
@@ -253,24 +305,15 @@ class LearnedSkillService {
253
305
  if (descEmbedding.length === 0) return null
254
306
 
255
307
  const sql = `
256
- LET $candidateRows = (
257
- SELECT
258
- id,
259
- vector::distance::knn() AS distance
260
- FROM ${TABLES.LEARNED_SKILL}
261
- WHERE organizationId = $organizationId
262
- AND status IN ['learned', 'verified', 'promoted']
263
- AND archivedAt IS NONE
264
- AND embedding <|3|> $embedding
265
- ORDER BY distance ASC
266
- );
267
-
268
308
  SELECT *,
269
309
  type::string(id) AS id,
270
310
  type::string(organizationId) AS organizationId,
271
311
  vector::similarity::cosine(embedding, $embedding) AS similarity
272
312
  FROM ${TABLES.LEARNED_SKILL}
273
- WHERE id IN $candidateRows.id
313
+ WHERE organizationId = $organizationId
314
+ AND status IN ['learned', 'verified', 'promoted']
315
+ AND archivedAt IS NONE
316
+ AND embedding <|3|> $embedding
274
317
  ORDER BY similarity DESC
275
318
  LIMIT 1
276
319
  `
@@ -1,7 +1,6 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  import { agentRoster } from '../config/agent-defaults'
4
- import { env } from '../config/env-shapes'
5
4
  import { aiLogger } from '../config/logger'
6
5
  import { Memory } from '../db/memory'
7
6
  import { isUniqueIndexConflict } from '../db/memory-store.helpers'
@@ -24,6 +23,7 @@ import {
24
23
  executeScopedRetrieval,
25
24
  scopedRetrievalToMap,
26
25
  } from '../runtime/retrieval-adapters'
26
+ import { getRuntimeConfig } from '../runtime/runtime-config'
27
27
  import { createMemoryRerankerAgent, memoryRerankerPrompt } from '../system-agents/memory-reranker.agent'
28
28
  import { createOrgMemoryAgent, orgMemoryPrompt } from '../system-agents/memory.agent'
29
29
  import { assessMemoryImportance, clampMemoryImportance } from './memory-assessment.service'
@@ -304,7 +304,7 @@ class MemoryService {
304
304
  scopeId: string
305
305
  memoryType: MemoryType
306
306
  }): Promise<string> {
307
- const limit = env.MEMORY_SEARCH_K
307
+ const limit = getRuntimeConfig().memory.searchK
308
308
  const candidateLimit = getCandidateLimit(limit)
309
309
 
310
310
  const candidates = await memory.searchCandidates(query, { scopeId, limit: candidateLimit, memoryType })
@@ -362,7 +362,8 @@ class MemoryService {
362
362
  aiLogger.info`[MEMORY_DEBUG] searchOrganizationMemoriesRaw - orgId: "${orgId}", scopeId: "${orgScopeId}"`
363
363
  const memory = this.getOrgMemory(orgId)
364
364
  const fastMode = options?.fastMode ?? true
365
- const limit = options?.limit ?? (fastMode ? Math.min(env.MEMORY_SEARCH_K, 4) : env.MEMORY_SEARCH_K)
365
+ const searchK = getRuntimeConfig().memory.searchK
366
+ const limit = options?.limit ?? (fastMode ? Math.min(searchK, 4) : searchK)
366
367
 
367
368
  try {
368
369
  const candidates = await memory.searchCandidates(query, {
@@ -489,7 +490,7 @@ class MemoryService {
489
490
  fastMode?: boolean
490
491
  allowMultiScopeRerank?: boolean
491
492
  }): Promise<string> {
492
- const limit = Math.min(env.MEMORY_SEARCH_K, MAX_MEMORY_RESULTS_PER_SCOPE)
493
+ const limit = Math.min(getRuntimeConfig().memory.searchK, MAX_MEMORY_RESULTS_PER_SCOPE)
493
494
  const candidateLimit = fastMode ? limit : getCandidateLimit(limit)
494
495
  const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
495
496
  const orgMemory = this.getOrgMemory(orgId)
@@ -6,7 +6,7 @@ import { workstreamMessageService } from './workstream-message.service'
6
6
 
7
7
  const APPROVAL_VERIFICATION_MESSAGE_WINDOW = 20
8
8
 
9
- export type VerifyMutatingApproval = (params: {
9
+ type VerifyMutatingApproval = (params: {
10
10
  workstreamId: string
11
11
  approvalReason: string
12
12
  approvalToken: string
@@ -1,4 +1,4 @@
1
- import { recordIdStringSchema } from '@lota-sdk/shared/schemas/common'
1
+ import { recordIdStringSchema } from '@lota-sdk/shared'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { BaseService } from '../db/base.service'
@@ -1,4 +1,4 @@
1
- import { recordIdStringSchema } from '@lota-sdk/shared/schemas/common'
1
+ import { recordIdStringSchema } from '@lota-sdk/shared'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { BaseService } from '../db/base.service'
@@ -0,0 +1,83 @@
1
+ import { PlanApprovalSchema } from '@lota-sdk/shared'
2
+ import type { PlanApprovalRecord, PlanApprovalStatus } from '@lota-sdk/shared'
3
+ import { RecordId } from 'surrealdb'
4
+
5
+ import type { RecordIdInput } from '../db/record-id'
6
+ import { ensureRecordId } from '../db/record-id'
7
+ import { databaseService } from '../db/service'
8
+ import type { DatabaseTransaction } from '../db/service'
9
+ import { TABLES } from '../db/tables'
10
+
11
+ class PlanApprovalService {
12
+ async createPendingApproval(params: {
13
+ tx: DatabaseTransaction
14
+ runId: RecordIdInput
15
+ nodeRunId: RecordIdInput
16
+ nodeId: string
17
+ requestedBy: string
18
+ presented: Record<string, unknown>
19
+ }): Promise<PlanApprovalRecord> {
20
+ const approvalId = new RecordId(TABLES.PLAN_APPROVAL, Bun.randomUUIDv7())
21
+ const created = await params.tx
22
+ .create(approvalId)
23
+ .content({
24
+ runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
25
+ nodeRunId: ensureRecordId(params.nodeRunId, TABLES.PLAN_NODE_RUN),
26
+ nodeId: params.nodeId,
27
+ status: 'pending',
28
+ requestedBy: params.requestedBy,
29
+ presented: params.presented,
30
+ requiredEdits: [],
31
+ })
32
+ .output('after')
33
+
34
+ return PlanApprovalSchema.parse(created)
35
+ }
36
+
37
+ async getApprovalById(approvalId: RecordIdInput): Promise<PlanApprovalRecord | null> {
38
+ return await databaseService.findOne(
39
+ TABLES.PLAN_APPROVAL,
40
+ { id: ensureRecordId(approvalId, TABLES.PLAN_APPROVAL) },
41
+ PlanApprovalSchema,
42
+ )
43
+ }
44
+
45
+ async getPendingApprovalForNodeRun(nodeRunId: RecordIdInput): Promise<PlanApprovalRecord | null> {
46
+ const approvals = await databaseService.findMany(
47
+ TABLES.PLAN_APPROVAL,
48
+ { nodeRunId: ensureRecordId(nodeRunId, TABLES.PLAN_NODE_RUN), status: 'pending' },
49
+ PlanApprovalSchema,
50
+ { orderBy: 'createdAt', orderDir: 'DESC', limit: 1 },
51
+ )
52
+
53
+ return approvals.at(0) ?? null
54
+ }
55
+
56
+ async updateApprovalResponse(params: {
57
+ tx: DatabaseTransaction
58
+ approval: PlanApprovalRecord
59
+ status: PlanApprovalStatus
60
+ response: Record<string, unknown>
61
+ respondedBy: string
62
+ approvalMessageId?: string
63
+ comments?: string
64
+ requiredEdits?: string[]
65
+ }): Promise<PlanApprovalRecord> {
66
+ const updated = await params.tx
67
+ .update(ensureRecordId(params.approval.id, TABLES.PLAN_APPROVAL))
68
+ .merge({
69
+ status: params.status,
70
+ response: params.response,
71
+ respondedBy: params.respondedBy,
72
+ ...(params.approvalMessageId ? { approvalMessageId: params.approvalMessageId } : {}),
73
+ ...(params.comments ? { comments: params.comments } : {}),
74
+ ...(params.requiredEdits ? { requiredEdits: params.requiredEdits } : {}),
75
+ respondedAt: new Date(),
76
+ })
77
+ .output('after')
78
+
79
+ return PlanApprovalSchema.parse(updated)
80
+ }
81
+ }
82
+
83
+ export const planApprovalService = new PlanApprovalService()
@@ -0,0 +1,44 @@
1
+ import { PlanArtifactSchema } from '@lota-sdk/shared'
2
+ import type { PlanArtifactRecord, PlanArtifactSubmission } from '@lota-sdk/shared'
3
+ import { RecordId } from 'surrealdb'
4
+
5
+ import type { RecordIdInput } from '../db/record-id'
6
+ import { ensureRecordId } from '../db/record-id'
7
+ import type { DatabaseTransaction } from '../db/service'
8
+ import { TABLES } from '../db/tables'
9
+
10
+ class PlanArtifactService {
11
+ async persistArtifacts(params: {
12
+ tx: DatabaseTransaction
13
+ runId: RecordIdInput
14
+ attemptId: RecordIdInput
15
+ nodeId: string
16
+ artifacts: PlanArtifactSubmission[]
17
+ }): Promise<PlanArtifactRecord[]> {
18
+ const records: PlanArtifactRecord[] = []
19
+
20
+ for (const artifact of params.artifacts) {
21
+ const artifactId = new RecordId(TABLES.PLAN_ARTIFACT, Bun.randomUUIDv7())
22
+ const created = await params.tx
23
+ .create(artifactId)
24
+ .content({
25
+ runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
26
+ attemptId: ensureRecordId(params.attemptId, TABLES.PLAN_NODE_ATTEMPT),
27
+ nodeId: params.nodeId,
28
+ name: artifact.name,
29
+ kind: artifact.kind,
30
+ pointer: artifact.pointer,
31
+ ...(artifact.schemaRef ? { schemaRef: artifact.schemaRef } : {}),
32
+ ...(artifact.description ? { description: artifact.description } : {}),
33
+ ...(artifact.payload ? { payload: artifact.payload } : {}),
34
+ })
35
+ .output('after')
36
+
37
+ records.push(PlanArtifactSchema.parse(created))
38
+ }
39
+
40
+ return records
41
+ }
42
+ }
43
+
44
+ export const planArtifactService = new PlanArtifactService()
@@ -0,0 +1,61 @@
1
+ import type { PlanDraft } from '@lota-sdk/shared'
2
+
3
+ function buildImplicitLinearEdges(draft: PlanDraft) {
4
+ if (draft.edges.length > 0 || draft.nodes.length <= 1) {
5
+ return draft.edges
6
+ }
7
+
8
+ return draft.nodes
9
+ .slice(0, -1)
10
+ .map((node, index) => ({
11
+ id: `edge_${node.id}_to_${draft.nodes[index + 1]?.id ?? index + 1}`,
12
+ source: node.id,
13
+ target: draft.nodes[index + 1].id,
14
+ map: {},
15
+ }))
16
+ }
17
+
18
+ class PlanBuilderService {
19
+ roleAssignment(draft: PlanDraft): PlanDraft {
20
+ return draft
21
+ }
22
+
23
+ structureDesign(draft: PlanDraft): PlanDraft {
24
+ return {
25
+ ...draft,
26
+ edges: buildImplicitLinearEdges(draft),
27
+ entryNodeIds: draft.entryNodeIds && draft.entryNodeIds.length > 0 ? draft.entryNodeIds : [draft.nodes[0].id],
28
+ }
29
+ }
30
+
31
+ semanticCompletion(draft: PlanDraft): PlanDraft {
32
+ return {
33
+ ...draft,
34
+ nodes: draft.nodes.map((node) => ({
35
+ ...node,
36
+ deliverables: [...node.deliverables],
37
+ successCriteria: [...node.successCriteria],
38
+ completionChecks: [...node.completionChecks],
39
+ failurePolicy: [...node.failurePolicy],
40
+ retryPolicy: { ...node.retryPolicy, retryOn: [...node.retryPolicy.retryOn] },
41
+ toolPolicy: { allow: [...node.toolPolicy.allow], deny: [...node.toolPolicy.deny] },
42
+ contextPolicy: {
43
+ retrievalScopes: [...node.contextPolicy.retrievalScopes],
44
+ attachmentPolicy: node.contextPolicy.attachmentPolicy,
45
+ webPolicy: node.contextPolicy.webPolicy,
46
+ },
47
+ })),
48
+ edges: draft.edges.map((edge) => ({ ...edge, map: { ...edge.map } })),
49
+ schemas: structuredClone(draft.schemas),
50
+ entryNodeIds: [...(draft.entryNodeIds ?? [])],
51
+ }
52
+ }
53
+
54
+ prepareDraft(draft: PlanDraft): PlanDraft {
55
+ const withRoles = this.roleAssignment(draft)
56
+ const withStructure = this.structureDesign(withRoles)
57
+ return this.semanticCompletion(withStructure)
58
+ }
59
+ }
60
+
61
+ export const planBuilderService = new PlanBuilderService()
@@ -0,0 +1,53 @@
1
+ import { PlanCheckpointSchema } from '@lota-sdk/shared'
2
+ import type { PlanCheckpointRecord, PlanRunStatus } from '@lota-sdk/shared'
3
+ import { RecordId } from 'surrealdb'
4
+
5
+ import type { RecordIdInput } from '../db/record-id'
6
+ import { ensureRecordId } from '../db/record-id'
7
+ import { databaseService } from '../db/service'
8
+ import type { DatabaseTransaction } from '../db/service'
9
+ import { TABLES } from '../db/tables'
10
+
11
+ class PlanCheckpointService {
12
+ async createCheckpoint(params: {
13
+ tx: DatabaseTransaction
14
+ runId: RecordIdInput
15
+ sequence: number
16
+ runStatus: PlanRunStatus
17
+ readyNodeIds: string[]
18
+ activeNodeIds: string[]
19
+ artifactIds: RecordIdInput[]
20
+ lastCompletedNodeIds: string[]
21
+ snapshot: Record<string, unknown>
22
+ }): Promise<PlanCheckpointRecord> {
23
+ const checkpointId = new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7())
24
+ const created = await params.tx
25
+ .create(checkpointId)
26
+ .content({
27
+ runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
28
+ sequence: params.sequence,
29
+ runStatus: params.runStatus,
30
+ readyNodeIds: [...params.readyNodeIds],
31
+ activeNodeIds: [...params.activeNodeIds],
32
+ artifactIds: params.artifactIds.map((artifactId) => ensureRecordId(artifactId, TABLES.PLAN_ARTIFACT)),
33
+ lastCompletedNodeIds: [...params.lastCompletedNodeIds],
34
+ snapshot: params.snapshot,
35
+ })
36
+ .output('after')
37
+
38
+ return PlanCheckpointSchema.parse(created)
39
+ }
40
+
41
+ async loadLatestForRun(runId: RecordIdInput): Promise<PlanCheckpointRecord | null> {
42
+ const checkpoints = await databaseService.findMany(
43
+ TABLES.PLAN_CHECKPOINT,
44
+ { runId: ensureRecordId(runId, TABLES.PLAN_RUN) },
45
+ PlanCheckpointSchema,
46
+ { orderBy: 'sequence', orderDir: 'DESC', limit: 1 },
47
+ )
48
+
49
+ return checkpoints.at(0) ?? null
50
+ }
51
+ }
52
+
53
+ export const planCheckpointService = new PlanCheckpointService()
@@ -0,0 +1,81 @@
1
+ import type { PlanDraft, PlanNodeSpec, PlanNodeSpecRecord } from '@lota-sdk/shared'
2
+
3
+ import { planValidatorService } from './plan-validator.service'
4
+
5
+ export interface CompiledPlanNode {
6
+ node: PlanNodeSpec
7
+ position: number
8
+ upstreamNodeIds: string[]
9
+ downstreamNodeIds: string[]
10
+ }
11
+
12
+ interface CompiledPlanDraft {
13
+ draft: PlanDraft
14
+ nodes: CompiledPlanNode[]
15
+ }
16
+
17
+ class PlanCompilerService {
18
+ compile(draft: PlanDraft): CompiledPlanDraft {
19
+ const validation = planValidatorService.validateDraft(draft)
20
+ if (validation.blocking.length > 0) {
21
+ throw new Error(
22
+ `Plan draft failed validation: ${validation.blocking.map((issue) => `${issue.code}: ${issue.message}`).join(' | ')}`,
23
+ )
24
+ }
25
+
26
+ const upstreamByNodeId = new Map<string, string[]>()
27
+ const downstreamByNodeId = new Map<string, string[]>()
28
+
29
+ for (const node of draft.nodes) {
30
+ upstreamByNodeId.set(node.id, [])
31
+ downstreamByNodeId.set(node.id, [])
32
+ }
33
+
34
+ for (const edge of draft.edges) {
35
+ upstreamByNodeId.get(edge.target)?.push(edge.source)
36
+ downstreamByNodeId.get(edge.source)?.push(edge.target)
37
+ }
38
+
39
+ return {
40
+ draft,
41
+ nodes: draft.nodes.map((node, index) => ({
42
+ node,
43
+ position: index,
44
+ upstreamNodeIds: [...(upstreamByNodeId.get(node.id) ?? [])],
45
+ downstreamNodeIds: [...(downstreamByNodeId.get(node.id) ?? [])],
46
+ })),
47
+ }
48
+ }
49
+
50
+ toNodeSpecRecords(
51
+ compiled: CompiledPlanDraft,
52
+ ): Array<Omit<PlanNodeSpecRecord, 'id' | 'planSpecId' | 'createdAt' | 'updatedAt'>> {
53
+ return compiled.nodes.map(({ node, position, upstreamNodeIds, downstreamNodeIds }) => ({
54
+ nodeId: node.id,
55
+ position,
56
+ type: node.type,
57
+ label: node.label,
58
+ owner: node.owner,
59
+ objective: node.objective,
60
+ instructions: node.instructions,
61
+ ...(node.inputSchemaRef ? { inputSchemaRef: node.inputSchemaRef } : {}),
62
+ ...(node.outputSchemaRef ? { outputSchemaRef: node.outputSchemaRef } : {}),
63
+ deliverables: [...node.deliverables],
64
+ successCriteria: [...node.successCriteria],
65
+ completionChecks: [...node.completionChecks],
66
+ retryPolicy: { ...node.retryPolicy, retryOn: [...node.retryPolicy.retryOn] },
67
+ failurePolicy: [...node.failurePolicy],
68
+ ...(node.timeoutMs ? { timeoutMs: node.timeoutMs } : {}),
69
+ toolPolicy: { allow: [...node.toolPolicy.allow], deny: [...node.toolPolicy.deny] },
70
+ contextPolicy: {
71
+ retrievalScopes: [...node.contextPolicy.retrievalScopes],
72
+ attachmentPolicy: node.contextPolicy.attachmentPolicy,
73
+ webPolicy: node.contextPolicy.webPolicy,
74
+ },
75
+ upstreamNodeIds,
76
+ downstreamNodeIds,
77
+ }))
78
+ }
79
+ }
80
+
81
+ export const planCompilerService = new PlanCompilerService()