Files
deepseek-harness/packages/context/file-reference-local/src/index.ts
T
Yichen Jiang 9b0f6f9017 Merge remote-tracking branch 'origin/master' into worktree/web-file-session-references
# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.md
#	.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md
#	.agents/notes/implemented/feature/2026-07-21-cross-session-references.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-21-cross-session-references.md
#	.agents/notes/implemented/feature/2026-07-21-cross-session-references.zh.md
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml
#	apps/cli/config/web.cordis.yml
#	apps/cli/package.json
#	apps/web/tests/scaffold.ts
#	docs/capability-seams.md
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	packages/client/connection/src/client/api.ts
#	packages/client/connection/src/client/fixture.ts
#	packages/client/connection/src/client/index.ts
#	packages/client/runtime/src/client/contract/session.ts
#	packages/client/runtime/src/client/sessions/session.ts
#	packages/client/runtime/tests/session.client.spec.ts
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/README.zh.md
#	packages/client/ui-conversation/src/client/chat/ChatView.tsx
#	packages/client/ui-conversation/src/client/chat/MessageItem.tsx
#	packages/client/ui-conversation/src/client/chat/chat-flow.ts
#	packages/client/ui-conversation/src/client/input/facade.ts
#	packages/client/ui-conversation/src/client/input/hub.ts
#	packages/client/ui-conversation/tests/apply-inject.client.spec.tsx
#	packages/client/ui-conversation/tests/chat-branch-tails.client.spec.tsx
#	packages/client/ui-conversation/tests/chat-view.client.spec.tsx
#	packages/client/ui-conversation/tests/input-bar.client.spec.tsx
#	packages/client/ui-conversation/tests/input-matrix.client.spec.tsx
#	packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx
#	packages/client/ui-conversation/tests/skeleton.client.spec.tsx
#	packages/client/ui-input-trigger/package.json
#	packages/client/ui-input-trigger/src/types.ts
#	packages/client/ui-jobs/README.i18n.yaml
#	packages/client/ui-slash/README.md
#	packages/client/ui-slash/README.zh.md
#	packages/client/ui-subagent/README.i18n.yaml
#	packages/client/ui-subagent/README.md
#	packages/client/ui-subagent/README.zh.md
#	packages/client/ui-subagent/package.json
#	packages/client/ui-subagent/src/client/index.ts
#	packages/client/ui-subagent/tests/browser-plugin.client.spec.ts
#	packages/client/ui-subagent/tsconfig.json
#	packages/context/session-reference/README.i18n.yaml
#	packages/context/session-reference/README.md
#	packages/context/session-reference/README.zh.md
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/session/README.i18n.yaml
#	packages/core/session/README.zh.md
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/README.md
#	packages/host/apiproxy/README.zh.md
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/src/api/index.ts
#	packages/host/apiproxy/src/api/rpc-map.ts
#	packages/host/apiproxy/src/api/rpc.schema.ts
#	packages/host/apiproxy/src/api/rpc.ts
#	packages/host/apiproxy/src/api/sessions.ts
#	packages/host/apiproxy/src/fetch/client.ts
#	packages/host/apiproxy/src/fetch/handler.ts
#	packages/host/apiproxy/src/index.ts
#	packages/host/apiproxy/tests/client-handler.spec.ts
#	packages/host/apiproxy/tsconfig.json
#	pnpm-lock.yaml
#	scripts/gen-cordis-catalog.ts
#	scripts/gen-doc-graphs.ts
#	scripts/verify-package-readme-model-experience.ts
#	tsconfig.base.json
#	tsconfig.client.json
#	vitest.config.ts
2026-08-14 16:18:40 +08:00

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 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