refactor(tool-fs): drop per-tool subpath plugins; adopt single-tsconfig build

Adapt the four fs packages to master's single-tsconfig build convention
(lib/types outDir + types path + files allowlist), brought in by the merge.

While doing so, drop dsh-tool-fs's /read//write//edit subpath plugins. They
were the only subpath-export package in the tree and forced bespoke tsdown,
tsconfig path, package.json files, and workspace-constraint handling that no
sibling tool package (e.g. dsh-tool-bash) carries, for a focused-deployment
use case no consumer needed. dsh-tool-fs is now a single root plugin that
registers read/write/edit, mirroring dsh-tool-bash; the per-tool registration
helpers stay internal modules the root composes. The file-context event-gate
RFC is amended to record the narrowed scope.
This commit is contained in:
Dudu-0223
2026-06-28 17:43:29 +08:00
parent 89b27ad442
commit 4a1177093a
16 changed files with 34 additions and 189 deletions

View File

@@ -49,7 +49,7 @@ The filesystem seam uses the same dependency direction as the bash trio:
`@deepseek-ai/dsh-tool-fs` depends on `@deepseek-ai/dsh-fs`, `@deepseek-ai/dsh-tools`, `@deepseek-ai/dsh-system-prompt`, and `cordis`. It registers model-facing tools and prompt sections. It must not import `node:fs`, `node:path`, or `@deepseek-ai/dsh-fs-local`; filesystem execution always goes through `ctx.fs`. If the implementation needs concrete agent or session helper types, those dependencies belong in `tool-fs`; they must not leak back into `dsh-fs`.
The root `tool-fs` plugin registers the full filesystem tool suite by composing the per-tool registration helpers (`read`, `write`, and `edit`). The same helpers are exposed as subpath plugins such as `@deepseek-ai/dsh-tool-fs/read`, `@deepseek-ai/dsh-tool-fs/write`, and `@deepseek-ai/dsh-tool-fs/edit` for focused deployments. Root and subpath plugins follow the same rule: they inject `fs` and never import an implementation package.
The root `tool-fs` plugin registers the full filesystem tool suite (`read`, `write`, and `edit`) by composing the per-tool registration helpers. It injects `fs` and never imports an implementation package.
## `ctx.fs` contract
@@ -117,7 +117,7 @@ The tool package must keep model-facing contracts stable when backends change. A
The first implementation requires a prior full `read` before updating an existing file with `write` or `edit`. `tool-fs` does not implement this by checking whether a tool named `read` ran or by reading the file-state cache. It passes the current execution context to `ctx.fs`, and `ctx.fs` derives the file-state owner and enforces file-state/stale-version policy. Creating a new file with `write` does not require prior state or an owner.
The root plugin registers the full suite by composing the per-tool registration helpers. The subpath plugins register one tool each for focused deployments and tests. Both forms inject `fs`, `tools`, and `systemPrompt`.
The root plugin registers the full suite by composing the per-tool registration helpers. It injects `fs`, `tools`, and `systemPrompt`.
## Migration plan
@@ -128,7 +128,7 @@ This RFC starts from `origin/master`, where no filesystem tool package exists ye
3. Add `packages/fs/tool-fs` with the model-facing `read`, `write`, and `edit` tools over `ctx.fs`.
4. Update `docs/architecture.md`, `packages/README.md`, package READMEs, build/typecheck config, and aggregate maintenance scripts such as `scripts/publint-all.ts`.
This first pass does not add a separate `@deepseek-ai/dsh-file-context` package. The file-state store lives behind `ctx.fs` so root and subpath `tool-fs` plugins share the same read-before-write/edit policy automatically.
This first pass does not add a separate `@deepseek-ai/dsh-file-context` package. The file-state store lives behind `ctx.fs` so the `tool-fs` plugin gets the read-before-write/edit policy automatically.
Example leaf configs stay bash-only in this landing. Wiring `examples/coding-agent` or `examples/acp-agent` to `dsh-fs-local` + `dsh-tool-fs` changes the model prompt, visible tool schemas, and ACP snapshot transcript, so it should land as a follow-up UX/example change with prompt and snapshot updates in the same PR.
@@ -156,7 +156,7 @@ Beyond the happy/sad paths above, `dsh-fs-local` tests must cover the defensive-
- **Concurrency / stale races.** The RFC names edit as race-prone (see Risks). Test that two concurrent write/edit operations against the same target settle deterministically: one succeeds and the other is rejected with `FS_STALE_VERSION` rather than silently overwriting, and that a successful edit refreshes recorded state so an immediately-following edit by the same owner proceeds.
- **HMR safety and disposal.** `dsh-fs-local` registers `ctx.fs` and owns the in-memory file-state store, so it needs its own HMR-safety test (register the backend on a fiber, dispose it, assert the `ctx.fs` provider is withdrawn and the file-state store is released — a later provider starts with no inherited state).
`dsh-tool-fs` tests cover the consumer surface with a fake `ctx.fs` implementation. They should verify tool schemas, argument validation, prompt-section registration, formatting of successful results, propagation of backend `FsError` codes into `isError` tool results through `ctx.tools.execute()`, that read/write/edit pass the current execution context or structural projection through to `ctx.fs`, root-plugin suite registration, subpath plugin registration, and HMR cleanup.
`dsh-tool-fs` tests cover the consumer surface with a fake `ctx.fs` implementation. They should verify tool schemas, argument validation, prompt-section registration, formatting of successful results, propagation of backend `FsError` codes into `isError` tool results through `ctx.tools.execute()`, that read/write/edit pass the current execution context or structural projection through to `ctx.fs`, root-plugin suite registration, and HMR cleanup.
Integration tests should load `dsh-fs-local` plus `dsh-tool-fs` and execute `read`, `write`, and `edit` through `ctx.tools.execute()` to prove the three packages work together without bypassing the tool registry. They must verify the world, not the tool's self-report: after a `write`/`edit`, read the file back from disk and assert byte-identical content (and that untouched files are unchanged), rather than trusting the returned `ContentBlock[]`. Each integration/e2e test owns its resources — create the harness in the test, run against a per-test temporary directory, and dispose the harness and remove the directory in `afterEach` even on failure or timeout.

