Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/server/core/domain/entities/query-log-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,24 @@ export type QueryLogTier = (typeof QUERY_LOG_TIERS)[number]

export type TierKey = `tier${QueryLogTier}`

/** Named tier constants — single source of truth for tier assignments in QueryExecutor. */
export const TIER_EXACT_CACHE: QueryLogTier = 0
export const TIER_FUZZY_CACHE: QueryLogTier = 1
export const TIER_DIRECT_SEARCH: QueryLogTier = 2
export const TIER_OPTIMIZED_LLM: QueryLogTier = 3
export const TIER_FULL_AGENTIC: QueryLogTier = 4

/** Human-readable labels for each resolution tier. */
export const QUERY_LOG_TIER_LABELS: Record<QueryLogTier, string> = {
0: 'exact cache hit',
1: 'fuzzy cache match',
2: 'direct search',
3: 'optimized LLM',
4: 'full agentic',
[TIER_DIRECT_SEARCH]: 'direct search',
[TIER_EXACT_CACHE]: 'exact cache hit',
[TIER_FULL_AGENTIC]: 'full agentic',
[TIER_FUZZY_CACHE]: 'fuzzy cache match',
[TIER_OPTIMIZED_LLM]: 'optimized LLM',
}

/** Tiers considered cache hits for cache-hit-rate calculation. */
export const CACHE_TIERS = [0, 1] as const satisfies readonly QueryLogTier[]
export const CACHE_TIERS = [TIER_EXACT_CACHE, TIER_FUZZY_CACHE] as const satisfies readonly QueryLogTier[]

export type ByTier = Record<TierKey, number> & {unknown: number}

Expand Down
24 changes: 22 additions & 2 deletions src/server/core/interfaces/executor/i-query-executor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {ICipherAgent} from '../../../../agent/core/interfaces/i-cipher-agent.js'
import type {QueryLogMatchedDoc, QueryLogSearchMetadata, QueryLogTier} from '../../domain/entities/query-log-entry.js'

/**
* Options for executing query with an injected agent.
Expand All @@ -11,6 +12,25 @@ export interface QueryExecuteOptions {
taskId: string
}

/**
* Structured result from QueryExecutor containing the response string
* plus metadata about how the query was resolved.
*
* Consumed by QueryLogHandler (ENG-1893) to persist query log entries.
*/
export type QueryExecutorResult = {
/** Documents matched during search (empty for cache hits) */
matchedDocs: QueryLogMatchedDoc[]
/** The response string (includes attribution footer) */
response: string
/** Search statistics (undefined for cache-only tiers 0/1) */
searchMetadata?: QueryLogSearchMetadata
/** Resolution tier: 0=exact cache, 1=fuzzy cache, 2=direct search, 3=optimized LLM, 4=full agentic */
tier: QueryLogTier
/** Wall-clock timing from method entry to return */
timing: {durationMs: number}
}

/**
* IQueryExecutor - Executes query tasks with an injected CipherAgent.
*
Expand All @@ -28,7 +48,7 @@ export interface IQueryExecutor {
*
* @param agent - Long-lived CipherAgent (managed by caller)
* @param options - Execution options (query)
* @returns Result string from agent execution
* @returns Structured result with response, tier, timing, and search metadata
*/
executeWithAgent(agent: ICipherAgent, options: QueryExecuteOptions): Promise<string>
executeWithAgent(agent: ICipherAgent, options: QueryExecuteOptions): Promise<QueryExecutorResult>
}
3 changes: 2 additions & 1 deletion src/server/infra/daemon/agent-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,8 @@ async function executeTask(
}

case 'query': {
result = await queryExecutor.executeWithAgent(agent, {query: content, taskId})
const queryResult = await queryExecutor.executeWithAgent(agent, {query: content, taskId})
result = queryResult.response

break
}
Expand Down
156 changes: 117 additions & 39 deletions src/server/infra/executor/query-executor.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
import { join } from 'node:path'

import type { ICipherAgent } from '../../../agent/core/interfaces/i-cipher-agent.js'
import type { IFileSystem } from '../../../agent/core/interfaces/i-file-system.js'
import type { ISearchKnowledgeService, SearchKnowledgeResult } from '../../../agent/infra/sandbox/tools-sdk.js'
import type { IQueryExecutor, QueryExecuteOptions } from '../../core/interfaces/executor/i-query-executor.js'

import { ABSTRACT_EXTENSION, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR } from '../../constants.js'
import { isDerivedArtifact } from '../context-tree/derived-artifact.js'
import { FileContextTreeManifestService } from '../context-tree/file-context-tree-manifest-service.js'
import {join} from 'node:path'

import type {ICipherAgent} from '../../../agent/core/interfaces/i-cipher-agent.js'
import type {IFileSystem} from '../../../agent/core/interfaces/i-file-system.js'
import type {ISearchKnowledgeService, SearchKnowledgeResult} from '../../../agent/infra/sandbox/tools-sdk.js'
import type {QueryLogMatchedDoc} from '../../core/domain/entities/query-log-entry.js'
import type {
IQueryExecutor,
QueryExecuteOptions,
QueryExecutorResult,
} from '../../core/interfaces/executor/i-query-executor.js'

