fs

package
v0.44.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 7, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package fs exposes LLM-callable filesystem tools (read, write, edit, apply_patch, glob, grep) over minimal per-operation backend ports. Backends implement only the capabilities they provide and own all content processing; the tools only marshal JSON into port calls and back.

Backends handle text files only: they must reject files that look binary and Write content that contains NUL bytes.

LocalExecutor is the local backend. NewLocalExecutor derives its own handle from an already-open os.Root with an absolute Name, so the host keeps that authority for its own inspection instead of resolving the pathname again; each side closes the handle it owns.

ApplyPatchTool.MutationPaths exposes prospective patch endpoints without I/O; hosts discover it through core/tool.Capability for approval or locking. Mutation tools carry ordinary backend errors and acknowledged effects as core/tool.CallError evidence for evaluation and reconciliation, not as a result for the model. Backends establish definite failure or refusal with core/tool.Failure.

Glob and Grep have dedicated ports so a remote backend answers a bulk query in one round trip instead of shipping every file to the agent.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNilExecutor        = errors.New("fs: executor must not be nil")
	ErrInvalidRoot        = errors.New("fs: executor root is invalid")
	ErrEmptyPath          = errors.New("fs: path must not be empty")
	ErrInvalidInput       = errors.New("fs: operation input is invalid")
	ErrPathOutsideRoot    = errors.New("fs: path is outside the executor root")
	ErrEmptyPattern       = errors.New("fs: pattern must not be empty")
	ErrRipgrepUnavailable = errors.New("fs: ripgrep is unavailable")

	// ErrMutationRejected proves that a write, edit, or patch stopped before
	// changing any file or directory. Backends must not return it after commit
	// begins, even when the only acknowledged changes are partial.
	ErrMutationRejected = errors.New("fs: mutation rejected without changes")
	ErrBinaryFile       = errors.New("fs: file appears to be binary; only text files are supported")

	// ErrFileTooLarge reports that the operation returned no partial file.
	ErrFileTooLarge = errors.New("fs: file exceeds the operation input limit")

	// ErrLineTooLarge preserves the one-based offending line through
	// LineNumber.
	ErrLineTooLarge = errors.New("fs: line exceeds the operation line limit")
)

Functions

func LineNumber added in v0.41.0

func LineNumber(err error) int

LineNumber returns the one-based line attached to an ErrLineTooLarge failure, or zero when the error is not line-specific.

Types

type ApplyPatchRequest

type ApplyPatchRequest struct {
	Patch string `` /* 187-byte string literal not displayed */
}

ApplyPatchRequest accepts Git-compatible create, modify, delete, and rename patches.

type ApplyPatchResponse

type ApplyPatchResponse struct {
	Files []PatchFileResponse `json:"files"`
	Hunks int                 `json:"hunks"`
}

ApplyPatchResponse reports acknowledged file mutations even when ApplyPatch returns an error. Files are listed in commit order. An interrupted move is reported as a created destination until the source has actually been removed.

type ApplyPatchTool

type ApplyPatchTool struct {
	// contains filtered or unexported fields
}

ApplyPatchTool preserves both complete and partial PatchApplier outcomes.

func NewApplyPatchTool

func NewApplyPatchTool(executor PatchApplier) (*ApplyPatchTool, error)

func (*ApplyPatchTool) Call

func (*ApplyPatchTool) Definition

func (a *ApplyPatchTool) Definition() chat.ToolDefinition

func (*ApplyPatchTool) MutationPaths added in v0.35.0

func (a *ApplyPatchTool) MutationPaths(arguments []byte) ([]string, error)

MutationPaths returns the sorted, unique file endpoints named by an invocation, without I/O. A rename includes both endpoints; /dev/null is never a target. Paths are lexically cleaned with Git's a/ and b/ prefixes removed; relative paths stay relative to the backend root and absolute paths stay absolute.

