diff --git a/internal/agent/tools/crush_logs.go b/internal/agent/tools/crush_logs.go
index 1de2ea3fc8ec1423d0b21f6099766b8f30880e01..0b0cb656cec2c7cd9803867bd91de0af09bb127b 100644
--- a/internal/agent/tools/crush_logs.go
+++ b/internal/agent/tools/crush_logs.go
@@ -5,6 +5,7 @@ import (
_ "embed"
"encoding/json"
"fmt"
+ "html/template"
"io"
"os"
"path/filepath"
@@ -18,8 +19,25 @@ import (
const CrushLogsToolName = "crush_logs"
-//go:embed crush_logs.md
-var crushLogsDescription string
+//go:embed crush_logs.md.tpl
+var crushLogsDescriptionTmpl []byte
+
+var crushLogsDescriptionTpl = template.Must(
+ template.New("crushLogsDescription").
+ Parse(string(crushLogsDescriptionTmpl)),
+)
+
+type crushLogsDescriptionData struct {
+ DefaultLines int
+ MaxLines int
+}
+
+func crushLogsDescription() string {
+ return renderTemplate(crushLogsDescriptionTpl, crushLogsDescriptionData{
+ DefaultLines: defaultLogLines,
+ MaxLines: maxLogLines,
+ })
+}
// Max line size to prevent memory issues with very long log lines (1 MB).
const maxLogLineSize = 1024 * 1024
@@ -58,7 +76,7 @@ type CrushLogsParams struct {
func NewCrushLogsTool(logFile string) fantasy.AgentTool {
return fantasy.NewAgentTool(
CrushLogsToolName,
- crushLogsDescription,
+ crushLogsDescription(),
func(ctx context.Context, params CrushLogsParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
result := runCrushLogs(logFile, params)
return fantasy.NewTextResponse(result), nil
diff --git a/internal/agent/tools/crush_logs.md b/internal/agent/tools/crush_logs.md.tpl
similarity index 56%
rename from internal/agent/tools/crush_logs.md
rename to internal/agent/tools/crush_logs.md.tpl
index 918c4505849b3caaf8eb5c1799f1a2b5b67db9b7..4879eb5cf46026669c403dc5f0f7c982465bbe7b 100644
--- a/internal/agent/tools/crush_logs.md
+++ b/internal/agent/tools/crush_logs.md.tpl
@@ -1,4 +1,4 @@
-Read Crush's internal application logs (default 50 entries, max 100); useful for diagnosing provider errors, tool failures, LSP/MCP issues.
+Read Crush's internal application logs (default {{ .DefaultLines }} entries, max {{ .MaxLines }}); useful for diagnosing provider errors, tool failures, LSP/MCP issues.
- Returns recent log entries from Crush's internal log file
@@ -8,6 +8,6 @@ Read Crush's internal application logs (default 50 entries, max 100); useful for
-- Default returns last 50 entries; use lines parameter for more (max 100)
+- Default returns last {{ .DefaultLines }} entries; use lines parameter for more (max {{ .MaxLines }})
- Look for ERROR and WARN entries first when diagnosing problems
diff --git a/internal/agent/tools/download.go b/internal/agent/tools/download.go
index 2cb7e19d61533cc44ca6cd9d1a235ef0c8f08b6a..1e04f9980cb35f595088dfc81d6572dd65351a58 100644
--- a/internal/agent/tools/download.go
+++ b/internal/agent/tools/download.go
@@ -5,6 +5,7 @@ import (
"context"
_ "embed"
"fmt"
+ "html/template"
"io"
"net/http"
"os"
@@ -31,8 +32,23 @@ type DownloadPermissionsParams struct {
const DownloadToolName = "download"
-//go:embed download.md
-var downloadDescription string
+//go:embed download.md.tpl
+var downloadDescriptionTmpl []byte
+
+var downloadDescriptionTpl = template.Must(
+ template.New("downloadDescription").
+ Parse(string(downloadDescriptionTmpl)),
+)
+
+type downloadDescriptionData struct {
+ MaxDownloadTimeout int
+}
+
+func downloadDescription() string {
+ return renderTemplate(downloadDescriptionTpl, downloadDescriptionData{
+ MaxDownloadTimeout: 600,
+ })
+}
func NewDownloadTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
if client == nil {
@@ -48,7 +64,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
}
return fantasy.NewParallelAgentTool(
DownloadToolName,
- downloadDescription,
+ downloadDescription(),
func(ctx context.Context, params DownloadParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.URL == "" {
return fantasy.NewTextErrorResponse("URL parameter is required"), nil
diff --git a/internal/agent/tools/download.md b/internal/agent/tools/download.md
deleted file mode 100644
index 3adb507e4d49300c8dbd2e263c9f9ec65f54629e..0000000000000000000000000000000000000000
--- a/internal/agent/tools/download.md
+++ /dev/null
@@ -1 +0,0 @@
-Download a URL directly to a local file (binary-safe, streaming, max 100MB); overwrites without warning. For reading content into context use fetch.
\ No newline at end of file
diff --git a/internal/agent/tools/download.md.tpl b/internal/agent/tools/download.md.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..2cdc6826934d70b4842fe2a4e912f830fd81fb20
--- /dev/null
+++ b/internal/agent/tools/download.md.tpl
@@ -0,0 +1 @@
+Download a URL directly to a local file (binary-safe, streaming, max {{ .MaxDownloadTimeout }}s timeout); overwrites without warning. For reading content into context use fetch.
diff --git a/internal/agent/tools/fetch.go b/internal/agent/tools/fetch.go
index 6a272b65067b221bb0d7bfe6675672c78af5b459..599a32f84c0c33eeaeee65385be2613e635544fc 100644
--- a/internal/agent/tools/fetch.go
+++ b/internal/agent/tools/fetch.go
@@ -30,6 +30,18 @@ var fetchDescriptionTpl = template.Must(
Parse(string(fetchDescriptionTmpl)),
)
+type fetchDescriptionData struct {
+ GhAvailable bool
+ MaxFetchSizeKB int
+}
+
+func fetchDescription() string {
+ return renderTemplate(fetchDescriptionTpl, fetchDescriptionData{
+ GhAvailable: ghAvailable,
+ MaxFetchSizeKB: MaxFetchSize / 1024,
+ })
+}
+
func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
if client == nil {
transport := http.DefaultTransport.(*http.Transport).Clone()
@@ -45,7 +57,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
return fantasy.NewParallelAgentTool(
FetchToolName,
- renderToolDescription(fetchDescriptionTpl),
+ fetchDescription(),
func(ctx context.Context, params FetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.URL == "" {
return fantasy.NewTextErrorResponse("URL parameter is required"), nil
diff --git a/internal/agent/tools/fetch.md.tpl b/internal/agent/tools/fetch.md.tpl
index a5ed224d540c5d592e9c8b53b911afab9af4ea94..1bffa00179aa77a69f6e30e1235a84caf3a22950 100644
--- a/internal/agent/tools/fetch.md.tpl
+++ b/internal/agent/tools/fetch.md.tpl
@@ -1,2 +1,2 @@
-Fetch raw content from a URL as text, markdown, or html (max 100KB); no AI processing. For analysis or extraction use agentic_fetch.
+Fetch raw content from a URL as text, markdown, or html (max {{ .MaxFetchSizeKB }}KB); no AI processing. For analysis or extraction use agentic_fetch.
{{- if .GhAvailable }} For GitHub content when an exact repo, issue, or PR link is provided, use `gh` CLI in bash instead.{{- end }}
diff --git a/internal/agent/tools/glob.go b/internal/agent/tools/glob.go
index fff81236027112f9722eb84e988926ada6f13f6c..d9c3162e755a655eca78f448c3d523c6e862523d 100644
--- a/internal/agent/tools/glob.go
+++ b/internal/agent/tools/glob.go
@@ -6,6 +6,7 @@ import (
"context"
_ "embed"
"fmt"
+ "html/template"
"log/slog"
"os/exec"
"path/filepath"
@@ -19,8 +20,23 @@ import (
const GlobToolName = "glob"
-//go:embed glob.md
-var globDescription string
+//go:embed glob.md.tpl
+var globDescriptionTmpl []byte
+
+var globDescriptionTpl = template.Must(
+ template.New("globDescription").
+ Parse(string(globDescriptionTmpl)),
+)
+
+type globDescriptionData struct {
+ MaxResults int
+}
+
+func globDescription() string {
+ return renderTemplate(globDescriptionTpl, globDescriptionData{
+ MaxResults: 100,
+ })
+}
type GlobParams struct {
Pattern string `json:"pattern" description:"The glob pattern to match files against"`
@@ -35,7 +51,7 @@ type GlobResponseMetadata struct {
func NewGlobTool(workingDir string) fantasy.AgentTool {
return fantasy.NewAgentTool(
GlobToolName,
- globDescription,
+ globDescription(),
func(ctx context.Context, params GlobParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.Pattern == "" {
return fantasy.NewTextErrorResponse("pattern is required"), nil
diff --git a/internal/agent/tools/glob.md b/internal/agent/tools/glob.md
deleted file mode 100644
index e1038856691883102e6cdf82315079d91ab25c68..0000000000000000000000000000000000000000
--- a/internal/agent/tools/glob.md
+++ /dev/null
@@ -1 +0,0 @@
-Find files by name/pattern (glob syntax), sorted by modification time; max 100 results; skips hidden files. Use grep to search file contents.
\ No newline at end of file
diff --git a/internal/agent/tools/glob.md.tpl b/internal/agent/tools/glob.md.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..23a256976b7b91339d4ac691dd4c771551d1838e
--- /dev/null
+++ b/internal/agent/tools/glob.md.tpl
@@ -0,0 +1 @@
+Find files by name/pattern (glob syntax), sorted by modification time; max {{ .MaxResults }} results; skips hidden files. Use grep to search file contents.
diff --git a/internal/agent/tools/grep.go b/internal/agent/tools/grep.go
index 9b75d400c7d0c1dadbb529ddf49e3301e4892d31..eab649181af705eaca0e6ed6c39007893b164760 100644
--- a/internal/agent/tools/grep.go
+++ b/internal/agent/tools/grep.go
@@ -8,6 +8,7 @@ import (
_ "embed"
"encoding/json"
"fmt"
+ "html/template"
"io"
"net/http"
"os"
@@ -89,8 +90,23 @@ const (
maxGrepContentWidth = 500
)
-//go:embed grep.md
-var grepDescription string
+//go:embed grep.md.tpl
+var grepDescriptionTmpl []byte
+
+var grepDescriptionTpl = template.Must(
+ template.New("grepDescription").
+ Parse(string(grepDescriptionTmpl)),
+)
+
+type grepDescriptionData struct {
+ MaxResults int
+}
+
+func grepDescription() string {
+ return renderTemplate(grepDescriptionTpl, grepDescriptionData{
+ MaxResults: 100,
+ })
+}
// escapeRegexPattern escapes special regex characters so they're treated as literal characters
func escapeRegexPattern(pattern string) string {
@@ -107,7 +123,7 @@ func escapeRegexPattern(pattern string) string {
func NewGrepTool(workingDir string, config config.ToolGrep) fantasy.AgentTool {
return fantasy.NewAgentTool(
GrepToolName,
- grepDescription,
+ grepDescription(),
func(ctx context.Context, params GrepParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.Pattern == "" {
return fantasy.NewTextErrorResponse("pattern is required"), nil
diff --git a/internal/agent/tools/grep.md b/internal/agent/tools/grep.md
deleted file mode 100644
index 057a20c6ca529455c45c902830af5b11a2726962..0000000000000000000000000000000000000000
--- a/internal/agent/tools/grep.md
+++ /dev/null
@@ -1 +0,0 @@
-Search file contents by regex or literal text; returns matching file paths sorted by modification time (max 100); respects .gitignore. Use glob to filter by filename, not contents.
\ No newline at end of file
diff --git a/internal/agent/tools/grep.md.tpl b/internal/agent/tools/grep.md.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..c1543a929bf2d5f03aa7a87811fc8122e07d7708
--- /dev/null
+++ b/internal/agent/tools/grep.md.tpl
@@ -0,0 +1 @@
+Search file contents by regex or literal text; returns matching file paths sorted by modification time (max {{ .MaxResults }}); respects .gitignore. Use glob to filter by filename, not contents.
diff --git a/internal/agent/tools/ls.go b/internal/agent/tools/ls.go
index 8bf5f0d414192df05bfa26713295a57fe45e6ae9..750e5797836b5efd884d9fe47b039ab4e00cc95a 100644
--- a/internal/agent/tools/ls.go
+++ b/internal/agent/tools/ls.go
@@ -5,6 +5,7 @@ import (
"context"
_ "embed"
"fmt"
+ "html/template"
"os"
"path/filepath"
"strings"
@@ -52,13 +53,28 @@ const (
maxLSFiles = 1000
)
-//go:embed ls.md
-var lsDescription string
+//go:embed ls.md.tpl
+var lsDescriptionTmpl []byte
+
+var lsDescriptionTpl = template.Must(
+ template.New("lsDescription").
+ Parse(string(lsDescriptionTmpl)),
+)
+
+type lsDescriptionData struct {
+ MaxFiles int
+}
+
+func lsDescription() string {
+ return renderTemplate(lsDescriptionTpl, lsDescriptionData{
+ MaxFiles: maxLSFiles,
+ })
+}
func NewLsTool(permissions permission.Service, workingDir string, lsConfig config.ToolLs) fantasy.AgentTool {
return fantasy.NewAgentTool(
LSToolName,
- lsDescription,
+ lsDescription(),
func(ctx context.Context, params LSParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
searchPath, err := fsext.Expand(cmp.Or(params.Path, workingDir))
if err != nil {
diff --git a/internal/agent/tools/ls.md b/internal/agent/tools/ls.md
deleted file mode 100644
index 4ff5001acf0ec4619fce6467d05da70c47cc58ac..0000000000000000000000000000000000000000
--- a/internal/agent/tools/ls.md
+++ /dev/null
@@ -1 +0,0 @@
-List files and directories as a tree; skips hidden files and common system dirs; max 1000 files. Use glob to find files by pattern, grep to search contents.
\ No newline at end of file
diff --git a/internal/agent/tools/ls.md.tpl b/internal/agent/tools/ls.md.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..7fa4c41be37987aefc79bc041364eef81801ef3c
--- /dev/null
+++ b/internal/agent/tools/ls.md.tpl
@@ -0,0 +1 @@
+List files and directories as a tree; skips hidden files and common system dirs; max {{ .MaxFiles }} files. Use glob to find files by pattern, grep to search contents.
diff --git a/internal/agent/tools/sourcegraph.go b/internal/agent/tools/sourcegraph.go
index 041503ce65d4bedf3245468d155147a372ab9cf6..ee7de67a6674ade83f19f2a78badda3e1c16bde2 100644
--- a/internal/agent/tools/sourcegraph.go
+++ b/internal/agent/tools/sourcegraph.go
@@ -6,6 +6,7 @@ import (
_ "embed"
"encoding/json"
"fmt"
+ "html/template"
"io"
"net/http"
"strings"
@@ -28,8 +29,23 @@ type SourcegraphResponseMetadata struct {
const SourcegraphToolName = "sourcegraph"
-//go:embed sourcegraph.md
-var sourcegraphDescription string
+//go:embed sourcegraph.md.tpl
+var sourcegraphDescriptionTmpl []byte
+
+var sourcegraphDescriptionTpl = template.Must(
+ template.New("sourcegraphDescription").
+ Parse(string(sourcegraphDescriptionTmpl)),
+)
+
+type sourcegraphDescriptionData struct {
+ MaxResults int
+}
+
+func sourcegraphDescription() string {
+ return renderTemplate(sourcegraphDescriptionTpl, sourcegraphDescriptionData{
+ MaxResults: 20,
+ })
+}
func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
if client == nil {
@@ -45,7 +61,7 @@ func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
}
return fantasy.NewParallelAgentTool(
SourcegraphToolName,
- sourcegraphDescription,
+ sourcegraphDescription(),
func(ctx context.Context, params SourcegraphParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.Query == "" {
return fantasy.NewTextErrorResponse("Query parameter is required"), nil
diff --git a/internal/agent/tools/sourcegraph.md b/internal/agent/tools/sourcegraph.md.tpl
similarity index 68%
rename from internal/agent/tools/sourcegraph.md
rename to internal/agent/tools/sourcegraph.md.tpl
index bbfd67d7fdfd12bd7eae9de252fc394f3062cbe5..693306874173efe130320c63bbfa24f2cdd2fad2 100644
--- a/internal/agent/tools/sourcegraph.md
+++ b/internal/agent/tools/sourcegraph.md.tpl
@@ -1 +1 @@
-Search code across public GitHub repositories via Sourcegraph; supports regex, language/repo/file filters, and symbol search (max 20 results). Only searches public repos.
\ No newline at end of file
+Search code across public GitHub repositories via Sourcegraph; supports regex, language/repo/file filters, and symbol search (max {{ .MaxResults }} results). Only searches public repos.
diff --git a/internal/agent/tools/tools.go b/internal/agent/tools/tools.go
index a18555a45f48294555179aac8c5f029fc0d39393..bf9eb179cc8320679fa42bf7a85b78e9a547cc5f 100644
--- a/internal/agent/tools/tools.go
+++ b/internal/agent/tools/tools.go
@@ -90,3 +90,12 @@ func renderToolDescription(tmpl *template.Template) string {
}
return out.String()
}
+
+// renderTemplate renders a Go template with the given data.
+func renderTemplate(tmpl *template.Template, data any) string {
+ var out bytes.Buffer
+ if err := tmpl.Execute(&out, data); err != nil {
+ panic("failed to execute tool description template: " + err.Error())
+ }
+ return out.String()
+}
diff --git a/internal/agent/tools/view.go b/internal/agent/tools/view.go
index fef294cecf17efb28d69fe3a9c3b1e8d65de11f2..bc16b0def26ac433641632a9db9bd0ca4dbe4fca 100644
--- a/internal/agent/tools/view.go
+++ b/internal/agent/tools/view.go
@@ -6,6 +6,7 @@ import (
_ "embed"
"errors"
"fmt"
+ "html/template"
"io"
"io/fs"
"net/http"
@@ -23,8 +24,25 @@ import (
"github.com/charmbracelet/crush/internal/skills"
)
-//go:embed view.md
-var viewDescription string
+//go:embed view.md.tpl
+var viewDescriptionTmpl []byte
+
+var viewDescriptionTpl = template.Must(
+ template.New("viewDescription").
+ Parse(string(viewDescriptionTmpl)),
+)
+
+type viewDescriptionData struct {
+ DefaultReadLimit int
+ MaxViewSizeKB int
+}
+
+func viewDescription() string {
+ return renderTemplate(viewDescriptionTpl, viewDescriptionData{
+ DefaultReadLimit: DefaultReadLimit,
+ MaxViewSizeKB: MaxViewSize / 1024,
+ })
+}
type ViewParams struct {
FilePath string `json:"file_path" description:"The path to the file to read"`
@@ -79,7 +97,7 @@ func NewViewTool(
) fantasy.AgentTool {
return fantasy.NewAgentTool(
ViewToolName,
- viewDescription,
+ viewDescription(),
func(ctx context.Context, params ViewParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.FilePath == "" {
return fantasy.NewTextErrorResponse("file_path is required"), nil
diff --git a/internal/agent/tools/view.md b/internal/agent/tools/view.md
deleted file mode 100644
index 6d616d661146fc3d17e2267a6ad3a9f73f807add..0000000000000000000000000000000000000000
--- a/internal/agent/tools/view.md
+++ /dev/null
@@ -1 +0,0 @@
-Read a file by path with line numbers; supports offset and line limit (default 2000, max 200KB); renders images (PNG, JPEG, GIF, BMP, SVG, WebP); use ls for directories.
diff --git a/internal/agent/tools/view.md.tpl b/internal/agent/tools/view.md.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..ff2d84022c5d98ec711701f77f9ff9b16b505b4d
--- /dev/null
+++ b/internal/agent/tools/view.md.tpl
@@ -0,0 +1 @@
+Read a file by path with line numbers; supports offset and line limit (default {{ .DefaultReadLimit }}, max {{ .MaxViewSizeKB }}KB returned file content section); renders images (PNG, JPEG, GIF, WebP); use ls for directories.