import {ABSTRACT_EXTENSION, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR} from '../../constants.js'
import {
TIER_DIRECT_SEARCH,
TIER_EXACT_CACHE,
TIER_FULL_AGENTIC,
TIER_FUZZY_CACHE,
TIER_OPTIMIZED_LLM,
} from '../../core/domain/entities/query-log-entry.js'
import {isDerivedArtifact} from '../context-tree/derived-artifact.js'
import {FileContextTreeManifestService} from '../context-tree/file-context-tree-manifest-service.js'
import {
canRespondDirectly,
type DirectSearchResult,
formatDirectResponse,
formatNotFoundResponse,
} from './direct-search-responder.js'
import { QueryResultCache } from './query-result-cache.js'
import {QueryResultCache} from './query-result-cache.js'

/** Attribution footer appended to all query responses */
const ATTRIBUTION_FOOTER = '\n\n---\nSource: ByteRover Knowledge Base'

/** Map search results to the matchedDocs shape for QueryExecutorResult. */
function buildMatchedDocs(sr: SearchKnowledgeResult | undefined): QueryLogMatchedDoc[] {
return (sr?.results ?? []).map((r) => ({path: r.path, score: r.score, title: r.title}))
}

/** Minimum normalized score to consider a result high-confidence for pre-fetching */
const SMART_ROUTING_SCORE_THRESHOLD = 0.7

Expand Down Expand Up @@ -63,7 +80,7 @@ export class QueryExecutor implements IQueryExecutor {
private static readonly FINGERPRINT_CACHE_TTL_MS = 30_000
private readonly baseDirectory?: string
private readonly cache?: QueryResultCache
private cachedFingerprint?: { expiresAt: number; value: string }
private cachedFingerprint?: {expiresAt: number; value: string}
private readonly fileSystem?: IFileSystem
private readonly searchService?: ISearchKnowledgeService

Expand All @@ -76,11 +93,12 @@ export class QueryExecutor implements IQueryExecutor {
}
}

public async executeWithAgent(agent: ICipherAgent, options: QueryExecuteOptions): Promise<string> {
const { query, taskId } = options
public async executeWithAgent(agent: ICipherAgent, options: QueryExecuteOptions): Promise<QueryExecutorResult> {
const startTime = Date.now()
const {query, taskId} = options

// Start search early — runs in parallel with fingerprint computation (independent operations)
const searchPromise = this.searchService?.search(query, { limit: SMART_ROUTING_MAX_DOCS })
const searchPromise = this.searchService?.search(query, {limit: SMART_ROUTING_MAX_DOCS})
// Prevent unhandled rejection if we return early (cache hit) while search is still pending
searchPromise?.catch(() => {})

Expand All @@ -90,15 +108,25 @@ export class QueryExecutor implements IQueryExecutor {
fingerprint = await this.computeContextTreeFingerprint()
const cached = this.cache.get(query, fingerprint)
if (cached) {
return cached + ATTRIBUTION_FOOTER
return {
matchedDocs: [],
response: cached + ATTRIBUTION_FOOTER,
tier: TIER_EXACT_CACHE,
timing: {durationMs: Date.now() - startTime},
}
}
}

// === Tier 1: Fuzzy cache match (~50ms) ===
if (this.cache && fingerprint) {
const fuzzyHit = this.cache.findSimilar(query, fingerprint)
if (fuzzyHit) {
return fuzzyHit + ATTRIBUTION_FOOTER
return {
matchedDocs: [],
response: fuzzyHit + ATTRIBUTION_FOOTER,
tier: TIER_FUZZY_CACHE,
timing: {durationMs: Date.now() - startTime},
}
}
}

Expand All @@ -122,7 +150,13 @@ export class QueryExecutor implements IQueryExecutor {
this.cache.set(query, response, fingerprint)
}

return response + ATTRIBUTION_FOOTER
return {
matchedDocs: [],
response: response + ATTRIBUTION_FOOTER,
searchMetadata: {resultCount: 0, topScore: 0, totalFound: 0},
tier: TIER_DIRECT_SEARCH,
timing: {durationMs: Date.now() - startTime},
}
}

// === Tier 2: Direct search response (~100-200ms) ===
Expand All @@ -133,7 +167,18 @@ export class QueryExecutor implements IQueryExecutor {
this.cache.set(query, directResult, fingerprint)
}

return directResult + ATTRIBUTION_FOOTER
return {
matchedDocs: buildMatchedDocs(searchResult),
response: directResult + ATTRIBUTION_FOOTER,
searchMetadata: {
cacheFingerprint: fingerprint,
resultCount: searchResult.results.length,
topScore: searchResult.results[0]?.score ?? 0,
totalFound: searchResult.totalFound,
},
tier: TIER_DIRECT_SEARCH,
timing: {durationMs: Date.now() - startTime},
}
}
}

