mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
141 lines
5.3 KiB
TypeScript
141 lines
5.3 KiB
TypeScript
/**
|
|
* Local-filesystem implementation of `ctx.fileReferences`.
|
|
*
|
|
* @module @deepseek-ai/dsh-file-reference-local
|
|
*/
|
|
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import z from '@deepseek-ai/schemastery'
|
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
import FileReferenceService, {
|
|
FILE_REFERENCE_PROMPT,
|
|
type FileReferenceCandidate,
|
|
} from '@deepseek-ai/dsh-file-reference'
|
|
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
import {
|
|
DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES,
|
|
DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
|
DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
|
WorkspaceFileSearch,
|
|
type FileSearchConfig,
|
|
} from './search.ts'
|
|
|
|
export {
|
|
DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES,
|
|
DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
|
DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
|
WorkspaceFileSearch,
|
|
} from './search.ts'
|
|
export type { FileSearchConfig } from './search.ts'
|
|
export { FILE_REFERENCE_PROMPT } from '@deepseek-ai/dsh-file-reference'
|
|
export { activeAtToken, formatFileMention } from '@deepseek-ai/dsh-file-reference/grammar'
|
|
|
|
/** Local file-reference discovery configuration. */
|
|
export interface Config {
|
|
/** Maximum ranked candidates returned for one query. */
|
|
maxResults?: number
|
|
/** Maximum indexed files and directories per agent workspace. */
|
|
maxEntries?: number
|
|
/** Directory basenames never traversed or offered. */
|
|
excludedDirectories?: string[]
|
|
}
|
|
|
|
/** Local-filesystem owner of the file-reference discovery service. */
|
|
export class LocalFileReferenceService extends FileReferenceService {
|
|
static inject = ['agents']
|
|
static Config: z<Config> = z.object({
|
|
maxResults: z.number().step(1).min(1).default(DEFAULT_FILE_SEARCH_MAX_RESULTS),
|
|
maxEntries: z.number().step(1).min(1).default(DEFAULT_FILE_SEARCH_MAX_ENTRIES),
|
|
excludedDirectories: z.array(z.string()).default([...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]),
|
|
})
|
|
|
|
private readonly config: FileSearchConfig
|
|
private readonly searches = new Map<Agent, WorkspaceFileSearch>()
|
|
private readonly promptFibers = new Map<Agent, ReturnType<Context['inject']>>()
|
|
private readonly promptDisposals = new Set<Promise<void>>()
|
|
|
|
constructor(ctx: Context, config: Config = {}) {
|
|
super(ctx)
|
|
this.config = {
|
|
maxResults: config.maxResults ?? DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
|
maxEntries: config.maxEntries ?? DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
|
excludedDirectories: config.excludedDirectories ?? DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES,
|
|
}
|
|
validateConfig(this.config)
|
|
|
|
const installPrompt = (agent: Agent): void => {
|
|
if (this.promptFibers.has(agent)) return
|
|
const fiber = agent.ctx.inject(['systemPrompt', 'tools'], (scope) => {
|
|
scope.systemPrompt.section({
|
|
name: 'context:file-reference',
|
|
order: FIRST_PARTY_SECTION_ORDER.FILE_REFERENCE,
|
|
text: () => agent.ctx.tools.get('read', agent) === undefined ? '' : FILE_REFERENCE_PROMPT,
|
|
})
|
|
})
|
|
this.promptFibers.set(agent, fiber)
|
|
}
|
|
const disposePrompt = (agent: Agent): void => {
|
|
const fiber = this.promptFibers.get(agent)
|
|
if (fiber === undefined) return
|
|
this.promptFibers.delete(agent)
|
|
const task = fiber.dispose().catch((error: unknown) => {
|
|
ctx.logger.warn(`file-reference-local: prompt cleanup failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
})
|
|
this.promptDisposals.add(task)
|
|
void task.finally(() => {
|
|
this.promptDisposals.delete(task)
|
|
})
|
|
}
|
|
for (const agent of ctx.agents.list()) installPrompt(agent)
|
|
ctx.on('agent/created', ({ agent }) => { installPrompt(agent) })
|
|
ctx.on('agent/disposed', ({ agent }) => {
|
|
this.searches.get(agent)?.dispose()
|
|
this.searches.delete(agent)
|
|
disposePrompt(agent)
|
|
})
|
|
ctx.on('session/event', (session, event) => {
|
|
if (event.type !== 'tool/result') return
|
|
const agent = ctx.agents.get(session.id)
|
|
if (agent !== undefined) this.searches.get(agent)?.invalidate()
|
|
})
|
|
ctx.effect(() => async () => {
|
|
for (const search of this.searches.values()) search.dispose()
|
|
this.searches.clear()
|
|
const promptFibers = [...this.promptFibers.values()]
|
|
this.promptFibers.clear()
|
|
await Promise.all([
|
|
...promptFibers.map(fiber => fiber.dispose()),
|
|
...this.promptDisposals,
|
|
])
|
|
}, 'file-reference-local: search cache')
|
|
}
|
|
|
|
override list(
|
|
agent: Agent,
|
|
query: string,
|
|
signal: AbortSignal,
|
|
): Promise<FileReferenceCandidate[]> {
|
|
let search = this.searches.get(agent)
|
|
if (search === undefined) {
|
|
search = new WorkspaceFileSearch(agent.session.header.cwd ?? process.cwd(), this.config)
|
|
this.searches.set(agent, search)
|
|
}
|
|
return search.list(query, signal)
|
|
}
|
|
}
|
|
|
|
function validateConfig(config: FileSearchConfig): void {
|
|
if (!Number.isSafeInteger(config.maxResults) || config.maxResults <= 0) {
|
|
throw new Error('file-reference-local: maxResults must be a positive safe integer')
|
|
}
|
|
if (!Number.isSafeInteger(config.maxEntries) || config.maxEntries <= 0) {
|
|
throw new Error('file-reference-local: maxEntries must be a positive safe integer')
|
|
}
|
|
if (config.excludedDirectories.some(name => name.length === 0 || name.includes('/') || name.includes('\\'))) {
|
|
throw new Error('file-reference-local: excludedDirectories entries must be non-empty directory basenames')
|
|
}
|
|
}
|
|
|
|
export default LocalFileReferenceService
|