View File

@@ -112,9 +112,9 @@ The `fs/*` decision events are **unbound waterfalls dispatched by the tool** (li
The tool keeps its model-facing schemas (`read`/`write`/`edit`, byte-for-byte unchanged) and prompt sections. The prompt guidance stays policy-first because a deployment loading the fs tools is expected to also load `dsh-file-context`: the model is still told to read before overwriting or editing, and any wording that says the "backend" requires that should be corrected to say the file-context policy requires it. The bare-provider fallback does not change the prompt stance.
`dsh-tool-fs` gains the executor responsibilities relocated from the old `fileContext` method service, including **read windowing** (`window.ts`, `READ_MAX_BYTES`, `READ_MAX_LINE_LENGTH`, `FileReadRequest`/`FileReadOutcome`/`FileTextLine`, `STREAM_MIN_SIZE`), which is the tool's rendering detail now that the tool owns the read. Those read-windowing types and helpers move into `dsh-tool-fs`; the policy plugin must not remain a type dependency for the tool.
`dsh-tool-fs` gains the executor responsibilities relocated from the old `fileContext` method service, including **read windowing** (`window.ts`, `READ_MAX_BYTES`, `READ_MAX_LINE_LENGTH`, `FileReadOutcome`/`FileTextLine`, `STREAM_MIN_SIZE`), which is the tool's rendering detail now that the tool owns the read. Those read-windowing types and helpers move into `dsh-tool-fs`; the policy plugin must not remain a type dependency for the tool.
`dsh-tool-fs` exposes each tool as a first-class **subpath plugin** (`/read`, `/write`, `/edit`) for focused deployments, plus a root plugin that composes all three. The `inject` change applies to **all four**: each of `read.ts`, `write.ts`, `edit.ts`, and `index.ts` drops `fileContext` from `inject` and adds `fs` (keeping `tools`/`systemPrompt`). Updating only the root plugin would leave a focused deployment that loads just `@deepseek-ai/dsh-tool-fs/edit` still coupled to the old method service, silently breaking the decoupling contract for exactly the deployments subpaths exist to serve.
`dsh-tool-fs` is a single root plugin that registers all three tools (`read`/`write`/`edit`), mirroring `dsh-tool-bash`. It injects `fs` (plus `tools`/`systemPrompt`), never `fileContext`. (The original proposal also exposed each tool as a `/read`/`/write`/`/edit` subpath plugin for focused deployments; that was dropped on implementation — no consumer needed a single-tool deployment, and the subpath publishing forced bespoke `tsdown`/`tsconfig`/`files`/workspace-constraint handling no sibling tool package carries. The per-tool registration helpers (`applyReadTool`/`applyWriteTool`/`applyEditTool`) remain internal modules the root plugin composes.)
`stat` budget is minimized by letting the waterfall produce the expectation lazily — the bare default returns `undefined` (no guard) and never stats:
@@ -154,10 +154,10 @@ This amends — does not reverse — [the split-fs-seam RFC](../simplification/2
## Acceptance Criteria
- All four `dsh-tool-fs` injection points — the root plugin AND the `/read`, `/write`, `/edit` subpath plugins — inject `fs` (+ `tools`/`systemPrompt`), not `fileContext`; each calls `ctx.fs` directly and dispatches the `fs/write-expectation`/`fs/edit-expectation` waterfalls (passing `exec` as the actor) and the contained `fs/observed` emit. Read windowing lives in `dsh-tool-fs`.
- The `dsh-tool-fs` root plugin injects `fs` (+ `tools`/`systemPrompt`), not `fileContext`; it calls `ctx.fs` directly and dispatches the `fs/write-expectation`/`fs/edit-expectation` waterfalls (passing `exec` as the actor) and the contained `fs/observed` emit. Read windowing lives in `dsh-tool-fs`. (No subpath plugins — see the Tool contract above.)
- `dsh-fs` declares the three events with `@mode` tags and an opaque `object` actor argument (no agent/session structure leaks into the provider vocabulary); the generated cordis catalog is regenerated.
- `dsh-file-context` is a plugin, not a service: it does not register `ctx.fileContext`, has no public `read`/`write`/`edit`/`resolve` methods, and does not inject `fs`; it registers the three listeners, keeps observed-state, and has HMR/disposal coverage (dispose the fiber, assert the gate no longer rewrites).
- **Bare-provider test**: a config WITHOUT `dsh-file-context` that loads a **subpath plugin** (e.g. just `@deepseek-ai/dsh-tool-fs/edit`, plus `/read`/`/write` as the scenario needs) boots, and `read`/`write`(create AND overwrite)/`edit` work through `dsh-tool-fs` against the real `dsh-fs-local`; an `edit` of an unread existing file and an overwrite of an existing unread file both succeed (unconditional bare-provider behavior), proving the subpath plugins — not just the root — carry no `fileContext` dependency. A bare-provider edit of a missing target reports `FS_STALE_VERSION`. With `dsh-file-context` present, the same unread `edit` is rejected `FS_NOT_OBSERVED` and the same unread overwrite uses `createIfAbsent` (rejected on an existing file).
- **Bare-provider test**: a config WITHOUT `dsh-file-context` boots the `dsh-tool-fs` root plugin, and `read`/`write`(create AND overwrite)/`edit` work against the real `dsh-fs-local`; an `edit` of an unread existing file and an overwrite of an existing unread file both succeed (unconditional bare-provider behavior), proving the tool carries no `fileContext` dependency. A bare-provider edit of a missing target reports `FS_STALE_VERSION`. With `dsh-file-context` present, the same unread `edit` is rejected `FS_NOT_OBSERVED` and the same unread overwrite uses `createIfAbsent` (rejected on an existing file).
- **Single-slot semantics**: a test registers a second `fs/edit-expectation` listener AFTER `dsh-file-context` and asserts it is NOT reached (first-wins short-circuit), and documents in a comment that a decider registered before/`prepend`ed would instead win — the slot is first-wins by convention, not an enforced invariant.
- **Contained observed recording**: a test with a synchronously throwing `fs/observed` listener performs a write/edit and asserts the tool result is still success (the completed mutation is not turned into an `isError`). The event contract requires synchronous side-effect-only listeners; the try/catch is the synchronous backstop, not async rejection handling.
- `dsh-fs` `writeText`/`editText` make `expected` optional (omit ⇒ unconditional); the `FsWriteExpectation` union is unchanged, and `dsh-file-context`'s guarded paths (`createIfAbsent`/`replaceIfVersion`/`{ version }`) behave exactly as today. A bare-provider test exercises an unconditional overwrite, an unconditional edit, and a missing-target edit reporting `FS_STALE_VERSION`.

View File

@@ -100,7 +100,7 @@ The following are deliberately out of scope for the first filesystem schema pass
- `edit` requires `file_path`, `old_string`, and `new_string`, accepts optional boolean `replace_all`, rejects empty `old_string`, and defaults `replace_all` to false.
- The registered JSON schemas use the snake_case field names in this RFC.
- The tool descriptions accurately describe that existing-file `write` and `edit` require a prior full read in the same execution context, while new-file `write` does not.
- The root plugin and subpath plugins register the same schemas.
- The `tool-fs` root plugin registers all three schemas.
Integration tests should execute `read`, `write`, and `edit` through `ctx.tools.execute()` with a fake or local `ctx.fs` provider and verify that model arguments are translated into the expected `ctx.fs` calls.