It shares LocalExecutor's parser and operation validation, so invalid or unsupported patches return no paths. A result does not establish authority, hunk applicability, or success; ApplyPatchResponse alone acknowledges effects.

Example
package main

import (
	"fmt"
	"os"

	toolfs "github.com/Tangerg/scope/tools/fs"
)

func main() {
	path, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	root, err := os.OpenRoot(path)
	if err != nil {
		panic(err)
	}
	defer root.Close()
	executor, err := toolfs.NewLocalExecutor(root)
	if err != nil {
		panic(err)
	}
	defer executor.Close()
	patch, err := toolfs.NewApplyPatchTool(executor)
	if err != nil {
		panic(err)
	}
	// Querying the prospective endpoints neither reads nor changes these files.
	paths, err := patch.MutationPaths([]byte(`{"patch":"diff --git a/old.txt b/new.txt\nsimilarity index 100%\nrename from old.txt\nrename to new.txt\n"}`))
	if err != nil {
		panic(err)
	}
	fmt.Println(paths)
}
Output:
[new.txt old.txt]

func (*ApplyPatchTool) Unwrap added in v0.19.0

func (a *ApplyPatchTool) Unwrap() toolcontract.Tool

type EditRequest

type EditRequest struct {
	Path       string `json:"path" jsonschema:"minLength=1" jsonschema_description:"File path, absolute or relative to the workspace root."`
	OldString  string `` /* 292-byte string literal not displayed */
	NewString  string `` /* 188-byte string literal not displayed */
	ReplaceAll bool   `` /* 141-byte string literal not displayed */
}

EditRequest drives one atomic exact-text replacement in the executor. Whitespace is significant, including indentation and string literal content.

type EditResponse

type EditResponse struct {
	Replacements int `json:"replacements"`
}

type EditTool

type EditTool struct {
	// contains filtered or unexported fields
}

func NewEditTool

func NewEditTool(executor Editor) (*EditTool, error)

func (*EditTool) Call

func (e *EditTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)

func (*EditTool) Definition

func (e *EditTool) Definition() chat.ToolDefinition

func (*EditTool) Unwrap added in v0.19.0

func (e *EditTool) Unwrap() toolcontract.Tool

type Editor

type Editor interface {
	Edit(ctx context.Context, request EditRequest) (EditResponse, error)
}

Editor keeps read-modify-write atomic inside the filesystem authority owner. ErrMutationRejected reports a definite rejection with no mutation. Other errors do not establish the outcome and must not be treated as safe to retry. Responses retain acknowledged replacements even on error.

type GlobRequest

type GlobRequest struct {
	Pattern    string `json:"pattern" jsonschema:"minLength=1" jsonschema_description:"Doublestar path pattern, such as **/*.go or src/**/*.ts."`
	Path       string `json:"path,omitempty" jsonschema_description:"Directory to search under. Defaults to the workspace root."`
	IgnoreCase bool   `json:"ignore_case,omitzero" jsonschema_description:"Match path components case-insensitively. Default false."`
	MaxResults int    `` /* 153-byte string literal not displayed */
}

GlobRequest narrows the executor's immutable authority to a relative subtree; Path can never replace or broaden that root.

type GlobResponse

type GlobResponse struct {
	Paths     []string `json:"paths"`
	Truncated bool     `json:"truncated,omitzero"`
}

type GlobTool

type GlobTool struct {
	// contains filtered or unexported fields
}

func NewGlobTool

func NewGlobTool(executor Globber) (*GlobTool, error)

func (*GlobTool) Call

func (g *GlobTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)

func (*GlobTool) ConcurrencyPolicy added in v0.19.0

func (g *GlobTool) ConcurrencyPolicy() func(toolcontract.Invocation) (string, bool)

func (*GlobTool) Definition

func (g *GlobTool) Definition() chat.ToolDefinition

func (*GlobTool) Unwrap added in v0.19.0

func (g *GlobTool) Unwrap() toolcontract.Tool

