Files
deepseek-harness/packages/fs/tool-fs/src/write.ts
T
Dudu-0223 f8e99b8740 refactor(tool-fs): consolidate read rendering; drop the fs/observed try-catch
Two cohesion cleanups on the filesystem tool package:

- Fold window.ts + types.ts + formatReadOutput into one cordis-free
  read-render.ts. Line windowing, the FileReadOutcome shape, and output
  formatting are one concern (the read tool's rendering); splitting them across
  three files added no value. read.ts is now just the tool (schema + I/O).

- Drop observe.ts and emit fs/observed with a plain ctx.emit in read/write/edit.
  The event is contractually a synchronous, side-effect-only recorder
  (file-context's listener is a WeakMap.set), so the per-call try/catch guarded
  against a contract violation that cannot happen under the shipped listener —
  defensive code for an impossible case. The event contract (dsh-fs JSDoc,
  README, RFC) is updated to state the fire-and-forget semantics plainly.
2026-06-29 10:34:08 +08:00

65 lines
3.0 KiB
TypeScript

/**
* The model-facing `write` tool: create or fully replace a UTF-8 text file. The
* tool is the executor: it dispatches the `fs/write-expectation` waterfall to
* obtain the optional version guard, calls `ctx.fs.writeText` directly, and
* emits `fs/observed`. The default thunk returns `undefined` (unconditional
* create-or-overwrite — the bare provider); a policy plugin
* (`@deepseek-ai/dsh-file-context`) occupies the single decision slot and
* returns `createIfAbsent`/`replaceIfVersion` instead. The tool stats ZERO
* times either way.
*
* @module @deepseek-ai/dsh-tool-fs/src/write
*/
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { FsWriteOutcome } from '@deepseek-ai/dsh-fs'
import type {} from '@deepseek-ai/dsh-fs'
import type {} from '@deepseek-ai/dsh-system-prompt'
/** Validate value constraints the schema DSL can't express. */
export function parseWriteArgs(args: { file_path: string; content: string }): { filePath: string; content: string } {
if (args.file_path.trim().length === 0) throw new Error('file_path must be a non-empty string')
return { filePath: args.file_path, content: args.content }
}
/** Format a write outcome as one model-facing text block body. */
export function formatWriteOutput(displayPath: string, outcome: FsWriteOutcome): string {
const verb = outcome.operation === 'create' ? 'Created' : 'Updated'
return `<path>${displayPath}</path>
<type>file</type>
<content>
${verb} file
</content>`
}
/** Register the `write` tool and its system-prompt guidance. */
export function applyWriteTool(ctx: Context): void {
ctx.systemPrompt.section({
name: 'tool:write',
order: 101,
text: 'Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default file-context policy requires it) and prefer edit for targeted changes.',
})
ctx.tools.register(defineTool({
name: 'write',
description: 'Create or fully replace a UTF-8 text file.',
parameters: {
file_path: { type: 'string', required: true, description: 'Path to write, resolved by the filesystem backend.' },
content: { type: 'string', required: true, description: 'Full UTF-8 text content to write.' },
},
async execute(args, exec): Promise<ContentBlock[]> {
const input = parseWriteArgs(args)
const target = await ctx.fs.resolve(input.filePath)
// Single-slot decision: the policy plugin produces createIfAbsent/
// replaceIfVersion; the bare default is undefined (unconditional). No stat.
const expectation = await ctx.waterfall('fs/write-expectation', target, exec, () => undefined)
const outcome = await ctx.fs.writeText(target, input.content, expectation, exec.signal)
// Record the observed version (a no-op when no policy plugin listens).
ctx.emit('fs/observed', target, outcome.version, exec)
return [{ type: 'text', text: formatWriteOutput(target.displayPath, outcome) }]
},
}))
}