forked from deepseek-ai/deepseek-harness
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
140 lines (130 loc) · 5.29 KB
/
Copy pathindex.ts
File metadata and controls
140 lines (130 loc) · 5.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/**
* 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 type {} 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: 99,
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