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.
Files changed (127) hide show
  1. package/.env.production +0 -4
  2. package/dist/agent/core/domain/swarm/types.d.ts +132 -0
  3. package/dist/agent/core/domain/swarm/types.js +128 -0
  4. package/dist/agent/core/domain/tools/constants.d.ts +2 -0
  5. package/dist/agent/core/domain/tools/constants.js +2 -0
  6. package/dist/agent/core/interfaces/i-memory-provider.d.ts +45 -0
  7. package/dist/agent/core/interfaces/i-memory-provider.js +1 -0
  8. package/dist/agent/core/interfaces/i-sandbox-service.d.ts +8 -0
  9. package/dist/agent/core/interfaces/i-swarm-coordinator.d.ts +127 -0
  10. package/dist/agent/core/interfaces/i-swarm-coordinator.js +1 -0
  11. package/dist/agent/infra/agent/service-initializer.js +48 -0
  12. package/dist/agent/infra/map/map-shared.d.ts +2 -2
  13. package/dist/agent/infra/sandbox/sandbox-service.d.ts +10 -0
  14. package/dist/agent/infra/sandbox/sandbox-service.js +13 -0
  15. package/dist/agent/infra/sandbox/tools-sdk.d.ts +25 -0
  16. package/dist/agent/infra/sandbox/tools-sdk.js +24 -1
  17. package/dist/agent/infra/swarm/adapters/byterover-adapter.d.ts +39 -0
  18. package/dist/agent/infra/swarm/adapters/byterover-adapter.js +62 -0
  19. package/dist/agent/infra/swarm/adapters/gbrain-adapter.d.ts +63 -0
  20. package/dist/agent/infra/swarm/adapters/gbrain-adapter.js +209 -0
  21. package/dist/agent/infra/swarm/adapters/local-markdown-adapter.d.ts +41 -0
  22. package/dist/agent/infra/swarm/adapters/local-markdown-adapter.js +256 -0
  23. package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.d.ts +29 -0
  24. package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.js +244 -0
  25. package/dist/agent/infra/swarm/adapters/obsidian-adapter.d.ts +37 -0
  26. package/dist/agent/infra/swarm/adapters/obsidian-adapter.js +201 -0
  27. package/dist/agent/infra/swarm/cli/query-renderer.d.ts +15 -0
  28. package/dist/agent/infra/swarm/cli/query-renderer.js +126 -0
  29. package/dist/agent/infra/swarm/config/swarm-config-loader.d.ts +14 -0
  30. package/dist/agent/infra/swarm/config/swarm-config-loader.js +82 -0
  31. package/dist/agent/infra/swarm/config/swarm-config-schema.d.ts +667 -0
  32. package/dist/agent/infra/swarm/config/swarm-config-schema.js +305 -0
  33. package/dist/agent/infra/swarm/provider-factory.d.ts +21 -0
  34. package/dist/agent/infra/swarm/provider-factory.js +67 -0
  35. package/dist/agent/infra/swarm/search-precision.d.ts +95 -0
  36. package/dist/agent/infra/swarm/search-precision.js +141 -0
  37. package/dist/agent/infra/swarm/swarm-coordinator.d.ts +59 -0
  38. package/dist/agent/infra/swarm/swarm-coordinator.js +436 -0
  39. package/dist/agent/infra/swarm/swarm-graph.d.ts +63 -0
  40. package/dist/agent/infra/swarm/swarm-graph.js +167 -0
  41. package/dist/agent/infra/swarm/swarm-merger.d.ts +29 -0
  42. package/dist/agent/infra/swarm/swarm-merger.js +66 -0
  43. package/dist/agent/infra/swarm/swarm-router.d.ts +12 -0
  44. package/dist/agent/infra/swarm/swarm-router.js +40 -0
  45. package/dist/agent/infra/swarm/swarm-write-router.d.ts +23 -0
  46. package/dist/agent/infra/swarm/swarm-write-router.js +45 -0
  47. package/dist/agent/infra/swarm/validation/config-validator.d.ts +16 -0
  48. package/dist/agent/infra/swarm/validation/config-validator.js +402 -0
  49. package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.d.ts +33 -0
  50. package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.js +27 -0
  51. package/dist/agent/infra/swarm/wizard/config-scaffolder.d.ts +36 -0
  52. package/dist/agent/infra/swarm/wizard/config-scaffolder.js +96 -0
  53. package/dist/agent/infra/swarm/wizard/provider-detector.d.ts +54 -0
  54. package/dist/agent/infra/swarm/wizard/provider-detector.js +153 -0
  55. package/dist/agent/infra/swarm/wizard/swarm-wizard.d.ts +61 -0
  56. package/dist/agent/infra/swarm/wizard/swarm-wizard.js +187 -0
  57. package/dist/agent/infra/system-prompt/contributors/index.d.ts +1 -0
  58. package/dist/agent/infra/system-prompt/contributors/index.js +1 -0
  59. package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.d.ts +15 -0
  60. package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.js +65 -0
  61. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +14 -14
  62. package/dist/agent/infra/tools/implementations/curate-tool.js +2 -0
  63. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +12 -2
  64. package/dist/agent/infra/tools/implementations/swarm-query-tool.d.ts +9 -0
  65. package/dist/agent/infra/tools/implementations/swarm-query-tool.js +44 -0
  66. package/dist/agent/infra/tools/implementations/swarm-store-tool.d.ts +9 -0
  67. package/dist/agent/infra/tools/implementations/swarm-store-tool.js +43 -0
  68. package/dist/agent/infra/tools/tool-provider.js +1 -0
  69. package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
  70. package/dist/agent/infra/tools/tool-registry.js +25 -1
  71. package/dist/agent/resources/tools/code_exec.txt +2 -0
  72. package/dist/agent/resources/tools/swarm_query.txt +38 -0
  73. package/dist/agent/resources/tools/swarm_store.txt +35 -0
  74. package/dist/oclif/commands/curate/index.d.ts +1 -0
  75. package/dist/oclif/commands/curate/index.js +15 -1
  76. package/dist/oclif/commands/query.d.ts +1 -0
  77. package/dist/oclif/commands/query.js +17 -3
  78. package/dist/oclif/commands/search.d.ts +20 -0
  79. package/dist/oclif/commands/search.js +186 -0
  80. package/dist/oclif/commands/status.js +4 -0
  81. package/dist/oclif/commands/swarm/curate.d.ts +13 -0
  82. package/dist/oclif/commands/swarm/curate.js +81 -0
  83. package/dist/oclif/commands/swarm/onboard.d.ts +6 -0
  84. package/dist/oclif/commands/swarm/onboard.js +233 -0
  85. package/dist/oclif/commands/swarm/query.d.ts +14 -0
  86. package/dist/oclif/commands/swarm/query.js +84 -0
  87. package/dist/oclif/commands/swarm/status.d.ts +41 -0
  88. package/dist/oclif/commands/swarm/status.js +278 -0
  89. package/dist/oclif/lib/daemon-client.js +0 -1
  90. package/dist/oclif/lib/search-format.d.ts +10 -0
  91. package/dist/oclif/lib/search-format.js +25 -0
  92. package/dist/oclif/lib/task-client.d.ts +6 -0
  93. package/dist/oclif/lib/task-client.js +10 -3
  94. package/dist/server/constants.d.ts +3 -2
  95. package/dist/server/constants.js +10 -7
  96. package/dist/server/core/domain/errors/task-error.d.ts +2 -2
  97. package/dist/server/core/domain/errors/task-error.js +5 -4
  98. package/dist/server/core/domain/source/source-schema.d.ts +6 -6
  99. package/dist/server/core/domain/transport/schemas.d.ts +14 -14
  100. package/dist/server/core/domain/transport/schemas.js +3 -3
  101. package/dist/server/core/interfaces/executor/i-search-executor.d.ts +34 -0
  102. package/dist/server/core/interfaces/executor/i-search-executor.js +1 -0
  103. package/dist/server/core/interfaces/executor/index.d.ts +1 -0
  104. package/dist/server/core/interfaces/executor/index.js +1 -0
  105. package/dist/server/infra/daemon/agent-process.js +20 -7
  106. package/dist/server/infra/executor/search-executor.d.ts +17 -0
  107. package/dist/server/infra/executor/search-executor.js +30 -0
  108. package/dist/server/infra/http/provider-model-fetchers.js +1 -0
  109. package/dist/server/infra/process/feature-handlers.js +13 -0
  110. package/dist/server/infra/project/project-registry.js +13 -1
  111. package/dist/server/infra/transport/handlers/locations-handler.d.ts +2 -0
  112. package/dist/server/infra/transport/handlers/locations-handler.js +16 -1
  113. package/dist/server/infra/transport/handlers/pull-handler.js +3 -3
  114. package/dist/server/infra/transport/handlers/push-handler.js +3 -3
  115. package/dist/server/infra/transport/handlers/status-handler.js +25 -18
  116. package/dist/server/infra/transport/handlers/vc-handler.d.ts +0 -4
  117. package/dist/server/infra/transport/handlers/vc-handler.js +5 -16
  118. package/dist/server/templates/skill/SKILL.md +188 -5
  119. package/dist/server/utils/gitignore.d.ts +1 -0
  120. package/dist/server/utils/gitignore.js +36 -4
  121. package/dist/shared/transport/search-content.d.ts +28 -0
  122. package/dist/shared/transport/search-content.js +38 -0
  123. package/dist/shared/transport/types/dto.d.ts +1 -1
  124. package/dist/tui/features/status/utils/format-status.js +5 -0
  125. package/dist/tui/utils/error-messages.js +2 -2
  126. package/oclif.manifest.json +581 -317
  127. 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[];