docs: rebalance prose cleanup and add trimming skill
This commit is contained in:
@@ -6,7 +6,7 @@ The **local-filesystem implementation** of the `ctx.fs` provider seam ([`@deepse
|
||||
import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
|
||||
|
||||
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
|
||||
// ctx.fs is now the local backend; load @deepseek-ai/dsh-fs-policy for the
|
||||
// ctx.fs uses the local backend; load @deepseek-ai/dsh-fs-policy for the
|
||||
// freshness policy gate and @deepseek-ai/dsh-tool-fs to expose read/write/edit.
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Cordis-free local-filesystem I/O for `@deepseek-ai/dsh-fs-local`. Kept separate from the
|
||||
* service class (mirroring `dsh-bash-local`'s `run.ts`) so the raw stat/read/write/edit
|
||||
* mechanics can be unit-tested without a Context.
|
||||
* Cordis-free local filesystem mechanics. This provider layer returns validated UTF-8 text,
|
||||
* streams large files, and rejects binary data; line windows belong to `dsh-tool-fs`. Writes
|
||||
* stage an exclusive owner-only file in a private sibling directory and atomically rename it.
|
||||
* @module @deepseek-ai/dsh-fs-local/fsio
|
||||
*/
|
||||
|
||||
@@ -108,8 +108,9 @@ export interface LocalDirEntry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a path to its absolute display path and realpath identity.
|
||||
*
|
||||
* Resolve a path to its absolute display path and realpath identity. For a missing target,
|
||||
* realpath the nearest existing ancestor and append the missing suffix, preserving identity
|
||||
* across symlinked ancestors before and after creation.
|
||||
* @param cwd - base directory a relative `path` resolves against.
|
||||
* @param path - absolute or relative path; empty/whitespace-only throws `FS_NOT_FOUND`.
|
||||
* @returns the absolute display path plus the realpath-derived stable target key.
|
||||
@@ -469,9 +470,8 @@ export async function readForEdit(
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort read of a file's current text for a before/after diff basis, used by an
|
||||
* overwrite.
|
||||
*
|
||||
* Best-effort overwrite diff basis. Binary or invalid UTF-8 returns `null` so the write still
|
||||
* succeeds and presentation falls back to a whole-file diff.
|
||||
* @param absolutePath - the file to read (typically a target key); it must exist.
|
||||
* @param signal - aborts the read (`FS_ABORTED`).
|
||||
* @returns the LF-normalized text, or null for a binary or non-UTF-8 file.
|
||||
@@ -489,8 +489,8 @@ export async function readTextForDiff(absolutePath: string, signal?: AbortSignal
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a literal replacement to LF-normalized content.
|
||||
*
|
||||
* Apply a literal replacement to LF-normalized content. Empty or missing search text throws
|
||||
* `FS_EDIT_NOT_FOUND`; multiple matches throw `FS_AMBIGUOUS_EDIT` unless `replaceAll` is true.
|
||||
* @param content - the current file content, already LF-normalized.
|
||||
* @param oldString - literal text to find; CRLF inside it is normalized to LF before
|
||||
* matching.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Local-filesystem implementation of the `ctx.fs` provider seam. {@link LocalFileSystem}
|
||||
* subclasses {@link FileSystem} and backs the seven text-storage primitives with the host
|
||||
* filesystem via {@link module:@deepseek-ai/dsh-fs-local/fsio}.
|
||||
* Host-filesystem implementation of `ctx.fs`. Realpath-derived target identity makes aliases
|
||||
* share stale guards, and writes through a symlink update its target without replacing the link.
|
||||
* @module @deepseek-ai/dsh-fs-local
|
||||
*/
|
||||
|
||||
@@ -177,6 +176,7 @@ export class LocalFileSystem extends FileSystem {
|
||||
const existing = await probe(target.targetKey)
|
||||
// Stale guard before literal matching: an edit based on an old read reports
|
||||
// FS_STALE_VERSION, not FS_EDIT_NOT_FOUND/FS_AMBIGUOUS_EDIT against newer content.
|
||||
// Missing targets use the same stale code on guarded and unconditional edit paths.
|
||||
if (!existing) throw new FsError(`cannot edit "${target.displayPath}": file changed since it was read`, 'FS_STALE_VERSION')
|
||||
if (existing.type !== 'file') throw new FsError(`cannot edit "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE')
|
||||
// expected === undefined: unconditional edit of the current content — no
|
||||
|
||||
@@ -37,7 +37,7 @@ Three `fs/*` events (declared by `@deepseek-ai/dsh-fs`, dispatched by `@deepseek
|
||||
|
||||
## Observed state is the prior-observation record; freshness is provider CAS
|
||||
|
||||
Observed state is a weak owner-to-target version map updated after every successful read or mutation. The plugin performs no filesystem I/O: it checks whether a version was observed and supplies that version to the provider's atomic mutation guard. State is discarded on plugin disposal and is not persisted across sessions.
|
||||
Observed state is a weak owner-to-target version map updated after every successful read or mutation; presence alone is the prior-observation record. The plugin performs no filesystem I/O: it supplies the observed version to the provider's atomic mutation guard. A windowed read observes the whole file version, so a later targeted edit is allowed only while that file remains unchanged. State is discarded on plugin disposal and is not persisted across sessions.
|
||||
|
||||
## Single-slot, first-wins
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* The fs-policy plugin: observed-state, read-before-edit, and "write/edit must be based on the
|
||||
* version you read" — added on top of the `ctx.fs` provider seam through the `fs/*` event
|
||||
* gate, not through a method service.
|
||||
* Event-only filesystem observation policy; it registers no service. A weak owner/target map
|
||||
* records every successful read or mutation, single-slot intent listeners supply that version,
|
||||
* and the provider performs the atomic freshness check. Without this plugin, tools retain the
|
||||
* bare provider's unconditional mutation behavior. See the package README for composition rules.
|
||||
* @module @deepseek-ai/dsh-fs-policy
|
||||
*/
|
||||
|
||||
@@ -110,7 +111,8 @@ export function apply(ctx: Context): void {
|
||||
// fs/edit-intent: occupy the single decision slot — do not call next().
|
||||
ctx.on('fs/edit-intent', (target, actor) => Promise.resolve().then(() => gate.editIntent(target, actor)))
|
||||
|
||||
// fs/observed: synchronous, side-effect-only WeakMap write.
|
||||
// fs/observed must remain synchronous and non-throwing: the mutation already succeeded, and
|
||||
// emit does not await promises. WeakMap.set satisfies that contract.
|
||||
ctx.on('fs/observed', (target, version, actor) => {
|
||||
gate.observe(target, version, actor)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/**
|
||||
* Tests for the fs-policy plugin: it registers no service, only the three `fs/*` listeners.
|
||||
*/
|
||||
/** Event-level policy tests; no filesystem provider is needed because the plugin performs no I/O. */
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* The filesystem provider seam (`ctx.fs`): an abstract service defining the text-storage
|
||||
* primitives a backend provides — resolve a path into a stable target, stat its metadata,
|
||||
* read/stream its text, write it atomically with an explicit intent, and apply a guarded
|
||||
* literal edit — without saying how.
|
||||
* Filesystem text-storage provider seam. Backends own stable target identity,
|
||||
* text decoding, binary rejection, and atomic mutations. Read windows and
|
||||
* observed-state policy stay in consumer and policy plugins; `editText` remains
|
||||
* here so version check, literal match, and rewrite share one critical section.
|
||||
* @module @deepseek-ai/dsh-fs
|
||||
*/
|
||||
|
||||
@@ -41,26 +41,25 @@ declare module 'cordis' {
|
||||
|
||||
interface Events {
|
||||
/**
|
||||
* Single-slot decision: produce the write intent for the next {@link
|
||||
* FileSystem.writeText}.
|
||||
*
|
||||
* Single-slot decision for the next {@link FileSystem.writeText}. Calling
|
||||
* `next()` yields the bare provider's unconditional write; the first listener
|
||||
* that returns an intent owns the decision rather than composing with peers.
|
||||
* @param target - the resolved target about to be written.
|
||||
* @param actor - the opaque tool-execution context the decider keys off.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>
|
||||
/**
|
||||
* Single-slot decision: produce the optional version guard for the next {@link
|
||||
* FileSystem.editText}.
|
||||
*
|
||||
* Single-slot decision for the next {@link FileSystem.editText}. Calling
|
||||
* `next()` yields an unconditional edit; the first returned guard wins.
|
||||
* @param target - the resolved target about to be edited.
|
||||
* @param actor - the opaque tool-execution context the decider keys off.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>
|
||||
/**
|
||||
* Record that an actor observed a target at a version, after a successful read/write/edit.
|
||||
*
|
||||
* Record a successful observation. Listeners must be synchronous recorders:
|
||||
* throws fail the tool call and returned promises are not awaited.
|
||||
* @param target - the target that was read/written/edited.
|
||||
* @param version - the version the actor now holds as its observation.
|
||||
* @param actor - the observing tool-execution context; undefined records nothing useful.
|
||||
@@ -71,9 +70,10 @@ declare module 'cordis' {
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract filesystem provider service. Subclass, implement the seven storage primitives, and
|
||||
* load the subclass as a plugin — it registers as `ctx.fs` (one implementation per context;
|
||||
* loading a second throws, cordis' standard duplicate-service behavior).
|
||||
* Abstract filesystem provider. Targets must preserve identity across aliases;
|
||||
* reads expose regular UTF-8 text or typed errors, listings are stable and
|
||||
* content-free, and mutations are atomic. Optional guards add stale protection
|
||||
* without changing the unguarded provider contract.
|
||||
*/
|
||||
export abstract class FileSystem extends Service {
|
||||
constructor(ctx: Context) {
|
||||
@@ -139,8 +139,9 @@ export abstract class FileSystem extends Service {
|
||||
abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal): Promise<FsWriteOutcome>
|
||||
|
||||
/**
|
||||
* Apply a literal edit to an existing UTF-8 text file.
|
||||
*
|
||||
* Atomically edit literal text. When supplied, the version guard is checked
|
||||
* before matching so stale content reports `FS_STALE_VERSION`; omission edits
|
||||
* the current content without a freshness precondition.
|
||||
* @param target - the resolved target to edit.
|
||||
* @param edit - the literal search/replace request.
|
||||
* @param expected - the version guard; omit for an unconditional edit.
|
||||
|
||||
@@ -90,11 +90,10 @@ export interface FsDirEntry {
|
||||
}
|
||||
|
||||
/**
|
||||
* The explicit intent of a guarded {@link FileSystem.writeText} call. `createIfAbsent` creates
|
||||
* a missing target and rejects an existing one with `FS_NOT_OBSERVED` (the path the policy
|
||||
* plugin uses when the owner has no prior read). `replaceIfVersion` replaces only when the
|
||||
* target exists at the observed version; a missing target or a version mismatch throws
|
||||
* `FS_STALE_VERSION`.
|
||||
* Guarded write intent. `createIfAbsent` rejects an existing target with
|
||||
* `FS_NOT_OBSERVED`; `replaceIfVersion` rejects absence or mismatch with
|
||||
* `FS_STALE_VERSION`. Omitting the intent from `writeText` means unconditional
|
||||
* create-or-overwrite, not a third union arm.
|
||||
*/
|
||||
export type FsWriteIntent =
|
||||
| { kind: 'createIfAbsent' }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Result-time contextual-diff computation for the `write`/`edit` tools.
|
||||
* Result-time contextual diff presentation for write and edit. Storage returns before/after
|
||||
* text; this model-facing layer derives one three-line-context card per applied hunk.
|
||||
* @module @deepseek-ai/dsh-tool-fs/src/diff
|
||||
*/
|
||||
|
||||
@@ -21,7 +22,8 @@ export type FsDiffMeta = { diffs: FileDiff[] }
|
||||
|
||||
/**
|
||||
* Compute one {@link FileDiff} per hunk between `before` and `after`, each carrying the
|
||||
* applied change plus {@link DIFF_CONTEXT} context lines.
|
||||
* applied change plus {@link DIFF_CONTEXT} context lines. Pure insertions use `oldText: null`,
|
||||
* patch-only no-newline markers are omitted, and scattered replacements remain separate hunks.
|
||||
*
|
||||
* @param path - the path stamped on every produced diff (the model-facing `file_path`; the
|
||||
* bridge relativizes it).
|
||||
@@ -65,7 +67,8 @@ function isFileDiff(value: unknown): value is FileDiff {
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow opaque live or replayed result metadata to non-empty file diffs.
|
||||
* Narrow opaque live or replayed result metadata to non-empty file diffs. Malformed metadata
|
||||
* returns `undefined` so presentation can fall back instead of throwing during replay.
|
||||
* @param meta - result metadata.
|
||||
* @returns validated hunks, or `undefined` for absent or malformed data.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* The model-facing `edit` tool: update an existing UTF-8 text file by replacing literal text,
|
||||
* requiring a unique match by default.
|
||||
* Model-facing literal edit, unique-match by default. It obtains an optional guard from the
|
||||
* single intent slot, calls `ctx.fs.editText` without a separate stat, then records the observed
|
||||
* version; no policy means an unconditional atomic edit.
|
||||
* @module @deepseek-ai/dsh-tool-fs/src/edit
|
||||
*/
|
||||
|
||||
@@ -88,7 +89,7 @@ export function applyEditTool(ctx: Context): void {
|
||||
)
|
||||
// Record the observed version (a no-op when no policy plugin listens).
|
||||
ctx.emit('fs/observed', target, outcome.version, exec)
|
||||
// The result-time applied-hunk diff (before→after with context lines).
|
||||
// An edit necessarily changes content, so result metadata carries at least one applied hunk.
|
||||
const diffs = computeHunkDiffs(input.filePath, outcome.before, outcome.after)
|
||||
return {
|
||||
content: [{ type: 'text', text: formatEditOutput(target.displayPath, input.replaceAll) }],
|
||||
@@ -106,7 +107,8 @@ export function applyEditTool(ctx: Context): void {
|
||||
locations: [{ path: args.file_path }],
|
||||
}
|
||||
},
|
||||
// Result-time display: the applied contextual-diff hunks carried on `meta`.
|
||||
// Applied metadata replaces the call-time snippet; errors or malformed replay metadata use
|
||||
// the generic result rendering.
|
||||
presentResult(args, result: ToolResult): DiffResultView | undefined {
|
||||
if (result.isError) return undefined
|
||||
const diffs = diffsFromMeta(result.meta)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* The model-facing filesystem tool suite (`read`, `write`, `edit`) over the `ctx.fs` provider
|
||||
* seam. This single plugin registers all three tools.
|
||||
* Model-facing read, write, and edit tools over `ctx.fs`. This package owns schemas, validation,
|
||||
* read windows, formatting, and observation events, never a concrete provider. An optional
|
||||
* event policy supplies mutation guards; without one the tools use unconditional provider calls.
|
||||
* @module @deepseek-ai/dsh-tool-fs
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Cordis-free read rendering for `@deepseek-ai/dsh-tool-fs`: turn a file's decoded text into a
|
||||
* bounded, line-numbered window (offset/limit, byte cap, per-line truncation) and format it as
|
||||
* the model-facing text block.
|
||||
* Pure read presentation: turn provider-decoded text into a bounded, line-numbered window and
|
||||
* model-facing envelope. Chunk scanning caps the current line, so even one newline-free giant
|
||||
* line cannot grow memory without bound.
|
||||
* @module @deepseek-ai/dsh-tool-fs/read-render
|
||||
*/
|
||||
|
||||
@@ -102,8 +102,8 @@ function finish(acc: WindowAccumulator, request: ReadWindow, displayPath: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a bounded, line-numbered window from a file's decoded text chunks.
|
||||
*
|
||||
* Build one window from streamed or whole-file chunks, enforcing line and byte caps and throwing
|
||||
* `FS_NOT_FOUND` when the requested offset is past EOF.
|
||||
* @param chunks - decoded text chunks in file order; chunk boundaries carry no meaning.
|
||||
* @param request - the resolved window; the caller has already applied its defaults and caps.
|
||||
* @param displayPath - the caller-facing path used in the offset-out-of-range error.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* The model-facing `read` tool: inspect a UTF-8 text file and return line-numbered content
|
||||
* with pagination guidance.
|
||||
* Model-facing UTF-8 read. It performs one provider stat for type, routing, and observed version,
|
||||
* streams large or size-unknown files, renders a bounded window, then emits the observation.
|
||||
* @module @deepseek-ai/dsh-tool-fs/src/read
|
||||
*/
|
||||
|
||||
@@ -90,6 +90,7 @@ export function applyReadTool(ctx: Context, caps: ReadToolCaps): void {
|
||||
const target = await ctx.fs.resolve(input.filePath, cwd !== undefined ? { cwd } : undefined)
|
||||
|
||||
// One stat: type check + size routing + the version recorded as observed.
|
||||
// A concurrent write can only make a later guarded mutation fail stale and require reread.
|
||||
const info = await ctx.fs.stat(target, exec.signal)
|
||||
if (!info) throw new FsError(`cannot read "${target.displayPath}": not found`, 'FS_NOT_FOUND')
|
||||
if (info.type !== 'file') throw new FsError(`cannot read "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE')
|
||||
@@ -119,7 +120,8 @@ export function applyReadTool(ctx: Context, caps: ReadToolCaps): void {
|
||||
},
|
||||
// Pure display: a generic card titled by the file with the read window appended (`Read
|
||||
// foo.txt (5 - 8)`), `read` kind (icon), and a follow-along location whose line is the
|
||||
// read's offset (defaulting to 1).
|
||||
// read's offset (defaulting to 1). The window reflects raw args, so an omitted limit keeps
|
||||
// the title bare instead of smuggling config into this pure presenter.
|
||||
presentCall(args): GenericCallView {
|
||||
const { offset, limit } = args
|
||||
const window = limit !== undefined && limit > 0
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* agent's per-session workspace (`exec.agent.session.header.cwd`), so each ACP session's
|
||||
* `read`/`write`/`edit` act on ITS workspace, not the server's launch dir — mirroring how
|
||||
* `dsh-tool-bash` defaults a bash `workdir` to the session cwd.
|
||||
* Non-agent calls return `undefined`, leaving the fallback in the provider rather than reading
|
||||
* `process.cwd()` at the tool seam.
|
||||
* @module @deepseek-ai/dsh-tool-fs/session-cwd
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* The model-facing `write` tool: create or fully replace a UTF-8 text file.
|
||||
* Model-facing full-file write. It obtains an optional intent from the single policy slot, calls
|
||||
* `ctx.fs.writeText` without a stat, then records the resulting version; no policy means an
|
||||
* unconditional atomic create-or-overwrite.
|
||||
* @module @deepseek-ai/dsh-tool-fs/src/write
|
||||
*/
|
||||
|
||||
@@ -67,7 +69,8 @@ export function applyWriteTool(ctx: Context): void {
|
||||
const outcome = await ctx.fs.writeText(target, input.content, intent, exec.signal)
|
||||
// Record the observed version (a no-op when no policy plugin listens).
|
||||
ctx.emit('fs/observed', target, outcome.version, exec)
|
||||
// Attach a contextual hunk as `meta` only for an overwrite (a before-version exists).
|
||||
// Overwrites carry applied hunks. Creates have no prior text, so result presentation uses
|
||||
// the args-derived whole-file diff instead.
|
||||
const diffs = outcome.before !== null ? computeHunkDiffs(input.filePath, outcome.before, outcome.after) : []
|
||||
return {
|
||||
content: [{ type: 'text', text: formatWriteOutput(target.displayPath, outcome) }],
|
||||
@@ -87,7 +90,8 @@ export function applyWriteTool(ctx: Context): void {
|
||||
},
|
||||
// Result-time display: a `diff` card so the completed `tool_call_update` re-installs the
|
||||
// diff rather than the model-facing result text (an ACP `tool_call_update.content` REPLACES
|
||||
// the call's content, so a text result would clobber the pending diff card).
|
||||
// the call's content, so a text result would clobber the pending diff card). Overwrites use
|
||||
// applied metadata; creates and identical overwrites use the replay-safe args fallback.
|
||||
presentResult(args, result: ToolResult): DiffResultView | undefined {
|
||||
if (result.isError) return undefined
|
||||
const diffs = diffsFromMeta(result.meta)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Integration tests: the real local backend (`dsh-fs-local`) plus the model tools
|
||||
* (`dsh-tool-fs`) as the executor, exercised through `ctx.tools.execute()` so nothing bypasses
|
||||
* the tool registry. Two deployments.
|
||||
* End-to-end tool-registry tests against the real local backend. The policy deployment verifies
|
||||
* observed-state and guarded mutation; the bare deployment proves unconditional tools have no
|
||||
* policy-service dependency. Assertions read files back byte-for-byte rather than trusting tool
|
||||
* messages.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -19,7 +20,7 @@ import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||
let dir: string
|
||||
let ctx: Context
|
||||
let fiber: Awaited<ReturnType<Context['plugin']>>
|
||||
// A stable session object stands in for an agent session (the file-state owner).
|
||||
// No header cwd: sessionCwd returns undefined and the provider's configured test dir applies.
|
||||
const session = { header: {} }
|
||||
|
||||
let callCounter = 0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Consumer-surface tests for the filesystem tools as the EXECUTOR.
|
||||
* Consumer-surface tests over a fake provider and the real policy collaborator: schemas,
|
||||
* validation, formatting, typed errors, intent dispatch, and observation-driven authorization.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
Reference in New Issue
Block a user