diff --git a/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md b/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md index 4faae84d83..0a4365bc82 100644 --- a/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md +++ b/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md @@ -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. diff --git a/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md b/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md index 999f795a08..23460b211f 100644 --- a/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md +++ b/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md @@ -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`. diff --git a/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md b/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md index fa572d4a0b..6e510c69b1 100644 --- a/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md +++ b/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md @@ -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. diff --git a/packages/fs/file-context/package.json b/packages/fs/file-context/package.json index 5d05d09f13..16ee567305 100644 --- a/packages/fs/file-context/package.json +++ b/packages/fs/file-context/package.json @@ -15,7 +15,9 @@ "./package.json": "./package.json" }, "files": [ - "lib", + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", diff --git a/packages/fs/fs-local/package.json b/packages/fs/fs-local/package.json index f1073981ff..4945684713 100644 --- a/packages/fs/fs-local/package.json +++ b/packages/fs/fs-local/package.json @@ -15,7 +15,9 @@ "./package.json": "./package.json" }, "files": [ - "lib", + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", diff --git a/packages/fs/fs/package.json b/packages/fs/fs/package.json index 65e71108a1..395816dbf9 100644 --- a/packages/fs/fs/package.json +++ b/packages/fs/fs/package.json @@ -15,7 +15,9 @@ "./package.json": "./package.json" }, "files": [ - "lib", + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", diff --git a/packages/fs/tool-fs/README.md b/packages/fs/tool-fs/README.md index 787a2cca9a..86298031de 100644 --- a/packages/fs/tool-fs/README.md +++ b/packages/fs/tool-fs/README.md @@ -11,14 +11,6 @@ await ctx.plugin(ToolFs) // this package — re `@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): - -```ts ignore-check -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](../../../docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md)) | Tool | Arguments | Behavior | diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json index a726bde6d4..5323bbadcf 100644 --- a/packages/fs/tool-fs/package.json +++ b/packages/fs/tool-fs/package.json @@ -11,23 +11,13 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, - "./read": { - "types": "./lib/types/read.d.ts", - "default": "./lib/read.js" - }, - "./write": { - "types": "./lib/types/write.d.ts", - "default": "./lib/write.js" - }, - "./edit": { - "types": "./lib/types/edit.d.ts", - "default": "./lib/edit.js" - }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ - "lib", + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", diff --git a/packages/fs/tool-fs/src/edit.ts b/packages/fs/tool-fs/src/edit.ts index d3725db1af..53029808ec 100644 --- a/packages/fs/tool-fs/src/edit.ts +++ b/packages/fs/tool-fs/src/edit.ts @@ -10,7 +10,7 @@ * tool stats ZERO times either way; a missing target is reported by the provider * as `FS_STALE_VERSION`. * - * @module @deepseek-ai/dsh-tool-fs/edit + * @module @deepseek-ai/dsh-tool-fs/src/edit */ import type { Context } from 'cordis' @@ -50,7 +50,7 @@ export function formatEditOutput(displayPath: string, outcome: FsEditOutcome): s } /** Register the `edit` tool and its system-prompt guidance. */ -export function apply(ctx: Context): void { +export function applyEditTool(ctx: Context): void { ctx.systemPrompt.section({ name: 'tool:edit', order: 102, @@ -84,12 +84,3 @@ export function apply(ctx: Context): void { }, })) } - -/** Cordis plugin name used by loader diagnostics. */ -export const name = 'fs-edit' - -/** Services required by the `edit` tool plugin. */ -export const inject = ['tools', 'fs', 'systemPrompt'] - -/** Named helper for direct registration in the root plugin and tests. */ -export const applyEditTool = apply diff --git a/packages/fs/tool-fs/src/index.ts b/packages/fs/tool-fs/src/index.ts index b57810185e..8c2123c3d4 100644 --- a/packages/fs/tool-fs/src/index.ts +++ b/packages/fs/tool-fs/src/index.ts @@ -1,9 +1,6 @@ /** * The model-facing filesystem tool suite (`read`, `write`, `edit`) over the - * `ctx.fs` provider seam. This root plugin registers all three tools by - * composing the per-tool registration helpers; each tool is also exposed as a - * subpath plugin (`@deepseek-ai/dsh-tool-fs/read`, `/write`, `/edit`) for focused - * deployments. + * `ctx.fs` provider seam. This single plugin registers all three tools. * * ## The tool is the executor; policy is an event gate * diff --git a/packages/fs/tool-fs/src/read.ts b/packages/fs/tool-fs/src/read.ts index bc12068553..54fd553011 100644 --- a/packages/fs/tool-fs/src/read.ts +++ b/packages/fs/tool-fs/src/read.ts @@ -8,7 +8,7 @@ * the model-facing schema, argument validation, read windowing, and result * formatting; the freshness/observation policy is not its concern. * - * @module @deepseek-ai/dsh-tool-fs/read + * @module @deepseek-ai/dsh-tool-fs/src/read */ import type { Context } from 'cordis' @@ -72,7 +72,7 @@ ${body} } /** Register the `read` tool and its system-prompt guidance. */ -export function apply(ctx: Context): void { +export function applyReadTool(ctx: Context): void { ctx.systemPrompt.section({ name: 'tool:read', order: 100, @@ -119,12 +119,3 @@ export function apply(ctx: Context): void { }, })) } - -/** Cordis plugin name used by loader diagnostics. */ -export const name = 'fs-read' - -/** Services required by the `read` tool plugin. */ -export const inject = ['tools', 'fs', 'systemPrompt'] - -/** Named helper for direct registration in the root plugin and tests. */ -export const applyReadTool = apply diff --git a/packages/fs/tool-fs/src/write.ts b/packages/fs/tool-fs/src/write.ts index e2b44ee78a..407744ec03 100644 --- a/packages/fs/tool-fs/src/write.ts +++ b/packages/fs/tool-fs/src/write.ts @@ -8,7 +8,7 @@ * returns `createIfAbsent`/`replaceIfVersion` instead. The tool stats ZERO * times either way. * - * @module @deepseek-ai/dsh-tool-fs/write + * @module @deepseek-ai/dsh-tool-fs/src/write */ import type { Context } from 'cordis' @@ -36,7 +36,7 @@ ${verb} file } /** Register the `write` tool and its system-prompt guidance. */ -export function apply(ctx: Context): void { +export function applyWriteTool(ctx: Context): void { ctx.systemPrompt.section({ name: 'tool:write', order: 101, @@ -62,12 +62,3 @@ export function apply(ctx: Context): void { }, })) } - -/** Cordis plugin name used by loader diagnostics. */ -export const name = 'fs-write' - -/** Services required by the `write` tool plugin. */ -export const inject = ['tools', 'fs', 'systemPrompt'] - -/** Named helper for direct registration in the root plugin and tests. */ -export const applyWriteTool = apply diff --git a/packages/fs/tool-fs/tests/integration.spec.ts b/packages/fs/tool-fs/tests/integration.spec.ts index 696741c334..cc99d52b80 100644 --- a/packages/fs/tool-fs/tests/integration.spec.ts +++ b/packages/fs/tool-fs/tests/integration.spec.ts @@ -5,10 +5,9 @@ * * - DEFAULT — with the real `dsh-file-context` policy gate plugin: read-before- * write/edit, version-guarded mutation, FS_NOT_OBSERVED for unread edits. - * - BARE — WITHOUT the policy plugin, loading only SUBPATH plugins: every - * `fs/*` waterfall falls through to its undefined default, so write/edit are - * unconditional. This proves the subpaths (not just the root) carry no policy - * dependency. + * - BARE — WITHOUT the policy plugin: every `fs/*` waterfall falls through to + * its undefined default, so write/edit are unconditional. This proves the + * tool carries no dependency on the policy plugin. * * These verify the WORLD — files are read back from disk and asserted * byte-for-byte — not the tool's self-report. @@ -25,9 +24,6 @@ import ToolRegistry from '@deepseek-ai/dsh-tools' import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local' import * as FileContext from '@deepseek-ai/dsh-file-context' import * as ToolFs from '@deepseek-ai/dsh-tool-fs' -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' let dir: string let ctx: Context @@ -243,18 +239,16 @@ describe('default deployment (with dsh-file-context)', () => { }) // -------------------------------------------------------------------------- -// BARE deployment: SUBPATH plugins only, NO policy gate. +// BARE deployment: the tool suite WITHOUT the policy gate. // -------------------------------------------------------------------------- -describe('bare provider (subpath plugins, no dsh-file-context)', () => { +describe('bare provider (no dsh-file-context)', () => { beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), 'dsh-tool-fs-bare-')) ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(LocalFileSystem, { cwd: dir }) - await ctx.plugin(readPlugin) - await ctx.plugin(writePlugin) - fiber = await ctx.plugin(editPlugin) + fiber = await ctx.plugin(ToolFs) }) it('read works (it never needed policy)', async () => { diff --git a/packages/fs/tool-fs/tests/subpaths.spec.ts b/packages/fs/tool-fs/tests/subpaths.spec.ts deleted file mode 100644 index 32a276ca25..0000000000 --- a/packages/fs/tool-fs/tests/subpaths.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Tests for the per-tool subpath plugins (`@deepseek-ai/dsh-tool-fs/read`, - * `/write`, `/edit`): each registers exactly one tool, injects the same services - * (`tools`, `fs`, `systemPrompt`) — NOT a policy service — and cleans up on - * disposal. They boot over the bare `ctx.fs` provider with NO - * `@deepseek-ai/dsh-file-context`, proving each subpath carries no policy-plugin - * dependency. - */ - -import { describe, expect, it } from 'vitest' -import { Context } from 'cordis' -import SystemPrompt from '@deepseek-ai/dsh-system-prompt' -import ToolRegistry from '@deepseek-ai/dsh-tools' -import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs' -import type { - FsEditOutcome, - FsInfo, - FsTarget, - FsWriteOutcome, -} from '@deepseek-ai/dsh-fs' -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' - -class StubFs extends FileSystem { - override async resolve(path: string): Promise { - return { inputPath: path, targetKey: FsTargetKey(path), displayPath: path } - } - override async stat(): Promise { - return { version: FsVersion('v'), type: 'file', size: 0 } - } - override async readText(): Promise { - return '' - } - override async streamText(): Promise> { - return (async function* () { yield '' })() - } - override async writeText(): Promise { - return { operation: 'create', version: FsVersion('v') } - } - override async editText(): Promise { - return { replacements: 1, replaceAll: false, version: FsVersion('v') } - } -} - -async function base() { - const ctx = new Context() - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRegistry) - await ctx.plugin(StubFs) - return ctx -} - -describe('subpath plugins', () => { - it('each registers exactly its one tool (over the bare provider, no policy plugin)', async () => { - const cases: Array<[unknown, string]> = [ - [readPlugin, 'read'], - [writePlugin, 'write'], - [editPlugin, 'edit'], - ] - for (const [plugin, toolName] of cases) { - const ctx = await base() - await ctx.plugin(plugin as Parameters[0]) - expect(ctx.tools.schemas().map(s => s.name)).toEqual([toolName]) - } - }) - - it('cleans up on disposal (HMR safety)', async () => { - const ctx = await base() - const fiber = await ctx.plugin(readPlugin as Parameters[0]) - expect(ctx.tools.schemas()).toHaveLength(1) - await fiber.dispose() - expect(ctx.tools.schemas()).toHaveLength(0) - }) - - it('stays pending without a ctx.fs provider', async () => { - const ctx = new Context() - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRegistry) - await ctx.plugin(writePlugin as Parameters[0]) - expect(ctx.tools.schemas()).toHaveLength(0) - }) -}) diff --git a/packages/fs/tool-fs/tsdown.config.ts b/packages/fs/tool-fs/tsdown.config.ts deleted file mode 100644 index ef07bcf108..0000000000 --- a/packages/fs/tool-fs/tsdown.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineConfig } from 'tsdown' - -/** - * tool-fs exposes one package root plus one entry per tool plugin, so each tool - * can be loaded or replaced independently as a subpath plugin - * (`@deepseek-ai/dsh-tool-fs/read`, `/write`, `/edit`). The root tsdown builds - * only `lib/types/index.js`, so this override adds the per-tool entries. tsdown - * reads the emitted JS under `lib/types` (from `tsc -b`); declarations come from - * `tsc -b` too (dts: false), matching every package. - */ -export default defineConfig({ - entry: ['lib/types/index.js', 'lib/types/read.js', 'lib/types/write.js', 'lib/types/edit.js'], - outDir: 'lib', - format: ['esm'], - platform: 'node', - target: 'es2024', - fixedExtension: false, - dts: false, - clean: false, -}) - diff --git a/tsconfig.base.json b/tsconfig.base.json index 309c36c33f..7ac09c1725 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -34,9 +34,6 @@ "@cordisjs/plugin-timer": ["./vendor/timer/src"], "@cordisjs/plugin-hmr": ["./vendor/hmr/src"], "@cordisjs/plugin-logger-console": ["./vendor/logger-console/src"], - "@deepseek-ai/dsh-tool-fs/read": ["./packages/fs/tool-fs/src/read.ts"], - "@deepseek-ai/dsh-tool-fs/write": ["./packages/fs/tool-fs/src/write.ts"], - "@deepseek-ai/dsh-tool-fs/edit": ["./packages/fs/tool-fs/src/edit.ts"], // One wildcard maps every @deepseek-ai/dsh- to its source. Package // dir names are unique across groups, so first-on-disk-wins resolution is // unambiguous; adding a package under an existing group needs no edit