fs

package
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Package fs exposes LLM-callable filesystem tools (read, write, edit, glob, grep) on top of minimal per-operation ports. Local, sandbox, and remote backends implement only the capabilities they provide; the tools themselves are thin adapters that marshal LLM JSON into port calls and back.

**Text files only.** Backend implementations MUST reject files that look binary (NUL byte in the first 8 KiB is a good default heuristic) and reject Write content that contains NUL bytes. Use the bash tool if you need to manipulate binary data.

**Tools stay thin.** All content processing — line windowing, binary detection, exact / fuzzy match, append-vs-overwrite — lives in the backend, not the tool. The tool's job is JSON in, JSON out.

Why Glob and Grep have dedicated ports (instead of "walk + match" in the tool layer): a remote backend cannot afford to ship every file across the wire to pattern-match on the agent side. Pushing bulk queries into their ports keeps remote implementations one round-trip per call.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNilExecutor rejects a tool without the backend capability it consumes.
	ErrNilExecutor = errors.New("fs: executor must not be nil")
	// ErrInvalidRoot identifies an authority root that cannot be fixed safely.
	ErrInvalidRoot = errors.New("fs: executor root is invalid")
	// ErrEmptyPath rejects an operation with no target identity.
	ErrEmptyPath = errors.New("fs: path must not be empty")
	// ErrInvalidInput identifies operation arguments rejected by the backend.
	ErrInvalidInput = errors.New("fs: operation input is invalid")

	// ErrPathOutsideRoot identifies an attempted authority escape.
	ErrPathOutsideRoot = errors.New("fs: path is outside the executor root")

	// ErrEmptyPattern rejects searches that do not define a query.
	ErrEmptyPattern = errors.New("fs: pattern must not be empty")

	// ErrRipgrepUnavailable reports that the local grep backend lacks its engine.
	ErrRipgrepUnavailable = errors.New("fs: ripgrep is unavailable")

	// ErrBinaryFile prevents text tools from silently corrupting binary content.
	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
	// ReadLineNumber.
	ErrLineTooLarge = errors.New("fs: line exceeds the operation line limit")
)

Functions

func ReadLineNumber

func ReadLineNumber(err error) int

ReadLineNumber 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 applies a Git-compatible unified diff. The local executor supports create, modify, delete, and Git rename patches, which makes a coordinated refactor one call.

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)

NewApplyPatchTool requires patch authority explicitly and derives one stable tool schema.

func (*ApplyPatchTool) Call

func (*ApplyPatchTool) Definition

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

func (*ApplyPatchTool) Unwrap added in v0.19.0

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

Unwrap exposes the typed input contract through tool decorators.

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   `` /* 142-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"`
}

EditResponse makes replacement cardinality observable to the model.

type EditTool

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

EditTool is the thin LLM-facing adapter for Editor.Edit. The executor owns validation and atomic replacement under the same contract.

func NewEditTool

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

NewEditTool retains only the atomic edit capability, not a full filesystem backend.

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

Unwrap exposes the typed input contract through tool decorators.

type Editor

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

Editor keeps read-modify-write atomic inside the filesystem authority owner.

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,omitempty" jsonschema_description:"Match path components case-insensitively. Default false."`
	MaxResults int    `` /* 154-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,omitempty"`
}

GlobResponse distinguishes a complete match set from a bounded prefix.

type GlobTool

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

GlobTool is the model-facing adapter for the narrow Globber port.

func NewGlobTool

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

NewGlobTool requires path-search authority explicitly.

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)

ConcurrencyPolicy declares independent calls to the concurrent-safe backend.

func (*GlobTool) Definition

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

func (*GlobTool) Unwrap added in v0.19.0

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

Unwrap exposes the typed input contract through tool decorators.

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"`
}

GrepFileCount is one entry of the "count" output mode.

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

	// Context is the symmetric "lines before AND after" shortcut.
	// BeforeContext / AfterContext override per-side when non-zero.
	Context       int
	BeforeContext int
	AfterContext  int

	// OutputMode picks the shape of GrepResponse. Its zero value resolves to
	// [GrepOutputContent].
	OutputMode GrepOutputMode

	MaxResults int
}

GrepInput is the backend search contract after model-facing fields have been normalized.

type GrepLine

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

GrepLine is one structured ripgrep line event.

type GrepLineKind

type GrepLineKind string

GrepLineKind distinguishes a matching line from requested surrounding context.

const (
	// GrepLineMatch identifies content matched by the regular expression.
	GrepLineMatch GrepLineKind = "match"

	// GrepLineContext identifies surrounding content requested for a 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

GrepOutputMode controls what GrepResponse populates.

const (
	// GrepOutputContent returns structured matching and context lines.
	GrepOutputContent GrepOutputMode = "content"
	// GrepOutputFilesWithMatches returns only paths containing a match.
	GrepOutputFilesWithMatches GrepOutputMode = "files_with_matches"
	// GrepOutputCount returns per-file match counts.
	GrepOutputCount GrepOutputMode = "count"
)

func (GrepOutputMode) Resolve

func (g GrepOutputMode) Resolve() GrepOutputMode

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,omitempty" jsonschema_description:"Case-insensitive search. Default false."`
	Multiline  bool `json:"multiline,omitempty" jsonschema_description:"Allow patterns to span line breaks. Default false. Requires ripgrep."`

	BeforeContextLines int `` /* 168-byte string literal not displayed */
	AfterContextLines  int `` /* 166-byte string literal not displayed */

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

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