Expand All @@ -156,9 +201,7 @@ export class QueryExecutor implements IQueryExecutor {
if (manifest) {
const resolved = await manifestService.resolveForInjection(manifest, query, this.baseDirectory)
if (resolved.length > 0) {
manifestContext = resolved
.map((e) => `[${e.type} ${e.path}]\n${e.content}`)
.join('\n\n---\n\n')
manifestContext = resolved.map((e) => `[${e.type} ${e.path}]\n${e.content}`).join('\n\n---\n\n')
}
}
} catch {
Expand Down Expand Up @@ -198,12 +241,12 @@ export class QueryExecutor implements IQueryExecutor {

// Query-optimized LLM overrides: tokens and lower temperature
const queryOverrides = prefetchedContext
? { maxIterations: 50, maxTokens: 1024, temperature: 0.3 }
: { maxIterations: 50, maxTokens: 2048, temperature: 0.5 }
? {maxIterations: 50, maxTokens: 1024, temperature: 0.3}
: {maxIterations: 50, maxTokens: 2048, temperature: 0.5}

try {
const response = await agent.executeOnSession(taskSessionId, prompt, {
executionContext: { commandType: 'query', ...queryOverrides },
executionContext: {commandType: 'query', ...queryOverrides},
taskId,
})

Expand All @@ -212,7 +255,19 @@ export class QueryExecutor implements IQueryExecutor {
this.cache.set(query, response, fingerprint)
}

return response + ATTRIBUTION_FOOTER
const tier = prefetchedContext ? TIER_OPTIMIZED_LLM : TIER_FULL_AGENTIC
return {
matchedDocs: buildMatchedDocs(searchResult),
response: response + ATTRIBUTION_FOOTER,
searchMetadata: {
cacheFingerprint: fingerprint,
resultCount: searchResult?.results.length ?? 0,
topScore: searchResult?.results[0]?.score ?? 0,
totalFound: searchResult?.totalFound ?? 0,
},
tier,
timing: {durationMs: Date.now() - startTime},
}
} finally {
// Clean up entire task session (sandbox + history) in one call
await agent.deleteTaskSession(taskSessionId)
Expand All @@ -227,9 +282,7 @@ export class QueryExecutor implements IQueryExecutor {
private buildPrefetchedContext(searchResult: SearchKnowledgeResult): string | undefined {
if (searchResult.totalFound === 0) return undefined

const highConfidenceResults = searchResult.results.filter(
(r) => r.score >= SMART_ROUTING_SCORE_THRESHOLD,
)
const highConfidenceResults = searchResult.results.filter((r) => r.score >= SMART_ROUTING_SCORE_THRESHOLD)

if (highConfidenceResults.length === 0) return undefined

Expand All @@ -253,13 +306,13 @@ export class QueryExecutor implements IQueryExecutor {
query: string,
options: {
manifestContext?: string
metadata: { hasPreFetched: boolean; resultCount: number; topScore: number; totalFound: number }
metadata: {hasPreFetched: boolean; resultCount: number; topScore: number; totalFound: number}
metaVar: string
prefetchedContext?: string
resultsVar: string
},
): string {
const { manifestContext, metadata, metaVar, prefetchedContext, resultsVar } = options
const {manifestContext, metadata, metaVar, prefetchedContext, resultsVar} = options
const groundingRules = `### Grounding Rules (CRITICAL)
- ONLY use information from the curated knowledge base (.brv/context-tree/)
- If no relevant knowledge is found, respond: "This topic is not covered in the knowledge base."
Expand Down Expand Up @@ -374,9 +427,36 @@ ${responseFormat}`
*/
private extractQueryEntities(query: string): string[] {
const stopwords = new Set([
'a', 'about', 'an', 'and', 'by', 'did', 'do', 'does', 'for', 'from',
'how', 'in', 'is', 'my', 'of', 'or', 'our', 'that', 'the', 'their',
'this', 'to', 'was', 'were', 'what', 'when', 'where', 'which', 'who', 'with',
'a',
'about',
'an',
'and',
'by',
'did',
'do',
'does',
'for',
'from',
'how',
'in',
'is',
'my',
'of',
'or',
'our',
'that',
'the',
'their',
'this',
'to',
'was',
'were',
'what',
'when',
'where',
'which',
'who',
'with',
])
const words = query.toLowerCase().split(/\s+/)

Expand All @@ -401,9 +481,7 @@ ${responseFormat}`

try {
const entitySearches = await Promise.allSettled(
entities.slice(0, 3).map((entity) =>
this.searchService!.search(entity, { limit: 3 }),
),
entities.slice(0, 3).map((entity) => this.searchService!.search(entity, {limit: 3})),
)

// Collect existing paths to deduplicate
Expand Down Expand Up @@ -456,13 +534,13 @@ ${responseFormat}`
let content = result.excerpt
try {
const ctPath = join(BRV_DIR, CONTEXT_TREE_DIR, result.path)
const { content: fullContent } = await this.fileSystem!.readFile(ctPath)
const {content: fullContent} = await this.fileSystem!.readFile(ctPath)
content = fullContent
} catch {
// Use excerpt if full read fails
}

return { content, path: result.path, score: result.score, title: result.title }
return {content, path: result.path, score: result.score, title: result.title}
}),
)

Expand Down
Loading
Loading