@lota-sdk/core 0.1.15 → 0.1.17

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 (159) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +12 -8
  9. package/src/ai/definitions.ts +81 -3
  10. package/src/ai/embedding-cache.ts +2 -4
  11. package/src/ai/index.ts +0 -2
  12. package/src/bifrost/bifrost.ts +2 -7
  13. package/src/bifrost/cache-headers.ts +8 -0
  14. package/src/bifrost/index.ts +1 -0
  15. package/src/config/agent-defaults.ts +31 -21
  16. package/src/config/agent-types.ts +11 -0
  17. package/src/config/constants.ts +2 -14
  18. package/src/config/debug-logger.ts +5 -1
  19. package/src/config/index.ts +3 -0
  20. package/src/config/model-constants.ts +16 -34
  21. package/src/config/search.ts +1 -15
  22. package/src/create-runtime.ts +269 -178
  23. package/src/db/cursor-pagination.ts +3 -6
  24. package/src/db/index.ts +2 -0
  25. package/src/db/memory-store.helpers.ts +1 -3
  26. package/src/db/memory-store.rows.ts +7 -7
  27. package/src/db/memory-store.ts +14 -18
  28. package/src/db/memory.ts +13 -13
  29. package/src/db/schema-fingerprint.ts +1 -3
  30. package/src/db/service.ts +153 -79
  31. package/src/db/startup.ts +6 -10
  32. package/src/db/surreal-mutation.ts +43 -0
  33. package/src/db/tables.ts +7 -0
  34. package/src/db/workstream-message-row.ts +15 -0
  35. package/src/embeddings/provider.ts +1 -1
  36. package/src/queues/context-compaction.queue.ts +15 -46
  37. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  38. package/src/queues/document-processor.queue.ts +2 -4
  39. package/src/queues/index.ts +3 -0
  40. package/src/queues/memory-consolidation.queue.ts +16 -51
  41. package/src/queues/plan-scheduler.queue.ts +97 -0
  42. package/src/queues/post-chat-memory.queue.ts +20 -55
  43. package/src/queues/queue-factory.ts +100 -0
  44. package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
  45. package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
  46. package/src/queues/skill-extraction.queue.ts +15 -47
  47. package/src/queues/workstream-title-generation.queue.ts +15 -47
  48. package/src/redis/connection.ts +6 -0
  49. package/src/redis/index.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +1 -2
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +109 -35
  53. package/src/runtime/approval-continuation.ts +12 -6
  54. package/src/runtime/context-compaction-runtime.ts +1 -1
  55. package/src/runtime/context-compaction.ts +24 -64
  56. package/src/runtime/execution-plan.ts +22 -18
  57. package/src/runtime/graph-designer.ts +15 -0
  58. package/src/runtime/helper-model.ts +9 -197
  59. package/src/runtime/index.ts +3 -1
  60. package/src/runtime/llm-content.ts +1 -1
  61. package/src/runtime/memory-block.ts +9 -11
  62. package/src/runtime/memory-pipeline.ts +6 -9
  63. package/src/runtime/plugin-resolution.ts +35 -0
  64. package/src/runtime/plugin-types.ts +72 -0
  65. package/src/runtime/retrieval-adapters.ts +1 -1
  66. package/src/runtime/runtime-config.ts +111 -14
  67. package/src/runtime/runtime-extensions.ts +2 -3
  68. package/src/runtime/runtime-worker-registry.ts +6 -0
  69. package/src/runtime/social-chat.ts +752 -0
  70. package/src/runtime/team-consultation-orchestrator.ts +45 -32
  71. package/src/runtime/team-consultation-prompts.ts +11 -2
  72. package/src/runtime/title-helpers.ts +2 -4
  73. package/src/runtime/workstream-chat-helpers.ts +1 -1
  74. package/src/services/adaptive-playbook.service.ts +152 -0
  75. package/src/services/agent-executor.service.ts +292 -0
  76. package/src/services/artifact-provenance.service.ts +172 -0
  77. package/src/services/attachment.service.ts +6 -11
  78. package/src/services/context-compaction.service.ts +72 -55
  79. package/src/services/context-enrichment.service.ts +33 -0
  80. package/src/services/coordination-registry.service.ts +117 -0
  81. package/src/services/document-chunk.service.ts +2 -4
  82. package/src/services/domain-agent-executor.service.ts +71 -0
  83. package/src/services/execution-plan.service.ts +269 -50
  84. package/src/services/feedback-loop.service.ts +96 -0
  85. package/src/services/global-orchestrator.service.ts +148 -0
  86. package/src/services/index.ts +27 -0
  87. package/src/services/institutional-memory.service.ts +145 -0
  88. package/src/services/learned-skill.service.ts +24 -5
  89. package/src/services/memory-assessment.service.ts +3 -2
  90. package/src/services/memory-utils.ts +3 -8
  91. package/src/services/memory.service.ts +49 -61
  92. package/src/services/monitoring-window.service.ts +86 -0
  93. package/src/services/mutating-approval.service.ts +1 -1
  94. package/src/services/node-workspace.service.ts +155 -0
  95. package/src/services/notification.service.ts +39 -0
  96. package/src/services/organization-member.service.ts +11 -4
  97. package/src/services/organization.service.ts +5 -5
  98. package/src/services/ownership-dispatcher.service.ts +403 -0
  99. package/src/services/plan-approval.service.ts +1 -1
  100. package/src/services/plan-builder.service.ts +1 -0
  101. package/src/services/plan-checkpoint.service.ts +30 -2
  102. package/src/services/plan-compiler.service.ts +5 -0
  103. package/src/services/plan-coordination.service.ts +152 -0
  104. package/src/services/plan-cycle.service.ts +284 -0
  105. package/src/services/plan-deadline.service.ts +287 -0
  106. package/src/services/plan-executor.service.ts +384 -40
  107. package/src/services/plan-run.service.ts +41 -7
  108. package/src/services/plan-scheduler.service.ts +240 -0
  109. package/src/services/plan-template.service.ts +117 -0
  110. package/src/services/plan-validator.service.ts +84 -2
  111. package/src/services/plan-workspace.service.ts +83 -0
  112. package/src/services/playbook-registry.service.ts +67 -0
  113. package/src/services/plugin-executor.service.ts +103 -0
  114. package/src/services/quality-metrics.service.ts +132 -0
  115. package/src/services/recent-activity.service.ts +28 -34
  116. package/src/services/skill-resolver.service.ts +19 -0
  117. package/src/services/social-chat-history.service.ts +197 -0
  118. package/src/services/system-executor.service.ts +105 -0
  119. package/src/services/workstream-message.service.ts +13 -37
  120. package/src/services/workstream-plan-registry.service.ts +22 -0
  121. package/src/services/workstream-title.service.ts +3 -1
  122. package/src/services/workstream-turn-preparation.service.ts +34 -89
  123. package/src/services/workstream.service.ts +33 -55
  124. package/src/services/workstream.types.ts +9 -9
  125. package/src/services/write-intent-validator.service.ts +81 -0
  126. package/src/storage/attachment-parser.ts +1 -1
  127. package/src/storage/attachment-utils.ts +1 -1
  128. package/src/storage/generated-document-storage.service.ts +3 -2
  129. package/src/system-agents/context-compaction.agent.ts +2 -0
  130. package/src/system-agents/delegated-agent-factory.ts +5 -0
  131. package/src/system-agents/memory-reranker.agent.ts +4 -2
  132. package/src/system-agents/memory.agent.ts +2 -0
  133. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -0
  134. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -0
  135. package/src/system-agents/skill-extractor.agent.ts +2 -0
  136. package/src/system-agents/skill-manager.agent.ts +2 -0
  137. package/src/system-agents/title-generator.agent.ts +2 -0
  138. package/src/tools/execution-plan.tool.ts +17 -23
  139. package/src/tools/index.ts +0 -1
  140. package/src/tools/research-topic.tool.ts +2 -0
  141. package/src/tools/team-think.tool.ts +5 -6
  142. package/src/utils/async.ts +2 -1
  143. package/src/utils/date-time.ts +4 -32
  144. package/src/utils/env.ts +8 -0
  145. package/src/utils/errors.ts +42 -10
  146. package/src/utils/index.ts +9 -0
  147. package/src/utils/string.ts +114 -1
  148. package/src/workers/index.ts +1 -0
  149. package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
  150. package/src/workers/regular-chat-memory-digest.runner.ts +45 -12
  151. package/src/workers/skill-extraction.runner.ts +26 -6
  152. package/src/workers/utils/file-section-chunker.ts +2 -1
  153. package/src/workers/utils/repo-structure-extractor.ts +2 -2
  154. package/src/workers/utils/repomix-file-sections.ts +2 -2
  155. package/src/workers/utils/sandbox-error.ts +11 -2
  156. package/src/workers/utils/workstream-message-query.ts +14 -25
  157. package/src/workers/worker-utils.ts +2 -2
  158. package/src/runtime/workstream-routing-policy.ts +0 -267
  159. package/src/tools/log-hello-world.tool.ts +0 -17
