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.
Files changed (66) hide show
  1. package/README.md +17 -3
  2. package/dist/agent/core/domain/tools/constants.d.ts +0 -15
  3. package/dist/agent/core/domain/tools/constants.js +0 -15
  4. package/dist/agent/core/interfaces/i-cipher-agent.d.ts +6 -0
  5. package/dist/agent/core/interfaces/i-curate-service.d.ts +12 -0
  6. package/dist/agent/infra/llm/internal-llm-service.d.ts +13 -0
  7. package/dist/agent/infra/llm/internal-llm-service.js +61 -21
  8. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +133 -0
  9. package/dist/agent/infra/tools/implementations/curate-tool.js +14 -0
  10. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +91 -14
  11. package/dist/agent/infra/tools/index.d.ts +0 -4
  12. package/dist/agent/infra/tools/index.js +0 -4
  13. package/dist/agent/infra/tools/tool-registry.js +0 -113
  14. package/dist/agent/resources/prompts/curate-detail-preservation.yml +73 -0
  15. package/dist/agent/resources/prompts/system-prompt.yml +69 -3
  16. package/dist/server/core/domain/knowledge/markdown-writer.d.ts +13 -0
  17. package/dist/server/core/domain/knowledge/markdown-writer.js +116 -8
  18. package/dist/server/infra/executor/curate-executor.js +1 -1
  19. package/dist/server/infra/executor/direct-search-responder.d.ts +45 -0
  20. package/dist/server/infra/executor/direct-search-responder.js +86 -0
  21. package/dist/server/infra/executor/folder-pack-executor.d.ts +13 -5
  22. package/dist/server/infra/executor/folder-pack-executor.js +739 -39
  23. package/dist/server/infra/executor/query-executor.d.ts +49 -3
  24. package/dist/server/infra/executor/query-executor.js +194 -9
  25. package/dist/server/infra/executor/query-result-cache.d.ts +87 -0
  26. package/dist/server/infra/executor/query-result-cache.js +127 -0
  27. package/dist/server/infra/executor/query-similarity.d.ts +28 -0
  28. package/dist/server/infra/executor/query-similarity.js +41 -0
  29. package/dist/server/infra/process/agent-worker.js +9 -2
  30. package/dist/server/infra/process/inline-agent-executor.js +16 -5
  31. package/dist/server/infra/usecase/curate-use-case.js +6 -1
  32. package/dist/server/infra/usecase/query-use-case.js +10 -0
  33. package/dist/server/utils/file-validator.js +78 -1
  34. package/dist/tui/hooks/use-slash-completion.js +25 -4
  35. package/oclif.manifest.json +1 -1
  36. package/package.json +1 -1
  37. package/dist/agent/infra/tools/implementations/bash-exec-tool.d.ts +0 -13
  38. package/dist/agent/infra/tools/implementations/bash-exec-tool.js +0 -110
  39. package/dist/agent/infra/tools/implementations/bash-output-tool.d.ts +0 -12
  40. package/dist/agent/infra/tools/implementations/bash-output-tool.js +0 -43
  41. package/dist/agent/infra/tools/implementations/batch-tool.d.ts +0 -12
  42. package/dist/agent/infra/tools/implementations/batch-tool.js +0 -142
  43. package/dist/agent/infra/tools/implementations/create-knowledge-topic-tool.d.ts +0 -11
  44. package/dist/agent/infra/tools/implementations/create-knowledge-topic-tool.js +0 -149
  45. package/dist/agent/infra/tools/implementations/delete-memory-tool.d.ts +0 -12
  46. package/dist/agent/infra/tools/implementations/delete-memory-tool.js +0 -37
  47. package/dist/agent/infra/tools/implementations/edit-file-tool.d.ts +0 -13
  48. package/dist/agent/infra/tools/implementations/edit-file-tool.js +0 -50
  49. package/dist/agent/infra/tools/implementations/edit-memory-tool.d.ts +0 -13
  50. package/dist/agent/infra/tools/implementations/edit-memory-tool.js +0 -53
  51. package/dist/agent/infra/tools/implementations/kill-process-tool.d.ts +0 -12
  52. package/dist/agent/infra/tools/implementations/kill-process-tool.js +0 -55
  53. package/dist/agent/infra/tools/implementations/list-memories-tool.d.ts +0 -12
  54. package/dist/agent/infra/tools/implementations/list-memories-tool.js +0 -63
  55. package/dist/agent/infra/tools/implementations/read-memory-tool.d.ts +0 -12
  56. package/dist/agent/infra/tools/implementations/read-memory-tool.js +0 -39
  57. package/dist/agent/infra/tools/implementations/read-todos-tool.d.ts +0 -11
  58. package/dist/agent/infra/tools/implementations/read-todos-tool.js +0 -39
  59. package/dist/agent/infra/tools/implementations/search-history-tool.d.ts +0 -10
  60. package/dist/agent/infra/tools/implementations/search-history-tool.js +0 -36
  61. package/dist/agent/infra/tools/implementations/spec-analyze-tool.d.ts +0 -7
  62. package/dist/agent/infra/tools/implementations/spec-analyze-tool.js +0 -78
  63. package/dist/agent/infra/tools/implementations/write-memory-tool.d.ts +0 -13
  64. package/dist/agent/infra/tools/implementations/write-memory-tool.js +0 -52
  65. package/dist/agent/infra/tools/implementations/write-todos-tool.d.ts +0 -13
  66. 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
- * Uses code_exec with tools.* SDK for programmatic search.
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
- * Uses code_exec with tools.* SDK for programmatic search.
23
- * Designed to minimize iterations while maintaining answer quality.
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
- * Uses code_exec with tools.* SDK for programmatic search.
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
- // Execute with query commandType
19
- // Agent uses its default session (created during start())
20
- // Task lifecycle is managed by Transport (task:started, task:completed, task:error)
21
- const prompt = this.buildQueryPrompt(query);
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
- * Uses code_exec with tools.* SDK for programmatic search.
32
- * Designed to minimize iterations while maintaining answer quality.
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
- const queryExecutor = new QueryExecutor();
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
- return new InlineAgent(agent);
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 = new QueryExecutor();
102
+ this.queryExecutor = queryExecutor;
92
103
  }
93
104
  // ===========================================================================
94
105
  // ITransportClient implementation