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 ¶
- Variables
- func ReadLineNumber(err error) int
- type ApplyPatchRequest
- type ApplyPatchResponse
- type ApplyPatchTool
- type EditRequest
- type EditResponse
- type EditTool
- type Editor
- type GlobRequest
- type GlobResponse
- type GlobTool
- type Globber
- type GrepFileCount
- type GrepInput
- type GrepLine
- type GrepLineKind
- type GrepOutputMode
- type GrepRequest
- type GrepResponse
- type GrepTool
- type Grepper
- type LocalExecutor
- func (l *LocalExecutor) ApplyPatch(ctx context.Context, in ApplyPatchRequest) (_ ApplyPatchResponse, err error)
- func (l *LocalExecutor) Edit(ctx context.Context, in EditRequest) (_ EditResponse, err error)
- func (l *LocalExecutor) Glob(ctx context.Context, in GlobRequest) (_ GlobResponse, err error)
- func (l *LocalExecutor) Grep(ctx context.Context, in GrepInput) (_ GrepResponse, err error)
- func (l *LocalExecutor) Read(ctx context.Context, in ReadInput) (_ ReadOutput, err error)
- func (l *LocalExecutor) Write(ctx context.Context, in WriteRequest) (_ WriteResponse, err error)
- type PatchApplier
- type PatchFileResponse
- type ReadInput
- type ReadOutput
- type ReadRequest
- type ReadResponse
- type ReadTool
- type Reader
- type WriteRequest
- type WriteResponse
- type WriteTool
- type Writer
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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 = 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 ¶
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 (a *ApplyPatchTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.