llmtools

package module
v0.14.0 Latest Latest
Warning

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

Go to latest
Published: Mar 30, 2026 License: MIT Imports: 16 Imported by: 1

README

LLM Tools for Go

Go Report Card lint test

Go-native, cross-platform tools for common local tasks, plus a small registry that makes them easy to expose to LLM tool-calling systems.

Table of contents

Features at a glance

  • Cross-platform implementations for Linux, macOS, and Windows.
  • Instance-owned tools with configurable path policy.
  • Deterministic text-editing tools for line-oriented workflows.
  • Shell command and script execution with timeouts, output caps, and command blocking.
  • Structured tool manifests and normalized outputs for text, files, and images.
File system tools

Grouped under fstool.

  • readfile

    • encoding=text: reads UTF-8 text and supports extracted text for PDFs.
    • encoding=binary: returns base64, emitting image outputs for image/* MIME types and file outputs otherwise.
    • Safety: size caps and symlink hardening.
  • writefile

    • encoding=text: write UTF-8 content.
    • encoding=binary: write base64-decoded bytes.
    • Options: overwrite, createParents (bounded), atomic writes, size caps, symlink hardening.
  • deletefile

    • Safe delete by moving to trash.
    • trashDir=auto tries system trash when possible and otherwise falls back to a local .trash directory.
  • searchfiles: recursively search file paths and UTF-8 text content using RE2 regex.

  • listdirectory: list entries under a directory, optionally filtered by glob.

  • statpath: inspect a path.

  • mimeforpath: best-effort MIME type detection.

  • mimeforextension: MIME lookup for an extension.

Execute tools

Grouped under exectool.

  • shellcommand

    • Execute one or more shell commands via a selected shell.
    • Supports session-like persistence for workDir and env across calls.
    • Sessions are not persistent shell processes.
  • runscript

    • Run an existing script from disk with arguments and environment overrides.
    • Uses extension-based interpreter selection via RunScriptPolicy.
Text processing tools

Grouped under texttool.

  • readtextrange: read lines, optionally constrained by unique start/end marker blocks.
  • findtext: find matches with context.
  • inserttextlines: insert lines at start/end or relative to a uniquely matched anchor block.
  • replacetextlines: replace exact line blocks, with optional disambiguation using adjacent lines.
  • deletetextlines: delete exact line blocks, with optional disambiguation using adjacent lines.
Image tools

Grouped under imagetool.

  • readimage: read intrinsic metadata and optionally include base64 content.

Package overview

  • llmtools: registry and tool registration helpers.
  • spec: tool manifests and output union types.
  • fstool: filesystem tools.
  • exectool: shell command execution and script execution.
  • texttool: safe, deterministic line-based text editing tools.
  • imagetool: image tools.

Registry

The registry provides:

  • tool registration and lookup by spec.FuncID
  • stable manifest ordering (Tools() sorted by slug + funcID)
  • per-registry default call timeout via WithDefaultCallTimeout
  • per-call timeout override via llmtools.WithCallTimeout(...)
  • panic-to-error recovery around tool execution

Tool outputs

  • Registry.Call returns []spec.ToolOutputUnion.
  • Most tools are registered via RegisterTypedAsTextTool, which wraps the Go result as JSON and returns it as a single text output item.
  • Tools that naturally return binary or image data can emit typed image or file outputs.

Path policy

All tools are instance-owned. Hosts can configure:

  • workBaseDir: base directory for resolving relative paths
  • allowedRoots: optional allowlist roots; when set, resolved tool paths must stay within these roots
  • blockSymlinks: optional symlink hardening for supported operations

This is path policy, not a general OS sandbox for child processes.

Examples

All examples are provided as end-to-end integration tests that:

  • start from a registry
  • register tools scoped to a temp directory
  • execute realistic read/modify loops, text edits, shell sessions, script execution, and binary/image workflows

Examples:

  • Text read/modify loop: text test
  • Filesystem, MIME, safe delete, binary, and image flows: fs + image test
  • Shell sessions, environment persistence, and runscript: exec test

Exec tool notes

Quick model

For exectool, the most useful mental model is:

  • workBaseDir chooses the default base for relative paths.
  • shellcommand.workDir chooses the actual child-process working directory.
  • shellcommand.env and runscript.env add per-call environment overrides.
  • Shell sessions persist tool-managed workDir and env, not shell process state.
  • Commands are executed in fresh child processes, not in a long-lived interactive terminal.
Shell selection and bootstrap defaults

shellcommand supports explicit shell selection with:

  • auto
  • bash, zsh, sh, dash, ksh, fish
  • pwsh, powershell, cmd

ExecTool also supports tool-level defaults:

  • WithDefaultShell(...)
  • WithBaseEnv(...)

If the caller does not provide them, NewExecTool(...) best-effort bootstraps defaults:

  • detects a preferred shell
  • captures a narrow, execution-oriented base environment

Auto shell detection order is:

  • Unix/macOS:
    1. $SHELL
    2. account login shell from the host
    3. fallback by platform/tool availability
  • Windows:
    1. pwsh
    2. powershell
    3. cmd

You can also call BootstrapDefaults(ctx) directly and pass the results yourself.

ctx := context.Background()
defs, _ := exectool.BootstrapDefaults(ctx)

tool, err := exectool.NewExecTool(
   exectool.WithWorkBaseDir(projectDir),
   exectool.WithDefaultShell(defs.DefaultShell),
   exectool.WithBaseEnv(defs.BaseEnv),
)
Working directory behavior

shellcommand resolves working directory in this order:

  1. args.workDir
  2. session workdir
  3. tool workBaseDir

Key points:

  • If workDir is omitted and there is no session workdir, execution starts in the tool workBaseDir.
  • If workBaseDir is omitted, the path policy falls back to the current process working directory unless allowedRoots is set, in which case it defaults to the first canonical allowed root.
  • Relative paths are resolved against workBaseDir unless a more specific absolute/derived path is supplied by the tool logic.

For stable desktop-app behavior, hosts should usually:

  • set workBaseDir explicitly
  • pass an absolute per-call workDir for the active project/workspace
Environment behavior

Effective environment merge order is:

  • shellcommand

    1. current process environment
    2. tool base env (WithBaseEnv or bootstrapped defaults)
    3. session env
    4. per-call env
  • runscript

    1. current process environment
    2. tool base env (WithBaseEnv or bootstrapped defaults)
    3. per-call env

Bootstrapped base env is intentionally filtered. It keeps variables that are usually relevant to command and interpreter resolution, for example:

  • command lookup: PATH, PATHEXT
  • home/temp/runtime basics: HOME, USERPROFILE, TMP, TEMP, TMPDIR, SHELL, COMSPEC, app-data dirs
  • locale basics: LANG, LC_*, TERM
  • common toolchain/version-manager variables: ASDF_*, PYENV_*, NVM_*, VOLTA_*, SDKMAN_*, GOPATH, GOBIN, JAVA_HOME, PNPM_HOME, CARGO_HOME, CONDA_*

This avoids importing unrelated prompt/theme variables while still making command lookup and common toolchains behave more like the user’s shell.

Shell sessions: what persists and what does not

shellcommand sessions are persistent tool state, not persistent shell processes.

What persists across calls when you reuse sessionID:

  • workDir passed as a tool argument
  • env passed as a tool argument

What does not persist:

  • cd ... issued inside a command string
  • export FOO=... issued inside a command string
  • aliases, functions, shell options, sourced files
  • any in-memory shell state

So this works predictably:

{
  "workDir": "/repo",
  "commands": ["go test ./..."]
}

But this is not persistent across separate command entries or later calls:

{
  "commands": ["cd /repo", "go test ./..."]
}

Each command entry is executed in a fresh shell process.

Command lookup and login-shell behavior

Working directory and command lookup are separate concerns.

  • Working directory controls where the child process starts.
  • Command lookup for bare names like golangci-lint depends on PATH and shell behavior.

Important nuance:

  • Commands are executed using non-persistent shell invocations.
  • You should not assume they behave exactly like an interactive terminal.
  • On Unix, zsh -c/bash -c behavior differs from an interactive login terminal.
  • On PowerShell, execution uses explicit non-interactive flags.

That is why BootstrapDefaults(...) exists: it captures a stable, tool-useful environment once, instead of relying on every command to run as an interactive login shell.

For predictable command execution, hosts should prefer:

  • explicit workDir
  • explicit or bootstrapped base env
  • explicit shell or WithDefaultShell(...)
  • absolute executable paths when needed
Run Script behavior

runscript:

  • executes a pre-existing script from disk
  • validates the script path through path policy
  • selects how to invoke the script from RunScriptPolicy

Default policy includes mappings such as:

  • .sh -> shell mode with sh
  • .bash -> shell mode with bash
  • .zsh -> shell mode with zsh
  • .ps1 -> direct PowerShell execution
  • .py -> interpreter mode with python3 on Unix and python on Windows

Behavioral notes:

  • runscript inherits tool base env and per-call env.
  • Interpreter availability still depends on the effective environment, especially PATH.
Execution limits and blocking

ExecutionPolicy controls:

  • timeout
  • output caps
  • max commands
  • max command length

There is also a non-overridable hard blocklist of command names and some heuristic blocking for obviously unsafe shell patterns.

The registry may also impose a call timeout. If you use both registry-level and tool-level timeouts, ensure the registry timeout is not shorter than the exec-tool timeout.

Important behavioral notes
  • executeParallel=true does not run commands concurrently. It means commands are treated as independent and execution does not stop on the first non-zero result.
  • shellcommand executes each command entry separately. If you need shell chaining semantics, put them in one command string, for example cd repo && go test ./....
  • workBaseDir is a default anchor, not a persistent current-directory concept by itself.
  • If you omit both workBaseDir and allowedRoots, relative/default behavior falls back to the host process working directory.
  • If you set allowedRoots but omit workBaseDir, the base defaults to the first canonical allowed root.
  • allowedRoots constrains tool-resolved paths; it does not turn child processes into a full OS sandbox.
  • For packaged desktop apps, relying on ambient process cwd or ambient process env is usually less stable than passing explicit values.

Development

  • Formatting follows gofumpt and golines via golangci-lint. Rules are in .golangci.yml.
  • Useful scripts are defined in taskfile.yml; requires Task.
  • Bug reports and PRs are welcome:
    • Keep the public API (package llmtools and spec) small and intentional.
    • Avoid leaking provider-specific types through the public surface; put them under internal/.
    • Please run tests and linters before sending a PR.

License

Copyright (c) 2026 - Present - Pankaj Pipada

All source code in this repository, unless otherwise noted, is licensed under the MIT License. See LICENSE for details.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RegisterBuiltins

func RegisterBuiltins(r *Registry) error

RegisterBuiltins registers the built-in tools into r.

func RegisterOutputsTool

func RegisterOutputsTool[T any](
	r *Registry,
	tool spec.Tool,
	fn func(context.Context, T) ([]spec.ToolOutputUnion, error),
) error

RegisterOutputsTool registers a typed tool function that directly returns []ToolOutputUnion. This is a function and not a method on struct as methods cannot have type params in go.

func RegisterTypedAsTextTool

func RegisterTypedAsTextTool[T, R any](
	r *Registry,
	tool spec.Tool,
	fn func(context.Context, T) (R, error),
) error

RegisterTypedAsTextTool registers a typed tool function whose output R is JSON-encodable. The JSON representation of R is wrapped into a single text block. This is a function and not a method on struct as methods cannot have type params in go.

Types

type CallOption

type CallOption func(*callOptions)

CallOption configures per-call behavior.

func WithCallTimeout

func WithCallTimeout(d time.Duration) CallOption

WithCallTimeout overrides the timeout for this single call. 0 means "no timeout" for this call (even if tool/registry default is non-zero).

type Registry

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

Registry provides lookup/register for Go tools by funcID, with json.RawMessage I/O.

func NewBuiltinRegistry

func NewBuiltinRegistry(opts ...RegistryOption) (*Registry, error)

NewBuiltinRegistry returns a Registry with all built-in tools registered. By default it applies a 10mins timeout, but callers can override it by passing WithDefaultCallTimeout as a later option.

func NewRegistry

func NewRegistry(opts ...RegistryOption) (*Registry, error)

func (*Registry) Call

func (r *Registry) Call(
	ctx context.Context,
	funcID spec.FuncID,
	in json.RawMessage,
	callOpts ...CallOption,
) ([]spec.ToolOutputUnion, error)

func (*Registry) Lookup

func (r *Registry) Lookup(funcID spec.FuncID) (spec.ToolFunc, bool)

func (*Registry) RegisterTool

func (r *Registry) RegisterTool(tool spec.Tool, fn spec.ToolFunc) error

func (*Registry) Tools

func (r *Registry) Tools() []spec.Tool

type RegistryOption

type RegistryOption func(*Registry) error

func WithDefaultCallTimeout

func WithDefaultCallTimeout(d time.Duration) RegistryOption

func WithLogger

func WithLogger(logger *slog.Logger) RegistryOption

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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