@andespindola/brainlink 0.1.0-beta.164 → 0.1.0-beta.166
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/README.md +14 -4
- package/dist/application/analyze-vault.js +1 -1
- package/dist/application/build-context.js +107 -14
- package/dist/application/server/routes.js +23 -3
- package/dist/cli/commands/read-commands.js +39 -3
- package/dist/cli/commands/write-commands.js +4 -2
- package/dist/infrastructure/config.js +22 -2
- package/dist/infrastructure/context-packs.js +122 -0
- package/dist/mcp/server.js +8 -3
- package/dist/mcp/tools.js +82 -7
- package/docs/AGENT_USAGE.md +8 -1
- package/docs/ARCHITECTURE.md +6 -0
- package/docs/QUICKSTART.md +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
|
|
|
71
71
|
- Full-text, semantic and hybrid retrieval on a local file index.
|
|
72
72
|
- Middle-out context assembly around the strongest chunk per document.
|
|
73
73
|
- In-process index and context caching with automatic invalidation on index updates.
|
|
74
|
+
- Optional CAG context packs at `.brainlink/context-packs/*.json`, derived from the current index signature and reusable across repeated context calls.
|
|
74
75
|
- HTTP graph server caches generated frontend assets and graph-layout JSON payloads by signature, and skips layout serialization when ETag returns `304`.
|
|
75
76
|
- Compressed-space prefiltering for `.blpk` packs before decryption and scan.
|
|
76
77
|
- Incremental indexing that reprocesses only changed markdown files and reuses existing chunks/embeddings for unchanged notes.
|
|
@@ -529,8 +530,9 @@ Available tools:
|
|
|
529
530
|
|
|
530
531
|
- `brainlink_bootstrap`: plug-and-play entrypoint that runs index + health checks and can return context in one call.
|
|
531
532
|
- `brainlink_policy`: read or update bootstrap/context-first policy, including presets (`preset: "fully-auto" | "strict"`).
|
|
532
|
-
- `brainlink_recommendations`: return an automatic action plan so agents can run Brainlink in the recommended order.
|
|
533
|
-
- `brainlink_context`: read indexed context for a task or question.
|
|
533
|
+
- `brainlink_recommendations`: return an automatic action plan so agents can run Brainlink in the recommended order, including RAG/CAG context strategy guidance.
|
|
534
|
+
- `brainlink_context`: read indexed context for a task or question; pass `strategy: "rag"` for fresh retrieval assembly, `strategy: "cag"` for persisted context packs or `strategy: "auto"` for CAG hits with RAG fallback.
|
|
535
|
+
- `brainlink_context_packs`: list or clear persisted CAG context packs.
|
|
534
536
|
- `brainlink_search`: search indexed notes.
|
|
535
537
|
- `brainlink_dedupe`: detect duplicate candidates using exact hash + semantic similarity scores.
|
|
536
538
|
- `brainlink_resolve_duplicate`: resolve duplicate pairs (`merge`, `link`, `ignore`) with connectivity-safe fallback edges.
|
|
@@ -555,6 +557,7 @@ By default, Brainlink enforces bootstrap and auto-runs it for read tools when se
|
|
|
555
557
|
If you disable `autoBootstrapOnRead` through `brainlink_policy`, read tools return a preflight instruction with suggested `brainlink_bootstrap` arguments.
|
|
556
558
|
`brainlink_bootstrap`, `brainlink_policy` and preflight responses include structured `nextActions` so MCP clients can continue automatically without custom parsing.
|
|
557
559
|
For one-call planning, use `brainlink_recommendations` to get the recommended tool sequence for the current vault/agent/query.
|
|
560
|
+
The MCP context tools are plug-and-play by default: omit `strategy` to use the configured default (`rag` unless changed), pass `strategy: "cag"` for repeated/stable task context, or pass `strategy: "auto"` so Brainlink chooses CAG on fresh pack hits and RAG otherwise. `brainlink_recommendations`, preflight responses and policy next actions include executable context arguments so clients can continue without custom parsing.
|
|
558
561
|
|
|
559
562
|
The same linking rule applies through MCP: `brainlink_context` is read-only, and real graph links require Markdown notes with explicit `## Context Links` sections. `brainlink_add_note` and `brainlink_add_file` reindex by default and include index + `writeConnectivity` metadata. Brainlink does not auto-link new notes to fallback hubs.
|
|
560
563
|
|
|
@@ -864,10 +867,15 @@ Context selection uses a middle-out strategy: it starts from the strongest chunk
|
|
|
864
867
|
blink context "question" --vault ./vault --limit 12 --tokens 2000
|
|
865
868
|
blink context "question" --vault ./vault --agent coding-agent --json
|
|
866
869
|
blink context "question" --vault ./vault --agent coding-agent --mode hybrid --json
|
|
870
|
+
blink context "question" --vault ./vault --agent coding-agent --strategy cag --json
|
|
871
|
+
blink context "question" --vault ./vault --agent coding-agent --strategy auto --json
|
|
872
|
+
blink context-packs --vault ./vault --json
|
|
873
|
+
blink context-packs --vault ./vault --stale --clear
|
|
867
874
|
```
|
|
868
875
|
|
|
869
876
|
Builds a compact context package for an agent.
|
|
870
877
|
Repeated calls with the same vault, agent, query, mode and token/limit settings are served from a short in-memory cache while the index is unchanged.
|
|
878
|
+
The default strategy is configured by `defaultContextStrategy` and starts as `rag`, which retrieves and assembles context from the current index. `--strategy cag` enables cache-augmented context generation by reading or refreshing a persisted context pack under `.brainlink/context-packs`; `--strategy auto` uses CAG when a fresh pack exists and RAG otherwise, refreshing a pack for future calls. Context responses include `cache`, `metrics`, `requestedStrategy` and `recommendedStrategy` metadata. Packs are derived artifacts and become stale when the index or volatile memory signature changes.
|
|
871
879
|
|
|
872
880
|
### `links`
|
|
873
881
|
|
|
@@ -1010,6 +1018,7 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
|
|
|
1010
1018
|
"autoCanonicalContextLinks": true,
|
|
1011
1019
|
"defaultSearchLimit": 10,
|
|
1012
1020
|
"defaultContextTokens": 2000,
|
|
1021
|
+
"defaultContextStrategy": "rag",
|
|
1013
1022
|
"embeddingProvider": "local",
|
|
1014
1023
|
"defaultSearchMode": "hybrid",
|
|
1015
1024
|
"chunkSize": 1200,
|
|
@@ -1024,7 +1033,8 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
|
|
|
1024
1033
|
"coding-agent": {
|
|
1025
1034
|
"defaultSearchMode": "semantic",
|
|
1026
1035
|
"defaultSearchLimit": 8,
|
|
1027
|
-
"defaultContextTokens": 2400
|
|
1036
|
+
"defaultContextTokens": 2400,
|
|
1037
|
+
"defaultContextStrategy": "auto"
|
|
1028
1038
|
},
|
|
1029
1039
|
"*": {
|
|
1030
1040
|
"defaultSearchMode": "hybrid"
|
|
@@ -1034,7 +1044,7 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
|
|
|
1034
1044
|
```
|
|
1035
1045
|
|
|
1036
1046
|
`defaultAgent` is optional. When set, CLI and MCP calls that omit `--agent`/`agent` use this value automatically. If not set, behavior remains as before.
|
|
1037
|
-
`agentProfiles` is optional. When present, CLI and MCP resolve `mode`, `limit` and `
|
|
1047
|
+
`agentProfiles` is optional. When present, CLI and MCP resolve `mode`, `limit`, `tokens` and context `strategy` per agent automatically, then fallback to global defaults.
|
|
1038
1048
|
|
|
1039
1049
|
`autoIndexOnWrite` is optional and defaults to `true`. Set it to `false` to defer indexing after writes.
|
|
1040
1050
|
`autoCanonicalContextLinks` is optional and defaults to `true`. When enabled, `blink add`, `brainlink_add_note` and `brainlink_add_file` add a canonical `## Context Links` entry to the inferred context hub, creating that hub when needed.
|
|
@@ -50,7 +50,7 @@ export const getExtendedStats = async (vaultPath, agentId) => {
|
|
|
50
50
|
await searchKnowledge(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), agentId, 'hybrid');
|
|
51
51
|
const searchLatency = performance.now() - searchStart;
|
|
52
52
|
const contextStart = performance.now();
|
|
53
|
-
await buildContextPackage(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), defaults.defaultContextTokens, agentId, 'hybrid');
|
|
53
|
+
await buildContextPackage(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), defaults.defaultContextTokens, agentId, 'hybrid', undefined, defaults.defaultContextCacheTtlMs);
|
|
54
54
|
const contextLatency = performance.now() - contextStart;
|
|
55
55
|
return {
|
|
56
56
|
stats,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
2
3
|
import { formatContextPackage, selectContextSections } from '../domain/context.js';
|
|
4
|
+
import { readContextPack, writeContextPack } from '../infrastructure/context-packs.js';
|
|
3
5
|
import { indexStoragePath } from '../infrastructure/file-index.js';
|
|
4
6
|
import { searchVolatileMemory, volatileMemoryStoragePath } from '../infrastructure/volatile-memory.js';
|
|
5
7
|
import { searchKnowledge } from './search-knowledge.js';
|
|
6
|
-
const contextCacheTtlMs = 45_000;
|
|
7
8
|
const contextCacheMaxEntries = 200;
|
|
8
9
|
const contextCache = new Map();
|
|
9
10
|
const readFileSignature = async (path) => {
|
|
@@ -15,16 +16,21 @@ const readFileSignature = async (path) => {
|
|
|
15
16
|
return '0:0';
|
|
16
17
|
}
|
|
17
18
|
};
|
|
18
|
-
const readContextDataSignature = async (vaultPath) => `${await readFileSignature(indexStoragePath(vaultPath))}|${await readFileSignature(volatileMemoryStoragePath(vaultPath))}`;
|
|
19
|
-
const toCacheKey = (vaultPath, query, limit, maxTokens, agentId, mode) => JSON.stringify({
|
|
19
|
+
export const readContextDataSignature = async (vaultPath) => `${await readFileSignature(indexStoragePath(vaultPath))}|${await readFileSignature(volatileMemoryStoragePath(vaultPath))}`;
|
|
20
|
+
const toCacheKey = (vaultPath, query, limit, maxTokens, agentId, mode, strategy) => JSON.stringify({
|
|
20
21
|
vaultPath,
|
|
21
22
|
query: query.trim().toLowerCase(),
|
|
22
23
|
limit,
|
|
23
24
|
maxTokens,
|
|
24
25
|
agentId: agentId?.trim().toLowerCase() ?? '*',
|
|
25
|
-
mode: mode ?? 'default'
|
|
26
|
+
mode: mode ?? 'default',
|
|
27
|
+
strategy
|
|
26
28
|
});
|
|
27
|
-
const
|
|
29
|
+
const withCacheMetadata = (context, cache) => ({
|
|
30
|
+
...context,
|
|
31
|
+
cache
|
|
32
|
+
});
|
|
33
|
+
const contextCacheGet = (key, dataSignature, contextCacheTtlMs) => {
|
|
28
34
|
const entry = contextCache.get(key);
|
|
29
35
|
if (!entry) {
|
|
30
36
|
return undefined;
|
|
@@ -34,7 +40,11 @@ const contextCacheGet = (key, dataSignature) => {
|
|
|
34
40
|
contextCache.delete(key);
|
|
35
41
|
return undefined;
|
|
36
42
|
}
|
|
37
|
-
return entry.context
|
|
43
|
+
return withCacheMetadata(entry.context, {
|
|
44
|
+
storage: 'memory',
|
|
45
|
+
status: 'hit',
|
|
46
|
+
dataSignature
|
|
47
|
+
});
|
|
38
48
|
};
|
|
39
49
|
const contextCacheSet = (entry) => {
|
|
40
50
|
contextCache.set(entry.key, entry);
|
|
@@ -45,32 +55,115 @@ const contextCacheSet = (entry) => {
|
|
|
45
55
|
const keys = Array.from(contextCache.keys()).slice(0, overflow);
|
|
46
56
|
keys.forEach((key) => contextCache.delete(key));
|
|
47
57
|
};
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
const elapsedMs = (start) => Math.round((performance.now() - start) * 1000) / 1000;
|
|
59
|
+
const estimateSectionTokens = (context) => Math.ceil(context.content.length / 4);
|
|
60
|
+
const emptyMetrics = (context, totalMs, overrides = {}) => ({
|
|
61
|
+
totalMs,
|
|
62
|
+
packReadMs: 0,
|
|
63
|
+
searchMs: 0,
|
|
64
|
+
selectionMs: 0,
|
|
65
|
+
volatileMs: 0,
|
|
66
|
+
packWriteMs: 0,
|
|
67
|
+
sectionCount: context.sections.length,
|
|
68
|
+
volatileSectionCount: context.volatileSections?.length ?? 0,
|
|
69
|
+
estimatedTokens: estimateSectionTokens(context),
|
|
70
|
+
...overrides
|
|
71
|
+
});
|
|
72
|
+
export const buildContextPackage = async (vaultPath, query, limit, maxTokens, agentId, mode, strategy = 'rag', contextCacheTtlMs = 120_000) => {
|
|
73
|
+
const totalStart = performance.now();
|
|
74
|
+
const cacheKey = toCacheKey(vaultPath, query, limit, maxTokens, agentId, mode, strategy);
|
|
50
75
|
const dataSignature = await readContextDataSignature(vaultPath);
|
|
51
|
-
const cached = contextCacheGet(cacheKey, dataSignature);
|
|
76
|
+
const cached = contextCacheGet(cacheKey, dataSignature, contextCacheTtlMs);
|
|
52
77
|
if (cached) {
|
|
53
78
|
return cached;
|
|
54
79
|
}
|
|
80
|
+
const shouldUseContextPack = strategy === 'cag' || strategy === 'auto';
|
|
81
|
+
let packReadMs = 0;
|
|
82
|
+
if (shouldUseContextPack) {
|
|
83
|
+
const packReadStart = performance.now();
|
|
84
|
+
const pack = await readContextPack(vaultPath, { query, limit, maxTokens, agentId, mode }, dataSignature);
|
|
85
|
+
packReadMs = elapsedMs(packReadStart);
|
|
86
|
+
if (pack.status === 'hit') {
|
|
87
|
+
const contextFromPack = {
|
|
88
|
+
...pack.context,
|
|
89
|
+
strategy: 'cag',
|
|
90
|
+
requestedStrategy: strategy,
|
|
91
|
+
recommendedStrategy: {
|
|
92
|
+
strategy: 'cag',
|
|
93
|
+
reason: strategy === 'auto'
|
|
94
|
+
? 'A fresh context pack exists, so auto selected CAG.'
|
|
95
|
+
: 'The requested CAG context pack is fresh.'
|
|
96
|
+
},
|
|
97
|
+
metrics: emptyMetrics(pack.context, elapsedMs(totalStart), { packReadMs })
|
|
98
|
+
};
|
|
99
|
+
contextCacheSet({
|
|
100
|
+
key: cacheKey,
|
|
101
|
+
createdAt: Date.now(),
|
|
102
|
+
dataSignature,
|
|
103
|
+
strategy,
|
|
104
|
+
context: contextFromPack
|
|
105
|
+
});
|
|
106
|
+
return contextFromPack;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const searchStart = performance.now();
|
|
55
110
|
const results = await searchKnowledge(vaultPath, query, limit, agentId, mode);
|
|
111
|
+
const searchMs = elapsedMs(searchStart);
|
|
112
|
+
const selectionStart = performance.now();
|
|
56
113
|
const durableSections = selectContextSections(results, maxTokens);
|
|
114
|
+
const selectionMs = elapsedMs(selectionStart);
|
|
115
|
+
const volatileStart = performance.now();
|
|
57
116
|
const volatileSections = await searchVolatileMemory(vaultPath, query, Math.min(3, limit), agentId, mode ?? 'hybrid');
|
|
117
|
+
const volatileMs = elapsedMs(volatileStart);
|
|
58
118
|
const sections = [...volatileSections, ...durableSections];
|
|
119
|
+
const effectiveStrategy = strategy === 'cag' ? 'cag' : 'rag';
|
|
59
120
|
const context = {
|
|
60
121
|
query,
|
|
61
122
|
sections,
|
|
62
123
|
content: formatContextPackage(query, sections),
|
|
63
|
-
...(volatileSections.length > 0 ? { volatileSections } : {})
|
|
124
|
+
...(volatileSections.length > 0 ? { volatileSections } : {}),
|
|
125
|
+
strategy: effectiveStrategy,
|
|
126
|
+
requestedStrategy: strategy,
|
|
127
|
+
recommendedStrategy: {
|
|
128
|
+
strategy: effectiveStrategy,
|
|
129
|
+
reason: strategy === 'auto'
|
|
130
|
+
? 'No fresh context pack was available, so auto selected fresh RAG assembly and refreshed a pack for future reuse.'
|
|
131
|
+
: effectiveStrategy === 'cag'
|
|
132
|
+
? 'CAG was requested; Brainlink refreshed the context pack from current retrieval results.'
|
|
133
|
+
: 'RAG was requested; Brainlink assembled context directly from current retrieval results.'
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const packWriteStart = performance.now();
|
|
137
|
+
const packPath = shouldUseContextPack
|
|
138
|
+
? await writeContextPack(vaultPath, { query, limit, maxTokens, agentId, mode }, dataSignature, context)
|
|
139
|
+
: undefined;
|
|
140
|
+
const packWriteMs = packPath ? elapsedMs(packWriteStart) : 0;
|
|
141
|
+
const contextWithMetadata = withCacheMetadata(context, {
|
|
142
|
+
storage: shouldUseContextPack ? 'context-pack' : 'memory',
|
|
143
|
+
status: shouldUseContextPack ? 'refresh' : 'miss',
|
|
144
|
+
dataSignature,
|
|
145
|
+
...(packPath ? { path: packPath } : {})
|
|
146
|
+
});
|
|
147
|
+
const contextWithMetrics = {
|
|
148
|
+
...contextWithMetadata,
|
|
149
|
+
metrics: emptyMetrics(contextWithMetadata, elapsedMs(totalStart), {
|
|
150
|
+
packReadMs,
|
|
151
|
+
searchMs,
|
|
152
|
+
selectionMs,
|
|
153
|
+
volatileMs,
|
|
154
|
+
packWriteMs
|
|
155
|
+
})
|
|
64
156
|
};
|
|
65
157
|
contextCacheSet({
|
|
66
158
|
key: cacheKey,
|
|
67
159
|
createdAt: Date.now(),
|
|
68
160
|
dataSignature,
|
|
69
|
-
|
|
161
|
+
strategy,
|
|
162
|
+
context: contextWithMetrics
|
|
70
163
|
});
|
|
71
|
-
return
|
|
164
|
+
return contextWithMetrics;
|
|
72
165
|
};
|
|
73
|
-
export const buildContext = async (vaultPath, query, limit, maxTokens, agentId, mode) => {
|
|
74
|
-
const contextPackage = await buildContextPackage(vaultPath, query, limit, maxTokens, agentId, mode);
|
|
166
|
+
export const buildContext = async (vaultPath, query, limit, maxTokens, agentId, mode, strategy, contextCacheTtlMs = 120_000) => {
|
|
167
|
+
const contextPackage = await buildContextPackage(vaultPath, query, limit, maxTokens, agentId, mode, strategy, contextCacheTtlMs);
|
|
75
168
|
return contextPackage.content;
|
|
76
169
|
};
|
|
@@ -11,7 +11,7 @@ import { listAgents } from '../list-agents.js';
|
|
|
11
11
|
import { listBacklinks, listLinks } from '../list-links.js';
|
|
12
12
|
import { searchGraphNodeIds } from '../search-graph-node-ids.js';
|
|
13
13
|
import { searchKnowledge } from '../search-knowledge.js';
|
|
14
|
-
import { loadBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
14
|
+
import { loadBrainlinkConfig, resolveAgentRuntimeDefaults, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
15
15
|
import { createClientCss } from '../frontend/client-css.js';
|
|
16
16
|
import { createClientHtml } from '../frontend/client-html.js';
|
|
17
17
|
import { createClientJs } from '../frontend/client-js.js';
|
|
@@ -20,12 +20,27 @@ import { createClientRenderWorkerJs } from '../frontend/client-render-worker-js.
|
|
|
20
20
|
import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
|
|
21
21
|
const readSearchMode = async (url) => {
|
|
22
22
|
const config = await loadBrainlinkConfig();
|
|
23
|
-
|
|
23
|
+
const defaults = resolveAgentRuntimeDefaults(config, readAgentQuery(url));
|
|
24
|
+
return sanitizeSearchMode(url.searchParams.get('mode'), defaults.defaultSearchMode);
|
|
25
|
+
};
|
|
26
|
+
const readContextStrategy = async (url) => {
|
|
27
|
+
const config = await loadBrainlinkConfig();
|
|
28
|
+
const defaults = resolveAgentRuntimeDefaults(config, readAgentQuery(url));
|
|
29
|
+
return sanitizeContextStrategy(url.searchParams.get('strategy'), defaults.defaultContextStrategy);
|
|
30
|
+
};
|
|
31
|
+
const readContextCacheTtlMs = async (url) => {
|
|
32
|
+
const config = await loadBrainlinkConfig();
|
|
33
|
+
const defaults = resolveAgentRuntimeDefaults(config, readAgentQuery(url));
|
|
34
|
+
return defaults.defaultContextCacheTtlMs;
|
|
24
35
|
};
|
|
25
36
|
const hasInvalidSearchMode = (url) => {
|
|
26
37
|
const mode = url.searchParams.get('mode');
|
|
27
38
|
return mode !== null && !['fts', 'semantic', 'hybrid'].includes(mode);
|
|
28
39
|
};
|
|
40
|
+
const hasInvalidContextStrategy = (url) => {
|
|
41
|
+
const strategy = url.searchParams.get('strategy');
|
|
42
|
+
return strategy !== null && !['rag', 'cag', 'auto'].includes(strategy);
|
|
43
|
+
};
|
|
29
44
|
const createResponse = (body, statusCode = 200, contentType = 'text/plain; charset=utf-8') => ({
|
|
30
45
|
body,
|
|
31
46
|
statusCode,
|
|
@@ -370,10 +385,15 @@ export const route = async (request, url, vaultPath) => {
|
|
|
370
385
|
const limit = parsePositiveInteger(url.searchParams.get('limit'), 12);
|
|
371
386
|
const tokens = parsePositiveInteger(url.searchParams.get('tokens'), 2000);
|
|
372
387
|
const mode = await readSearchMode(url);
|
|
388
|
+
const strategy = await readContextStrategy(url);
|
|
389
|
+
const contextCacheTtlMs = await readContextCacheTtlMs(url);
|
|
373
390
|
if (hasInvalidSearchMode(url)) {
|
|
374
391
|
return createResponse(createJsonResponse({ error: 'Invalid mode. Use fts, semantic or hybrid.' }), 400, contentTypes['.json']);
|
|
375
392
|
}
|
|
376
|
-
|
|
393
|
+
if (hasInvalidContextStrategy(url)) {
|
|
394
|
+
return createResponse(createJsonResponse({ error: 'Invalid strategy. Use rag, cag or auto.' }), 400, contentTypes['.json']);
|
|
395
|
+
}
|
|
396
|
+
return createResponse(createJsonResponse(await buildContextPackage(vaultPath, query, limit, tokens, readAgentQuery(url), mode, strategy, contextCacheTtlMs)), 200, contentTypes['.json']);
|
|
377
397
|
}
|
|
378
398
|
if (isReadMethod(request) && url.pathname === '/api/links') {
|
|
379
399
|
return createResponse(createJsonResponse({ links: await listLinks(vaultPath, readAgentQuery(url)) }), 200, contentTypes['.json']);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { getBrokenLinksReport, getExtendedStats, getOrphansReport, getStats, validateVault } from '../../application/analyze-vault.js';
|
|
2
|
-
import { buildContextPackage } from '../../application/build-context.js';
|
|
2
|
+
import { buildContextPackage, readContextDataSignature } from '../../application/build-context.js';
|
|
3
3
|
import { getGraph } from '../../application/get-graph.js';
|
|
4
4
|
import { listAgents } from '../../application/list-agents.js';
|
|
5
5
|
import { listBacklinks, listLinks } from '../../application/list-links.js';
|
|
6
6
|
import { searchKnowledge } from '../../application/search-knowledge.js';
|
|
7
|
-
import { sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
7
|
+
import { sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
8
|
+
import { clearContextPacks, listContextPacks } from '../../infrastructure/context-packs.js';
|
|
8
9
|
import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
|
|
9
10
|
export const registerReadCommands = (program) => {
|
|
10
11
|
program
|
|
@@ -61,14 +62,49 @@ export const registerReadCommands = (program) => {
|
|
|
61
62
|
.option('-l, --limit <limit>', 'maximum search results before context selection')
|
|
62
63
|
.option('-t, --tokens <tokens>', 'maximum estimated context tokens')
|
|
63
64
|
.option('-m, --mode <mode>', 'search mode: fts, semantic or hybrid')
|
|
65
|
+
.option('--strategy <strategy>', 'context strategy: rag, cag or auto')
|
|
64
66
|
.option('--json', 'print machine-readable JSON')
|
|
65
67
|
.description('build a compact context package for an agent')
|
|
66
68
|
.action(async (query, options) => {
|
|
67
69
|
const resolved = await resolveOptions(options);
|
|
68
70
|
const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
|
|
69
|
-
const
|
|
71
|
+
const strategy = sanitizeContextStrategy(options.strategy, resolved.defaults.defaultContextStrategy);
|
|
72
|
+
const contextPackage = await buildContextPackage(resolved.vault, query, parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit), parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens), resolved.agent, mode, strategy, resolved.defaults.defaultContextCacheTtlMs);
|
|
70
73
|
print(options.json, contextPackage, () => contextPackage.content);
|
|
71
74
|
});
|
|
75
|
+
program
|
|
76
|
+
.command('context-packs')
|
|
77
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
78
|
+
.option('-a, --agent <agent>', 'accepted for consistency; context packs are already keyed by agent')
|
|
79
|
+
.option('--stale', 'operate only on packs stale for the current index and volatile-memory signature')
|
|
80
|
+
.option('--clear', 'remove context packs instead of listing them')
|
|
81
|
+
.option('--json', 'print machine-readable JSON')
|
|
82
|
+
.description('list or clear persisted CAG context packs')
|
|
83
|
+
.action(async (options) => {
|
|
84
|
+
const resolved = await resolveOptions(options);
|
|
85
|
+
const dataSignature = await readContextDataSignature(resolved.vault);
|
|
86
|
+
if (options.clear) {
|
|
87
|
+
const result = await clearContextPacks(resolved.vault, {
|
|
88
|
+
staleOnly: options.stale === true,
|
|
89
|
+
dataSignature
|
|
90
|
+
});
|
|
91
|
+
print(options.json, { vault: resolved.vault, dataSignature, ...result }, () => [
|
|
92
|
+
`Removed context packs: ${result.removed.length}`,
|
|
93
|
+
`Kept context packs: ${result.kept.length}`
|
|
94
|
+
].join('\n'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const packs = await listContextPacks(resolved.vault, dataSignature);
|
|
98
|
+
const visiblePacks = options.stale ? packs.filter((pack) => pack.stale) : packs;
|
|
99
|
+
print(options.json, { vault: resolved.vault, dataSignature, packs: visiblePacks }, () => visiblePacks.length === 0
|
|
100
|
+
? 'No context packs found.'
|
|
101
|
+
: visiblePacks
|
|
102
|
+
.map((pack) => [
|
|
103
|
+
`${pack.filename} ${pack.stale ? 'stale' : 'fresh'} ${pack.sizeBytes} bytes`,
|
|
104
|
+
pack.key ? `query="${pack.key.query}" agent=${pack.key.agentId ?? '*'} mode=${pack.key.mode ?? 'default'} limit=${pack.key.limit} tokens=${pack.key.maxTokens}` : 'unreadable pack'
|
|
105
|
+
].join('\n'))
|
|
106
|
+
.join('\n\n'));
|
|
107
|
+
});
|
|
72
108
|
program
|
|
73
109
|
.command('graph')
|
|
74
110
|
.option('-v, --vault <vault>', 'vault directory')
|
|
@@ -15,7 +15,7 @@ import { createOfflinePackBackup } from '../../application/offline-pack-backup.j
|
|
|
15
15
|
import { startServer } from '../../application/start-server.js';
|
|
16
16
|
import { startVaultWatcher } from '../../application/watch-vault.js';
|
|
17
17
|
import { doctorVault, getStats, validateVault } from '../../application/analyze-vault.js';
|
|
18
|
-
import { defaultBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
18
|
+
import { defaultBrainlinkConfig, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
19
19
|
import { loadBrainlinkConfig } from '../../infrastructure/config.js';
|
|
20
20
|
import { assertVaultAllowed, ensureVault, isBucketVaultPath } from '../../infrastructure/file-system-vault.js';
|
|
21
21
|
import { getBootstrapPolicy, getBootstrapSessionStatus, touchBootstrapSession } from '../../infrastructure/session-state.js';
|
|
@@ -1111,6 +1111,7 @@ export const registerWriteCommands = (program) => {
|
|
|
1111
1111
|
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
1112
1112
|
.option('--query <query>', 'optional task query to return immediate grounded context')
|
|
1113
1113
|
.option('--mode <mode>', 'search mode for context (fts|semantic|hybrid)')
|
|
1114
|
+
.option('--strategy <strategy>', 'context strategy for context (rag|cag|auto)')
|
|
1114
1115
|
.option('--limit <limit>', 'maximum context sections')
|
|
1115
1116
|
.option('--tokens <tokens>', 'maximum context token budget')
|
|
1116
1117
|
.option('--no-install-agent', 'skip agent MCP/plugin installation and upgrade automation')
|
|
@@ -1125,6 +1126,7 @@ export const registerWriteCommands = (program) => {
|
|
|
1125
1126
|
const limit = parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit);
|
|
1126
1127
|
const tokens = parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens);
|
|
1127
1128
|
const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
|
|
1129
|
+
const strategy = sanitizeContextStrategy(options.strategy, resolved.defaults.defaultContextStrategy);
|
|
1128
1130
|
const index = await indexVault(resolved.vault);
|
|
1129
1131
|
const stats = await getStats(resolved.vault, resolved.agent);
|
|
1130
1132
|
const validation = await validateVault(resolved.vault, resolved.agent);
|
|
@@ -1133,7 +1135,7 @@ export const registerWriteCommands = (program) => {
|
|
|
1133
1135
|
const policy = await getBootstrapPolicy();
|
|
1134
1136
|
const bootstrapStatus = await getBootstrapSessionStatus(resolved.vault, resolved.agent);
|
|
1135
1137
|
const context = options.query
|
|
1136
|
-
? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode)
|
|
1138
|
+
? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode, strategy, resolved.defaults.defaultContextCacheTtlMs)
|
|
1137
1139
|
: null;
|
|
1138
1140
|
const agentIntegration = options.installAgent === false
|
|
1139
1141
|
? null
|
|
@@ -13,6 +13,8 @@ export const defaultBrainlinkConfig = {
|
|
|
13
13
|
autoCanonicalContextLinks: true,
|
|
14
14
|
defaultSearchLimit: 10,
|
|
15
15
|
defaultContextTokens: 2000,
|
|
16
|
+
defaultContextStrategy: 'rag',
|
|
17
|
+
defaultContextCacheTtlMs: 120_000,
|
|
16
18
|
embeddingProvider: 'local',
|
|
17
19
|
defaultSearchMode: 'hybrid',
|
|
18
20
|
chunkSize: 1200,
|
|
@@ -41,8 +43,10 @@ const safeCwd = () => {
|
|
|
41
43
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
42
44
|
const embeddingProviders = new Set(['none', 'local']);
|
|
43
45
|
const searchModes = new Set(['fts', 'semantic', 'hybrid']);
|
|
46
|
+
const contextStrategies = new Set(['rag', 'cag', 'auto']);
|
|
44
47
|
const sanitizeEmbeddingProvider = (value) => typeof value === 'string' && embeddingProviders.has(value) ? value : defaultBrainlinkConfig.embeddingProvider;
|
|
45
48
|
export const sanitizeSearchMode = (value, fallback = defaultBrainlinkConfig.defaultSearchMode) => typeof value === 'string' && searchModes.has(value) ? value : fallback;
|
|
49
|
+
export const sanitizeContextStrategy = (value, fallback = 'rag') => typeof value === 'string' && contextStrategies.has(value) ? value : fallback;
|
|
46
50
|
const sanitizeAllowedVaults = (value) => Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.trim().length > 0) : [];
|
|
47
51
|
const sanitizePositiveNumber = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
48
52
|
const sanitizeIntegerInRange = (value, fallback, minimum, maximum) => {
|
|
@@ -84,10 +88,20 @@ const sanitizeAgentProfile = (value) => {
|
|
|
84
88
|
const defaultSearchMode = typeof value.defaultSearchMode === 'string' && searchModes.has(value.defaultSearchMode)
|
|
85
89
|
? value.defaultSearchMode
|
|
86
90
|
: undefined;
|
|
91
|
+
const defaultContextStrategy = typeof value.defaultContextStrategy === 'string' && contextStrategies.has(value.defaultContextStrategy)
|
|
92
|
+
? value.defaultContextStrategy
|
|
93
|
+
: undefined;
|
|
94
|
+
const defaultContextCacheTtlMs = typeof value.defaultContextCacheTtlMs === 'number' &&
|
|
95
|
+
Number.isFinite(value.defaultContextCacheTtlMs) &&
|
|
96
|
+
value.defaultContextCacheTtlMs > 0
|
|
97
|
+
? Math.floor(value.defaultContextCacheTtlMs)
|
|
98
|
+
: undefined;
|
|
87
99
|
const profile = {
|
|
88
100
|
...(defaultSearchLimit ? { defaultSearchLimit } : {}),
|
|
89
101
|
...(defaultContextTokens ? { defaultContextTokens } : {}),
|
|
90
|
-
...(defaultSearchMode ? { defaultSearchMode } : {})
|
|
102
|
+
...(defaultSearchMode ? { defaultSearchMode } : {}),
|
|
103
|
+
...(defaultContextStrategy ? { defaultContextStrategy } : {}),
|
|
104
|
+
...(defaultContextCacheTtlMs ? { defaultContextCacheTtlMs } : {})
|
|
91
105
|
};
|
|
92
106
|
return Object.keys(profile).length > 0 ? profile : null;
|
|
93
107
|
};
|
|
@@ -169,6 +183,10 @@ const sanitizeConfig = (value) => ({
|
|
|
169
183
|
defaultContextTokens: typeof value.defaultContextTokens === 'number' && value.defaultContextTokens > 0
|
|
170
184
|
? value.defaultContextTokens
|
|
171
185
|
: defaultBrainlinkConfig.defaultContextTokens,
|
|
186
|
+
defaultContextStrategy: sanitizeContextStrategy(value.defaultContextStrategy, defaultBrainlinkConfig.defaultContextStrategy),
|
|
187
|
+
defaultContextCacheTtlMs: typeof value.defaultContextCacheTtlMs === 'number' && Number.isFinite(value.defaultContextCacheTtlMs) && value.defaultContextCacheTtlMs > 0
|
|
188
|
+
? Math.floor(value.defaultContextCacheTtlMs)
|
|
189
|
+
: defaultBrainlinkConfig.defaultContextCacheTtlMs,
|
|
172
190
|
allowedVaults: [...sanitizeAllowedVaults(value.allowedVaults), ...readAllowedVaultsFromEnv()],
|
|
173
191
|
chunkSize: typeof value.chunkSize === 'number' && value.chunkSize > 0 ? value.chunkSize : defaultBrainlinkConfig.chunkSize,
|
|
174
192
|
searchPack: sanitizeSearchPackConfig(value.searchPack),
|
|
@@ -182,7 +200,9 @@ export const resolveAgentRuntimeDefaults = (config, agent) => {
|
|
|
182
200
|
return {
|
|
183
201
|
defaultSearchLimit: profile?.defaultSearchLimit ?? config.defaultSearchLimit,
|
|
184
202
|
defaultContextTokens: profile?.defaultContextTokens ?? config.defaultContextTokens,
|
|
185
|
-
defaultSearchMode: profile?.defaultSearchMode ?? config.defaultSearchMode
|
|
203
|
+
defaultSearchMode: profile?.defaultSearchMode ?? config.defaultSearchMode,
|
|
204
|
+
defaultContextStrategy: profile?.defaultContextStrategy ?? config.defaultContextStrategy,
|
|
205
|
+
defaultContextCacheTtlMs: profile?.defaultContextCacheTtlMs ?? config.defaultContextCacheTtlMs
|
|
186
206
|
};
|
|
187
207
|
};
|
|
188
208
|
const mergeConfigLayers = (layers) => layers.reduce((state, config) => ({
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
const normalizePackKey = (key) => ({
|
|
5
|
+
query: key.query.trim().toLowerCase(),
|
|
6
|
+
limit: key.limit,
|
|
7
|
+
maxTokens: key.maxTokens,
|
|
8
|
+
agentId: key.agentId?.trim().toLowerCase() || undefined,
|
|
9
|
+
mode: key.mode
|
|
10
|
+
});
|
|
11
|
+
export const contextPacksDirectory = (vaultPath) => join(vaultPath, '.brainlink', 'context-packs');
|
|
12
|
+
export const contextPackPath = (vaultPath, key) => {
|
|
13
|
+
const digest = createHash('sha256').update(JSON.stringify(normalizePackKey(key))).digest('hex');
|
|
14
|
+
return join(contextPacksDirectory(vaultPath), `${digest}.json`);
|
|
15
|
+
};
|
|
16
|
+
const isStoredContextPack = (value) => {
|
|
17
|
+
if (!value || typeof value !== 'object') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const candidate = value;
|
|
21
|
+
return candidate.version === 1 && typeof candidate.dataSignature === 'string' && Boolean(candidate.context);
|
|
22
|
+
};
|
|
23
|
+
const readStoredContextPack = async (path) => {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
26
|
+
return isStoredContextPack(parsed) ? parsed : null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const toContextPackSummary = async (path, dataSignature) => {
|
|
33
|
+
const [info, stored] = await Promise.all([
|
|
34
|
+
stat(path),
|
|
35
|
+
readStoredContextPack(path)
|
|
36
|
+
]);
|
|
37
|
+
const stale = !stored || (typeof dataSignature === 'string' && stored.dataSignature !== dataSignature);
|
|
38
|
+
return {
|
|
39
|
+
path,
|
|
40
|
+
filename: basename(path),
|
|
41
|
+
createdAt: stored?.createdAt ?? null,
|
|
42
|
+
dataSignature: stored?.dataSignature ?? null,
|
|
43
|
+
key: stored?.key ?? null,
|
|
44
|
+
sizeBytes: info.size,
|
|
45
|
+
stale
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
export const listContextPacks = async (vaultPath, dataSignature) => {
|
|
49
|
+
const directory = contextPacksDirectory(vaultPath);
|
|
50
|
+
try {
|
|
51
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
52
|
+
const paths = entries
|
|
53
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
54
|
+
.map((entry) => join(directory, entry.name));
|
|
55
|
+
const summaries = await Promise.all(paths.map((path) => toContextPackSummary(path, dataSignature)));
|
|
56
|
+
return summaries.sort((left, right) => (right.createdAt ?? '').localeCompare(left.createdAt ?? '') || left.filename.localeCompare(right.filename));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
export const clearContextPacks = async (vaultPath, options = {}) => {
|
|
66
|
+
const packs = await listContextPacks(vaultPath, options.dataSignature);
|
|
67
|
+
const removed = options.staleOnly ? packs.filter((pack) => pack.stale) : packs;
|
|
68
|
+
const kept = options.staleOnly ? packs.filter((pack) => !pack.stale) : [];
|
|
69
|
+
await Promise.all(removed.map((pack) => rm(pack.path, { force: true })));
|
|
70
|
+
return { removed, kept };
|
|
71
|
+
};
|
|
72
|
+
export const readContextPack = async (vaultPath, key, dataSignature) => {
|
|
73
|
+
const path = contextPackPath(vaultPath, key);
|
|
74
|
+
try {
|
|
75
|
+
const parsed = await readStoredContextPack(path);
|
|
76
|
+
if (!parsed) {
|
|
77
|
+
return { status: 'stale', path };
|
|
78
|
+
}
|
|
79
|
+
if (parsed.dataSignature !== dataSignature) {
|
|
80
|
+
return { status: 'stale', path };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
status: 'hit',
|
|
84
|
+
path,
|
|
85
|
+
context: {
|
|
86
|
+
...parsed.context,
|
|
87
|
+
strategy: 'cag',
|
|
88
|
+
cache: {
|
|
89
|
+
storage: 'context-pack',
|
|
90
|
+
status: 'hit',
|
|
91
|
+
dataSignature,
|
|
92
|
+
path
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { status: 'miss', path };
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
export const writeContextPack = async (vaultPath, key, dataSignature, context) => {
|
|
102
|
+
const path = contextPackPath(vaultPath, key);
|
|
103
|
+
const stored = {
|
|
104
|
+
version: 1,
|
|
105
|
+
createdAt: new Date().toISOString(),
|
|
106
|
+
dataSignature,
|
|
107
|
+
key: normalizePackKey(key),
|
|
108
|
+
context: {
|
|
109
|
+
...context,
|
|
110
|
+
strategy: 'cag',
|
|
111
|
+
cache: {
|
|
112
|
+
storage: 'context-pack',
|
|
113
|
+
status: 'refresh',
|
|
114
|
+
dataSignature,
|
|
115
|
+
path
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
await mkdir(contextPacksDirectory(vaultPath), { recursive: true });
|
|
120
|
+
await writeFile(path, `${JSON.stringify(stored, null, 2)}\n`, 'utf8');
|
|
121
|
+
return path;
|
|
122
|
+
};
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools.js';
|
|
2
|
+
import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools.js';
|
|
3
3
|
import { getRuntimeVersion } from './runtime.js';
|
|
4
4
|
export const createBrainlinkMcpServer = () => {
|
|
5
5
|
const server = new McpServer({
|
|
@@ -25,14 +25,19 @@ export const createBrainlinkMcpServer = () => {
|
|
|
25
25
|
}, versionTool);
|
|
26
26
|
server.registerTool('brainlink_recommendations', {
|
|
27
27
|
title: 'Brainlink Recommended MCP Workflow',
|
|
28
|
-
description: 'Return a plug-and-play action plan for this vault/agent, including policy, bootstrap, context retrieval and durable write guidance.',
|
|
28
|
+
description: 'Return a plug-and-play action plan for this vault/agent, including policy, bootstrap, RAG/CAG context strategy, retrieval and durable write guidance.',
|
|
29
29
|
inputSchema: recommendationsInputSchema
|
|
30
30
|
}, recommendationsTool);
|
|
31
31
|
server.registerTool('brainlink_context', {
|
|
32
32
|
title: 'Build Brainlink Context',
|
|
33
|
-
description: 'Read indexed Brainlink memory for a task or question. Usually called after brainlink_bootstrap. This is read-only and does not create graph links.',
|
|
33
|
+
description: 'Read indexed Brainlink memory for a task or question. Agents can choose strategy per call: rag for fresh retrieval assembly, cag for persisted context packs, or auto for pack-hit CAG with RAG fallback. Usually called after brainlink_bootstrap. This is read-only and does not create graph links.',
|
|
34
34
|
inputSchema: contextInputSchema
|
|
35
35
|
}, contextTool);
|
|
36
|
+
server.registerTool('brainlink_context_packs', {
|
|
37
|
+
title: 'Manage Brainlink Context Packs',
|
|
38
|
+
description: 'List or clear persisted CAG context packs. Packs are derived artifacts and can be rebuilt from Markdown/index state.',
|
|
39
|
+
inputSchema: contextPacksInputSchema
|
|
40
|
+
}, contextPacksTool);
|
|
36
41
|
server.registerTool('brainlink_search', {
|
|
37
42
|
title: 'Search Brainlink Memory',
|
|
38
43
|
description: 'Search indexed Brainlink notes with FTS, semantic or hybrid retrieval.',
|
package/dist/mcp/tools.js
CHANGED
|
@@ -3,14 +3,15 @@ import { basename, extname } from 'node:path';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../application/analyze-vault.js';
|
|
5
5
|
import { addNoteWithMetadata } from '../application/add-note.js';
|
|
6
|
-
import { buildContextPackage } from '../application/build-context.js';
|
|
6
|
+
import { buildContextPackage, readContextDataSignature } from '../application/build-context.js';
|
|
7
7
|
import { canonicalizeContextLinks } from '../application/canonical-context-links.js';
|
|
8
8
|
import { resolveDuplicateNotes, scanDuplicateNotes } from '../application/dedupe-notes.js';
|
|
9
9
|
import { getGraph } from '../application/get-graph.js';
|
|
10
10
|
import { getGraphContexts } from '../application/get-graph-contexts.js';
|
|
11
11
|
import { indexVault } from '../application/index-vault.js';
|
|
12
12
|
import { searchKnowledge } from '../application/search-knowledge.js';
|
|
13
|
-
import { resolveAgentRuntimeDefaults, sanitizeSearchMode } from '../infrastructure/config.js';
|
|
13
|
+
import { resolveAgentRuntimeDefaults, sanitizeContextStrategy, sanitizeSearchMode } from '../infrastructure/config.js';
|
|
14
|
+
import { clearContextPacks, listContextPacks } from '../infrastructure/context-packs.js';
|
|
14
15
|
import { loadBrainlinkConfig } from '../infrastructure/config.js';
|
|
15
16
|
import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
|
|
16
17
|
import { addVolatileMemory, clearVolatileMemory } from '../infrastructure/volatile-memory.js';
|
|
@@ -40,6 +41,12 @@ const agentInput = {
|
|
|
40
41
|
const searchModeInput = {
|
|
41
42
|
mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode. Defaults to the Brainlink config value.')
|
|
42
43
|
};
|
|
44
|
+
const contextStrategyInput = {
|
|
45
|
+
strategy: z
|
|
46
|
+
.enum(['rag', 'cag', 'auto'])
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Context strategy per call. Use rag for fresh retrieval assembly, cag to reuse persisted context packs when fresh, or auto to choose CAG on fresh pack hits and RAG otherwise. Defaults to the Brainlink config value.')
|
|
49
|
+
};
|
|
43
50
|
const resolveExecutionContext = async (input) => {
|
|
44
51
|
const config = await loadBrainlinkConfig();
|
|
45
52
|
const vault = await assertVaultAllowed(input.vault ?? config.vault, config.allowedVaults);
|
|
@@ -122,12 +129,14 @@ const ensureBootstrapReady = async (context, input, toolName) => {
|
|
|
122
129
|
};
|
|
123
130
|
}
|
|
124
131
|
const mode = typeof input.mode === 'string' && ['fts', 'semantic', 'hybrid'].includes(input.mode) ? input.mode : 'hybrid';
|
|
132
|
+
const strategy = sanitizeContextStrategy(typeof input.strategy === 'string' ? input.strategy : undefined, 'rag');
|
|
125
133
|
const query = typeof input.query === 'string' && input.query.trim().length > 0 ? input.query : undefined;
|
|
126
134
|
const bootstrapArgs = {
|
|
127
135
|
vault: context.vault,
|
|
128
136
|
...(context.agent ? { agent: context.agent } : {}),
|
|
129
137
|
...(query ? { query } : {}),
|
|
130
|
-
mode
|
|
138
|
+
mode,
|
|
139
|
+
strategy
|
|
131
140
|
};
|
|
132
141
|
const nextActions = [
|
|
133
142
|
{
|
|
@@ -178,6 +187,7 @@ const ensureContextReady = async (context, input, toolName) => {
|
|
|
178
187
|
? input.contextQuery
|
|
179
188
|
: '<task>';
|
|
180
189
|
const mode = sanitizeSearchMode(typeof input.mode === 'string' ? input.mode : undefined, context.defaults.defaultSearchMode);
|
|
190
|
+
const strategy = sanitizeContextStrategy(typeof input.strategy === 'string' ? input.strategy : undefined, context.defaults.defaultContextStrategy);
|
|
181
191
|
const limit = typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0
|
|
182
192
|
? input.limit
|
|
183
193
|
: typeof input.contextLimit === 'number' && Number.isFinite(input.contextLimit) && input.contextLimit > 0
|
|
@@ -193,6 +203,7 @@ const ensureContextReady = async (context, input, toolName) => {
|
|
|
193
203
|
...(context.agent ? { agent: context.agent } : {}),
|
|
194
204
|
query: queryFromInput,
|
|
195
205
|
mode,
|
|
206
|
+
strategy,
|
|
196
207
|
limit,
|
|
197
208
|
tokens
|
|
198
209
|
};
|
|
@@ -219,10 +230,17 @@ export const contextInputSchema = {
|
|
|
219
230
|
...vaultInput,
|
|
220
231
|
...agentInput,
|
|
221
232
|
...searchModeInput,
|
|
233
|
+
...contextStrategyInput,
|
|
222
234
|
query: z.string().min(1).describe('Task or question to retrieve Brainlink context for.'),
|
|
223
235
|
limit: optionalPositiveInteger().describe('Maximum search results before context selection.'),
|
|
224
236
|
tokens: optionalPositiveInteger().describe('Maximum estimated context tokens.')
|
|
225
237
|
};
|
|
238
|
+
export const contextPacksInputSchema = {
|
|
239
|
+
...vaultInput,
|
|
240
|
+
...agentInput,
|
|
241
|
+
action: z.enum(['list', 'clear']).optional().default('list').describe('Action to perform on persisted CAG context packs.'),
|
|
242
|
+
staleOnly: z.boolean().optional().default(false).describe('When clearing, remove only packs stale for the current index and volatile-memory signature.')
|
|
243
|
+
};
|
|
226
244
|
export const searchInputSchema = {
|
|
227
245
|
...vaultInput,
|
|
228
246
|
...agentInput,
|
|
@@ -311,6 +329,7 @@ export const syncInputSchema = {
|
|
|
311
329
|
...agentInput,
|
|
312
330
|
contextQuery: z.string().min(1).optional().describe('Optional context smoke query. Omit to skip context probe.'),
|
|
313
331
|
mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode for the optional context probe. Defaults to config value.'),
|
|
332
|
+
strategy: z.enum(['rag', 'cag', 'auto']).optional().describe('Context strategy for the optional context probe. Defaults to the Brainlink config value.'),
|
|
314
333
|
contextLimit: optionalPositiveInteger().describe('Context smoke result limit when contextQuery is provided.'),
|
|
315
334
|
contextTokens: optionalPositiveInteger().describe('Context smoke token target when contextQuery is provided.')
|
|
316
335
|
};
|
|
@@ -318,6 +337,7 @@ export const bootstrapInputSchema = {
|
|
|
318
337
|
...vaultInput,
|
|
319
338
|
...agentInput,
|
|
320
339
|
...searchModeInput,
|
|
340
|
+
...contextStrategyInput,
|
|
321
341
|
query: z
|
|
322
342
|
.string()
|
|
323
343
|
.min(1)
|
|
@@ -350,6 +370,7 @@ export const recommendationsInputSchema = {
|
|
|
350
370
|
...vaultInput,
|
|
351
371
|
...agentInput,
|
|
352
372
|
...searchModeInput,
|
|
373
|
+
...contextStrategyInput,
|
|
353
374
|
query: z.string().min(1).optional().describe('Optional current task query to generate context-focused recommendations.'),
|
|
354
375
|
limit: optionalPositiveInteger().describe('Optional context limit override for generated recommendations.'),
|
|
355
376
|
tokens: optionalPositiveInteger().describe('Optional context token budget override for generated recommendations.')
|
|
@@ -375,14 +396,16 @@ export const contextTool = async (input) => {
|
|
|
375
396
|
return readiness.preflight;
|
|
376
397
|
}
|
|
377
398
|
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
399
|
+
const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
|
|
378
400
|
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
379
401
|
const tokens = input.tokens ?? context.defaults.defaultContextTokens;
|
|
380
|
-
const contextPackage = await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode);
|
|
402
|
+
const contextPackage = await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs);
|
|
381
403
|
const contextSession = await touchContextSession(context.vault, context.agent);
|
|
382
404
|
return jsonResult({
|
|
383
405
|
vault: context.vault,
|
|
384
406
|
agent: context.agent,
|
|
385
407
|
mode,
|
|
408
|
+
strategy,
|
|
386
409
|
limit,
|
|
387
410
|
tokens,
|
|
388
411
|
contextSession,
|
|
@@ -390,6 +413,32 @@ export const contextTool = async (input) => {
|
|
|
390
413
|
...contextPackage
|
|
391
414
|
});
|
|
392
415
|
};
|
|
416
|
+
export const contextPacksTool = async (input) => {
|
|
417
|
+
const context = await resolveExecutionContext(input);
|
|
418
|
+
const dataSignature = await readContextDataSignature(context.vault);
|
|
419
|
+
if (input.action === 'clear') {
|
|
420
|
+
const result = await clearContextPacks(context.vault, {
|
|
421
|
+
staleOnly: input.staleOnly === true,
|
|
422
|
+
dataSignature
|
|
423
|
+
});
|
|
424
|
+
return jsonResult({
|
|
425
|
+
vault: context.vault,
|
|
426
|
+
agent: context.agent,
|
|
427
|
+
dataSignature,
|
|
428
|
+
action: 'clear',
|
|
429
|
+
staleOnly: input.staleOnly === true,
|
|
430
|
+
...result
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const packs = await listContextPacks(context.vault, dataSignature);
|
|
434
|
+
return jsonResult({
|
|
435
|
+
vault: context.vault,
|
|
436
|
+
agent: context.agent,
|
|
437
|
+
dataSignature,
|
|
438
|
+
action: 'list',
|
|
439
|
+
packs
|
|
440
|
+
});
|
|
441
|
+
};
|
|
393
442
|
export const searchTool = async (input) => {
|
|
394
443
|
const context = await resolveExecutionContext(input);
|
|
395
444
|
const readiness = await ensureBootstrapReady(context, input, 'brainlink_search');
|
|
@@ -666,14 +715,16 @@ export const syncTool = async (input) => {
|
|
|
666
715
|
return jsonResult(response);
|
|
667
716
|
}
|
|
668
717
|
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
718
|
+
const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
|
|
669
719
|
const contextLimit = input.contextLimit ?? context.defaults.defaultSearchLimit;
|
|
670
720
|
const contextTokens = input.contextTokens ?? context.defaults.defaultContextTokens;
|
|
671
|
-
const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode);
|
|
721
|
+
const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs);
|
|
672
722
|
const contextSession = await touchContextSession(context.vault, context.agent);
|
|
673
723
|
return jsonResult({
|
|
674
724
|
...response,
|
|
675
725
|
context: {
|
|
676
726
|
mode,
|
|
727
|
+
strategy,
|
|
677
728
|
contextSession,
|
|
678
729
|
...contextPackage
|
|
679
730
|
}
|
|
@@ -685,10 +736,11 @@ export const bootstrapTool = async (input) => {
|
|
|
685
736
|
const stats = await getStats(context.vault, context.agent);
|
|
686
737
|
const validation = await validateVault(context.vault, context.agent);
|
|
687
738
|
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
739
|
+
const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
|
|
688
740
|
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
689
741
|
const tokens = input.tokens ?? context.defaults.defaultContextTokens;
|
|
690
742
|
const contextPackage = input.query
|
|
691
|
-
? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode)
|
|
743
|
+
? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs)
|
|
692
744
|
: undefined;
|
|
693
745
|
const contextSession = input.query ? await touchContextSession(context.vault, context.agent) : undefined;
|
|
694
746
|
const guidance = stats.documentCount === 0
|
|
@@ -742,6 +794,7 @@ export const bootstrapTool = async (input) => {
|
|
|
742
794
|
...(context.agent ? { agent: context.agent } : {}),
|
|
743
795
|
query: '<task>',
|
|
744
796
|
mode,
|
|
797
|
+
strategy,
|
|
745
798
|
limit,
|
|
746
799
|
tokens
|
|
747
800
|
}
|
|
@@ -751,6 +804,7 @@ export const bootstrapTool = async (input) => {
|
|
|
751
804
|
vault: context.vault,
|
|
752
805
|
agent: context.agent,
|
|
753
806
|
mode,
|
|
807
|
+
strategy,
|
|
754
808
|
limit,
|
|
755
809
|
tokens,
|
|
756
810
|
index,
|
|
@@ -806,7 +860,8 @@ export const policyTool = async (input) => {
|
|
|
806
860
|
args: {
|
|
807
861
|
vault: context.vault,
|
|
808
862
|
...(context.agent ? { agent: context.agent } : {}),
|
|
809
|
-
mode: context.defaults.defaultSearchMode
|
|
863
|
+
mode: context.defaults.defaultSearchMode,
|
|
864
|
+
strategy: context.defaults.defaultContextStrategy
|
|
810
865
|
}
|
|
811
866
|
}
|
|
812
867
|
];
|
|
@@ -821,6 +876,7 @@ export const policyTool = async (input) => {
|
|
|
821
876
|
...(context.agent ? { agent: context.agent } : {}),
|
|
822
877
|
query: '<task>',
|
|
823
878
|
mode: context.defaults.defaultSearchMode,
|
|
879
|
+
strategy: context.defaults.defaultContextStrategy,
|
|
824
880
|
limit: context.defaults.defaultSearchLimit,
|
|
825
881
|
tokens: context.defaults.defaultContextTokens
|
|
826
882
|
}
|
|
@@ -852,6 +908,7 @@ export const recommendationsTool = async (input) => {
|
|
|
852
908
|
const contextStatus = await getContextSessionStatus(context.vault, context.agent);
|
|
853
909
|
const stats = await getStats(context.vault, context.agent);
|
|
854
910
|
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
911
|
+
const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
|
|
855
912
|
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
856
913
|
const tokens = input.tokens ?? context.defaults.defaultContextTokens;
|
|
857
914
|
const query = input.query?.trim();
|
|
@@ -876,6 +933,7 @@ export const recommendationsTool = async (input) => {
|
|
|
876
933
|
vault: context.vault,
|
|
877
934
|
...(context.agent ? { agent: context.agent } : {}),
|
|
878
935
|
mode,
|
|
936
|
+
strategy,
|
|
879
937
|
...(query ? { query } : {})
|
|
880
938
|
}
|
|
881
939
|
}
|
|
@@ -891,6 +949,7 @@ export const recommendationsTool = async (input) => {
|
|
|
891
949
|
...(context.agent ? { agent: context.agent } : {}),
|
|
892
950
|
query: query ?? '<task>',
|
|
893
951
|
mode,
|
|
952
|
+
strategy,
|
|
894
953
|
limit,
|
|
895
954
|
tokens
|
|
896
955
|
}
|
|
@@ -926,6 +985,7 @@ export const recommendationsTool = async (input) => {
|
|
|
926
985
|
...(context.agent ? { agent: context.agent } : {}),
|
|
927
986
|
query: query ?? '<task>',
|
|
928
987
|
mode,
|
|
988
|
+
strategy,
|
|
929
989
|
limit,
|
|
930
990
|
tokens
|
|
931
991
|
}
|
|
@@ -957,9 +1017,24 @@ export const recommendationsTool = async (input) => {
|
|
|
957
1017
|
agent: context.agent,
|
|
958
1018
|
defaults: {
|
|
959
1019
|
mode,
|
|
1020
|
+
strategy,
|
|
960
1021
|
limit,
|
|
961
1022
|
tokens
|
|
962
1023
|
},
|
|
1024
|
+
contextStrategies: [
|
|
1025
|
+
{
|
|
1026
|
+
strategy: 'rag',
|
|
1027
|
+
useWhen: 'Use for fresh retrieval and context assembly from the current index.'
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
strategy: 'cag',
|
|
1031
|
+
useWhen: 'Use for repeated or stable task context so Brainlink can reuse a fresh persisted context pack.'
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
strategy: 'auto',
|
|
1035
|
+
useWhen: 'Use when the agent wants Brainlink to choose CAG on fresh pack hits and RAG otherwise.'
|
|
1036
|
+
}
|
|
1037
|
+
],
|
|
963
1038
|
policy,
|
|
964
1039
|
bootstrapStatus,
|
|
965
1040
|
contextStatus,
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -51,7 +51,7 @@ Set `BRAINLINK_HOME` when the whole Brainlink home directory should live somewhe
|
|
|
51
51
|
Use `blink config where` and `blink config doctor` to inspect active paths and effective source.
|
|
52
52
|
|
|
53
53
|
You can also set `defaultAgent` in `brainlink.config.json` / `.brainlink.json` (for example `"defaultAgent": "coding-agent"`). When set, CLI commands and MCP calls reuse it when `--agent`/`agent` is not passed.
|
|
54
|
-
You can set `agentProfiles` to define per-agent defaults for `defaultSearchMode`, `defaultSearchLimit` and `
|
|
54
|
+
You can set `agentProfiles` to define per-agent defaults for `defaultSearchMode`, `defaultSearchLimit`, `defaultContextTokens`, `defaultContextStrategy` and `defaultContextCacheTtlMs`.
|
|
55
55
|
You can tune search-pack compression with `searchPack.rowChunkSize`, `searchPack.compressionLevel` and `searchPack.useDictionary`.
|
|
56
56
|
Guardrails for benchmark acceptance are configured with `searchPack.guardrailMinSavingsPercent` and `searchPack.guardrailMaxLatencyRegressionPercent`.
|
|
57
57
|
|
|
@@ -542,9 +542,14 @@ blink context "how does authentication work?" --vault ./vault --limit 12 --token
|
|
|
542
542
|
blink context "how does authentication work?" --vault ./vault --json
|
|
543
543
|
blink context "how does authentication work?" --vault ./vault --agent coding-agent --json
|
|
544
544
|
blink context "how does authentication work?" --vault ./vault --agent coding-agent --mode hybrid --json
|
|
545
|
+
blink context "how does authentication work?" --vault ./vault --agent coding-agent --strategy cag --json
|
|
546
|
+
blink context "how does authentication work?" --vault ./vault --agent coding-agent --strategy auto --json
|
|
547
|
+
blink context-packs --vault ./vault --json
|
|
548
|
+
blink context-packs --vault ./vault --stale --clear
|
|
545
549
|
```
|
|
546
550
|
|
|
547
551
|
This returns a Markdown context package optimized for prompt injection.
|
|
552
|
+
The default strategy is configured by `defaultContextStrategy` and starts as `rag`. Use `--strategy cag` when the same agent/task context should be served from a persisted context pack when the index and volatile-memory signatures are unchanged. Use `--strategy auto` when Brainlink should use CAG on fresh pack hits and RAG otherwise. Context responses include `cache`, `metrics`, `requestedStrategy` and `recommendedStrategy` metadata. CAG packs live under `.brainlink/context-packs` and are rebuildable derived artifacts.
|
|
548
553
|
|
|
549
554
|
### Inspect Links
|
|
550
555
|
|
|
@@ -670,6 +675,7 @@ Available MCP tools:
|
|
|
670
675
|
- `brainlink_policy`
|
|
671
676
|
- `brainlink_recommendations`
|
|
672
677
|
- `brainlink_context`
|
|
678
|
+
- `brainlink_context_packs`
|
|
673
679
|
- `brainlink_search`
|
|
674
680
|
- `brainlink_dedupe`
|
|
675
681
|
- `brainlink_resolve_duplicate`
|
|
@@ -693,6 +699,7 @@ If `autoBootstrapOnRead` or `enforceContextFirst` are disabled through `brainlin
|
|
|
693
699
|
`brainlink_bootstrap`, `brainlink_policy` and preflight responses include structured `nextActions` so clients can continue tool flows automatically.
|
|
694
700
|
`brainlink_policy` also accepts policy presets (`fully-auto`, `strict`) so MCP clients can switch behavior in one call.
|
|
695
701
|
`brainlink_recommendations` returns the suggested execution order so an agent can follow Brainlink best practices automatically.
|
|
702
|
+
MCP context is plug-and-play: agents may omit `strategy` for the configured default, pass `strategy: "rag"` for explicit fresh retrieval assembly, pass `strategy: "cag"` to reuse persisted context packs when the task context is stable or repeated, or pass `strategy: "auto"` to use CAG on fresh pack hits and RAG otherwise. `brainlink_recommendations`, policy next actions and preflight responses return executable context arguments, so clients can continue without custom parsing. `brainlink_context_packs` lists or clears persisted CAG packs.
|
|
696
703
|
|
|
697
704
|
MCP clients can pass `vault` and `agent` arguments per tool call. Set `BRAINLINK_ALLOWED_VAULTS` when exposing Brainlink to an external agent process so a tool cannot pass arbitrary vault paths:
|
|
698
705
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -136,6 +136,8 @@ read markdown files
|
|
|
136
136
|
|
|
137
137
|
```txt
|
|
138
138
|
question
|
|
139
|
+
-> selected strategy: rag | cag | auto
|
|
140
|
+
-> if cag/auto and fresh context pack exists, return cached Markdown package
|
|
139
141
|
-> selected mode: fts | semantic | hybrid
|
|
140
142
|
-> optional query embedding
|
|
141
143
|
-> optional compressed pack prefilter (token bloom)
|
|
@@ -145,8 +147,12 @@ question
|
|
|
145
147
|
-> ranked chunks with textScore and semanticScore
|
|
146
148
|
-> token-budget selection
|
|
147
149
|
-> Markdown context package
|
|
150
|
+
-> if cag/auto, persist derived context pack with current data signature
|
|
151
|
+
-> return cache, strategy and timing metrics
|
|
148
152
|
```
|
|
149
153
|
|
|
154
|
+
Context packs are stored under `.brainlink/context-packs/*.json`. They are derived artifacts keyed by query, agent, mode, limit and token budget, and are invalidated by the combined index and volatile-memory signature. Markdown remains the source of truth. The `auto` strategy uses CAG on fresh pack hits and falls back to RAG assembly otherwise, refreshing a pack for later calls.
|
|
155
|
+
|
|
150
156
|
## Graph Server Flow
|
|
151
157
|
|
|
152
158
|
```txt
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -50,7 +50,9 @@ Optional per-agent retrieval defaults in `brainlink.config.json`:
|
|
|
50
50
|
"coding-agent": {
|
|
51
51
|
"defaultSearchMode": "semantic",
|
|
52
52
|
"defaultSearchLimit": 8,
|
|
53
|
-
"defaultContextTokens": 2400
|
|
53
|
+
"defaultContextTokens": 2400,
|
|
54
|
+
"defaultContextStrategy": "auto",
|
|
55
|
+
"defaultContextCacheTtlMs": 120000
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
}
|
package/package.json
CHANGED