@@ -0,0 +1,172 @@
1
+ import type { ImpactAnalysis, PlanNodeRunRecord, ProvenanceChain, ProvenanceNode } from '@lota-sdk/shared'
2
+
3
+ import { recordIdToString } from '../db/record-id'
4
+
5
+ class ArtifactProvenanceService {
6
+ /**
7
+ * Backward: trace which upstream nodes/artifacts produced the inputs for this artifact's node.
8
+ * From nodeId, follow edges upstream. At each upstream node, collect its artifacts.
9
+ * Continue until maxDepth or no more upstreams.
10
+ */
11
+ async traceBackward(params: { runId: string; nodeId: string; maxDepth?: number }): Promise<ProvenanceChain> {
12
+ const { planRunService } = await import('./plan-run.service')
13
+
14
+ const run = await planRunService.getRunById(params.runId)
15
+ const spec = await planRunService.getPlanSpecById(run.planSpecId)
16
+ const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
17
+ const artifacts = await planRunService.listArtifacts(run.id)
18
+
19
+ const nodeSpecsByNodeId = new Map(nodeSpecs.map((n) => [n.nodeId, n]))
20
+ const artifactsByNodeId = new Map<string, typeof artifacts>()
21
+ for (const artifact of artifacts) {
22
+ const list = artifactsByNodeId.get(artifact.nodeId) ?? []
23
+ list.push(artifact)
24
+ artifactsByNodeId.set(artifact.nodeId, list)
25
+ }
26
+
27
+ const maxDepth = params.maxDepth ?? 10
28
+ const chain: ProvenanceNode[] = []
29
+ const visited = new Set<string>()
30
+ const queue: Array<{ nodeId: string; depth: number }> = [{ nodeId: params.nodeId, depth: 0 }]
31
+
32
+ while (queue.length > 0) {
33
+ const entry = queue.shift()
34
+ if (!entry || visited.has(entry.nodeId) || entry.depth > maxDepth) continue
35
+ visited.add(entry.nodeId)
36
+
37
+ const nodeSpec = nodeSpecsByNodeId.get(entry.nodeId)
38
+ if (!nodeSpec) continue
39
+
40
+ const nodeArtifacts = (artifactsByNodeId.get(entry.nodeId) ?? []).map((a) => ({
41
+ name: a.name,
42
+ kind: a.kind,
43
+ id: recordIdToString(a.id),
44
+ }))
45
+
46
+ chain.push({ nodeId: entry.nodeId, label: nodeSpec.label, artifacts: nodeArtifacts })
47
+
48
+ // Walk upstream
49
+ for (const upstreamId of nodeSpec.upstreamNodeIds) {
50
+ if (!visited.has(upstreamId)) {
51
+ queue.push({ nodeId: upstreamId, depth: entry.depth + 1 })
52
+ }
53
+ }
54
+ }
55
+
56
+ return { root: { nodeId: params.nodeId }, chain, depth: Math.max(0, chain.length - 1) }
57
+ }
58
+
59
+ /**
60
+ * Forward: trace which downstream nodes consume this artifact's node output.
61
+ * Same as backward but follow edges downstream.
62
+ */
63
+ async traceForward(params: { runId: string; nodeId: string; maxDepth?: number }): Promise<ProvenanceChain> {
64
+ const { planRunService } = await import('./plan-run.service')
65
+
66
+ const run = await planRunService.getRunById(params.runId)
67
+ const spec = await planRunService.getPlanSpecById(run.planSpecId)
68
+ const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
69
+ const artifacts = await planRunService.listArtifacts(run.id)
70
+
71
+ const nodeSpecsByNodeId = new Map(nodeSpecs.map((n) => [n.nodeId, n]))
72
+ const artifactsByNodeId = new Map<string, typeof artifacts>()
73
+ for (const artifact of artifacts) {
74
+ const list = artifactsByNodeId.get(artifact.nodeId) ?? []
75
+ list.push(artifact)
76
+ artifactsByNodeId.set(artifact.nodeId, list)
77
+ }
78
+
79
+ const maxDepth = params.maxDepth ?? 10
80
+ const chain: ProvenanceNode[] = []
81
+ const visited = new Set<string>()
82
+ const queue: Array<{ nodeId: string; depth: number }> = [{ nodeId: params.nodeId, depth: 0 }]
83
+
84
+ while (queue.length > 0) {
85
+ const entry = queue.shift()
86
+ if (!entry || visited.has(entry.nodeId) || entry.depth > maxDepth) continue
87
+ visited.add(entry.nodeId)
88
+
89
+ const nodeSpec = nodeSpecsByNodeId.get(entry.nodeId)
90
+ if (!nodeSpec) continue
91
+
92
+ const nodeArtifacts = (artifactsByNodeId.get(entry.nodeId) ?? []).map((a) => ({
93
+ name: a.name,
94
+ kind: a.kind,
95
+ id: recordIdToString(a.id),
96
+ }))
97
+
98
+ chain.push({ nodeId: entry.nodeId, label: nodeSpec.label, artifacts: nodeArtifacts })
99
+
100
+ // Walk downstream
101
+ for (const downstreamId of nodeSpec.downstreamNodeIds) {
102
+ if (!visited.has(downstreamId)) {
103
+ queue.push({ nodeId: downstreamId, depth: entry.depth + 1 })
104
+ }
105
+ }
106
+ }
107
+
108
+ return { root: { nodeId: params.nodeId }, chain, depth: Math.max(0, chain.length - 1) }
109
+ }
110
+
111
+ /**
112
+ * Impact: which downstream nodes/artifacts would be affected.
113
+ * Forward traversal collecting all affected nodes and their artifacts.
114
+ * Includes non-completed nodes.
115
+ */
116
+ async analyzeImpact(params: { runId: string; nodeId: string }): Promise<ImpactAnalysis> {
117
+ const { planRunService } = await import('./plan-run.service')
118
+
119
+ const run = await planRunService.getRunById(params.runId)
120
+ const spec = await planRunService.getPlanSpecById(run.planSpecId)
121
+ const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
122
+ const nodeRuns = await planRunService.listNodeRuns(run.id)
123
+ const artifacts = await planRunService.listArtifacts(run.id)
124
+
125
+ const nodeSpecsByNodeId = new Map(nodeSpecs.map((n) => [n.nodeId, n]))
126
+ const nodeRunsByNodeId = new Map(nodeRuns.map((n: PlanNodeRunRecord) => [n.nodeId, n]))
127
+
128
+ const affectedNodes: Array<{ nodeId: string; label: string; status: PlanNodeRunRecord['status'] }> = []
129
+ const affectedArtifacts: Array<{ name: string; nodeId: string }> = []
130
+
131
+ const visited = new Set<string>()
132
+ const queue: string[] = []
133
+
134
+ // Seed: downstream neighbors of the source node
135
+ const sourceSpec = nodeSpecsByNodeId.get(params.nodeId)
136
+ if (sourceSpec) {
137
+ for (const downstreamId of sourceSpec.downstreamNodeIds) {
138
+ queue.push(downstreamId)
139
+ }
140
+ }
141
+
142
+ while (queue.length > 0) {
143
+ const nodeId = queue.shift()
144
+ if (!nodeId || visited.has(nodeId)) continue
145
+ visited.add(nodeId)
146
+
147
+ const nodeSpec = nodeSpecsByNodeId.get(nodeId)
148
+ if (!nodeSpec) continue
149
+
150
+ const nodeRun = nodeRunsByNodeId.get(nodeId)
151
+ affectedNodes.push({ nodeId, label: nodeSpec.label, status: nodeRun?.status ?? 'pending' })
152
+
153
+ // Collect artifacts for this node
154
+ for (const artifact of artifacts.filter((a) => a.nodeId === nodeId)) {
155
+ affectedArtifacts.push({ name: artifact.name, nodeId })
156
+ }
157
+
158
+ // Continue downstream
159
+ for (const downstreamId of nodeSpec.downstreamNodeIds) {
160
+ if (!visited.has(downstreamId)) queue.push(downstreamId)
161
+ }
162
+ }
163
+
164
+ return {
165
+ sourceNodeId: params.nodeId,
166
+ affectedNodes: affectedNodes as ImpactAnalysis['affectedNodes'],
167
+ affectedArtifacts,
168
+ }
169
+ }
170
+ }
171
+
172
+ export const artifactProvenanceService = new ArtifactProvenanceService()
@@ -57,7 +57,7 @@ class AttachmentService {
57
57
  name: string
58
58
  contentType: string
59
59
  }): Promise<string> {
60
- return await attachmentStorageService.extractStoredAttachmentText({ storageKey, name, contentType })
60
+ return attachmentStorageService.extractStoredAttachmentText({ storageKey, name, contentType })
61
61
  }
