byterover-cli 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.production +0 -4
- package/dist/agent/core/domain/swarm/types.d.ts +132 -0
- package/dist/agent/core/domain/swarm/types.js +128 -0
- package/dist/agent/core/domain/tools/constants.d.ts +2 -0
- package/dist/agent/core/domain/tools/constants.js +2 -0
- package/dist/agent/core/interfaces/i-memory-provider.d.ts +45 -0
- package/dist/agent/core/interfaces/i-memory-provider.js +1 -0
- package/dist/agent/core/interfaces/i-sandbox-service.d.ts +8 -0
- package/dist/agent/core/interfaces/i-swarm-coordinator.d.ts +127 -0
- package/dist/agent/core/interfaces/i-swarm-coordinator.js +1 -0
- package/dist/agent/infra/agent/service-initializer.js +48 -0
- package/dist/agent/infra/map/map-shared.d.ts +2 -2
- package/dist/agent/infra/sandbox/sandbox-service.d.ts +10 -0
- package/dist/agent/infra/sandbox/sandbox-service.js +13 -0
- package/dist/agent/infra/sandbox/tools-sdk.d.ts +25 -0
- package/dist/agent/infra/sandbox/tools-sdk.js +24 -1
- package/dist/agent/infra/swarm/adapters/byterover-adapter.d.ts +39 -0
- package/dist/agent/infra/swarm/adapters/byterover-adapter.js +62 -0
- package/dist/agent/infra/swarm/adapters/gbrain-adapter.d.ts +63 -0
- package/dist/agent/infra/swarm/adapters/gbrain-adapter.js +209 -0
- package/dist/agent/infra/swarm/adapters/local-markdown-adapter.d.ts +41 -0
- package/dist/agent/infra/swarm/adapters/local-markdown-adapter.js +256 -0
- package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.d.ts +29 -0
- package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.js +244 -0
- package/dist/agent/infra/swarm/adapters/obsidian-adapter.d.ts +37 -0
- package/dist/agent/infra/swarm/adapters/obsidian-adapter.js +201 -0
- package/dist/agent/infra/swarm/cli/query-renderer.d.ts +15 -0
- package/dist/agent/infra/swarm/cli/query-renderer.js +126 -0
- package/dist/agent/infra/swarm/config/swarm-config-loader.d.ts +14 -0
- package/dist/agent/infra/swarm/config/swarm-config-loader.js +82 -0
- package/dist/agent/infra/swarm/config/swarm-config-schema.d.ts +667 -0
- package/dist/agent/infra/swarm/config/swarm-config-schema.js +305 -0
- package/dist/agent/infra/swarm/provider-factory.d.ts +21 -0
- package/dist/agent/infra/swarm/provider-factory.js +67 -0
- package/dist/agent/infra/swarm/search-precision.d.ts +95 -0
- package/dist/agent/infra/swarm/search-precision.js +141 -0
- package/dist/agent/infra/swarm/swarm-coordinator.d.ts +59 -0
- package/dist/agent/infra/swarm/swarm-coordinator.js +436 -0
- package/dist/agent/infra/swarm/swarm-graph.d.ts +63 -0
- package/dist/agent/infra/swarm/swarm-graph.js +167 -0
- package/dist/agent/infra/swarm/swarm-merger.d.ts +29 -0
- package/dist/agent/infra/swarm/swarm-merger.js +66 -0
- package/dist/agent/infra/swarm/swarm-router.d.ts +12 -0
- package/dist/agent/infra/swarm/swarm-router.js +40 -0
- package/dist/agent/infra/swarm/swarm-write-router.d.ts +23 -0
- package/dist/agent/infra/swarm/swarm-write-router.js +45 -0
- package/dist/agent/infra/swarm/validation/config-validator.d.ts +16 -0
- package/dist/agent/infra/swarm/validation/config-validator.js +402 -0
- package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.d.ts +33 -0
- package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.js +27 -0
- package/dist/agent/infra/swarm/wizard/config-scaffolder.d.ts +36 -0
- package/dist/agent/infra/swarm/wizard/config-scaffolder.js +96 -0
- package/dist/agent/infra/swarm/wizard/provider-detector.d.ts +54 -0
- package/dist/agent/infra/swarm/wizard/provider-detector.js +153 -0
- package/dist/agent/infra/swarm/wizard/swarm-wizard.d.ts +61 -0
- package/dist/agent/infra/swarm/wizard/swarm-wizard.js +187 -0
- package/dist/agent/infra/system-prompt/contributors/index.d.ts +1 -0
- package/dist/agent/infra/system-prompt/contributors/index.js +1 -0
- package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.d.ts +15 -0
- package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.js +65 -0
- package/dist/agent/infra/tools/implementations/curate-tool.d.ts +14 -14
- package/dist/agent/infra/tools/implementations/curate-tool.js +2 -0
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +12 -2
- package/dist/agent/infra/tools/implementations/swarm-query-tool.d.ts +9 -0
- package/dist/agent/infra/tools/implementations/swarm-query-tool.js +44 -0
- package/dist/agent/infra/tools/implementations/swarm-store-tool.d.ts +9 -0
- package/dist/agent/infra/tools/implementations/swarm-store-tool.js +43 -0
- package/dist/agent/infra/tools/tool-provider.js +1 -0
- package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
- package/dist/agent/infra/tools/tool-registry.js +25 -1
- package/dist/agent/resources/tools/code_exec.txt +2 -0
- package/dist/agent/resources/tools/swarm_query.txt +38 -0
- package/dist/agent/resources/tools/swarm_store.txt +35 -0
- package/dist/oclif/commands/curate/index.d.ts +1 -0
- package/dist/oclif/commands/curate/index.js +15 -1
- package/dist/oclif/commands/query.d.ts +1 -0
- package/dist/oclif/commands/query.js +17 -3
- package/dist/oclif/commands/search.d.ts +20 -0
- package/dist/oclif/commands/search.js +186 -0
- package/dist/oclif/commands/status.js +4 -0
- package/dist/oclif/commands/swarm/curate.d.ts +13 -0
- package/dist/oclif/commands/swarm/curate.js +81 -0
- package/dist/oclif/commands/swarm/onboard.d.ts +6 -0
- package/dist/oclif/commands/swarm/onboard.js +233 -0
- package/dist/oclif/commands/swarm/query.d.ts +14 -0
- package/dist/oclif/commands/swarm/query.js +84 -0
- package/dist/oclif/commands/swarm/status.d.ts +41 -0
- package/dist/oclif/commands/swarm/status.js +278 -0
- package/dist/oclif/lib/daemon-client.js +0 -1
- package/dist/oclif/lib/search-format.d.ts +10 -0
- package/dist/oclif/lib/search-format.js +25 -0
- package/dist/oclif/lib/task-client.d.ts +6 -0
- package/dist/oclif/lib/task-client.js +10 -3
- package/dist/server/constants.d.ts +3 -2
- package/dist/server/constants.js +10 -7
- package/dist/server/core/domain/errors/task-error.d.ts +2 -2
- package/dist/server/core/domain/errors/task-error.js +5 -4
- package/dist/server/core/domain/source/source-schema.d.ts +6 -6
- package/dist/server/core/domain/transport/schemas.d.ts +14 -14
- package/dist/server/core/domain/transport/schemas.js +3 -3
- package/dist/server/core/interfaces/executor/i-search-executor.d.ts +34 -0
- package/dist/server/core/interfaces/executor/i-search-executor.js +1 -0
- package/dist/server/core/interfaces/executor/index.d.ts +1 -0
- package/dist/server/core/interfaces/executor/index.js +1 -0
- package/dist/server/infra/daemon/agent-process.js +20 -7
- package/dist/server/infra/executor/search-executor.d.ts +17 -0
- package/dist/server/infra/executor/search-executor.js +30 -0
- package/dist/server/infra/http/provider-model-fetchers.js +1 -0
- package/dist/server/infra/process/feature-handlers.js +13 -0
- package/dist/server/infra/project/project-registry.js +13 -1
- package/dist/server/infra/transport/handlers/locations-handler.d.ts +2 -0
- package/dist/server/infra/transport/handlers/locations-handler.js +16 -1
- package/dist/server/infra/transport/handlers/pull-handler.js +3 -3
- package/dist/server/infra/transport/handlers/push-handler.js +3 -3
- package/dist/server/infra/transport/handlers/status-handler.js +25 -18
- package/dist/server/infra/transport/handlers/vc-handler.d.ts +0 -4
- package/dist/server/infra/transport/handlers/vc-handler.js +5 -16
- package/dist/server/templates/skill/SKILL.md +188 -5
- package/dist/server/utils/gitignore.d.ts +1 -0
- package/dist/server/utils/gitignore.js +36 -4
- package/dist/shared/transport/search-content.d.ts +28 -0
- package/dist/shared/transport/search-content.js +38 -0
- package/dist/shared/transport/types/dto.d.ts +1 -1
- package/dist/tui/features/status/utils/format-status.js +5 -0
- package/dist/tui/utils/error-messages.js +2 -2
- package/oclif.manifest.json +581 -317
- package/package.json +2 -2
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { SwarmGraph } from './swarm-graph.js';
|
|
4
|
+
import { mergeResults } from './swarm-merger.js';
|
|
5
|
+
import { classifyQuery, selectProviders } from './swarm-router.js';
|
|
6
|
+
import { classifyWrite, selectWriteTarget } from './swarm-write-router.js';
|
|
7
|
+
const execFileAsync = promisify(execFileCb);
|
|
8
|
+
const MAX_ARG_LENGTH = 200_000; // ~200KB safe for most OS arg limits
|
|
9
|
+
async function execBrvCurate(content) {
|
|
10
|
+
if (content.length > MAX_ARG_LENGTH) {
|
|
11
|
+
throw new Error(`Content too large for CLI argument (${content.length} bytes, max ${MAX_ARG_LENGTH}). Use brv curate directly.`);
|
|
12
|
+
}
|
|
13
|
+
let stdout;
|
|
14
|
+
try {
|
|
15
|
+
;
|
|
16
|
+
({ stdout } = await execFileAsync('brv', ['curate', '--detach', '--format', 'json', content], {
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
timeout: 30_000,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error instanceof Error) {
|
|
23
|
+
const stderr = error.stderr?.trim();
|
|
24
|
+
throw new Error(stderr ?? error.message);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(String(error));
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(stdout);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new Error(`Failed to parse brv curate output: ${stdout.slice(0, 200)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Default provider weights for RRF fusion.
|
|
37
|
+
* ByteRover (home provider) gets highest weight.
|
|
38
|
+
*/
|
|
39
|
+
const DEFAULT_WEIGHTS = {
|
|
40
|
+
byterover: 1,
|
|
41
|
+
gbrain: 0.85,
|
|
42
|
+
hindsight: 0.8,
|
|
43
|
+
honcho: 0.75,
|
|
44
|
+
'local-markdown': 0.8,
|
|
45
|
+
'memory-wiki': 0.9,
|
|
46
|
+
obsidian: 0.85,
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the weight for a provider ID.
|
|
50
|
+
* Supports prefixed IDs like `local-markdown:notes` → matches `local-markdown`.
|
|
51
|
+
*/
|
|
52
|
+
function resolveWeight(providerId) {
|
|
53
|
+
if (DEFAULT_WEIGHTS[providerId] !== undefined) {
|
|
54
|
+
return DEFAULT_WEIGHTS[providerId];
|
|
55
|
+
}
|
|
56
|
+
// Try prefix match (e.g., local-markdown:notes → local-markdown)
|
|
57
|
+
for (const [key, weight] of Object.entries(DEFAULT_WEIGHTS)) {
|
|
58
|
+
if (providerId.startsWith(`${key}:`)) {
|
|
59
|
+
return weight;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return 0.5;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Expand generic provider names in enrichment edges to concrete provider IDs,
|
|
66
|
+
* then deduplicate and drop self-edges and cycles.
|
|
67
|
+
*
|
|
68
|
+
* Config edges may use "local-markdown" as a shorthand, but actual providers
|
|
69
|
+
* are registered as "local-markdown:notes", "local-markdown:docs", etc.
|
|
70
|
+
* This function expands each generic endpoint to all matching concrete IDs,
|
|
71
|
+
* removes duplicates and self-edges, and detects/drops cycles.
|
|
72
|
+
*/
|
|
73
|
+
function expandEnrichmentEdges(configEdges, providerIds) {
|
|
74
|
+
// 1. Expand generic endpoints to concrete IDs
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const expanded = [];
|
|
77
|
+
for (const edge of configEdges) {
|
|
78
|
+
const fromIds = resolveEndpoint(edge.from, providerIds);
|
|
79
|
+
const toIds = resolveEndpoint(edge.to, providerIds);
|
|
80
|
+
for (const from of fromIds) {
|
|
81
|
+
for (const to of toIds) {
|
|
82
|
+
// Drop self-edges
|
|
83
|
+
if (from === to)
|
|
84
|
+
continue;
|
|
85
|
+
// Deduplicate
|
|
86
|
+
const key = `${from}->${to}`;
|
|
87
|
+
if (seen.has(key))
|
|
88
|
+
continue;
|
|
89
|
+
seen.add(key);
|
|
90
|
+
expanded.push({ from, to });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// 2. Drop all edges if the expanded graph has cycles
|
|
95
|
+
if (hasCycleInEdges(expanded)) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
return expanded;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Detect cycles in an edge list using DFS.
|
|
102
|
+
*/
|
|
103
|
+
function hasCycleInEdges(edges) {
|
|
104
|
+
const adjacency = new Map();
|
|
105
|
+
for (const edge of edges) {
|
|
106
|
+
const existing = adjacency.get(edge.from) ?? [];
|
|
107
|
+
existing.push(edge.to);
|
|
108
|
+
adjacency.set(edge.from, existing);
|
|
109
|
+
}
|
|
110
|
+
const visited = new Set();
|
|
111
|
+
const inStack = new Set();
|
|
112
|
+
function dfs(node) {
|
|
113
|
+
if (inStack.has(node))
|
|
114
|
+
return true;
|
|
115
|
+
if (visited.has(node))
|
|
116
|
+
return false;
|
|
117
|
+
visited.add(node);
|
|
118
|
+
inStack.add(node);
|
|
119
|
+
for (const neighbor of adjacency.get(node) ?? []) {
|
|
120
|
+
if (dfs(neighbor))
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
inStack.delete(node);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const allNodes = new Set([...edges.map((e) => e.from), ...edges.map((e) => e.to)]);
|
|
127
|
+
for (const node of allNodes) {
|
|
128
|
+
if (dfs(node))
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a config endpoint to one or more concrete provider IDs.
|
|
135
|
+
* Prefers prefix expansion over exact match so "local-markdown" expands to
|
|
136
|
+
* concrete folder IDs rather than staying generic.
|
|
137
|
+
*/
|
|
138
|
+
function resolveEndpoint(endpoint, providerIds) {
|
|
139
|
+
// Prefer prefix expansion: "local-markdown" → ["local-markdown:notes", "local-markdown:docs"]
|
|
140
|
+
const prefixMatches = providerIds.filter((id) => id.startsWith(`${endpoint}:`));
|
|
141
|
+
if (prefixMatches.length > 0)
|
|
142
|
+
return prefixMatches;
|
|
143
|
+
// Exact match (for providers without sub-IDs like "obsidian", "byterover")
|
|
144
|
+
if (providerIds.includes(endpoint))
|
|
145
|
+
return [endpoint];
|
|
146
|
+
// No match — return as-is (will be a no-op in the graph, but doesn't crash)
|
|
147
|
+
return [endpoint];
|
|
148
|
+
}
|
|
149
|
+
export class SwarmCoordinator {
|
|
150
|
+
config;
|
|
151
|
+
curateFallback;
|
|
152
|
+
graph;
|
|
153
|
+
healthCache = new Map();
|
|
154
|
+
maxCacheSize = 20;
|
|
155
|
+
providers;
|
|
156
|
+
resultCache = new Map();
|
|
157
|
+
resultCacheTtlMs;
|
|
158
|
+
totalQueries = 0;
|
|
159
|
+
constructor(providers, config, curateFallback) {
|
|
160
|
+
this.providers = providers;
|
|
161
|
+
this.config = config;
|
|
162
|
+
this.curateFallback = curateFallback ?? execBrvCurate;
|
|
163
|
+
this.resultCacheTtlMs = config.performance.resultCacheTtlMs ?? 10_000;
|
|
164
|
+
this.graph = new SwarmGraph(providers, {
|
|
165
|
+
timeoutMs: config.performance.maxQueryLatencyMs,
|
|
166
|
+
});
|
|
167
|
+
// Wire enrichment edges from config into the graph engine.
|
|
168
|
+
// Expand generic provider names (e.g. "local-markdown") to concrete IDs
|
|
169
|
+
// (e.g. "local-markdown:notes", "local-markdown:docs") so the graph can match them.
|
|
170
|
+
const configEdges = config.enrichment?.edges ?? [];
|
|
171
|
+
if (configEdges.length > 0) {
|
|
172
|
+
const providerIds = providers.map((p) => p.id);
|
|
173
|
+
const expanded = expandEnrichmentEdges(configEdges, providerIds);
|
|
174
|
+
this.graph.setEnrichmentEdges(expanded);
|
|
175
|
+
}
|
|
176
|
+
// Initialize health cache — assume all healthy until checked
|
|
177
|
+
for (const p of providers) {
|
|
178
|
+
this.healthCache.set(p.id, true);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Execute a swarm query: classify → select providers → execute in parallel → fuse results.
|
|
183
|
+
*/
|
|
184
|
+
async execute(request) {
|
|
185
|
+
// Cache check — return early if identical query was recently executed
|
|
186
|
+
const cacheKey = this.buildCacheKey(request);
|
|
187
|
+
const cached = this.resultCache.get(cacheKey);
|
|
188
|
+
if (cached) {
|
|
189
|
+
if (Date.now() - cached.timestamp < this.resultCacheTtlMs) {
|
|
190
|
+
// Move to end for LRU semantics
|
|
191
|
+
this.resultCache.delete(cacheKey);
|
|
192
|
+
this.resultCache.set(cacheKey, cached);
|
|
193
|
+
this.totalQueries++;
|
|
194
|
+
return { ...cached.result, results: cached.result.results.map((r) => ({ ...r, metadata: { ...r.metadata } })) };
|
|
195
|
+
}
|
|
196
|
+
// Expired — clean up stale entry
|
|
197
|
+
this.resultCache.delete(cacheKey);
|
|
198
|
+
}
|
|
199
|
+
const start = Date.now();
|
|
200
|
+
// 1. Classify query type
|
|
201
|
+
const queryType = request.type ?? classifyQuery(request.query);
|
|
202
|
+
// 2. Select active providers based on query type, excluding unhealthy ones
|
|
203
|
+
const healthyIds = this.providers.filter((p) => this.healthCache.get(p.id) !== false).map((p) => p.id);
|
|
204
|
+
const activeIds = selectProviders(queryType, healthyIds);
|
|
205
|
+
// 3. Estimate total cost
|
|
206
|
+
let costCents = 0;
|
|
207
|
+
for (const id of activeIds) {
|
|
208
|
+
const provider = this.providers.find((p) => p.id === id);
|
|
209
|
+
if (provider) {
|
|
210
|
+
costCents += provider.estimateCost(request).estimatedCostCents;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// 4. Execute via SwarmGraph (parallel with timeout)
|
|
214
|
+
const resultSets = await this.graph.execute(request, activeIds);
|
|
215
|
+
// 5. Build provider weights map
|
|
216
|
+
const weights = new Map();
|
|
217
|
+
for (const id of activeIds) {
|
|
218
|
+
weights.set(id, resolveWeight(id));
|
|
219
|
+
}
|
|
220
|
+
// 6. Fuse results via RRF merger
|
|
221
|
+
const maxResults = request.maxResults ?? this.config.routing.defaultMaxResults;
|
|
222
|
+
const merged = mergeResults(resultSets, weights, {
|
|
223
|
+
K: this.config.routing.rrfK,
|
|
224
|
+
maxResults,
|
|
225
|
+
minRRFScore: this.config.routing.minRrfScore,
|
|
226
|
+
rrfGapRatio: this.config.routing.rrfGapRatio,
|
|
227
|
+
});
|
|
228
|
+
// 7. Collect execution metadata from graph
|
|
229
|
+
const graphMeta = this.graph.getLastExecutionMeta();
|
|
230
|
+
const providerMeta = { ...graphMeta?.providers };
|
|
231
|
+
// 8. Record excluded providers (available but not selected by the routing matrix).
|
|
232
|
+
// Note: healthCache.get() returns undefined for unchecked providers, which is
|
|
233
|
+
// intentionally treated as healthy (!== false) — providers start healthy until proven otherwise.
|
|
234
|
+
const activeSet = new Set(activeIds);
|
|
235
|
+
for (const p of this.providers) {
|
|
236
|
+
if (!activeSet.has(p.id) && !providerMeta[p.id]) {
|
|
237
|
+
const healthy = this.healthCache.get(p.id) !== false;
|
|
238
|
+
providerMeta[p.id] = {
|
|
239
|
+
excludeReason: healthy ? `not in selection matrix for ${queryType}` : 'unhealthy',
|
|
240
|
+
latencyMs: 0,
|
|
241
|
+
resultCount: 0,
|
|
242
|
+
selected: false,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
this.totalQueries++;
|
|
247
|
+
const result = {
|
|
248
|
+
meta: {
|
|
249
|
+
costCents,
|
|
250
|
+
providers: providerMeta,
|
|
251
|
+
queryType,
|
|
252
|
+
totalLatencyMs: Date.now() - start,
|
|
253
|
+
},
|
|
254
|
+
results: merged,
|
|
255
|
+
};
|
|
256
|
+
// Cache store — deep-clone to prevent mutation of cached data via returned references
|
|
257
|
+
if (this.resultCacheTtlMs > 0) {
|
|
258
|
+
this.resultCache.set(cacheKey, {
|
|
259
|
+
result: { ...result, results: result.results.map((r) => ({ ...r, metadata: { ...r.metadata } })) },
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
});
|
|
262
|
+
this.evictIfOverSize();
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get info about all registered providers and their cached health status.
|
|
268
|
+
*/
|
|
269
|
+
getActiveProviders() {
|
|
270
|
+
return this.providers.map((p) => ({
|
|
271
|
+
capabilities: p.capabilities,
|
|
272
|
+
healthy: this.healthCache.get(p.id) ?? true,
|
|
273
|
+
id: p.id,
|
|
274
|
+
type: p.type,
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Get a presentation-oriented summary of the swarm state.
|
|
279
|
+
*/
|
|
280
|
+
getSummary() {
|
|
281
|
+
const providerInfos = this.providers.map((p) => ({
|
|
282
|
+
capabilities: p.capabilities,
|
|
283
|
+
healthy: this.healthCache.get(p.id) ?? true,
|
|
284
|
+
id: p.id,
|
|
285
|
+
type: p.type,
|
|
286
|
+
}));
|
|
287
|
+
const activeCount = providerInfos.filter((p) => p.healthy).length;
|
|
288
|
+
const avgLatencyMs = this.providers.length > 0
|
|
289
|
+
? this.providers.reduce((sum, p) => sum + p.capabilities.avgLatencyMs, 0) / this.providers.length
|
|
290
|
+
: 0;
|
|
291
|
+
return {
|
|
292
|
+
activeCount,
|
|
293
|
+
avgLatencyMs,
|
|
294
|
+
learningStatus: 'cold-start',
|
|
295
|
+
monthlyBudgetCents: this.config.budget?.globalMonthlyCapCents ?? 0,
|
|
296
|
+
monthlySpendCents: 0,
|
|
297
|
+
providers: providerInfos,
|
|
298
|
+
totalCount: this.providers.length,
|
|
299
|
+
totalQueries: this.totalQueries,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Run health checks on all providers and update the cache.
|
|
304
|
+
*/
|
|
305
|
+
async refreshHealth() {
|
|
306
|
+
const results = await Promise.all(this.providers.map(async (p) => {
|
|
307
|
+
const health = await p.healthCheck();
|
|
308
|
+
this.healthCache.set(p.id, health.available);
|
|
309
|
+
return {
|
|
310
|
+
capabilities: p.capabilities,
|
|
311
|
+
healthy: health.available,
|
|
312
|
+
id: p.id,
|
|
313
|
+
type: p.type,
|
|
314
|
+
};
|
|
315
|
+
}));
|
|
316
|
+
this.resultCache.clear();
|
|
317
|
+
return results;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Store knowledge in the best writable provider.
|
|
321
|
+
*
|
|
322
|
+
* Routing:
|
|
323
|
+
* 1. If request.provider is set → use that provider (verify writable + healthy)
|
|
324
|
+
* 2. If request.contentType is set → use it as write type (skip classification)
|
|
325
|
+
* 3. Otherwise → classifyWrite(content) → selectWriteTarget()
|
|
326
|
+
*/
|
|
327
|
+
async store(request) {
|
|
328
|
+
const start = Date.now();
|
|
329
|
+
let target;
|
|
330
|
+
if (request.provider) {
|
|
331
|
+
// Explicit provider target
|
|
332
|
+
const provider = this.providers.find((p) => p.id === request.provider);
|
|
333
|
+
if (!provider) {
|
|
334
|
+
return {
|
|
335
|
+
error: `Provider '${request.provider}' not found`,
|
|
336
|
+
id: '',
|
|
337
|
+
latencyMs: 0,
|
|
338
|
+
provider: request.provider,
|
|
339
|
+
success: false,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (!provider.capabilities.writeSupported) {
|
|
343
|
+
return {
|
|
344
|
+
error: `Provider '${request.provider}' does not support writes`,
|
|
345
|
+
id: '',
|
|
346
|
+
latencyMs: 0,
|
|
347
|
+
provider: request.provider,
|
|
348
|
+
success: false,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (this.healthCache.get(provider.id) === false) {
|
|
352
|
+
return {
|
|
353
|
+
error: `Provider '${request.provider}' is not healthy`,
|
|
354
|
+
id: '',
|
|
355
|
+
latencyMs: 0,
|
|
356
|
+
provider: request.provider,
|
|
357
|
+
success: false,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
target = provider;
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
// Auto-route: classify content type, then select target
|
|
364
|
+
const writeType = request.contentType ?? classifyWrite(request.content);
|
|
365
|
+
const selected = selectWriteTarget(writeType, this.providers, this.healthCache);
|
|
366
|
+
if (!selected) {
|
|
367
|
+
return this.fallbackToByterover(request, start);
|
|
368
|
+
}
|
|
369
|
+
target = selected;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const result = await target.store({
|
|
373
|
+
content: request.content,
|
|
374
|
+
metadata: {
|
|
375
|
+
source: 'swarm-curate',
|
|
376
|
+
timestamp: Date.now(),
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
if (result.success) {
|
|
380
|
+
this.resultCache.clear();
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
id: result.id,
|
|
384
|
+
latencyMs: Date.now() - start,
|
|
385
|
+
provider: target.id,
|
|
386
|
+
success: result.success,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
return {
|
|
391
|
+
error: error instanceof Error ? error.message : String(error),
|
|
392
|
+
id: '',
|
|
393
|
+
latencyMs: Date.now() - start,
|
|
394
|
+
provider: target.id,
|
|
395
|
+
success: false,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
buildCacheKey(request) {
|
|
400
|
+
const q = request.query.toLowerCase().trim().replaceAll(/\s+/g, ' ');
|
|
401
|
+
const scope = request.scope ?? '';
|
|
402
|
+
const max = request.maxResults ?? this.config.routing.defaultMaxResults;
|
|
403
|
+
return JSON.stringify([q, scope, max, request.type, request.timeRange]);
|
|
404
|
+
}
|
|
405
|
+
evictIfOverSize() {
|
|
406
|
+
if (this.resultCache.size <= this.maxCacheSize)
|
|
407
|
+
return;
|
|
408
|
+
const firstKey = this.resultCache.keys().next().value;
|
|
409
|
+
if (firstKey !== undefined) {
|
|
410
|
+
this.resultCache.delete(firstKey);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async fallbackToByterover(request, start) {
|
|
414
|
+
try {
|
|
415
|
+
const parsed = await this.curateFallback(request.content);
|
|
416
|
+
return {
|
|
417
|
+
error: parsed.success === true ? undefined : (parsed.error ?? 'brv curate returned success: false'),
|
|
418
|
+
fallback: true,
|
|
419
|
+
id: parsed.data?.logId ?? parsed.data?.taskId ?? '',
|
|
420
|
+
latencyMs: Date.now() - start,
|
|
421
|
+
provider: 'byterover',
|
|
422
|
+
success: parsed.success === true,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
return {
|
|
427
|
+
error: error instanceof Error ? error.message : String(error),
|
|
428
|
+
fallback: true,
|
|
429
|
+
id: '',
|
|
430
|
+
latencyMs: Date.now() - start,
|
|
431
|
+
provider: 'byterover',
|
|
432
|
+
success: false,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { QueryRequest, QueryResult } from '../../core/domain/swarm/types.js';
|
|
2
|
+
import type { IMemoryProvider } from '../../core/interfaces/i-memory-provider.js';
|
|
3
|
+
import type { ProviderQueryMeta } from '../../core/interfaces/i-swarm-coordinator.js';
|
|
4
|
+
/**
|
|
5
|
+
* Per-provider execution metadata from the last query.
|
|
6
|
+
*/
|
|
7
|
+
export type GraphExecutionMeta = {
|
|
8
|
+
providers: Record<string, ProviderQueryMeta>;
|
|
9
|
+
totalLatencyMs: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Options for the swarm graph.
|
|
13
|
+
*/
|
|
14
|
+
export type SwarmGraphOptions = {
|
|
15
|
+
/** Timeout per provider in milliseconds (default: 2000) */
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* An enrichment edge: provider `from` feeds results to provider `to`.
|
|
20
|
+
*/
|
|
21
|
+
export type EnrichmentEdge = {
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Swarm Graph — executes providers in topological levels with enrichment chains.
|
|
27
|
+
*
|
|
28
|
+
* Level 0: providers with no incoming enrichment edges (run in parallel).
|
|
29
|
+
* Level 1+: providers that depend on earlier-level results (run after predecessors complete).
|
|
30
|
+
*
|
|
31
|
+
* Supports arbitrary DAG depth (A→B→C) and fan-in (A→C, B→C).
|
|
32
|
+
* Fan-in nodes receive merged enrichment from ALL predecessors.
|
|
33
|
+
*/
|
|
34
|
+
export declare class SwarmGraph {
|
|
35
|
+
private edges;
|
|
36
|
+
private lastMeta?;
|
|
37
|
+
private readonly providerMap;
|
|
38
|
+
private readonly timeoutMs;
|
|
39
|
+
constructor(providers: IMemoryProvider[], options?: SwarmGraphOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Execute a query across the specified active providers.
|
|
42
|
+
* Providers are grouped into topological levels based on enrichment edges.
|
|
43
|
+
* Level-0 runs in parallel; level-1+ providers receive enrichment from predecessors.
|
|
44
|
+
*/
|
|
45
|
+
execute(request: QueryRequest, activeProviderIds: string[]): Promise<Map<string, QueryResult[]>>;
|
|
46
|
+
/**
|
|
47
|
+
* Get execution metadata from the last query.
|
|
48
|
+
*/
|
|
49
|
+
getLastExecutionMeta(): GraphExecutionMeta | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Configure enrichment edges between providers.
|
|
52
|
+
* An edge { from: 'A', to: 'B' } means B runs after A and receives A's results.
|
|
53
|
+
*/
|
|
54
|
+
setEnrichmentEdges(edges: EnrichmentEdge[]): void;
|
|
55
|
+
/**
|
|
56
|
+
* Compute topological execution levels using Kahn's algorithm.
|
|
57
|
+
*
|
|
58
|
+
* Supports arbitrary DAG depth (A→B→C→...) and fan-in (A→C, B→C).
|
|
59
|
+
* Returns levels grouped for parallel execution and a map of
|
|
60
|
+
* provider ID → ALL predecessor IDs that feed it enrichment.
|
|
61
|
+
*/
|
|
62
|
+
private buildExecutionLevels;
|
|
63
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execute a query against a provider with a timeout.
|
|
3
|
+
* Returns empty results if the provider times out or throws.
|
|
4
|
+
*/
|
|
5
|
+
async function queryWithTimeout(provider, request, timeoutMs) {
|
|
6
|
+
const start = Date.now();
|
|
7
|
+
try {
|
|
8
|
+
const result = await Promise.race([
|
|
9
|
+
provider.query(request),
|
|
10
|
+
new Promise((_resolve, reject) => {
|
|
11
|
+
setTimeout(() => { reject(new Error(`Provider ${provider.id} timed out after ${timeoutMs}ms`)); }, timeoutMs);
|
|
12
|
+
}),
|
|
13
|
+
]);
|
|
14
|
+
return { latencyMs: Date.now() - start, results: result };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { latencyMs: Date.now() - start, results: [] };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build enrichment data by merging results from multiple predecessor providers.
|
|
22
|
+
*/
|
|
23
|
+
function buildEnrichment(allResults) {
|
|
24
|
+
const excerpts = allResults
|
|
25
|
+
.map((r) => r.content)
|
|
26
|
+
.filter((c) => c.length > 0);
|
|
27
|
+
return { excerpts };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Swarm Graph — executes providers in topological levels with enrichment chains.
|
|
31
|
+
*
|
|
32
|
+
* Level 0: providers with no incoming enrichment edges (run in parallel).
|
|
33
|
+
* Level 1+: providers that depend on earlier-level results (run after predecessors complete).
|
|
34
|
+
*
|
|
35
|
+
* Supports arbitrary DAG depth (A→B→C) and fan-in (A→C, B→C).
|
|
36
|
+
* Fan-in nodes receive merged enrichment from ALL predecessors.
|
|
37
|
+
*/
|
|
38
|
+
export class SwarmGraph {
|
|
39
|
+
edges = [];
|
|
40
|
+
lastMeta;
|
|
41
|
+
providerMap;
|
|
42
|
+
timeoutMs;
|
|
43
|
+
constructor(providers, options) {
|
|
44
|
+
this.providerMap = new Map(providers.map((p) => [p.id, p]));
|
|
45
|
+
this.timeoutMs = options?.timeoutMs ?? 2000;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Execute a query across the specified active providers.
|
|
49
|
+
* Providers are grouped into topological levels based on enrichment edges.
|
|
50
|
+
* Level-0 runs in parallel; level-1+ providers receive enrichment from predecessors.
|
|
51
|
+
*/
|
|
52
|
+
async execute(request, activeProviderIds) {
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
const results = new Map();
|
|
55
|
+
const providerMeta = {};
|
|
56
|
+
const activeSet = new Set(activeProviderIds);
|
|
57
|
+
// Build execution levels
|
|
58
|
+
const { levels, predecessors } = this.buildExecutionLevels(activeSet);
|
|
59
|
+
// Execute each level sequentially; providers within a level run in parallel
|
|
60
|
+
for (const level of levels) {
|
|
61
|
+
const executions = level
|
|
62
|
+
.map((id) => {
|
|
63
|
+
const provider = this.providerMap.get(id);
|
|
64
|
+
if (!provider)
|
|
65
|
+
return;
|
|
66
|
+
// Build enriched request by merging results from ALL predecessors
|
|
67
|
+
let enrichedRequest = request;
|
|
68
|
+
let enrichmentForMeta;
|
|
69
|
+
const predIds = predecessors.get(id);
|
|
70
|
+
if (predIds && predIds.length > 0) {
|
|
71
|
+
const allPredResults = [];
|
|
72
|
+
for (const predId of predIds) {
|
|
73
|
+
const predResults = results.get(predId);
|
|
74
|
+
if (predResults) {
|
|
75
|
+
allPredResults.push(...predResults);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (allPredResults.length > 0) {
|
|
79
|
+
enrichmentForMeta = buildEnrichment(allPredResults);
|
|
80
|
+
enrichedRequest = {
|
|
81
|
+
...request,
|
|
82
|
+
enrichment: enrichmentForMeta,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return queryWithTimeout(provider, enrichedRequest, this.timeoutMs).then((outcome) => {
|
|
87
|
+
results.set(id, outcome.results);
|
|
88
|
+
providerMeta[id] = {
|
|
89
|
+
enrichedBy: predIds && predIds.length > 0 ? predIds.join(',') : undefined,
|
|
90
|
+
enrichmentExcerpts: enrichmentForMeta?.excerpts?.slice(0, 10).map((k) => k.split(/\s+/).slice(0, 5).join(' ')),
|
|
91
|
+
latencyMs: outcome.latencyMs,
|
|
92
|
+
resultCount: outcome.results.length,
|
|
93
|
+
selected: true,
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
})
|
|
97
|
+
.filter(Boolean);
|
|
98
|
+
// eslint-disable-next-line no-await-in-loop -- levels must run sequentially
|
|
99
|
+
await Promise.all(executions);
|
|
100
|
+
}
|
|
101
|
+
this.lastMeta = {
|
|
102
|
+
providers: providerMeta,
|
|
103
|
+
totalLatencyMs: Date.now() - start,
|
|
104
|
+
};
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get execution metadata from the last query.
|
|
109
|
+
*/
|
|
110
|
+
getLastExecutionMeta() {
|
|
111
|
+
return this.lastMeta;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Configure enrichment edges between providers.
|
|
115
|
+
* An edge { from: 'A', to: 'B' } means B runs after A and receives A's results.
|
|
116
|
+
*/
|
|
117
|
+
setEnrichmentEdges(edges) {
|
|
118
|
+
this.edges = edges;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Compute topological execution levels using Kahn's algorithm.
|
|
122
|
+
*
|
|
123
|
+
* Supports arbitrary DAG depth (A→B→C→...) and fan-in (A→C, B→C).
|
|
124
|
+
* Returns levels grouped for parallel execution and a map of
|
|
125
|
+
* provider ID → ALL predecessor IDs that feed it enrichment.
|
|
126
|
+
*/
|
|
127
|
+
buildExecutionLevels(activeSet) {
|
|
128
|
+
// Filter edges to only active providers on both sides
|
|
129
|
+
const activeEdges = this.edges.filter((e) => activeSet.has(e.from) && activeSet.has(e.to));
|
|
130
|
+
// Build the predecessors map (to → [from1, from2, ...])
|
|
131
|
+
const predecessors = new Map();
|
|
132
|
+
for (const edge of activeEdges) {
|
|
133
|
+
const existing = predecessors.get(edge.to) ?? [];
|
|
134
|
+
existing.push(edge.from);
|
|
135
|
+
predecessors.set(edge.to, existing);
|
|
136
|
+
}
|
|
137
|
+
// Build in-degree counts and adjacency list
|
|
138
|
+
const inDegree = new Map();
|
|
139
|
+
const successors = new Map();
|
|
140
|
+
for (const id of activeSet) {
|
|
141
|
+
inDegree.set(id, 0);
|
|
142
|
+
successors.set(id, []);
|
|
143
|
+
}
|
|
144
|
+
for (const edge of activeEdges) {
|
|
145
|
+
inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
|
|
146
|
+
successors.get(edge.from).push(edge.to);
|
|
147
|
+
}
|
|
148
|
+
// Kahn's algorithm: peel off nodes with in-degree 0, level by level
|
|
149
|
+
const levels = [];
|
|
150
|
+
let currentLevel = [...activeSet].filter((id) => inDegree.get(id) === 0);
|
|
151
|
+
while (currentLevel.length > 0) {
|
|
152
|
+
levels.push(currentLevel);
|
|
153
|
+
const nextLevel = [];
|
|
154
|
+
for (const id of currentLevel) {
|
|
155
|
+
for (const succ of successors.get(id) ?? []) {
|
|
156
|
+
const newDegree = (inDegree.get(succ) ?? 1) - 1;
|
|
157
|
+
inDegree.set(succ, newDegree);
|
|
158
|
+
if (newDegree === 0) {
|
|
159
|
+
nextLevel.push(succ);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
currentLevel = nextLevel;
|
|
164
|
+
}
|
|
165
|
+
return { levels, predecessors };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { QueryResult } from '../../core/domain/swarm/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for the RRF merger.
|
|
4
|
+
*/
|
|
5
|
+
export type MergerOptions = {
|
|
6
|
+
/** RRF constant (default: 60) */
|
|
7
|
+
K?: number;
|
|
8
|
+
/** Maximum results to return (default: 10) */
|
|
9
|
+
maxResults?: number;
|
|
10
|
+
/** T4: Drop results with RRF score below this absolute threshold (RRF-space, not 0-1) */
|
|
11
|
+
minRRFScore?: number;
|
|
12
|
+
/** T5: Drop results where rrfScore < topRRF * ratio (0, 1] */
|
|
13
|
+
rrfGapRatio?: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Fuse results from multiple providers using Weighted Reciprocal Rank Fusion.
|
|
17
|
+
*
|
|
18
|
+
* Algorithm:
|
|
19
|
+
* RRF_score(r) = Σᵢ wᵢ / (K + rankᵢ(r))
|
|
20
|
+
* where wᵢ is the provider weight and rankᵢ(r) is the 0-based rank.
|
|
21
|
+
*
|
|
22
|
+
* Deduplication: results with identical content are merged (highest provider weight kept).
|
|
23
|
+
*
|
|
24
|
+
* @param resultSets - Map of provider ID → ranked results
|
|
25
|
+
* @param providerWeights - Map of provider ID → weight (0-1)
|
|
26
|
+
* @param options - Merger options
|
|
27
|
+
* @returns Fused, ranked, deduplicated results
|
|
28
|
+
*/
|
|
29
|
+
export declare function mergeResults(resultSets: Map<string, QueryResult[]>, providerWeights: Map<string, number>, options?: MergerOptions): QueryResult[];
|