GrepRequest is the LLM-facing argument shape for the grep tool.

Notes on pattern syntax: the underlying engine is ripgrep. Literal braces / brackets need escaping (`interface\{\}` to find `interface{}`). By default patterns match within a single line; set `multiline=true` for patterns that span newlines.

type GrepResponse

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

GrepResponse is the LLM-facing return shape. Exactly one of lines / files / counts is populated based on the request's output_mode.

type GrepTool

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

GrepTool is the model-facing adapter for the narrow Grepper port.

func NewGrepTool

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

NewGrepTool requires content-search authority explicitly.

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)

ConcurrencyPolicy declares independent calls to the concurrent-safe backend.

func (*GrepTool) Definition

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

func (*GrepTool) Unwrap added in v0.19.0

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

Unwrap exposes the typed input contract through tool decorators.

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 platform-neutral doublestar matcher and never follows directory symlinks while walking. Cancellation is checked between filesystem operations even when the pattern has no matches.
  • Grep consumes ripgrep's structured JSON protocol and returns ErrRipgrepUnavailable when rg is not installed.
  • Write and Edit serialize per file via [LocalExecutor.lockPath] so concurrent tool calls on the same path can't tear.
  • Read normalises CRLF→LF and strips UTF-8 BOM; Write and Edit restore both when the existing file uses them.

func NewLocalExecutor

func NewLocalExecutor(root string) (*LocalExecutor, error)

NewLocalExecutor fixes one immutable directory-tree authority for every operation performed by the returned backend.

func (*LocalExecutor) ApplyPatch

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

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 lock — concurrent reads are fine and a slightly stale read while another goroutine writes is acceptable (atomic-rename in Write means the caller sees either the old file in full or the new file in full, never a torn write).

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. Implementations may create parent directories while committing files.

type PatchFileResponse

type PatchFileResponse struct {
	// Path is where the file ended up.
	Path    string `json:"path"`
	Hunks   int    `json:"hunks"`
	Created bool   `json:"created,omitempty"`
	Deleted bool   `json:"deleted,omitempty"`
	// MovedFrom is the path the file left, set only for a move. Path alone would
	// say a file exists somewhere new without saying which one stopped existing.
	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
}

ReadInput is line-based. The executor handles binary detection and line windowing — the tool only forwards what the LLM asked for.

type ReadOutput

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

ReadOutput reports the admitted line window and whole-file size without leaking backend implementation details.

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    `` /* 132-byte string literal not displayed */
	MaxLines  int    `` /* 141-byte string literal not displayed */
}

ReadRequest is the LLM-facing argument shape for the read tool. StartLine is 1-based to match editor, grep, and language-server conventions.

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,omitempty"`
}

ReadResponse is the LLM-facing return shape. StartLine / EndLine are 1-based inclusive.

type ReadTool

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

ReadTool adapts a concurrent-safe Reader and declares parallel calls. The Reader owns synchronization across tool instances and Processes.

func NewReadTool

func NewReadTool(executor Reader) (*ReadTool, error)

NewReadTool requires read authority explicitly and derives one stable schema.

Example
package main

import (
	"fmt"

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

func main() {
	executor, err := toolfs.NewLocalExecutor(".")
	if err != nil {
		panic(err)
	}
	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)

ConcurrencyPolicy declares independent calls to the concurrent-safe backend.

func (*ReadTool) Definition

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

func (*ReadTool) Unwrap added in v0.19.0

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

Unwrap exposes the typed input contract through tool decorators.

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 same backend. Private buffers, cursors, and caches remain the backend's concern. This promise permits ReadTool to advertise parallel reads; it is stronger than the absence of filesystem writes. LocalExecutor satisfies the contract.

type WriteRequest

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

WriteRequest replaces one complete text file beneath the backend authority.

type WriteResponse

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

WriteResponse makes the committed byte count observable to the model.

type WriteTool

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

WriteTool is the model-facing adapter for the narrow Writer port.

func NewWriteTool

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

NewWriteTool requires write authority explicitly and derives one stable schema.

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

Unwrap exposes the typed input contract through tool decorators.

type Writer

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

Writer is the narrow backend port consumed by WriteTool.

Jump to

Keyboard shortcuts

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