62
62
 
63
63
  async extractStoredAttachmentPages({
@@ -69,7 +69,7 @@ class AttachmentService {
69
69
  name: string
70
70
  contentType: string
71
71
  }): Promise<{ pageMode: 'logical' | 'pdf'; pages: string[] }> {
72
- return await attachmentStorageService.extractStoredAttachmentPages({ storageKey, name, contentType })
72
+ return attachmentStorageService.extractStoredAttachmentPages({ storageKey, name, contentType })
73
73
  }
74
74
 
75
75
  async readFilePartsFromUpload({
@@ -85,7 +85,7 @@ class AttachmentService {
85
85
  part?: number
86
86
  pagesPerPart?: number
87
87
  }) {
88
- return await attachmentStorageService.readFilePartsFromUpload({
88
+ return attachmentStorageService.readFilePartsFromUpload({
89
89
  upload,
90
90
  orgId: toOrgId(orgId),
91
91
  userId: toUserId(userId),
@@ -111,7 +111,7 @@ class AttachmentService {
111
111
  content: string
112
112
  contentType: string
113
113
  }): Promise<{ storageKey: string; sizeBytes: number }> {
114
- return await attachmentStorageService.writeOrganizationDocument({
114
+ return attachmentStorageService.writeOrganizationDocument({
115
115
  orgId: toOrgId(orgId),
116
116
  namespace,
117
117
  relativePath,
@@ -131,12 +131,7 @@ class AttachmentService {
131
131
  namespace: string
132
132
  relativePath: string
133
133
  }) {
134
- return await attachmentStorageService.uploadOrganizationDocument({
135
- file,
136
- orgId: toOrgId(orgId),
137
- namespace,
138
- relativePath,
139
- })
134
+ return attachmentStorageService.uploadOrganizationDocument({ file, orgId: toOrgId(orgId), namespace, relativePath })
140
135
  }
141
136
 
142
137
  async uploadWorkstreamAttachment({
@@ -148,7 +143,7 @@ class AttachmentService {
148
143
  orgId: RecordIdRef
149
144
  userId: RecordIdRef
150
145
  }): Promise<SdkUploadedWorkstreamAttachment> {
151
- return await attachmentStorageService.uploadWorkstreamAttachment({
146
+ return attachmentStorageService.uploadWorkstreamAttachment({
152
147
  file,
153
148
  orgId: toOrgId(orgId),
154
149
  userId: toUserId(userId),
@@ -5,6 +5,8 @@ import type { RecordIdRef } from '../db/record-id'
5
5
  import { recordIdToString } from '../db/record-id'
6
6
  import { databaseService } from '../db/service'
7
7
  import { TABLES } from '../db/tables'
8
+ import { getRedisConnection } from '../redis/connection-accessor'
9
+ import { withRedisLeaseLock } from '../redis/redis-lease-lock'
8
10
  import { parseWorkstreamState, toStateFieldsUpdated } from '../runtime/context-compaction'
9
11
  import { CONTEXT_WINDOW_TOKENS, WORKSTREAM_RAW_TAIL_MESSAGES } from '../runtime/context-compaction-constants'
10
12
  import type { WorkstreamState } from '../runtime/workstream-state'
@@ -47,68 +49,83 @@ class ContextCompactionService {
47
49
  workstreamId: RecordIdRef
48
50
  contextSize?: number
49
51
  }): Promise<{ compacted: boolean; state: WorkstreamState | null }> {
50
- const workstream = await databaseService.findOne(TABLES.WORKSTREAM, { id: params.workstreamId }, WorkstreamSchema)
51
- if (!workstream) {
52
- throw new Error(
53
- `Workstream not found for compaction: ${recordIdToString(params.workstreamId, TABLES.WORKSTREAM)}`,
54
- )
55
- }
56
-
57
- const currentState = parseWorkstreamState(workstream.state)
58
- const liveMessages = await workstreamMessageService.listMessagesAfterCursor(
59
- params.workstreamId,
60
- typeof workstream.lastCompactedMessageId === 'string' ? workstream.lastCompactedMessageId : undefined,
61
- )
52
+ const entityId = recordIdToString(params.workstreamId, TABLES.WORKSTREAM)
62
53
 
63
- const result = await contextCompactionRuntime.compactHistory({
64
- summaryText: typeof workstream.compactionSummary === 'string' ? workstream.compactionSummary : '',
65
- liveMessages,
66
- tailMessageCount: WORKSTREAM_RAW_TAIL_MESSAGES,
67
- contextSize: params.contextSize,
68
- existingState: currentState,
69
- })
70
-
71
- if (!result.compacted || !result.lastCompactedMessageId) {
72
- return { compacted: false, state: currentState }
73
- }
74
-
75
- if (result.compactedMessages.length > 0) {
76
- await workstreamMessageService.upsertMessages({
77
- workstreamId: params.workstreamId,
78
- messages: result.compactedMessages,
79
- })
80
- }
81
-
82
- await databaseService.update(
83
- TABLES.WORKSTREAM,
84
- params.workstreamId,
54
+ return withRedisLeaseLock(
85
55
  {
86
- compactionSummary: result.summaryText,
87
- lastCompactedMessageId: result.lastCompactedMessageId,
88
- state: result.state,
56
+ redis: getRedisConnection(),
57
+ lockKey: `compaction:lock:${entityId}`,
58
+ lockTtlMs: 120_000,
59
+ maxWaitMs: 30_000,
60
+ label: 'context-compaction',
61
+ },
62
+ async () => {
63
+ const workstream = await databaseService.findOne(
64
+ TABLES.WORKSTREAM,
65
+ { id: params.workstreamId },
66
+ WorkstreamSchema,
67
+ )
68
+ if (!workstream) {
69
+ throw new Error(`Workstream not found for compaction: ${entityId}`)
70
+ }
71
+
72
+ const currentState = parseWorkstreamState(workstream.state)
73
+ const liveMessages = await workstreamMessageService.listMessagesAfterCursor(
74
+ params.workstreamId,
75
+ typeof workstream.lastCompactedMessageId === 'string' ? workstream.lastCompactedMessageId : undefined,
76
+ )
77
+
78
+ const result = await contextCompactionRuntime.compactHistory({
79
+ summaryText: typeof workstream.compactionSummary === 'string' ? workstream.compactionSummary : '',
80
+ liveMessages,
81
+ tailMessageCount: WORKSTREAM_RAW_TAIL_MESSAGES,
82
+ contextSize: params.contextSize,
83
+ existingState: currentState,
84
+ })
85
+
86
+ if (!result.compacted || !result.lastCompactedMessageId) {
87
+ return { compacted: false, state: currentState }
88
+ }
89
+
90
+ if (result.compactedMessages.length > 0) {
91
+ await workstreamMessageService.upsertMessages({
92
+ workstreamId: params.workstreamId,
93
+ messages: result.compactedMessages,
94
+ })
95
+ }
96
+
97
+ await databaseService.update(
98
+ TABLES.WORKSTREAM,
99
+ params.workstreamId,
100
+ {
101
+ compactionSummary: result.summaryText,
102
+ lastCompactedMessageId: result.lastCompactedMessageId,
103
+ state: result.state,
104
+ },
105
+ WorkstreamSchema,
106
+ )
107
+
108
+ this.logCompactionMetrics({
109
+ domain: 'workstream',
110
+ entityId,
111
+ inputChars: result.inputChars,
112
+ outputChars: result.outputChars,
113
+ savedChars: Math.max(0, result.inputChars - result.outputChars),
114
+ summaryLength: result.summaryText.length,
115
+ compactedMessageCount: result.compactedMessageCount,
116
+ remainingMessageCount: result.remainingMessageCount,
117
+ estimatedTokens: result.estimatedTokens,
118
+ stateFieldsUpdated: toStateFieldsUpdated(result.stateDelta),
119
+ conflictsDetected: result.stateDelta.conflicts?.length ?? 0,
120
+ })
121
+
122
+ return { compacted: true, state: result.state }
89
123
  },
90
- WorkstreamSchema,
91
124
  )
92
-
93
- this.logCompactionMetrics({
94
- domain: 'workstream',
95
- entityId: recordIdToString(params.workstreamId, TABLES.WORKSTREAM),
96
- inputChars: result.inputChars,
97
- outputChars: result.outputChars,
98
- savedChars: Math.max(0, result.inputChars - result.outputChars),
99
- summaryLength: result.summaryText.length,
100
- compactedMessageCount: result.compactedMessageCount,
101
- remainingMessageCount: result.remainingMessageCount,
102
- estimatedTokens: result.estimatedTokens,
103
- stateFieldsUpdated: toStateFieldsUpdated(result.stateDelta),
104
- conflictsDetected: result.stateDelta.conflicts?.length ?? 0,
105
- })
106
-
107
- return { compacted: true, state: result.state }
108
125
  }
109
126
 
110
127
  async compactMemoryBlock(params: { previousSummary: string; newEntriesText: string }): Promise<string> {
111
- return await compactMemoryBlockSummary(params)
128
+ return compactMemoryBlockSummary(params)
112
129
  }
113
130
 
114
131
  private logCompactionMetrics(metrics: PersistedCompactionMetrics): void {
@@ -0,0 +1,33 @@
1
+ import type { ContextEnrichment } from '@lota-sdk/shared'
2
+
3
+ import { serverLogger } from '../config/logger'
4
+ import { getRuntimeConfig } from '../runtime/runtime-config'
5
+
6
+ class ContextEnrichmentService {
7
+ async enrichForPlanCreation(params: { objective: string; organizationId: string }): Promise<ContextEnrichment[]> {
8
+ const pluginRuntime = getRuntimeConfig().pluginRuntime ?? {}
9
+ const enrichers = Object.values(pluginRuntime).flatMap((plugin) => plugin.contextEnrichers ?? [])
10
+
11
+ if (enrichers.length === 0) return []
12
+
13
+ const results = await Promise.allSettled(
14
+ enrichers.map((enricher) =>
15
+ enricher.enrich({ objective: params.objective, organizationId: params.organizationId }),
16
+ ),
17
+ )
18
+
19
+ const enrichments: ContextEnrichment[] = []
20
+ for (let i = 0; i < results.length; i++) {
21
+ const result = results[i]
22
+ if (result.status === 'fulfilled') {
23
+ enrichments.push({ domain: enrichers[i].domain, data: result.value.data, confidence: result.value.confidence })
24
+ } else {
25
+ serverLogger.warn`Context enricher "${enrichers[i].domain}" failed: ${result.reason}`
26
+ }
27
+ }
28
+
29
+ return enrichments.sort((a, b) => b.confidence - a.confidence)
30
+ }
31
+ }
32
+
33
+ export const contextEnrichmentService = new ContextEnrichmentService()
@@ -0,0 +1,117 @@
1
+ import type { SignalDeclaration } from '@lota-sdk/shared'
2
+
3
+ import { serverLogger } from '../config/logger'
4
+ import type { LotaPlugin } from '../runtime/plugin-types'
5
+ import { getRuntimeConfig } from '../runtime/runtime-config'
6
+ import type { PlanValidationIssueInput } from './plan-validator.service'
7
+
8
+ export interface EmittedSignal {
9
+ signal: string
10
+ payload: unknown
11
+ sourcePlugin: string
12
+ consumers: string[]
13
+ emittedAt: Date
14
+ }
15
+
16
+ const MAX_EMITTED_SIGNALS = 1000
17
+
18
+ class CoordinationRegistryService {
19
+ private producers = new Map<string, string[]>()
20
+ private consumers = new Map<string, string[]>()
21
+ private _emittedSignals: EmittedSignal[] = []
22
+
23
+ register(pluginRef: string, signals: SignalDeclaration[]): void {
24
+ for (const signal of signals) {
25
+ if (signal.direction === 'produces') {
26
+ const existing = this.producers.get(signal.signalName) ?? []
27
+ existing.push(pluginRef)
28
+ this.producers.set(signal.signalName, existing)
29
+ } else {
30
+ const existing = this.consumers.get(signal.signalName) ?? []
31
+ existing.push(pluginRef)
32
+ this.consumers.set(signal.signalName, existing)
33
+ }
34
+ }
35
+ }
36
+
37
+ async emit(signal: string, payload: unknown, sourcePlugin: string): Promise<void> {
38
+ const consumers = this.consumers.get(signal) ?? []
39
+
40
+ this._emittedSignals.push({ signal, payload, sourcePlugin, consumers: [...consumers], emittedAt: new Date() })
41
+ if (this._emittedSignals.length > MAX_EMITTED_SIGNALS) {
42
+ this._emittedSignals.shift()
43
+ }
44
+
45
+ serverLogger.debug(
46
+ `Signal "${signal}" emitted by "${sourcePlugin}" — ${consumers.length} consumer(s): ${consumers.join(', ') || 'none'}`,
47
+ )
48
+
49
+ if (consumers.length === 0) return
50
+
51
+ let pluginRuntime: Record<string, unknown> = {}
52
+ try {
53
+ pluginRuntime = getRuntimeConfig().pluginRuntime ?? {}
54
+ } catch {
55
+ // Runtime not yet configured — skip plugin dispatch
56
+ }
57
+
58
+ for (const consumerRef of consumers) {
59
+ const plugin = pluginRuntime[consumerRef] as LotaPlugin | undefined
60
+ if (plugin?.onSignal) {
61
+ try {
62
+ await plugin.onSignal(signal, payload, sourcePlugin)
63
+ } catch (error) {
64
+ serverLogger.warn`Signal handler for "${consumerRef}" failed on signal "${signal}": ${error}`
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ getEmittedSignals(signalName?: string): EmittedSignal[] {
71
+ if (!signalName) return [...this._emittedSignals]
72
+ return this._emittedSignals.filter((entry) => entry.signal === signalName)
73
+ }
74
+
75
+ getProducers(signalName: string): string[] {
76
+ return this.producers.get(signalName) ?? []
77
+ }
78
+
79
+ getConsumers(signalName: string): string[] {
80
+ return this.consumers.get(signalName) ?? []
81
+ }
82
+
83
+ validate(): PlanValidationIssueInput[] {
84
+ const issues: PlanValidationIssueInput[] = []
85
+
86
+ for (const [signalName] of this.producers) {
87
+ if ((this.consumers.get(signalName) ?? []).length === 0) {
88
+ issues.push({
89
+ severity: 'warning',
90
+ code: 'signal_no_consumer',
91
+ message: `Signal "${signalName}" is produced but has no consumers.`,
92
+ })
93
+ }
94
+ }
95
+
96
+ for (const [signalName] of this.consumers) {
97
+ if ((this.producers.get(signalName) ?? []).length === 0) {
98
+ issues.push({
99
+ severity: 'warning',
100
+ code: 'signal_no_producer',
101
+ message: `Signal "${signalName}" is consumed but has no producers.`,
102
+ })
103
+ }
104
+ }
105
+
106
+ return issues
107
+ }
108
+
109
+ /** Reset internal state. Intended for testing. */
110
+ _reset(): void {
111
+ this.producers.clear()
112
+ this.consumers.clear()
113
+ this._emittedSignals = []
114
+ }
115
+ }
116
+
117
+ export const coordinationRegistryService = new CoordinationRegistryService()
@@ -1,5 +1,3 @@
1
- import { createHash } from 'node:crypto'
2
-
3
1
  import { chunkMarkdownDocument, chunkPagedDocument, chunkPlainTextDocument } from '../document/org-document-chunking'
4
2
  import type { ParsedDocumentChunk } from '../document/org-document-chunking'
5
3
  import { getDefaultEmbeddings } from '../embeddings/provider'
@@ -56,7 +54,7 @@ export class DocumentChunkService {
56
54
  }
57
55
 
58
56
  hashContent(content: string): string {
59
- return createHash('sha256').update(content).digest('hex')
57
+ return new Bun.CryptoHasher('sha256').update(content).digest('hex')
60
58
  }
61
59
 
62
60
  // Uses 4 chars/token (conservative estimate for document content which tends
@@ -66,7 +64,7 @@ export class DocumentChunkService {
66
64
  }
67
65
 
68
66
  async embedQuery(query: string): Promise<number[]> {
69
- return await this.embeddings.embedQuery(query)
67
+ return this.embeddings.embedQuery(query)
70
68
  }
71
69
 
72
70
  async syncVersionedChunks<TRecord, TPayload>(params: {
@@ -0,0 +1,71 @@
1
+ import type { OwnershipDispatchContext, PlanArtifactSubmission, PlanNodeResult, PlanNodeSpec } from '@lota-sdk/shared'
2
+
3
+ import type { LotaPlugin, PluginDomainAgentDefinition } from '../runtime/plugin-types'
4
+ import { getRuntimeConfig } from '../runtime/runtime-config'
5
+ import type { PlanValidationIssueInput } from './plan-validator.service'
6
+
7
+ class DomainAgentExecutorService {
8
+ private registry = new Map<string, { pluginRef: string; definition: PluginDomainAgentDefinition }>()
9
+
10
+ configure(pluginRuntime: Record<string, LotaPlugin>): void {
11
+ this.registry.clear()
12
+ for (const [pluginRef, plugin] of Object.entries(pluginRuntime)) {
13
+ if (plugin.domainAgents) {
14
+ for (const def of plugin.domainAgents) {
15
+ this.registry.set(def.agentId, { pluginRef, definition: def })
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ hasAgent(agentId: string): boolean {
22
+ return this.registry.has(agentId)
23
+ }
24
+
25
+ validateOwner(agentId: string, nodeId: string): PlanValidationIssueInput[] {
26
+ if (!this.registry.has(agentId)) {
27
+ return [
28
+ {
29
+ severity: 'blocking',
30
+ code: 'domain_agent_missing',
31
+ message: `Node "${nodeId}" references unknown domain agent "${agentId}".`,
32
+ nodeId,
33
+ },
34
+ ]
35
+ }
36
+ return []
37
+ }
38
+
39
+ async executeNode(params: {
40
+ nodeSpec: PlanNodeSpec
41
+ resolvedInput: Record<string, unknown>
42
+ inputArtifacts: PlanArtifactSubmission[]
43
+ context: OwnershipDispatchContext
44
+ }): Promise<PlanNodeResult> {
45
+ const entry = this.registry.get(params.nodeSpec.owner.ref)
46
+ if (!entry) {
47
+ throw new Error(`Domain agent "${params.nodeSpec.owner.ref}" not registered.`)
48
+ }
49
+
50
+ const plugin = (getRuntimeConfig().pluginRuntime as Record<string, LotaPlugin | undefined> | undefined)?.[
51
+ entry.pluginRef
52
+ ]
53
+ if (!plugin?.nodeExecutor) {
54
+ throw new Error(`Plugin "${entry.pluginRef}" does not have a nodeExecutor.`)
55
+ }
56
+
57
+ return plugin.nodeExecutor.executeNode({
58
+ operation: `domain-agent:${entry.definition.agentId}`,
59
+ nodeSpec: params.nodeSpec,
60
+ inputs: params.resolvedInput,
61
+ context: {
62
+ organizationId: params.context.organizationId,
63
+ workstreamId: params.context.workstreamId,
64
+ planId: params.context.planId,
65
+ nodeId: params.context.nodeId,
66
+ },
67
+ })
68
+ }
69
+ }
70
+
71
+ export const domainAgentExecutorService = new DomainAgentExecutorService()