byterover-cli 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/dist/agent/core/domain/tools/constants.d.ts +0 -15
- package/dist/agent/core/domain/tools/constants.js +0 -15
- package/dist/agent/core/interfaces/i-cipher-agent.d.ts +6 -0
- package/dist/agent/core/interfaces/i-curate-service.d.ts +12 -0
- package/dist/agent/infra/llm/internal-llm-service.d.ts +13 -0
- package/dist/agent/infra/llm/internal-llm-service.js +61 -21
- package/dist/agent/infra/tools/implementations/curate-tool.d.ts +133 -0
- package/dist/agent/infra/tools/implementations/curate-tool.js +14 -0
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +91 -14
- package/dist/agent/infra/tools/index.d.ts +0 -4
- package/dist/agent/infra/tools/index.js +0 -4
- package/dist/agent/infra/tools/tool-registry.js +0 -113
- package/dist/agent/resources/prompts/curate-detail-preservation.yml +73 -0
- package/dist/agent/resources/prompts/system-prompt.yml +69 -3
- package/dist/server/core/domain/knowledge/markdown-writer.d.ts +13 -0
- package/dist/server/core/domain/knowledge/markdown-writer.js +116 -8
- package/dist/server/infra/executor/curate-executor.js +1 -1
- package/dist/server/infra/executor/direct-search-responder.d.ts +45 -0
- package/dist/server/infra/executor/direct-search-responder.js +86 -0
- package/dist/server/infra/executor/folder-pack-executor.d.ts +13 -5
- package/dist/server/infra/executor/folder-pack-executor.js +739 -39
- package/dist/server/infra/executor/query-executor.d.ts +49 -3
- package/dist/server/infra/executor/query-executor.js +194 -9
- package/dist/server/infra/executor/query-result-cache.d.ts +87 -0
- package/dist/server/infra/executor/query-result-cache.js +127 -0
- package/dist/server/infra/executor/query-similarity.d.ts +28 -0
- package/dist/server/infra/executor/query-similarity.js +41 -0
- package/dist/server/infra/process/agent-worker.js +9 -2
- package/dist/server/infra/process/inline-agent-executor.js +16 -5
- package/dist/server/infra/usecase/curate-use-case.js +6 -1
- package/dist/server/infra/usecase/query-use-case.js +10 -0
- package/dist/server/utils/file-validator.js +78 -1
- package/dist/tui/hooks/use-slash-completion.js +25 -4
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/agent/infra/tools/implementations/bash-exec-tool.d.ts +0 -13
- package/dist/agent/infra/tools/implementations/bash-exec-tool.js +0 -110
- package/dist/agent/infra/tools/implementations/bash-output-tool.d.ts +0 -12
- package/dist/agent/infra/tools/implementations/bash-output-tool.js +0 -43
- package/dist/agent/infra/tools/implementations/batch-tool.d.ts +0 -12
- package/dist/agent/infra/tools/implementations/batch-tool.js +0 -142
- package/dist/agent/infra/tools/implementations/create-knowledge-topic-tool.d.ts +0 -11
- package/dist/agent/infra/tools/implementations/create-knowledge-topic-tool.js +0 -149
- package/dist/agent/infra/tools/implementations/delete-memory-tool.d.ts +0 -12
- package/dist/agent/infra/tools/implementations/delete-memory-tool.js +0 -37
- package/dist/agent/infra/tools/implementations/edit-file-tool.d.ts +0 -13
- package/dist/agent/infra/tools/implementations/edit-file-tool.js +0 -50
- package/dist/agent/infra/tools/implementations/edit-memory-tool.d.ts +0 -13
- package/dist/agent/infra/tools/implementations/edit-memory-tool.js +0 -53
- package/dist/agent/infra/tools/implementations/kill-process-tool.d.ts +0 -12
- package/dist/agent/infra/tools/implementations/kill-process-tool.js +0 -55
- package/dist/agent/infra/tools/implementations/list-memories-tool.d.ts +0 -12
- package/dist/agent/infra/tools/implementations/list-memories-tool.js +0 -63
- package/dist/agent/infra/tools/implementations/read-memory-tool.d.ts +0 -12
- package/dist/agent/infra/tools/implementations/read-memory-tool.js +0 -39
- package/dist/agent/infra/tools/implementations/read-todos-tool.d.ts +0 -11
- package/dist/agent/infra/tools/implementations/read-todos-tool.js +0 -39
- package/dist/agent/infra/tools/implementations/search-history-tool.d.ts +0 -10
- package/dist/agent/infra/tools/implementations/search-history-tool.js +0 -36
- package/dist/agent/infra/tools/implementations/spec-analyze-tool.d.ts +0 -7
- package/dist/agent/infra/tools/implementations/spec-analyze-tool.js +0 -78
- package/dist/agent/infra/tools/implementations/write-memory-tool.d.ts +0 -13
- package/dist/agent/infra/tools/implementations/write-memory-tool.js +0 -52
- package/dist/agent/infra/tools/implementations/write-todos-tool.d.ts +0 -13
- package/dist/agent/infra/tools/implementations/write-todos-tool.js +0 -121
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import type { ICipherAgent } from '../../../agent/core/interfaces/i-cipher-agent.js';
|
|
2
|
+
import type { IFileSystem } from '../../../agent/core/interfaces/i-file-system.js';
|
|
3
|
+
import type { ISearchKnowledgeService } from '../../../agent/infra/sandbox/tools-sdk.js';
|
|
2
4
|
import type { IQueryExecutor, QueryExecuteOptions } from '../../core/interfaces/executor/i-query-executor.js';
|
|
5
|
+
/**
|
|
6
|
+
* Optional dependencies for QueryExecutor.
|
|
7
|
+
* All fields are optional — without them, the executor falls back to the original behavior.
|
|
8
|
+
*/
|
|
9
|
+
export interface QueryExecutorDeps {
|
|
10
|
+
/** Enable query result caching (default: false) */
|
|
11
|
+
enableCache?: boolean;
|
|
12
|
+
/** File system for reading full document content and computing fingerprints */
|
|
13
|
+
fileSystem?: IFileSystem;
|
|
14
|
+
/** Search service for pre-fetching relevant context before calling the LLM */
|
|
15
|
+
searchService?: ISearchKnowledgeService;
|
|
16
|
+
}
|
|
3
17
|
/**
|
|
4
18
|
* QueryExecutor - Executes query tasks with an injected CipherAgent.
|
|
5
19
|
*
|
|
@@ -12,15 +26,47 @@ import type { IQueryExecutor, QueryExecuteOptions } from '../../core/interfaces/
|
|
|
12
26
|
* - Transport handles task lifecycle (task:started, task:completed, task:error)
|
|
13
27
|
* - Executor focuses solely on query execution
|
|
14
28
|
*
|
|
15
|
-
*
|
|
29
|
+
* Tiered response strategy (fastest to slowest):
|
|
30
|
+
* - Tier 0: Exact cache hit (0ms)
|
|
31
|
+
* - Tier 1: Fuzzy cache match via Jaccard similarity (~50ms)
|
|
32
|
+
* - Tier 2: Direct search response without LLM (~100-200ms)
|
|
33
|
+
* - Tier 3: Optimized single LLM call with pre-fetched context (<5s)
|
|
34
|
+
* - Tier 4: Full agentic loop fallback (8-15s)
|
|
16
35
|
*/
|
|
17
36
|
export declare class QueryExecutor implements IQueryExecutor {
|
|
37
|
+
private static readonly FINGERPRINT_CACHE_TTL_MS;
|
|
38
|
+
private readonly cache?;
|
|
39
|
+
private cachedFingerprint?;
|
|
40
|
+
private readonly fileSystem?;
|
|
41
|
+
private readonly searchService?;
|
|
42
|
+
constructor(deps?: QueryExecutorDeps);
|
|
18
43
|
executeWithAgent(agent: ICipherAgent, options: QueryExecuteOptions): Promise<string>;
|
|
44
|
+
/**
|
|
45
|
+
* Build pre-fetched context string from search results for LLM prompt injection.
|
|
46
|
+
* Synchronous — uses already-fetched search results (no additional I/O for excerpts).
|
|
47
|
+
* Full document reads happen only for high-confidence results.
|
|
48
|
+
*/
|
|
49
|
+
private buildPrefetchedContext;
|
|
19
50
|
/**
|
|
20
51
|
* Build a streamlined query prompt optimized for fast, accurate responses.
|
|
21
52
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
53
|
+
* When pre-fetched context is available, the prompt instructs the LLM to answer
|
|
54
|
+
* directly from the provided context (reducing LLM calls from 2+ to 1).
|
|
55
|
+
* When no context is available, falls back to tool-based search.
|
|
24
56
|
*/
|
|
25
57
|
private buildQueryPrompt;
|
|
58
|
+
/**
|
|
59
|
+
* Compute a context tree fingerprint cheaply using file mtimes.
|
|
60
|
+
* Used for cache invalidation — if any file in the context tree changes,
|
|
61
|
+
* the fingerprint changes and cached results are invalidated.
|
|
62
|
+
*/
|
|
63
|
+
private computeContextTreeFingerprint;
|
|
64
|
+
/**
|
|
65
|
+
* Attempt to produce a direct response from search results without LLM.
|
|
66
|
+
* Returns formatted response if high-confidence dominant match found, undefined otherwise.
|
|
67
|
+
*
|
|
68
|
+
* Uses higher thresholds than smart routing (score >= 8, 2x dominance)
|
|
69
|
+
* to ensure only clearly answerable queries bypass the LLM.
|
|
70
|
+
*/
|
|
71
|
+
private tryDirectSearchResponse;
|
|
26
72
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR } from '../../constants.js';
|
|
3
|
+
import { canRespondDirectly, formatDirectResponse, formatNotFoundResponse, } from './direct-search-responder.js';
|
|
4
|
+
import { QueryResultCache } from './query-result-cache.js';
|
|
5
|
+
/** Minimum MiniSearch score to consider a result high-confidence for pre-fetching */
|
|
6
|
+
const SMART_ROUTING_SCORE_THRESHOLD = 5;
|
|
7
|
+
/** Maximum number of documents to pre-fetch and inject into the prompt */
|
|
8
|
+
const SMART_ROUTING_MAX_DOCS = 5;
|
|
1
9
|
/**
|
|
2
10
|
* QueryExecutor - Executes query tasks with an injected CipherAgent.
|
|
3
11
|
*
|
|
@@ -10,28 +18,137 @@
|
|
|
10
18
|
* - Transport handles task lifecycle (task:started, task:completed, task:error)
|
|
11
19
|
* - Executor focuses solely on query execution
|
|
12
20
|
*
|
|
13
|
-
*
|
|
21
|
+
* Tiered response strategy (fastest to slowest):
|
|
22
|
+
* - Tier 0: Exact cache hit (0ms)
|
|
23
|
+
* - Tier 1: Fuzzy cache match via Jaccard similarity (~50ms)
|
|
24
|
+
* - Tier 2: Direct search response without LLM (~100-200ms)
|
|
25
|
+
* - Tier 3: Optimized single LLM call with pre-fetched context (<5s)
|
|
26
|
+
* - Tier 4: Full agentic loop fallback (8-15s)
|
|
14
27
|
*/
|
|
15
28
|
export class QueryExecutor {
|
|
29
|
+
static FINGERPRINT_CACHE_TTL_MS = 30_000;
|
|
30
|
+
cache;
|
|
31
|
+
cachedFingerprint;
|
|
32
|
+
fileSystem;
|
|
33
|
+
searchService;
|
|
34
|
+
constructor(deps) {
|
|
35
|
+
this.fileSystem = deps?.fileSystem;
|
|
36
|
+
this.searchService = deps?.searchService;
|
|
37
|
+
if (deps?.enableCache) {
|
|
38
|
+
this.cache = new QueryResultCache();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
16
41
|
async executeWithAgent(agent, options) {
|
|
17
42
|
const { query, taskId } = options;
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
43
|
+
// Start search early — runs in parallel with fingerprint computation (independent operations)
|
|
44
|
+
const searchPromise = this.searchService?.search(query, { limit: SMART_ROUTING_MAX_DOCS });
|
|
45
|
+
// Prevent unhandled rejection if we return early (cache hit) while search is still pending
|
|
46
|
+
searchPromise?.catch(() => { });
|
|
47
|
+
// === Tier 0: Exact cache hit (0ms) ===
|
|
48
|
+
let fingerprint;
|
|
49
|
+
if (this.cache && this.fileSystem) {
|
|
50
|
+
fingerprint = await this.computeContextTreeFingerprint();
|
|
51
|
+
const cached = this.cache.get(query, fingerprint);
|
|
52
|
+
if (cached) {
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// === Tier 1: Fuzzy cache match (~50ms) ===
|
|
57
|
+
if (this.cache && fingerprint) {
|
|
58
|
+
const fuzzyHit = this.cache.findSimilar(query, fingerprint);
|
|
59
|
+
if (fuzzyHit) {
|
|
60
|
+
return fuzzyHit;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Await search result (already started in parallel with fingerprint computation)
|
|
64
|
+
let searchResult;
|
|
65
|
+
try {
|
|
66
|
+
searchResult = await searchPromise;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Search failed, proceed without pre-fetched context
|
|
70
|
+
}
|
|
71
|
+
// === OOD short-circuit: no results means topic not covered ===
|
|
72
|
+
if (searchResult && searchResult.results.length === 0) {
|
|
73
|
+
const response = formatNotFoundResponse(query);
|
|
74
|
+
if (this.cache && fingerprint) {
|
|
75
|
+
this.cache.set(query, response, fingerprint);
|
|
76
|
+
}
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
// === Tier 2: Direct search response (~100-200ms) ===
|
|
80
|
+
if (searchResult && this.fileSystem) {
|
|
81
|
+
const directResult = await this.tryDirectSearchResponse(query, searchResult);
|
|
82
|
+
if (directResult) {
|
|
83
|
+
if (this.cache && fingerprint) {
|
|
84
|
+
this.cache.set(query, directResult, fingerprint);
|
|
85
|
+
}
|
|
86
|
+
return directResult;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// === Tier 3: Optimized LLM call with pre-fetched context (<5s) ===
|
|
90
|
+
let prefetchedContext;
|
|
91
|
+
if (searchResult && this.fileSystem) {
|
|
92
|
+
prefetchedContext = this.buildPrefetchedContext(searchResult);
|
|
93
|
+
}
|
|
94
|
+
const prompt = this.buildQueryPrompt(query, prefetchedContext);
|
|
95
|
+
// Query-optimized LLM overrides: fewer tokens, iterations, and lower temperature
|
|
96
|
+
const queryOverrides = prefetchedContext
|
|
97
|
+
? { maxIterations: 2, maxTokens: 1024, temperature: 0.3 }
|
|
98
|
+
: { maxIterations: 3, maxTokens: 2048, temperature: 0.5 };
|
|
22
99
|
const response = await agent.execute(prompt, {
|
|
23
|
-
executionContext: { commandType: 'query' },
|
|
100
|
+
executionContext: { commandType: 'query', ...queryOverrides },
|
|
24
101
|
taskId,
|
|
25
102
|
});
|
|
103
|
+
// Store in cache for future Tier 0/1 hits
|
|
104
|
+
if (this.cache && fingerprint) {
|
|
105
|
+
this.cache.set(query, response, fingerprint);
|
|
106
|
+
}
|
|
26
107
|
return response;
|
|
27
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Build pre-fetched context string from search results for LLM prompt injection.
|
|
111
|
+
* Synchronous — uses already-fetched search results (no additional I/O for excerpts).
|
|
112
|
+
* Full document reads happen only for high-confidence results.
|
|
113
|
+
*/
|
|
114
|
+
buildPrefetchedContext(searchResult) {
|
|
115
|
+
if (searchResult.totalFound === 0)
|
|
116
|
+
return undefined;
|
|
117
|
+
const highConfidenceResults = searchResult.results.filter((r) => r.score >= SMART_ROUTING_SCORE_THRESHOLD);
|
|
118
|
+
if (highConfidenceResults.length === 0)
|
|
119
|
+
return undefined;
|
|
120
|
+
const sections = highConfidenceResults.map((r) => `### ${r.title}\n**Source**: .brv/context-tree/${r.path}\n\n${r.excerpt}`);
|
|
121
|
+
return sections.join('\n\n---\n\n');
|
|
122
|
+
}
|
|
28
123
|
/**
|
|
29
124
|
* Build a streamlined query prompt optimized for fast, accurate responses.
|
|
30
125
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
126
|
+
* When pre-fetched context is available, the prompt instructs the LLM to answer
|
|
127
|
+
* directly from the provided context (reducing LLM calls from 2+ to 1).
|
|
128
|
+
* When no context is available, falls back to tool-based search.
|
|
33
129
|
*/
|
|
34
|
-
buildQueryPrompt(query) {
|
|
130
|
+
buildQueryPrompt(query, prefetchedContext) {
|
|
131
|
+
if (prefetchedContext) {
|
|
132
|
+
return `## User Query
|
|
133
|
+
${query}
|
|
134
|
+
|
|
135
|
+
## Pre-fetched Context
|
|
136
|
+
The following relevant knowledge was found in the context tree:
|
|
137
|
+
|
|
138
|
+
${prefetchedContext}
|
|
139
|
+
|
|
140
|
+
## Instructions
|
|
141
|
+
|
|
142
|
+
Answer the user's question using the pre-fetched context above.
|
|
143
|
+
If the pre-fetched context does not directly address the user's query topic, respond that the topic is not covered in the knowledge base. Do not attempt to answer from tangentially related content.
|
|
144
|
+
If the context is insufficient but relevant, you may use \`code_exec\` with the \`tools.*\` SDK for additional searches.
|
|
145
|
+
|
|
146
|
+
### Response Format
|
|
147
|
+
- **Summary**: Direct answer (2-3 sentences)
|
|
148
|
+
- **Details**: Key findings with explanations
|
|
149
|
+
- **Sources**: File paths from .brv/context-tree/
|
|
150
|
+
- **Gaps**: Note any aspects not covered`;
|
|
151
|
+
}
|
|
35
152
|
return `## User Query
|
|
36
153
|
${query}
|
|
37
154
|
|
|
@@ -46,4 +163,72 @@ Use \`code_exec\` to run a programmatic search with the \`tools.*\` SDK.
|
|
|
46
163
|
- **Sources**: File paths from .brv/context-tree/
|
|
47
164
|
- **Gaps**: Note any aspects not covered`;
|
|
48
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Compute a context tree fingerprint cheaply using file mtimes.
|
|
168
|
+
* Used for cache invalidation — if any file in the context tree changes,
|
|
169
|
+
* the fingerprint changes and cached results are invalidated.
|
|
170
|
+
*/
|
|
171
|
+
async computeContextTreeFingerprint() {
|
|
172
|
+
// Fast path: return cached fingerprint if still valid (avoids globFiles I/O)
|
|
173
|
+
if (this.cachedFingerprint && Date.now() < this.cachedFingerprint.expiresAt) {
|
|
174
|
+
return this.cachedFingerprint.value;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const contextTreePath = join(BRV_DIR, CONTEXT_TREE_DIR);
|
|
178
|
+
const globResult = await this.fileSystem.globFiles(`**/*${CONTEXT_FILE_EXTENSION}`, {
|
|
179
|
+
cwd: contextTreePath,
|
|
180
|
+
includeMetadata: true,
|
|
181
|
+
maxResults: 10_000,
|
|
182
|
+
respectGitignore: false,
|
|
183
|
+
});
|
|
184
|
+
const files = globResult.files.map((f) => ({
|
|
185
|
+
mtime: f.modified?.getTime() ?? 0,
|
|
186
|
+
path: f.path,
|
|
187
|
+
}));
|
|
188
|
+
const fingerprint = QueryResultCache.computeFingerprint(files);
|
|
189
|
+
this.cachedFingerprint = {
|
|
190
|
+
expiresAt: Date.now() + QueryExecutor.FINGERPRINT_CACHE_TTL_MS,
|
|
191
|
+
value: fingerprint,
|
|
192
|
+
};
|
|
193
|
+
return fingerprint;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return 'unknown';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Attempt to produce a direct response from search results without LLM.
|
|
201
|
+
* Returns formatted response if high-confidence dominant match found, undefined otherwise.
|
|
202
|
+
*
|
|
203
|
+
* Uses higher thresholds than smart routing (score >= 8, 2x dominance)
|
|
204
|
+
* to ensure only clearly answerable queries bypass the LLM.
|
|
205
|
+
*/
|
|
206
|
+
async tryDirectSearchResponse(query, searchResult) {
|
|
207
|
+
try {
|
|
208
|
+
if (searchResult.totalFound === 0)
|
|
209
|
+
return undefined;
|
|
210
|
+
// Build full results with content
|
|
211
|
+
const fullResults = await Promise.all(searchResult.results
|
|
212
|
+
.filter((r) => r.score >= SMART_ROUTING_SCORE_THRESHOLD)
|
|
213
|
+
.slice(0, SMART_ROUTING_MAX_DOCS)
|
|
214
|
+
.map(async (result) => {
|
|
215
|
+
let content = result.excerpt;
|
|
216
|
+
try {
|
|
217
|
+
const ctPath = join(BRV_DIR, CONTEXT_TREE_DIR, result.path);
|
|
218
|
+
const { content: fullContent } = await this.fileSystem.readFile(ctPath);
|
|
219
|
+
content = fullContent;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Use excerpt if full read fails
|
|
223
|
+
}
|
|
224
|
+
return { content, path: result.path, score: result.score, title: result.title };
|
|
225
|
+
}));
|
|
226
|
+
if (!canRespondDirectly(fullResults))
|
|
227
|
+
return undefined;
|
|
228
|
+
return formatDirectResponse(query, fullResults);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
49
234
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type QueryTokens } from './query-similarity.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cached query result entry.
|
|
4
|
+
*/
|
|
5
|
+
export interface QueryCacheEntry {
|
|
6
|
+
/** Cached response content */
|
|
7
|
+
content: string;
|
|
8
|
+
/** Context tree fingerprint at cache time */
|
|
9
|
+
fingerprint: string;
|
|
10
|
+
/** Timestamp when cached */
|
|
11
|
+
storedAt: number;
|
|
12
|
+
/** Pre-computed tokens for fuzzy similarity matching */
|
|
13
|
+
tokens: QueryTokens;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for QueryResultCache.
|
|
17
|
+
*/
|
|
18
|
+
export interface QueryResultCacheOptions {
|
|
19
|
+
/** Maximum number of entries (default: 50) */
|
|
20
|
+
maxSize?: number;
|
|
21
|
+
/** TTL in milliseconds (default: 60000) */
|
|
22
|
+
ttlMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* In-memory LRU cache for query results with TTL and context tree fingerprint validation.
|
|
26
|
+
*
|
|
27
|
+
* Follows the same pattern as PromptCache (src/agent/infra/system-prompt/prompt-cache.ts):
|
|
28
|
+
* - Map-based storage with configurable max size
|
|
29
|
+
* - LRU eviction when at capacity
|
|
30
|
+
* - TTL-based expiration
|
|
31
|
+
* - Fingerprint-based invalidation when the context tree changes
|
|
32
|
+
* - Fuzzy matching for semantically similar queries via Jaccard similarity
|
|
33
|
+
*/
|
|
34
|
+
export declare class QueryResultCache {
|
|
35
|
+
private readonly cache;
|
|
36
|
+
private readonly maxSize;
|
|
37
|
+
private readonly ttlMs;
|
|
38
|
+
constructor(options?: QueryResultCacheOptions);
|
|
39
|
+
/**
|
|
40
|
+
* Compute a context tree fingerprint from file mtimes.
|
|
41
|
+
* Uses sorted paths + mtimes to create a deterministic hash.
|
|
42
|
+
* If any file changes (added, removed, modified), the fingerprint changes.
|
|
43
|
+
*
|
|
44
|
+
* @param files - Array of file paths with modification times
|
|
45
|
+
* @returns 16-character hex fingerprint
|
|
46
|
+
*/
|
|
47
|
+
static computeFingerprint(files: Array<{
|
|
48
|
+
mtime: number;
|
|
49
|
+
path: string;
|
|
50
|
+
}>): string;
|
|
51
|
+
/** Clear all entries. */
|
|
52
|
+
clear(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Find a cached result by fuzzy similarity.
|
|
55
|
+
* Returns the highest-similarity match above threshold, or undefined.
|
|
56
|
+
* Called after exact-match `get()` fails.
|
|
57
|
+
*
|
|
58
|
+
* @param query - User query string
|
|
59
|
+
* @param currentFingerprint - Current context tree fingerprint
|
|
60
|
+
* @returns Cached response content or undefined
|
|
61
|
+
*/
|
|
62
|
+
findSimilar(query: string, currentFingerprint: string): string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Get a cached result if valid.
|
|
65
|
+
* Returns undefined if entry doesn't exist, TTL expired, or fingerprint mismatch.
|
|
66
|
+
*
|
|
67
|
+
* @param query - User query string
|
|
68
|
+
* @param currentFingerprint - Current context tree fingerprint
|
|
69
|
+
* @returns Cached response content or undefined
|
|
70
|
+
*/
|
|
71
|
+
get(query: string, currentFingerprint: string): string | undefined;
|
|
72
|
+
/** Get cache statistics. */
|
|
73
|
+
getStats(): {
|
|
74
|
+
maxSize: number;
|
|
75
|
+
size: number;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Store a result in the cache.
|
|
79
|
+
*
|
|
80
|
+
* @param query - User query string
|
|
81
|
+
* @param content - Response content to cache
|
|
82
|
+
* @param fingerprint - Context tree fingerprint at cache time
|
|
83
|
+
*/
|
|
84
|
+
set(query: string, content: string, fingerprint: string): void;
|
|
85
|
+
/** Normalize query for cache key consistency. */
|
|
86
|
+
private normalizeQuery;
|
|
87
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { FUZZY_SIMILARITY_THRESHOLD, jaccardSimilarity, tokenizeQuery } from './query-similarity.js';
|
|
3
|
+
/**
|
|
4
|
+
* In-memory LRU cache for query results with TTL and context tree fingerprint validation.
|
|
5
|
+
*
|
|
6
|
+
* Follows the same pattern as PromptCache (src/agent/infra/system-prompt/prompt-cache.ts):
|
|
7
|
+
* - Map-based storage with configurable max size
|
|
8
|
+
* - LRU eviction when at capacity
|
|
9
|
+
* - TTL-based expiration
|
|
10
|
+
* - Fingerprint-based invalidation when the context tree changes
|
|
11
|
+
* - Fuzzy matching for semantically similar queries via Jaccard similarity
|
|
12
|
+
*/
|
|
13
|
+
export class QueryResultCache {
|
|
14
|
+
cache = new Map();
|
|
15
|
+
maxSize;
|
|
16
|
+
ttlMs;
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.maxSize = options.maxSize ?? 50;
|
|
19
|
+
this.ttlMs = options.ttlMs ?? 60_000;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compute a context tree fingerprint from file mtimes.
|
|
23
|
+
* Uses sorted paths + mtimes to create a deterministic hash.
|
|
24
|
+
* If any file changes (added, removed, modified), the fingerprint changes.
|
|
25
|
+
*
|
|
26
|
+
* @param files - Array of file paths with modification times
|
|
27
|
+
* @returns 16-character hex fingerprint
|
|
28
|
+
*/
|
|
29
|
+
static computeFingerprint(files) {
|
|
30
|
+
if (files.length === 0)
|
|
31
|
+
return 'empty';
|
|
32
|
+
const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
33
|
+
const data = sorted.map((f) => `${f.path}:${f.mtime}`).join('|');
|
|
34
|
+
return createHash('md5').update(data).digest('hex').slice(0, 16);
|
|
35
|
+
}
|
|
36
|
+
/** Clear all entries. */
|
|
37
|
+
clear() {
|
|
38
|
+
this.cache.clear();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Find a cached result by fuzzy similarity.
|
|
42
|
+
* Returns the highest-similarity match above threshold, or undefined.
|
|
43
|
+
* Called after exact-match `get()` fails.
|
|
44
|
+
*
|
|
45
|
+
* @param query - User query string
|
|
46
|
+
* @param currentFingerprint - Current context tree fingerprint
|
|
47
|
+
* @returns Cached response content or undefined
|
|
48
|
+
*/
|
|
49
|
+
findSimilar(query, currentFingerprint) {
|
|
50
|
+
const queryTokens = tokenizeQuery(query);
|
|
51
|
+
// Skip fuzzy matching if query has very few meaningful tokens
|
|
52
|
+
if (queryTokens.tokenSet.size < 2)
|
|
53
|
+
return undefined;
|
|
54
|
+
let bestMatch;
|
|
55
|
+
for (const [, entry] of this.cache) {
|
|
56
|
+
// Check fingerprint + TTL first (cheap filters)
|
|
57
|
+
if (entry.fingerprint !== currentFingerprint)
|
|
58
|
+
continue;
|
|
59
|
+
if (Date.now() - entry.storedAt > this.ttlMs)
|
|
60
|
+
continue;
|
|
61
|
+
const similarity = jaccardSimilarity(queryTokens.tokenSet, entry.tokens.tokenSet);
|
|
62
|
+
if (similarity >= FUZZY_SIMILARITY_THRESHOLD && (!bestMatch || similarity > bestMatch.similarity)) {
|
|
63
|
+
bestMatch = { content: entry.content, similarity };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return bestMatch?.content;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get a cached result if valid.
|
|
70
|
+
* Returns undefined if entry doesn't exist, TTL expired, or fingerprint mismatch.
|
|
71
|
+
*
|
|
72
|
+
* @param query - User query string
|
|
73
|
+
* @param currentFingerprint - Current context tree fingerprint
|
|
74
|
+
* @returns Cached response content or undefined
|
|
75
|
+
*/
|
|
76
|
+
get(query, currentFingerprint) {
|
|
77
|
+
const key = this.normalizeQuery(query);
|
|
78
|
+
const entry = this.cache.get(key);
|
|
79
|
+
if (!entry)
|
|
80
|
+
return undefined;
|
|
81
|
+
// Check TTL
|
|
82
|
+
if (Date.now() - entry.storedAt > this.ttlMs) {
|
|
83
|
+
this.cache.delete(key);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
// Check fingerprint (context tree changed?)
|
|
87
|
+
if (entry.fingerprint !== currentFingerprint) {
|
|
88
|
+
this.cache.delete(key);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return entry.content;
|
|
92
|
+
}
|
|
93
|
+
/** Get cache statistics. */
|
|
94
|
+
getStats() {
|
|
95
|
+
return {
|
|
96
|
+
maxSize: this.maxSize,
|
|
97
|
+
size: this.cache.size,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Store a result in the cache.
|
|
102
|
+
*
|
|
103
|
+
* @param query - User query string
|
|
104
|
+
* @param content - Response content to cache
|
|
105
|
+
* @param fingerprint - Context tree fingerprint at cache time
|
|
106
|
+
*/
|
|
107
|
+
set(query, content, fingerprint) {
|
|
108
|
+
const key = this.normalizeQuery(query);
|
|
109
|
+
const tokens = tokenizeQuery(query);
|
|
110
|
+
// Evict oldest entry if at capacity
|
|
111
|
+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
112
|
+
const oldestKey = this.cache.keys().next().value;
|
|
113
|
+
if (oldestKey)
|
|
114
|
+
this.cache.delete(oldestKey);
|
|
115
|
+
}
|
|
116
|
+
this.cache.set(key, {
|
|
117
|
+
content,
|
|
118
|
+
fingerprint,
|
|
119
|
+
storedAt: Date.now(),
|
|
120
|
+
tokens,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Normalize query for cache key consistency. */
|
|
124
|
+
normalizeQuery(query) {
|
|
125
|
+
return query.toLowerCase().trim().replaceAll(/\s+/g, ' ');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-computed query tokens for similarity comparison.
|
|
3
|
+
*/
|
|
4
|
+
export interface QueryTokens {
|
|
5
|
+
/** Original normalized form (for exact match) */
|
|
6
|
+
normalized: string;
|
|
7
|
+
/** Stopword-filtered bag of words */
|
|
8
|
+
tokenSet: Set<string>;
|
|
9
|
+
}
|
|
10
|
+
/** Minimum Jaccard similarity to consider a fuzzy cache match */
|
|
11
|
+
export declare const FUZZY_SIMILARITY_THRESHOLD = 0.6;
|
|
12
|
+
/**
|
|
13
|
+
* Tokenize and prepare a query for similarity comparison.
|
|
14
|
+
* Uses the same stopword library already used by SearchKnowledgeService.
|
|
15
|
+
*
|
|
16
|
+
* @param query - Raw user query string
|
|
17
|
+
* @returns Pre-computed tokens for similarity comparison
|
|
18
|
+
*/
|
|
19
|
+
export declare function tokenizeQuery(query: string): QueryTokens;
|
|
20
|
+
/**
|
|
21
|
+
* Compute Jaccard similarity between two token sets.
|
|
22
|
+
* Returns value between 0 (no overlap) and 1 (identical).
|
|
23
|
+
*
|
|
24
|
+
* @param a - First token set
|
|
25
|
+
* @param b - Second token set
|
|
26
|
+
* @returns Similarity score between 0 and 1
|
|
27
|
+
*/
|
|
28
|
+
export declare function jaccardSimilarity(a: Set<string>, b: Set<string>): number;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { removeStopwords } from 'stopword';
|
|
2
|
+
/** Minimum Jaccard similarity to consider a fuzzy cache match */
|
|
3
|
+
export const FUZZY_SIMILARITY_THRESHOLD = 0.6;
|
|
4
|
+
/**
|
|
5
|
+
* Tokenize and prepare a query for similarity comparison.
|
|
6
|
+
* Uses the same stopword library already used by SearchKnowledgeService.
|
|
7
|
+
*
|
|
8
|
+
* @param query - Raw user query string
|
|
9
|
+
* @returns Pre-computed tokens for similarity comparison
|
|
10
|
+
*/
|
|
11
|
+
export function tokenizeQuery(query) {
|
|
12
|
+
const normalized = query.toLowerCase().trim().replaceAll(/\s+/g, ' ');
|
|
13
|
+
const words = normalized.split(' ');
|
|
14
|
+
const filtered = removeStopwords(words).filter((w) => w.length >= 2);
|
|
15
|
+
return {
|
|
16
|
+
normalized,
|
|
17
|
+
tokenSet: new Set(filtered),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Compute Jaccard similarity between two token sets.
|
|
22
|
+
* Returns value between 0 (no overlap) and 1 (identical).
|
|
23
|
+
*
|
|
24
|
+
* @param a - First token set
|
|
25
|
+
* @param b - Second token set
|
|
26
|
+
* @returns Similarity score between 0 and 1
|
|
27
|
+
*/
|
|
28
|
+
export function jaccardSimilarity(a, b) {
|
|
29
|
+
if (a.size === 0 && b.size === 0)
|
|
30
|
+
return 1;
|
|
31
|
+
if (a.size === 0 || b.size === 0)
|
|
32
|
+
return 0;
|
|
33
|
+
const [smaller, larger] = a.size <= b.size ? [a, b] : [b, a];
|
|
34
|
+
let intersection = 0;
|
|
35
|
+
for (const token of smaller) {
|
|
36
|
+
if (larger.has(token))
|
|
37
|
+
intersection++;
|
|
38
|
+
}
|
|
39
|
+
const union = a.size + b.size - intersection;
|
|
40
|
+
return union === 0 ? 0 : intersection / union;
|
|
41
|
+
}
|
|
@@ -22,6 +22,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
22
22
|
import { CipherAgent } from '../../../agent/infra/agent/index.js';
|
|
23
23
|
import { FileSystemService } from '../../../agent/infra/file-system/file-system-service.js';
|
|
24
24
|
import { FolderPackService } from '../../../agent/infra/folder-pack/folder-pack-service.js';
|
|
25
|
+
import { createSearchKnowledgeService } from '../../../agent/infra/tools/implementations/search-knowledge-service.js';
|
|
25
26
|
import { getCurrentConfig } from '../../config/environment.js';
|
|
26
27
|
import { DEFAULT_LLM_MODEL, PROJECT } from '../../constants.js';
|
|
27
28
|
import { AgentNotInitializedError, NotAuthenticatedError, ProcessorNotInitError, serializeTaskError, } from '../../core/domain/errors/task-error.js';
|
|
@@ -682,10 +683,16 @@ async function tryInitializeAgent(forceReinit = false) {
|
|
|
682
683
|
}
|
|
683
684
|
// Create Executors
|
|
684
685
|
const curateExecutor = new CurateExecutor();
|
|
685
|
-
|
|
686
|
-
// Create FolderPackExecutor with required dependencies
|
|
686
|
+
// Create shared FileSystemService (used by FolderPackExecutor and QueryExecutor)
|
|
687
687
|
const fileSystemService = new FileSystemService();
|
|
688
688
|
await fileSystemService.initialize();
|
|
689
|
+
// Create QueryExecutor with smart routing and caching dependencies
|
|
690
|
+
const searchService = createSearchKnowledgeService(fileSystemService);
|
|
691
|
+
const queryExecutor = new QueryExecutor({
|
|
692
|
+
enableCache: true,
|
|
693
|
+
fileSystem: fileSystemService,
|
|
694
|
+
searchService,
|
|
695
|
+
});
|
|
689
696
|
const folderPackService = new FolderPackService(fileSystemService);
|
|
690
697
|
await folderPackService.initialize();
|
|
691
698
|
const folderPackExecutor = new FolderPackExecutor(folderPackService);
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { randomUUID } from 'node:crypto';
|
|
16
16
|
import { CipherAgent } from '../../../agent/infra/agent/index.js';
|
|
17
|
+
import { FileSystemService } from '../../../agent/infra/file-system/file-system-service.js';
|
|
18
|
+
import { createSearchKnowledgeService } from '../../../agent/infra/tools/implementations/search-knowledge-service.js';
|
|
17
19
|
import { getCurrentConfig } from '../../config/environment.js';
|
|
18
20
|
import { DEFAULT_LLM_MODEL, PROJECT } from '../../constants.js';
|
|
19
21
|
import { NotAuthenticatedError, serializeTaskError } from '../../core/domain/errors/task-error.js';
|
|
@@ -28,8 +30,8 @@ import { createTokenStore } from '../storage/token-store.js';
|
|
|
28
30
|
*/
|
|
29
31
|
export class InlineAgent {
|
|
30
32
|
transportClient;
|
|
31
|
-
constructor(agent) {
|
|
32
|
-
this.transportClient = new InlineTransportClient(agent);
|
|
33
|
+
constructor(agent, queryExecutor) {
|
|
34
|
+
this.transportClient = new InlineTransportClient(agent, queryExecutor);
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
37
|
* Async factory — loads auth/config, creates and starts CipherAgent.
|
|
@@ -69,7 +71,16 @@ export class InlineAgent {
|
|
|
69
71
|
await agent.start();
|
|
70
72
|
const sessionId = `inline-session-${randomUUID()}`;
|
|
71
73
|
await agent.createSession(sessionId);
|
|
72
|
-
|
|
74
|
+
// Create FileSystemService for smart routing and caching
|
|
75
|
+
const fileSystemService = new FileSystemService();
|
|
76
|
+
await fileSystemService.initialize();
|
|
77
|
+
const searchService = createSearchKnowledgeService(fileSystemService);
|
|
78
|
+
const queryExecutor = new QueryExecutor({
|
|
79
|
+
enableCache: true,
|
|
80
|
+
fileSystem: fileSystemService,
|
|
81
|
+
searchService,
|
|
82
|
+
});
|
|
83
|
+
return new InlineAgent(agent, queryExecutor);
|
|
73
84
|
}
|
|
74
85
|
}
|
|
75
86
|
/**
|
|
@@ -85,10 +96,10 @@ class InlineTransportClient {
|
|
|
85
96
|
curateExecutor;
|
|
86
97
|
handlers = new Map();
|
|
87
98
|
queryExecutor;
|
|
88
|
-
constructor(agent) {
|
|
99
|
+
constructor(agent, queryExecutor) {
|
|
89
100
|
this.agent = agent;
|
|
90
101
|
this.curateExecutor = new CurateExecutor();
|
|
91
|
-
this.queryExecutor =
|
|
102
|
+
this.queryExecutor = queryExecutor;
|
|
92
103
|
}
|
|
93
104
|
// ===========================================================================
|
|
94
105
|
// ITransportClient implementation
|