Files
deepseek-harness/packages/fs/tool-fs
Dudu-0223 1059166cb1 fix: address codex review round 2
- edit tool: add the read-before-edit requirement to the model-facing prompt
  (with the just-created/edited-this-session exception), matching write's
  guidance so the model doesn't only learn it via a failed FS_NOT_OBSERVED call.
- fsspec-style-fs-seam RFC: correct the acceptance criteria that still claimed
  a ctx.fileContext service and a fileContext inject — the landed design is the
  fs/* event gate with the tool injecting fs.
- filesystem-tool-schemas RFC: replace the stale "prior full file state"
  edit requirement with version-freshness wording (any windowed read authorizes
  a fresh edit; no partial-view flag).
2026-06-28 14:15:04 +08:00
..
2026-06-28 14:15:04 +08:00
2026-06-28 14:02:51 +08:00

@deepseek-ai/dsh-tool-fs

The model-facing filesystem tools — read, write, edit — and their executor. This is the consumer layer of the filesystem stack: it owns tool names, JSON schemas, argument validation, prompt sections, read windowing, and result formatting. It reads/writes/edits through the ctx.fs provider seam (@deepseek-ai/dsh-fs) directly — it injects fs (plus tools/systemPrompt), not a policy service. The freshness/observation policy is contributed by a separate plugin (@deepseek-ai/dsh-file-context) through the fs/* event gate; the tool is not method-coupled to it.

// Default deployment: a ctx.fs provider, the policy plugin, then the tools.
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() }) // @deepseek-ai/dsh-fs-local
await ctx.plugin(FileContext)                             // @deepseek-ai/dsh-file-context (policy gate)
await ctx.plugin(ToolFs)                                  // this package — registers read/write/edit

@deepseek-ai/dsh-file-context is optional: omit it and the tools run against the bare provider (unconditional write/overwrite/edit, no observed-state). A deployment that loads these tools is expected to also load it, so the behavior is read-before-write/edit.

Each tool also ships as a subpath plugin for focused deployments (each injects fs, not a policy service):

import * as readPlugin from '@deepseek-ai/dsh-tool-fs/read'
import * as writePlugin from '@deepseek-ai/dsh-tool-fs/write'
import * as editPlugin from '@deepseek-ai/dsh-tool-fs/edit'

Tools (schemas per the filesystem tool schemas RFC)

Tool Arguments Behavior
read file_path, offset?, limit? Line-numbered UTF-8 content with a pagination footer. offset is 1-based; limit defaults to and caps at 2000 lines.
write file_path, content Create or fully replace a file. With the policy plugin: overwriting an existing file requires a prior read at the unchanged version; creating a new file does not. Without it: unconditional.
edit file_path, non-empty old_string, new_string, replace_all? Literal replacement; unique match required unless replace_all is true. With the policy plugin: requires a prior read (any window) and the file unchanged since. Without it: unconditional.

Field names are snake_case to match Claude Code and existing harness tool schemas.

The tool is the executor; policy is an event gate

The tools do not inject a policy service or inspect any cache. Each tool resolves the path via ctx.fs.resolve(), then:

  • read — one ctx.fs.stat (type + size routing + version), then readText/streamText, then builds the line window, then emits a contained fs/observed. (1 stat.)
  • write — ctx.waterfall('fs/write-expectation', target, exec, () => undefined) for the optional guard, then ctx.fs.writeText(target, content, expectation), then fs/observed. (0 stat.)
  • edit — ctx.waterfall('fs/edit-expectation', target, exec, () => undefined) for the optional guard, then ctx.fs.editText(target, edit, expectation), then fs/observed. (0 stat.)

The tool passes exec (the tool-execution context) as the opaque actor on every dispatch. The default thunks return undefined (the unconstrained bare provider). When @deepseek-ai/dsh-file-context is loaded it occupies the single decision slot — returning createIfAbsent/replaceIfVersion/{ version } or throwing FS_NOT_OBSERVED — and records on fs/observed. Backend errors (FsError) and a thrown FS_NOT_OBSERVED flow through ToolRegistry.execute() and become isError tool results with their { name, code } attached.

fs/observed never fails the tool

fs/observed fires AFTER the read/write/edit already succeeded, so the tool wraps the emit in a try/catch (src/observe.ts) that logs and swallows a synchronous listener bug — otherwise a recording failure would turn a completed mutation into an isError. The event contract requires synchronous, side-effect-only listeners; this is the synchronous backstop, not async-error handling.

The line-windowing mechanics live in src/window.ts (Cordis-free, independently unit-tested); src/read.ts/write.ts/edit.ts are the tool executors and src/index.ts composes them.