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 GlobInput
- 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 GlobInput) (_ 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 WriteInput) (_ WriteResponse, err error)
- type PatchApplier
- type PatchFileResponse
- type ReadInput
- type ReadOutput
- type ReadRequest
- type ReadResponse
- type ReadTool
- type Reader
- type WriteInput
- type WriteRequest
- type WriteResponse
- type WriteTool
- type Writer
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( 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") ErrBinaryFile = errors.New("fs: file appears to be binary; only text files are supported") ErrFileTooLarge = errors.New("fs: file exceeds the operation input limit") 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"`
}
type ApplyPatchTool ¶
type ApplyPatchTool struct {
// contains filtered or unexported fields
}
func NewApplyPatchTool ¶
func NewApplyPatchTool(executor PatchApplier) *ApplyPatchTool
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 `` /* 157-byte string literal not displayed */
ReplaceAll bool `` /* 142-byte string literal not displayed */
}
EditRequest drives Read → exact-string replace → Write atomically in the executor. Match policy (exact today, fuzzy in future) remains an executor concern.
type EditResponse ¶
type EditResponse struct {
Replacements int `json:"replacements"`
}
type EditTool ¶
type EditTool struct {
// contains filtered or unexported fields
}
EditTool is the thin LLM-facing adapter for Editor.Edit. The match-and-replace logic lives in the executor so a backend upgrade can swap match policy without changing the tool.
func NewEditTool ¶
func (*EditTool) Call ¶
func (e *EditTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)
func (*EditTool) ConcurrencyKey ¶
func (e *EditTool) ConcurrencyKey(invocation toolcontract.Invocation) (key string, concurrent bool)
ConcurrencyKey opts edit into concurrent execution keyed on its target file — the tool loop's optional concurrency contract (a tool reports per call whether it may overlap others and the resource it conflicts on). The loop parallelizes edits to DISTINCT files and serializes edits to the SAME file. An unparseable / empty path yields no key (no known conflict); the call still fails its own validation in Call.
func (*EditTool) Definition ¶
func (e *EditTool) Definition() chat.ToolDefinition
type Editor ¶
type Editor interface {
Edit(ctx context.Context, request EditRequest) (EditResponse, error)
}
type GlobInput ¶
type GlobInput struct {
Pattern string
Path string // "" = executor's authority root
IgnoreCase bool
MaxResults int // 0 = executor default
}
GlobInput uses doublestar syntax. Path narrows the executor's immutable authority to a relative subtree; it can never replace or broaden that root.
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 */
}
type GlobResponse ¶
type GlobTool ¶
type GlobTool struct {
// contains filtered or unexported fields
}
func NewGlobTool ¶
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, in GlobInput) (GlobResponse, error)
}
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
}
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 GrepOutputMode = "content" GrepOutputFilesWithMatches GrepOutputMode = "files_with_matches" 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
}
func NewGrepTool ¶
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)
}
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.
- 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
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 GlobInput) (_ 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 WriteInput) (_ WriteResponse, err error)
type PatchApplier ¶
type PatchApplier interface {
ApplyPatch(ctx context.Context, request ApplyPatchRequest) (ApplyPatchResponse, error)
}
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"`
}
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 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
}
func NewReadTool ¶
Example ¶
package main
import (
"fmt"
toolfs "github.com/Tangerg/scope/tools/fs"
)
func main() {
read := toolfs.NewReadTool(nil)
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 WriteInput ¶
type WriteRequest ¶
type WriteResponse ¶
type WriteResponse struct {
BytesWritten int `json:"bytes_written"`
}
type WriteTool ¶
type WriteTool struct {
// contains filtered or unexported fields
}
func NewWriteTool ¶
func (*WriteTool) Call ¶
func (w *WriteTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)
func (*WriteTool) ConcurrencyKey ¶
func (w *WriteTool) ConcurrencyKey(invocation toolcontract.Invocation) (key string, concurrent bool)
ConcurrencyKey opts write into concurrent execution keyed on its target file (the tool loop's optional concurrency contract): distinct-file writes run in parallel, same-file writes serialize. An unparseable / empty path yields no key (no known conflict).
func (*WriteTool) Definition ¶
func (w *WriteTool) Definition() chat.ToolDefinition
type Writer ¶
type Writer interface {
Write(ctx context.Context, in WriteInput) (WriteResponse, error)
}