type Globber

type Globber interface {
	Glob(ctx context.Context, request GlobRequest) (GlobResponse, error)
}

Globber lets remote backends search paths without exposing directory walking as many tool calls. Like Reader, it must support concurrent backend calls.

type GrepFileCount

type GrepFileCount struct {
	Path  string `json:"path"`
	Count int    `json:"count"`
}

type GrepInput

type GrepInput struct {
	Pattern    string // regex
	Path       string // file or directory below the executor's authority root
	Glob       string // optional file filter ("*.go", "**/*.ts", ...)
	FileType   string // rg-style ("go", "ts", "rust", ...). Backend decides mapping.
	IgnoreCase bool
	Multiline  bool

	BeforeContext int
	AfterContext  int

	OutputMode GrepOutputMode // zero resolves to GrepOutputContent
	MaxResults int
}

type GrepLine

type GrepLine struct {
	Path string       `json:"path"`
	Line int          `json:"line"` // 1-based
	Text string       `json:"text"`
	Kind GrepLineKind `json:"kind"`
}

type GrepLineKind

type GrepLineKind string
const (
	GrepLineMatch   GrepLineKind = "match"
	GrepLineContext GrepLineKind = "context"
)

func (GrepLineKind) String

func (g GrepLineKind) String() string

func (GrepLineKind) Valid

func (g GrepLineKind) Valid() bool

type GrepOutputMode

type GrepOutputMode string
const (
	GrepOutputContent          GrepOutputMode = "content"
	GrepOutputFilesWithMatches GrepOutputMode = "files_with_matches"
	GrepOutputCount            GrepOutputMode = "count"
)

func (GrepOutputMode) Normalize added in v0.24.0

func (g GrepOutputMode) Normalize() (GrepOutputMode, error)

func (GrepOutputMode) Valid

func (g GrepOutputMode) Valid() bool

type GrepRequest

type GrepRequest struct {
	Pattern  string `json:"pattern" jsonschema:"minLength=1" jsonschema_description:"Regular expression in ripgrep syntax."`
	Path     string `json:"path,omitempty" jsonschema_description:"File or directory to search. Defaults to the workspace root."`
	FileGlob string `json:"file_glob,omitempty" jsonschema_description:"Optional file filter glob, such as **/*.go."`
	FileType string `json:"file_type,omitempty" jsonschema_description:"Optional ripgrep file type, such as go, ts, or rust."`

	IgnoreCase bool `json:"ignore_case,omitzero" jsonschema_description:"Case-insensitive search. Default false."`
	Multiline  bool `json:"multiline,omitzero" jsonschema_description:"Allow patterns to span line breaks. Default false. Requires ripgrep."`

	BeforeContextLines int `` /* 167-byte string literal not displayed */
	AfterContextLines  int `` /* 165-byte string literal not displayed */

	OutputMode GrepOutputMode `` /* 182-byte string literal not displayed */

	MaxResults int `` /* 152-byte string literal not displayed */
}

GrepRequest patterns use ripgrep syntax. Matches stay within a single line unless Multiline is set.

type GrepResponse

type GrepResponse struct {
	Lines     []GrepLine      `json:"lines,omitempty"`
	Files     []string        `json:"files,omitempty"`
	Counts    []GrepFileCount `json:"counts,omitempty"`
	Truncated bool            `json:"truncated,omitzero"`
}

Exactly one of Lines, Files, or Counts is populated according to OutputMode.

type GrepTool

type GrepTool struct {
	// contains filtered or unexported fields
}

func NewGrepTool

func NewGrepTool(executor Grepper) (*GrepTool, error)

func (*GrepTool) Call

func (g *GrepTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)

func (*GrepTool) ConcurrencyPolicy added in v0.19.0

func (g *GrepTool) ConcurrencyPolicy() func(toolcontract.Invocation) (string, bool)

func (*GrepTool) Definition

