diff --git a/docs/core-data-structures/filesystem.md b/docs/core-data-structures/filesystem.md index e384ffcc92..e13322dc5a 100644 --- a/docs/core-data-structures/filesystem.md +++ b/docs/core-data-structures/filesystem.md @@ -12,7 +12,6 @@ Every operation resolves a user-supplied path to an opaque backend target first. ```ts type-equiv interface FsTarget { - inputPath: string targetKey: FsTargetKey displayPath: string } @@ -81,8 +80,6 @@ interface FsEditRequest { ```ts type-equiv interface FsEditOutcome { - replacements: number - replaceAll: boolean version: FsVersion before: string after: string @@ -109,16 +106,14 @@ interface FsPolicyExec { ## Read outcome (consumer / read rendering) -A text read is bounded by line window, byte cap, and backend limits. The outcome the model-facing `read` tool renders carries the file's version at read time; there is no `full`/`partial` view — authorization is freshness-based, so any windowed read can authorize a later write/edit when the file is unchanged. Read windowing and this outcome shape live in `dsh-tool-fs` (the executor that owns the read), not in the policy plugin. +A text read is bounded by line window, byte cap, and backend limits. The outcome the model-facing `read` tool renders is purely presentational; there is no `full`/`partial` view — authorization is freshness-based (the tool emits `fs/observed` with the stat's version directly), so any windowed read can authorize a later write/edit when the file is unchanged. Read windowing and this outcome shape live in `dsh-tool-fs` (the executor that owns the read), not in the policy plugin. ```ts type-equiv interface FileReadOutcome { offset: number - limit: number lines: FileTextLine[] totalLines: number truncatedByBytes?: true - version: FsVersion } ``` diff --git a/docs/rfc/README.md b/docs/rfc/README.md index d53684bf6b..9ba9d5ccbc 100644 --- a/docs/rfc/README.md +++ b/docs/rfc/README.md @@ -58,7 +58,6 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | [Fold the stdio UI helper into the stdio app](proposed/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 | | [Prune dead core-spine surface — `SurfaceManager.invalidate()`, the loop-internal exports, `ToolExecutionResult.callId`](proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md) | 2026-07-04 | | [Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger)](proposed/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) | 2026-07-04 | -| [Prune write-only fields and a dead routing knob from the fs seam](proposed/simplification/2026-07-04-prune-write-only-fs-surface.md) | 2026-07-04 | | [Remove the `agent/steering` mirror emit](proposed/simplification/2026-07-04-remove-agent-steering-mirror.md) | 2026-07-04 | | [Share the app bins' boot glue instead of maintaining twin copies](proposed/simplification/2026-07-04-share-app-bin-boot-glue.md) | 2026-07-04 | | [Tighten the hook-protocol contract — dialect, discarded fields, double defaults, and lib-owned `hook/result` semantics](proposed/simplification/2026-07-04-tighten-hook-protocol-contract.md) | 2026-07-04 | @@ -120,6 +119,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | [Stop mirroring durable boundaries as agent events](implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md) | 2026-06-20 | | [Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin](implemented/simplification/2026-06-26-fsspec-style-fs-seam.md) | 2026-06-26 | | [Stop mirroring the token stream as an agent event](implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md) | 2026-07-02 | +| [Prune write-only fields and a dead routing knob from the fs seam](implemented/simplification/2026-07-04-prune-write-only-fs-surface.md) | 2026-07-04 | ### Architecture diff --git a/docs/rfc/proposed/simplification/2026-07-04-prune-write-only-fs-surface.md b/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md similarity index 59% rename from docs/rfc/proposed/simplification/2026-07-04-prune-write-only-fs-surface.md rename to docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md index 0a4bc14d89..1efb9c1ca3 100644 --- a/docs/rfc/proposed/simplification/2026-07-04-prune-write-only-fs-surface.md +++ b/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md @@ -1,27 +1,27 @@ # RFC: Prune write-only fields and a dead routing knob from the fs seam -Status: proposed +Status: implemented (proposed and accepted 2026-07-04) ## Problem -The [fs seam split](../../implemented/simplification/2026-06-26-fsspec-style-fs-seam.md) moved read routing and policy out of the backend into `dsh-tool-fs` and `dsh-fs-policy`. Four pieces of surface kept the pre-split shape — populated on every call, read by nobody: +The [fs seam split](2026-06-26-fsspec-style-fs-seam.md) moved read routing and policy out of the backend into `dsh-tool-fs` and `dsh-fs-policy`. Four pieces of surface kept the pre-split shape — populated on every call, read by nobody: -1. **`STREAM_MIN_SIZE` + `FsIoInternals.streamMinSize` in `dsh-fs-local`** (`packages/fs/fs-local/src/fsio.ts`, re-exported from `packages/fs/fs-local/src/index.ts`): zero readers anywhere, including fs-local's own source and tests. The backend has no read routing — `readWholeText`/`streamWholeText` are separate primitives the caller chooses between — and the real routing constant lives in the consumer (`packages/fs/tool-fs/src/read.ts`, compared against `info.size`). Two mirrors of the 10 MiB fact; the backend's is dead, and the knob's JSDoc claims a "read routing" override that does not exist. -2. **`FsTarget.inputPath`** (`packages/fs/fs/src/types.ts`): every backend and every test fake must fabricate a "diagnostics only" value with zero production readers — the policy plugin and every error message use `targetKey`/`displayPath`. The `listDir` producer exposes the semantic wobble: directory children get the bare entry name, which was nobody's "input". -3. **`FsEditOutcome.replacements` + `.replaceAll`** (`packages/fs/fs/src/types.ts`): `replacements` has zero production readers (the single-match policy itself stays — it is enforced by the `FS_AMBIGUOUS_EDIT`/`FS_EDIT_NOT_FOUND` throws inside the backend, whose error message keeps the internal count); `replaceAll` is read only by `formatEditOutput` in `packages/fs/tool-fs/src/edit.ts` — as an echo of the `replace_all` argument the tool already holds. Shrunk, `FsEditOutcome` becomes `{ version, before, after }`, parallel to `FsWriteOutcome`'s genuinely backend-discovered fields. -4. **`FileReadOutcome.limit` + `.version`** (`packages/fs/tool-fs/src/read-render.ts`): populated by the read tool, but `formatReadOutput` renders `offset`/`lines`/`totalLines`/`truncatedByBytes` only, and the `fs/observed` emit uses `info.version` directly rather than the outcome copy. +1. **`STREAM_MIN_SIZE` + `FsIoInternals.streamMinSize` in `dsh-fs-local`** (`packages/fs/fs-local/src/fsio.ts`, re-exported from `packages/fs/fs-local/src/index.ts`): zero readers anywhere, including fs-local's own source and tests. The backend has no read routing — `readWholeText`/`streamWholeText` are separate primitives the caller chooses between — and the real routing constant lives in the consumer (`packages/fs/tool-fs/src/read.ts`, compared against `info.size`). Two mirrors of the 10 MiB fact; the backend's was dead, and the knob's JSDoc claimed a "read routing" override that did not exist. +2. **`FsTarget.inputPath`** (`packages/fs/fs/src/types.ts`): every backend and every test fake had to fabricate a "diagnostics only" value with zero production readers — the policy plugin and every error message use `targetKey`/`displayPath`. The `listDir` producer exposed the semantic wobble: directory children got the bare entry name, which was nobody's "input". +3. **`FsEditOutcome.replacements` + `.replaceAll`** (`packages/fs/fs/src/types.ts`): `replacements` had zero production readers (the single-match policy itself stays — it is enforced by the `FS_AMBIGUOUS_EDIT`/`FS_EDIT_NOT_FOUND` throws inside the backend, whose error message keeps the internal count); `replaceAll` was read only by `formatEditOutput` in `packages/fs/tool-fs/src/edit.ts` — as an echo of the `replace_all` argument the tool already holds. Shrunk, `FsEditOutcome` is `{ version, before, after }`, parallel to `FsWriteOutcome`'s genuinely backend-discovered fields. +4. **`FileReadOutcome.limit` + `.version`** (`packages/fs/tool-fs/src/read-render.ts`): populated by the read tool, but `formatReadOutput` renders `offset`/`lines`/`totalLines`/`truncatedByBytes` only, and the `fs/observed` emit uses `info.version` directly rather than an outcome copy. -## Proposal +## Decision -Delete the fs-local constant, its re-export, and the `streamMinSize` knob (the remaining `FsIoInternals` knobs are genuinely used by the atomic-write tests); drop `inputPath` from `FsTarget`; shrink `FsEditOutcome` to `{ version, before, after }` and pass `replaceAll` to `formatEditOutput` from the parsed args; drop `limit`/`version` from `FileReadOutcome`. Update the [filesystem.md](../../../core-data-structures/filesystem.md) pastes, the type-equiv manifest, `packages/fs/fs/README.md`, and the test fakes that currently must fabricate the removed fields. +Delete the fs-local constant, its re-export, and the `streamMinSize` knob (the remaining `FsIoInternals` knobs are genuinely used by the atomic-write tests); drop `inputPath` from `FsTarget`; shrink `FsEditOutcome` to `{ version, before, after }` and pass `replaceAll` to `formatEditOutput` from the parsed args; drop `limit`/`version` from `FileReadOutcome`. The [filesystem.md](../../../core-data-structures/filesystem.md) pastes, `packages/fs/fs/README.md`, and the test fakes that had to fabricate the removed fields shrink with the types. ## Why not keep them? -A future permission/containment layer might want the pre-resolution path for error text — but it would want the *request*, which every call site still holds. "N occurrences replaced" might become model-facing text — a behavior change to design when wanted, and the backend-internal count survives for its error message. A read footer might display `limit` — everything the footer shows already derives from `lines`/`totalLines`. Meanwhile every current and future backend (remote, native) must fabricate wire fields nobody consumes, and every test fake must satisfy them. +A future permission/containment layer might want the pre-resolution path for error text — but it would want the *request*, which every call site still holds. "N occurrences replaced" might become model-facing text — a behavior change to design when wanted, and the backend-internal count survives for its error message. A read footer might display `limit` — everything the footer shows already derives from `lines`/`totalLines`. Meanwhile every current and future backend (remote, native) would have to fabricate wire fields nobody consumes, and every test fake would have to satisfy them. ## Acceptance criteria -- The removed surfaces are gone — `STREAM_MIN_SIZE`/`streamMinSize` in `dsh-fs-local`, `FsTarget.inputPath`, `FsEditOutcome.replacements`/`.replaceAll`, and `FileReadOutcome.limit`/`.version` — while the request-side `replaceAll` (`FsEditSpec`) and the version fields on the other outcome types are untouched; doc pastes and the manifest in sync; the suite is green with the shrunk fakes. +- The removed surfaces are gone — `STREAM_MIN_SIZE`/`streamMinSize` in `dsh-fs-local`, `FsTarget.inputPath`, `FsEditOutcome.replacements`/`.replaceAll`, and `FileReadOutcome.limit`/`.version` — while the request-side `replaceAll` (`FsEditRequest`) and the version fields on the other outcome types are untouched; doc pastes and the manifest in sync; the suite is green with the shrunk fakes. - `formatEditOutput`'s emitted text is unchanged for both `replace_all` branches, so no snapshot golden churns. ## Risks diff --git a/packages/fs/fs-local/src/fsio.ts b/packages/fs/fs-local/src/fsio.ts index 980cb4764d..b1b9c25397 100644 --- a/packages/fs/fs-local/src/fsio.ts +++ b/packages/fs/fs-local/src/fsio.ts @@ -26,9 +26,6 @@ import { basename, dirname, join, resolve } from 'node:path' import { TextDecoder } from 'node:util' import { FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs' -/** Files at or above this size stream their text; smaller files read whole. */ -export const STREAM_MIN_SIZE = 10 * 1024 * 1024 - const BINARY_SAMPLE_BYTES = 8192 function isENOENT(error: unknown): boolean { @@ -85,13 +82,11 @@ function versionOf(info: Stats): FsVersion { } /** - * Test seam: lets specs force the streaming read path (via a small - * `streamMinSize`) and pin the temp-file name (to prove exclusive-open - * behavior) without a 10 MB fixture or a name race. + * Test seam: lets specs pin the atomic-write temp names (to prove + * exclusive-open behavior without a name race) and observe the staged temp + * file before it is renamed over the target. */ export interface FsIoInternals { - /** Override {@link STREAM_MIN_SIZE} for read routing. */ - streamMinSize?: number /** Override the generated private staging-dir name (relative to the target dir). */ tempDirName?: (writePath: string) => string /** Override the generated temp-file name (relative to the private staging dir). */ diff --git a/packages/fs/fs-local/src/index.ts b/packages/fs/fs-local/src/index.ts index af74847ed1..0ad8365d4a 100644 --- a/packages/fs/fs-local/src/index.ts +++ b/packages/fs/fs-local/src/index.ts @@ -41,7 +41,6 @@ import { import type { FsIoInternals } from './fsio.ts' export { - STREAM_MIN_SIZE, applyLiteralEdit, listDirectory, probe, @@ -105,7 +104,7 @@ export class LocalFileSystem extends FileSystem { override async resolve(path: string, opts?: { cwd?: string }): Promise { const local = await resolveLocalTarget(opts?.cwd ?? this.config.cwd, path) - return { inputPath: path, targetKey: local.targetKey, displayPath: local.displayPath } + return { targetKey: local.targetKey, displayPath: local.displayPath } } override async stat(target: FsTarget, signal?: AbortSignal): Promise { @@ -128,7 +127,7 @@ export class LocalFileSystem extends FileSystem { return entries.map(entry => ({ name: entry.name, type: entry.type, - target: { inputPath: entry.name, targetKey: entry.target.targetKey, displayPath: entry.target.displayPath }, + target: { targetKey: entry.target.targetKey, displayPath: entry.target.displayPath }, ...(entry.version !== undefined ? { version: entry.version } : {}), ...(entry.size !== undefined ? { size: entry.size } : {}), })) @@ -211,8 +210,6 @@ export class LocalFileSystem extends FileSystem { const after = await probe(target.targetKey) return { - replacements: edited.replacements, - replaceAll: edit.replaceAll, version: this.versionAfterWrite(after, target), // The LF-normalized before/after text (the applied-hunk diff basis); // line-ending restoration is a storage detail the diff ignores. diff --git a/packages/fs/fs-local/tests/filesystem.spec.ts b/packages/fs/fs-local/tests/filesystem.spec.ts index 212074ddae..997021d346 100644 --- a/packages/fs/fs-local/tests/filesystem.spec.ts +++ b/packages/fs/fs-local/tests/filesystem.spec.ts @@ -138,12 +138,6 @@ describe('listDir', () => { join(dir, 'skills', 'dir-skill'), join(dir, 'skills', 'zeta.md'), ]) - expect(entries.map(entry => entry.target.inputPath)).toEqual([ - 'alpha.md', - 'broken-link', - 'dir-skill', - 'zeta.md', - ]) const materializedEntries = entries.filter(entry => entry.version !== undefined) expect(materializedEntries.map(entry => entry.target.targetKey)) .toEqual(await Promise.all(materializedEntries.map(entry => realpath(entry.target.displayPath)))) @@ -335,7 +329,7 @@ describe('editText', () => { await writeFile(join(dir, 'a.txt'), 'hello world') const target = await fs.resolve('a.txt') const outcome = await fs.editText(target, { oldString: 'world', newString: 'there', replaceAll: false }, { version: await versionOf(target) }) - expect(outcome.replacements).toBe(1) + expect(outcome.after).toBe('hello there') expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('hello there') }) @@ -365,7 +359,7 @@ describe('editText', () => { const target = await fs.resolve('a.txt') // No version guard: any current content is edited, regardless of version. const outcome = await fs.editText(target, { oldString: 'world', newString: 'there', replaceAll: false }) - expect(outcome.replacements).toBe(1) + expect(outcome.after).toBe('hello there') expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('hello there') }) @@ -411,7 +405,7 @@ describe('editText', () => { await writeFile(join(dir, 'a.txt'), 'a a a') const target = await fs.resolve('a.txt') const outcome = await fs.editText(target, { oldString: 'a', newString: 'b', replaceAll: true }, { version: await versionOf(target) }) - expect(outcome.replacements).toBe(3) + expect(outcome.after).toBe('b b b') expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('b b b') }) @@ -457,7 +451,7 @@ describe('editText', () => { // The version the first edit returned is a valid guard for a second edit — // no intervening re-stat needed. const second = await fs.editText(target, { oldString: 'two', newString: 'TWO', replaceAll: false }, { version: first.version }) - expect(second.replacements).toBe(1) + expect(second.after).toBe('ONE TWO') expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('ONE TWO') }) diff --git a/packages/fs/fs-policy/tests/policy.spec.ts b/packages/fs/fs-policy/tests/policy.spec.ts index 2ea61ffcd1..c41cf45701 100644 --- a/packages/fs/fs-policy/tests/policy.spec.ts +++ b/packages/fs/fs-policy/tests/policy.spec.ts @@ -18,7 +18,7 @@ import * as FsPolicy from '@deepseek-ai/dsh-fs-policy' import type { FsPolicyExec } from '@deepseek-ai/dsh-fs-policy' function target(path: string): FsTarget { - return { inputPath: path, targetKey: FsTargetKey(path), displayPath: path } + return { targetKey: FsTargetKey(path), displayPath: path } } const ownerExec = (session: object): FsPolicyExec => ({ agent: { session } }) diff --git a/packages/fs/fs/README.md b/packages/fs/fs/README.md index 4bd152cab9..7e7c7250de 100644 --- a/packages/fs/fs/README.md +++ b/packages/fs/fs/README.md @@ -19,7 +19,7 @@ A backend subclasses `FileSystem` and implements seven primitives. | Member | Semantics | |---|---| -| `resolve(path, opts?)` | Resolve a path into a stable `FsTarget` (`inputPath`, opaque `targetKey`, `displayPath`). `opts.cwd` is the base a relative `path` resolves against (a caller supplies its session workspace; absolute paths ignore it; omitted ⇒ the backend default). Async — a remote backend may need I/O. The same file via different paths must yield the same `targetKey`. | +| `resolve(path, opts?)` | Resolve a path into a stable `FsTarget` (opaque `targetKey`, `displayPath`). `opts.cwd` is the base a relative `path` resolves against (a caller supplies its session workspace; absolute paths ignore it; omitted ⇒ the backend default). Async — a remote backend may need I/O. The same file via different paths must yield the same `targetKey`. | | `stat(target, signal?)` | Return `FsInfo` metadata (`version`, `type`, optional `size`), or `undefined` when the target is absent. Never content. | | `readText(target, signal?)` | Read the whole regular text file as one decoded string. Owns regular-file checks, UTF-8 decoding, binary/NUL rejection (`FS_NOT_TEXT`). | | `streamText(target, signal?)` | Stream the same text as decoded chunks for large files (cross-chunk UTF-8 decoding stays here). | diff --git a/packages/fs/fs/src/types.ts b/packages/fs/fs/src/types.ts index ed946389d2..9351a373db 100644 --- a/packages/fs/fs/src/types.ts +++ b/packages/fs/fs/src/types.ts @@ -52,8 +52,6 @@ export function FsVersion(v: string): FsVersion { * this; every other operation takes it. */ export interface FsTarget { - /** The original model/plugin-supplied path, for diagnostics only. */ - inputPath: string /** Opaque key for stale guards and target lookup. */ targetKey: FsTargetKey /** @@ -142,10 +140,6 @@ export interface FsEditRequest { /** Outcome of a literal edit. */ export interface FsEditOutcome { - /** Number of literal replacements applied. */ - replacements: number - /** Whether every match was replaced. */ - replaceAll: boolean /** Opaque version of the file after the edit. */ version: FsVersion /** diff --git a/packages/fs/fs/tests/service.spec.ts b/packages/fs/fs/tests/service.spec.ts index d4edd9260f..86ba782c96 100644 --- a/packages/fs/fs/tests/service.spec.ts +++ b/packages/fs/fs/tests/service.spec.ts @@ -23,7 +23,7 @@ class FakeFileSystem extends FileSystem { files = new Map() override async resolve(path: string): Promise { - return { inputPath: path, targetKey: FsTargetKey(path), displayPath: path } + return { targetKey: FsTargetKey(path), displayPath: path } } override async stat(target: FsTarget): Promise { const content = this.files.get(target.targetKey) @@ -45,7 +45,7 @@ class FakeFileSystem extends FileSystem { { name: 'alpha.md', type: 'file', - target: { inputPath: 'skills/alpha.md', targetKey: FsTargetKey('skills/alpha.md'), displayPath: 'skills/alpha.md' }, + target: { targetKey: FsTargetKey('skills/alpha.md'), displayPath: 'skills/alpha.md' }, size: 2, version: FsVersion('v1'), }, @@ -60,7 +60,7 @@ class FakeFileSystem extends FileSystem { const content = this.files.get(target.targetKey) ?? '' const after = content.split(edit.oldString).join(edit.newString) this.files.set(target.targetKey, after) - return { replacements: 1, replaceAll: edit.replaceAll, version: FsVersion('v3'), before: content, after } + return { version: FsVersion('v3'), before: content, after } } } @@ -108,7 +108,7 @@ describe('FileSystem provider seam', () => { expect(entries).toEqual([{ name: 'alpha.md', type: 'file', - target: { inputPath: 'skills/alpha.md', targetKey: 'skills/alpha.md', displayPath: 'skills/alpha.md' }, + target: { targetKey: 'skills/alpha.md', displayPath: 'skills/alpha.md' }, size: 2, version: 'v1', }]) diff --git a/packages/fs/tool-fs/src/edit.ts b/packages/fs/tool-fs/src/edit.ts index 1a39cb63dd..5ede545b03 100644 --- a/packages/fs/tool-fs/src/edit.ts +++ b/packages/fs/tool-fs/src/edit.ts @@ -16,7 +16,6 @@ import type { Context } from 'cordis' import { defineTool } from '@deepseek-ai/dsh-tools' import type { DiffCallView, DiffResultView, ToolResult } from '@deepseek-ai/dsh-tools' import type { ContentBlock } from '@deepseek-ai/dsh-llm' -import type { FsEditOutcome } from '@deepseek-ai/dsh-fs' import type {} from '@deepseek-ai/dsh-fs' import type {} from '@deepseek-ai/dsh-system-prompt' import { computeHunkDiffs, diffsFromMeta, type FsDiffMeta } from './diff.ts' @@ -43,9 +42,9 @@ export function parseEditArgs(args: { file_path: string; old_string: string; new } } -/** Format an edit outcome as a Claude-style model-facing success message. */ -export function formatEditOutput(displayPath: string, outcome: FsEditOutcome): string { - return outcome.replaceAll +/** Format an edit success (single-match or replace-all) as a Claude-style model-facing message. */ +export function formatEditOutput(displayPath: string, replaceAll: boolean): string { + return replaceAll ? `The file ${displayPath} has been updated. All occurrences were successfully replaced.` : `The file ${displayPath} has been updated successfully.` } @@ -91,7 +90,7 @@ export function applyEditTool(ctx: Context): void { // relativizes it). const diffs = computeHunkDiffs(input.filePath, outcome.before, outcome.after) return { - content: [{ type: 'text', text: formatEditOutput(target.displayPath, outcome) }], + content: [{ type: 'text', text: formatEditOutput(target.displayPath, input.replaceAll) }], meta: { diffs }, } }, diff --git a/packages/fs/tool-fs/src/read-render.ts b/packages/fs/tool-fs/src/read-render.ts index 97a1384792..4dbbb6b77b 100644 --- a/packages/fs/tool-fs/src/read-render.ts +++ b/packages/fs/tool-fs/src/read-render.ts @@ -17,7 +17,6 @@ */ import { FsError } from '@deepseek-ai/dsh-fs' -import type { FsVersion } from '@deepseek-ai/dsh-fs' /** Maximum characters returned for a single line. */ export const READ_MAX_LINE_LENGTH = 2000 @@ -58,16 +57,12 @@ export interface WindowResult { export interface FileReadOutcome { /** 1-based first line requested. */ offset: number - /** Maximum number of lines requested. */ - limit: number /** Returned lines, already numbered. */ lines: FileTextLine[] /** Total line count in the file, unless `truncatedByBytes` stopped scanning early. */ totalLines: number /** Whether selected output hit the byte cap before EOF or the requested limit. */ truncatedByBytes?: true - /** Opaque version of the file at read time. */ - version: FsVersion } interface WindowAccumulator { diff --git a/packages/fs/tool-fs/src/read.ts b/packages/fs/tool-fs/src/read.ts index c984b53c9f..094a2abd27 100644 --- a/packages/fs/tool-fs/src/read.ts +++ b/packages/fs/tool-fs/src/read.ts @@ -90,10 +90,8 @@ export function applyReadTool(ctx: Context): void { const outcome: FileReadOutcome = { offset: input.offset, - limit: input.limit, lines: window.lines, totalLines: window.totalLines, - version: info.version, ...window.truncatedByBytes ? { truncatedByBytes: true } : {}, } // Record the observed version (a no-op when no policy plugin listens). The diff --git a/packages/fs/tool-fs/tests/tools.spec.ts b/packages/fs/tool-fs/tests/tools.spec.ts index 6272ac5c9d..2049063549 100644 --- a/packages/fs/tool-fs/tests/tools.spec.ts +++ b/packages/fs/tool-fs/tests/tools.spec.ts @@ -40,7 +40,7 @@ class FakeFs extends FileSystem { } override async resolve(path: string): Promise { - return { inputPath: path, targetKey: FsTargetKey(`key:${path}`), displayPath: `/abs/${path}` } + return { targetKey: FsTargetKey(`key:${path}`), displayPath: `/abs/${path}` } } override async stat(target: FsTarget): Promise { this.throwIfArmed() @@ -71,7 +71,7 @@ class FakeFs extends FileSystem { const content = this.files.get(target.targetKey) ?? '' const after = content.split(edit.oldString).join(edit.newString) this.files.set(target.targetKey, after) - return { replacements: 1, replaceAll: edit.replaceAll, version: FsVersion('v3'), before: content, after } + return { version: FsVersion('v3'), before: content, after } } } @@ -252,7 +252,7 @@ describe('read tool', () => { }) describe('formatReadOutput footer variants', () => { - const base: FileReadOutcome = { offset: 1, limit: 2000, lines: [{ number: 1, text: 'x' }], totalLines: 1, version: FsVersion('v') } + const base: FileReadOutcome = { offset: 1, lines: [{ number: 1, text: 'x' }], totalLines: 1 } it('reports a byte-capped read', () => { const out = formatReadOutput('/f', { ...base, totalLines: 99, truncatedByBytes: true })