@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.
- package/infrastructure/schema/00_identity.surql +0 -2
- package/infrastructure/schema/01_memory.surql +1 -1
- package/infrastructure/schema/02_execution_plan.surql +62 -1
- package/infrastructure/schema/03_learned_skill.surql +1 -1
- package/infrastructure/schema/06_playbook.surql +25 -0
- package/infrastructure/schema/07_institutional_memory.surql +13 -0
- package/infrastructure/schema/08_quality_metrics.surql +17 -0
- package/package.json +12 -8
- package/src/ai/definitions.ts +81 -3
- package/src/ai/embedding-cache.ts +2 -4
- package/src/ai/index.ts +0 -2
- package/src/bifrost/bifrost.ts +2 -7
- package/src/bifrost/cache-headers.ts +8 -0
- package/src/bifrost/index.ts +1 -0
- package/src/config/agent-defaults.ts +31 -21
- package/src/config/agent-types.ts +11 -0
- package/src/config/constants.ts +2 -14
- package/src/config/debug-logger.ts +5 -1
- package/src/config/index.ts +3 -0
- package/src/config/model-constants.ts +16 -34
- package/src/config/search.ts +1 -15
- package/src/create-runtime.ts +269 -178
- package/src/db/cursor-pagination.ts +3 -6
- package/src/db/index.ts +2 -0
- package/src/db/memory-store.helpers.ts +1 -3
- package/src/db/memory-store.rows.ts +7 -7
- package/src/db/memory-store.ts +14 -18
- package/src/db/memory.ts +13 -13
- package/src/db/schema-fingerprint.ts +1 -3
- package/src/db/service.ts +153 -79
- package/src/db/startup.ts +6 -10
- package/src/db/surreal-mutation.ts +43 -0
- package/src/db/tables.ts +7 -0
- package/src/db/workstream-message-row.ts +15 -0
- package/src/embeddings/provider.ts +1 -1
- package/src/queues/context-compaction.queue.ts +15 -46
- package/src/queues/delayed-node-promotion.queue.ts +41 -0
- package/src/queues/document-processor.queue.ts +2 -4
- package/src/queues/index.ts +3 -0
- package/src/queues/memory-consolidation.queue.ts +16 -51
- package/src/queues/plan-scheduler.queue.ts +97 -0
- package/src/queues/post-chat-memory.queue.ts +20 -55
- package/src/queues/queue-factory.ts +100 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
- package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
- package/src/queues/skill-extraction.queue.ts +15 -47
- package/src/queues/workstream-title-generation.queue.ts +15 -47
- package/src/redis/connection.ts +6 -0
- package/src/redis/index.ts +1 -1
- package/src/redis/redis-lease-lock.ts +1 -2
- package/src/redis/stream-context.ts +11 -0
- package/src/runtime/agent-runtime-policy.ts +109 -35
- package/src/runtime/approval-continuation.ts +12 -6
- package/src/runtime/context-compaction-runtime.ts +1 -1
- package/src/runtime/context-compaction.ts +24 -64
- package/src/runtime/execution-plan.ts +22 -18
- package/src/runtime/graph-designer.ts +15 -0
- package/src/runtime/helper-model.ts +9 -197
- package/src/runtime/index.ts +3 -1
- package/src/runtime/llm-content.ts +1 -1
- package/src/runtime/memory-block.ts +9 -11
- package/src/runtime/memory-pipeline.ts +6 -9
- package/src/runtime/plugin-resolution.ts +35 -0
- package/src/runtime/plugin-types.ts +72 -0
- package/src/runtime/retrieval-adapters.ts +1 -1
- package/src/runtime/runtime-config.ts +111 -14
- package/src/runtime/runtime-extensions.ts +2 -3
- package/src/runtime/runtime-worker-registry.ts +6 -0
- package/src/runtime/social-chat.ts +752 -0
- package/src/runtime/team-consultation-orchestrator.ts +45 -32
- package/src/runtime/team-consultation-prompts.ts +11 -2
- package/src/runtime/title-helpers.ts +2 -4
- package/src/runtime/workstream-chat-helpers.ts +1 -1
- package/src/services/adaptive-playbook.service.ts +152 -0
- package/src/services/agent-executor.service.ts +292 -0
- package/src/services/artifact-provenance.service.ts +172 -0
- package/src/services/attachment.service.ts +6 -11
- package/src/services/context-compaction.service.ts +72 -55
- package/src/services/context-enrichment.service.ts +33 -0
- package/src/services/coordination-registry.service.ts +117 -0
- package/src/services/document-chunk.service.ts +2 -4
- package/src/services/domain-agent-executor.service.ts +71 -0
- package/src/services/execution-plan.service.ts +269 -50
- package/src/services/feedback-loop.service.ts +96 -0
- package/src/services/global-orchestrator.service.ts +148 -0
- package/src/services/index.ts +27 -0
- package/src/services/institutional-memory.service.ts +145 -0
- package/src/services/learned-skill.service.ts +24 -5
- package/src/services/memory-assessment.service.ts +3 -2
- package/src/services/memory-utils.ts +3 -8
- package/src/services/memory.service.ts +49 -61
- package/src/services/monitoring-window.service.ts +86 -0
- package/src/services/mutating-approval.service.ts +1 -1
- package/src/services/node-workspace.service.ts +155 -0
- package/src/services/notification.service.ts +39 -0
- package/src/services/organization-member.service.ts +11 -4
- package/src/services/organization.service.ts +5 -5
- package/src/services/ownership-dispatcher.service.ts +403 -0
- package/src/services/plan-approval.service.ts +1 -1
- package/src/services/plan-builder.service.ts +1 -0
- package/src/services/plan-checkpoint.service.ts +30 -2
- package/src/services/plan-compiler.service.ts +5 -0
- package/src/services/plan-coordination.service.ts +152 -0
- package/src/services/plan-cycle.service.ts +284 -0
- package/src/services/plan-deadline.service.ts +287 -0
- package/src/services/plan-executor.service.ts +384 -40
- package/src/services/plan-run.service.ts +41 -7
- package/src/services/plan-scheduler.service.ts +240 -0
- package/src/services/plan-template.service.ts +117 -0
- package/src/services/plan-validator.service.ts +84 -2
- package/src/services/plan-workspace.service.ts +83 -0
- package/src/services/playbook-registry.service.ts +67 -0
- package/src/services/plugin-executor.service.ts +103 -0
- package/src/services/quality-metrics.service.ts +132 -0
- package/src/services/recent-activity.service.ts +28 -34
- package/src/services/skill-resolver.service.ts +19 -0
- package/src/services/social-chat-history.service.ts +197 -0
- package/src/services/system-executor.service.ts +105 -0
- package/src/services/workstream-message.service.ts +13 -37
- package/src/services/workstream-plan-registry.service.ts +22 -0
- package/src/services/workstream-title.service.ts +3 -1
- package/src/services/workstream-turn-preparation.service.ts +34 -89
- package/src/services/workstream.service.ts +33 -55
- package/src/services/workstream.types.ts +9 -9
- package/src/services/write-intent-validator.service.ts +81 -0
- package/src/storage/attachment-parser.ts +1 -1
- package/src/storage/attachment-utils.ts +1 -1
- package/src/storage/generated-document-storage.service.ts +3 -2
- package/src/system-agents/context-compaction.agent.ts +2 -0
- package/src/system-agents/delegated-agent-factory.ts +5 -0
- package/src/system-agents/memory-reranker.agent.ts +4 -2
- package/src/system-agents/memory.agent.ts +2 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -0
- package/src/system-agents/skill-extractor.agent.ts +2 -0
- package/src/system-agents/skill-manager.agent.ts +2 -0
- package/src/system-agents/title-generator.agent.ts +2 -0
- package/src/tools/execution-plan.tool.ts +17 -23
- package/src/tools/index.ts +0 -1
- package/src/tools/research-topic.tool.ts +2 -0
- package/src/tools/team-think.tool.ts +5 -6
- package/src/utils/async.ts +2 -1
- package/src/utils/date-time.ts +4 -32
- package/src/utils/env.ts +8 -0
- package/src/utils/errors.ts +42 -10
- package/src/utils/index.ts +9 -0
- package/src/utils/string.ts +114 -1
- package/src/workers/index.ts +1 -0
- package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
- package/src/workers/regular-chat-memory-digest.runner.ts +45 -12
- package/src/workers/skill-extraction.runner.ts +26 -6
- package/src/workers/utils/file-section-chunker.ts +2 -1
- package/src/workers/utils/repo-structure-extractor.ts +2 -2
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/sandbox-error.ts +11 -2
- package/src/workers/utils/workstream-message-query.ts +14 -25
- package/src/workers/worker-utils.ts +2 -2
- package/src/runtime/workstream-routing-policy.ts +0 -267
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
|
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
|
|
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()
|