func (g *GrepTool) Definition() chat.ToolDefinition

func (*GrepTool) Unwrap added in v0.19.0

func (g *GrepTool) Unwrap() toolcontract.Tool

type Grepper

type Grepper interface {
	Grep(ctx context.Context, in GrepInput) (GrepResponse, error)
}

Grepper lets a backend own its content-search engine and filesystem boundary. Like Reader, it must support concurrent backend calls.

type LocalExecutor

type LocalExecutor struct {
	// contains filtered or unexported fields
}

LocalExecutor is the reference local filesystem backend. Its constructor grants one immutable directory-tree authority; operation inputs may narrow that authority but cannot replace or escape it.

  • Glob uses the doublestar matcher and never follows directory symlinks.
  • Grep streams files opened through os.Root to ripgrep. It skips hidden entries and symlinks, uses doublestar path filters, and does not consult ignore files. Missing rg returns ErrRipgrepUnavailable.
  • Mutations serialize within this executor and pin parent directories. Directory aliases are supported; leaf symlinks are rejected.
  • Read normalizes CRLF to LF and strips a UTF-8 BOM; Write and Edit restore both when the existing file uses them.

func NewLocalExecutor

func NewLocalExecutor(root *os.Root) (*LocalExecutor, error)

NewLocalExecutor derives an independent directory handle from root, never reopening its pathname, so host policies can inspect the same directory the executor uses even after its pathname is replaced.

root must be open and have an absolute Name, which anchors absolute operation paths. The caller keeps ownership of root and must also Close the executor; closing either handle does not revoke the other. Sharing a root does not make separately issued host and executor operations atomic.

func (*LocalExecutor) ApplyPatch

func (l *LocalExecutor) ApplyPatch(ctx context.Context, in ApplyPatchRequest) (_ ApplyPatchResponse, err error)

func (*LocalExecutor) Close added in v0.22.0

func (l *LocalExecutor) Close() error

Close prevents new operations. Operations that already acquired their own directory handle may finish independently.

func (*LocalExecutor) Edit

func (l *LocalExecutor) Edit(ctx context.Context, in EditRequest) (_ EditResponse, err error)

func (*LocalExecutor) Glob

func (l *LocalExecutor) Glob(ctx context.Context, in GlobRequest) (_ GlobResponse, err error)

func (*LocalExecutor) Grep

func (l *LocalExecutor) Grep(ctx context.Context, in GrepInput) (_ GrepResponse, err error)

func (*LocalExecutor) Read

func (l *LocalExecutor) Read(ctx context.Context, in ReadInput) (_ ReadOutput, err error)

Read does not serialize with mutations: atomic replacement means it observes either the complete previous file or the complete new one.

func (*LocalExecutor) Write

func (l *LocalExecutor) Write(ctx context.Context, in WriteRequest) (_ WriteResponse, err error)

type PatchApplier

type PatchApplier interface {
	ApplyPatch(ctx context.Context, request ApplyPatchRequest) (ApplyPatchResponse, error)
}

PatchApplier validates a complete patch before mutation and reports every acknowledged file effect, including on error. A multi-file patch is not a filesystem transaction: commit failures can leave earlier changes applied. Patch endpoints must follow ApplyPatchTool.MutationPaths so hosts can inspect the complete prospective write set before invoking the backend. A core/tool.Failure owns the complete model-visible output of an established unsuccessful outcome; ErrMutationRejected establishes rejection before any mutation; any other error preserves uncertainty.

type PatchFileResponse

type PatchFileResponse struct {
	Path    string `json:"path"`
	Hunks   int    `json:"hunks"`
	Created bool   `json:"created,omitzero"`
	Deleted bool   `json:"deleted,omitzero"`
	// MovedFrom is set only after a move removes its source.
	MovedFrom string `json:"moved_from,omitempty"`
}

PatchFileResponse preserves create, delete, and move identity separately. LocalExecutor reports paths relative to its authority root, even when patch headers use absolute paths.

