Files
deepseek-harness/packages/fs/tool-fs
Tianyi Cui e64623ebfd refactor(fs): prune write-only fields and the dead routing knob from the seam
The fs seam split left four pieces of pre-split surface populated on
every call and read by nobody:

- STREAM_MIN_SIZE + FsIoInternals.streamMinSize in dsh-fs-local: the
  backend has no read routing (readWholeText/streamWholeText are
  separate primitives the caller picks), and the real 10 MiB routing
  constant lives in dsh-tool-fs's read tool. Delete the dead mirror and
  the knob whose JSDoc claimed an override that did not exist; the
  remaining FsIoInternals knobs stay (the atomic-write tests use them).
- FsTarget.inputPath: a "diagnostics only" field every backend and test
  fake had to fabricate, with zero production readers (policy and error
  messages use targetKey/displayPath). listDir gave children the bare
  entry name, which was nobody's input.
- FsEditOutcome.replacements/.replaceAll: replacements had no reader
  (the single-match policy is enforced by the FS_AMBIGUOUS_EDIT /
  FS_EDIT_NOT_FOUND throws, whose message keeps the internal count);
  replaceAll only echoed the replace_all argument back to
  formatEditOutput, which now takes it from the parsed args. The
  outcome shrinks to { version, before, after }, parallel to
  FsWriteOutcome's backend-discovered fields. Emitted text is unchanged
  for both branches (no snapshot churn).
- FileReadOutcome.limit/.version: formatReadOutput renders
  offset/lines/totalLines/truncatedByBytes only, and the fs/observed
  emit uses info.version directly.

Backends shed four fabrication obligations and gain none. Doc pastes
(core-data-structures/filesystem.md), the dsh-fs README resolve row,
and the test fakes shrink with the types. RFC moved to
implemented/simplification and amended to the shipped shape
(FsEditSpec -> FsEditRequest name fix; manifest rows needed no change).
2026-07-04 15:37:43 +08:00
..

@deepseek-ai/dsh-tool-fs

The model-facing filesystem toolsread, 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-fs-policy) 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(FsPolicy)                             // @deepseek-ai/dsh-fs-policy (policy gate)
await ctx.plugin(ToolFs)                                  // this package — registers read/write/edit

@deepseek-ai/dsh-fs-policy 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.

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(path, { cwd }) — passing the calling agent's session cwd (exec.agent.session.header.cwd) so a relative path resolves against the session's workspace, matching dsh-tool-bash (see the per-session cwd RFC) — then:

  • read — one ctx.fs.stat (type + size routing + version), then readText/streamText, then builds the line window, then emits fs/observed with a plain ctx.emit. (1 stat.)
  • writectx.waterfall('fs/write-intent', target, exec, () => undefined) for the optional guard, then ctx.fs.writeText(target, content, intent), then fs/observed. (0 stat.)
  • editctx.waterfall('fs/edit-intent', target, exec, () => undefined) for the optional guard, then ctx.fs.editText(target, edit, intent), 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-fs-policy 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 is fire-and-forget

fs/observed fires AFTER the read/write/edit already succeeded, via a plain ctx.emit. A listener is contractually a synchronous, side-effect-only recorder (@deepseek-ai/dsh-fs-policy's is a WeakMap.set); the tool does not guard the emit, so a listener that throws would surface as the tool's isError result — async or fallible observation does not belong on this event.

The read rendering (line windowing + output formatting) lives in src/read-render.ts (Cordis-free, independently unit-tested); src/read.ts/write.ts/edit.ts are the tool executors and src/index.ts composes them.