fs

package
v0.17.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 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

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

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) ConcurrencyKey

func (g *GlobTool) ConcurrencyKey(toolcontract.Invocation) (key string, concurrent bool)

ConcurrencyKey opts glob into parallel execution — a read-only filename search has no conflict (the tool loop's optional concurrency contract).

func (*GlobTool) Definition

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

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.

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) ConcurrencyKey

func (g *GrepTool) ConcurrencyKey(toolcontract.Invocation) (key string, concurrent bool)

ConcurrencyKey opts grep into parallel execution — a read-only content search has no conflict (the tool loop's optional concurrency contract).

func (*GrepTool) Definition

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

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.

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 is the model-facing adapter for the narrow Reader port.

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) ConcurrencyKey

func (r *ReadTool) ConcurrencyKey(toolcontract.Invocation) (key string, concurrent bool)

ConcurrencyKey opts read into parallel execution — a pure read has no resource conflict (the tool loop's optional concurrency contract), so the loop runs several reads (and reads alongside other parallel tools) at once.

func (*ReadTool) Definition

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

type Reader

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

Each tool depends on the smallest backend capability it consumes. A backend may implement any combination of these ports; LocalExecutor implements all of them without forcing remote or policy-specific backends to grow unrelated methods.

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

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