type ReadInput

type ReadInput struct {
	Path           string
	Offset         int   // 0-based line offset; negative is clamped to 0
	Limit          int   // 0 = read to end of file
	MaxInputBytes  int64 // 0 = executor default
	MaxLineBytes   int   // 0 = executor default
	MaxOutputBytes int   // 0 = executor default
	PartialLine    bool  // admit a UTF-8 prefix when the output cap splits a line
}

type ReadOutput

type ReadOutput struct {
	Content    string
	StartLine  int
	EndLine    int
	TotalLines int
	Truncated  bool
}

type ReadRequest

type ReadRequest struct {
	Path      string `json:"path" jsonschema:"minLength=1" jsonschema_description:"File path, absolute or relative to the workspace root."`
	StartLine int    `` /* 131-byte string literal not displayed */
	MaxLines  int    `` /* 140-byte string literal not displayed */
}

StartLine is one-based to match editors, grep, and language servers.

type ReadResponse

type ReadResponse struct {
	Content    string `json:"content"`
	StartLine  int    `json:"start_line"`
	EndLine    int    `json:"end_line"`
	TotalLines int    `json:"total_lines"`
	Truncated  bool   `json:"truncated,omitzero"`
}

StartLine and EndLine are one-based and inclusive.

type ReadTool

type ReadTool struct {
	// contains filtered or unexported fields
}

ReadTool declares parallel calls on the strength of the Reader contract.

func NewReadTool

func NewReadTool(executor Reader) (*ReadTool, error)
Example
package main

import (
	"fmt"
	"os"

	toolfs "github.com/Tangerg/scope/tools/fs"
)

func main() {
	path, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	root, err := os.OpenRoot(path)
	if err != nil {
		panic(err)
	}
	defer root.Close()
	executor, err := toolfs.NewLocalExecutor(root)
	if err != nil {
		panic(err)
	}
	defer executor.Close()
	read, err := toolfs.NewReadTool(executor)
	if err != nil {
		panic(err)
	}
	definition := read.Definition()

	fmt.Println(definition.Name)
}
Output:
read

func (*ReadTool) Call

func (r *ReadTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)

func (*ReadTool) ConcurrencyPolicy added in v0.19.0

func (r *ReadTool) ConcurrencyPolicy() func(toolcontract.Invocation) (string, bool)

func (*ReadTool) Definition

func (r *ReadTool) Definition() chat.ToolDefinition

func (*ReadTool) Unwrap added in v0.19.0

func (r *ReadTool) Unwrap() toolcontract.Tool

type Reader

type Reader interface {
	Read(ctx context.Context, in ReadInput) (ReadOutput, error)
}

Reader is the read backend used by ReadTool. Implementations must support concurrent calls, including calls through other tools sharing the backend; ReadTool advertises parallel reads on that promise.

type WriteRequest

type WriteRequest struct {
	Path    string `` /* 162-byte string literal not displayed */
	Content string `` /* 133-byte string literal not displayed */
}

type WriteResponse

type WriteResponse struct {
	BytesWritten int `json:"bytes_written"`
}

type WriteTool

type WriteTool struct {
	// contains filtered or unexported fields
}

func NewWriteTool

func NewWriteTool(executor Writer) (*WriteTool, error)

func (*WriteTool) Call

func (w *WriteTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)

func (*WriteTool) Definition

func (w *WriteTool) Definition() chat.ToolDefinition

func (*WriteTool) Unwrap added in v0.19.0

func (w *WriteTool) Unwrap() toolcontract.Tool

type Writer

type Writer interface {
	Write(ctx context.Context, request WriteRequest) (WriteResponse, error)
}

Writer is the narrow backend port consumed by WriteTool. On error the response retains acknowledged writes; an ordinary error does not establish whether a remote write committed or whether it is safe to repeat. ErrMutationRejected explicitly establishes that no mutation began.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL