diff --git a/internal/agent/tools/bash.go b/internal/agent/tools/bash.go index 41b767244819399778d16bfedb818e187b4073be..d623d58bc418cf2810958f86ecb2472724129deb 100644 --- a/internal/agent/tools/bash.go +++ b/internal/agent/tools/bash.go @@ -53,7 +53,7 @@ const ( BashNoOutput = "no output" ) -//go:embed bash.tpl +//go:embed bash.md.tpl var bashDescriptionTmpl []byte var bashDescriptionTpl = template.Must( @@ -66,6 +66,7 @@ type bashDescriptionData struct { MaxOutputLength int Attribution config.Attribution ModelID string + RgAvailable bool } var bannedCommands = []string{ @@ -149,6 +150,7 @@ func bashDescription(attribution *config.Attribution, modelID string) string { MaxOutputLength: MaxOutputLength, Attribution: *attribution, ModelID: modelID, + RgAvailable: getRg() != "", }); err != nil { // this should never happen. panic("failed to execute bash description template: " + err.Error()) diff --git a/internal/agent/tools/bash.tpl b/internal/agent/tools/bash.md.tpl similarity index 96% rename from internal/agent/tools/bash.tpl rename to internal/agent/tools/bash.md.tpl index cfdc6b107e4e9a521341b6d3674badd20fca6c22..19094eef2ff50c8ab2b4f24b003c02cc4cbab803 100644 --- a/internal/agent/tools/bash.tpl +++ b/internal/agent/tools/bash.md.tpl @@ -21,6 +21,9 @@ Common shell builtins and core utils available on Windows. - Chain with ';' or '&&', avoid newlines except in quoted strings - Each command runs in independent shell (no state persistence between calls) - Prefer absolute paths over 'cd' (use 'cd' only if user explicitly requests) +{{- if .RgAvailable }} +- Ripgrep (`rg`) is available; prefer it over `grep` for faster, more intuitive searching +{{- end }} @@ -82,7 +85,7 @@ When user asks to create git commit: 6. Run git status to verify. -Notes: Use "git commit -am" when possible, don't stage unrelated files, NEVER update config, don't push, no -i flags, no empty commits, return empty response. +Notes: Use "git commit -am" when possible, don't stage unrelated files, NEVER update config, don't push, no -i flags, no empty commits, return empty response, when rebasing always use -m or GIT_EDITOR=true. diff --git a/internal/agent/tools/fetch.go b/internal/agent/tools/fetch.go index d58a37fdd046ba0113d7f6935548c712be2fefde..6a272b65067b221bb0d7bfe6675672c78af5b459 100644 --- a/internal/agent/tools/fetch.go +++ b/internal/agent/tools/fetch.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" "fmt" + "html/template" "io" "net/http" "strings" @@ -21,8 +22,13 @@ const ( MaxFetchSize = 100 * 1024 // 100KB ) -//go:embed fetch.md -var fetchDescription string +//go:embed fetch.md.tpl +var fetchDescriptionTmpl []byte + +var fetchDescriptionTpl = template.Must( + template.New("fetchDescription"). + Parse(string(fetchDescriptionTmpl)), +) func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool { if client == nil { @@ -39,7 +45,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt return fantasy.NewParallelAgentTool( FetchToolName, - fetchDescription, + renderToolDescription(fetchDescriptionTpl), 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 b/internal/agent/tools/fetch.md deleted file mode 100644 index 151c7f36c83a923abfae3574eacf31acc96de978..0000000000000000000000000000000000000000 --- a/internal/agent/tools/fetch.md +++ /dev/null @@ -1 +0,0 @@ -Fetch raw content from a URL as text, markdown, or html (max 100KB); no AI processing. For analysis or extraction use agentic_fetch. \ No newline at end of file diff --git a/internal/agent/tools/fetch.md.tpl b/internal/agent/tools/fetch.md.tpl new file mode 100644 index 0000000000000000000000000000000000000000..a5ed224d540c5d592e9c8b53b911afab9af4ea94 --- /dev/null +++ b/internal/agent/tools/fetch.md.tpl @@ -0,0 +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. +{{- 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/tools.go b/internal/agent/tools/tools.go index fb67ed544693c3b0ac226adad873bbbd0f45555d..a18555a45f48294555179aac8c5f029fc0d39393 100644 --- a/internal/agent/tools/tools.go +++ b/internal/agent/tools/tools.go @@ -1,7 +1,10 @@ package tools import ( + "bytes" "context" + "html/template" + "os/exec" "charm.land/fantasy" ) @@ -64,3 +67,26 @@ func NewPermissionDeniedResponse() fantasy.ToolResponse { resp.StopTurn = true return resp } + +// ghAvailable indicates whether the `gh` CLI is available on PATH. +var ghAvailable = func() bool { + _, err := exec.LookPath("gh") + return err == nil +}() + +// toolDescriptionData is the common data structure for tool description templates. +type toolDescriptionData struct { + GhAvailable bool +} + +// renderToolDescription renders a tool description template with the given data. +func renderToolDescription(tmpl *template.Template) string { + data := toolDescriptionData{ + GhAvailable: ghAvailable, + } + 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/web_fetch.go b/internal/agent/tools/web_fetch.go index 31a6f0888f421b5b04c49090e6010002dee1de89..f52c29c60f440e631b97ed3700ccc084cefbf7a5 100644 --- a/internal/agent/tools/web_fetch.go +++ b/internal/agent/tools/web_fetch.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" "fmt" + "html/template" "net/http" "os" "strings" @@ -12,8 +13,13 @@ import ( "charm.land/fantasy" ) -//go:embed web_fetch.md -var webFetchToolDescription string +//go:embed web_fetch.md.tpl +var webFetchDescriptionTmpl []byte + +var webFetchDescriptionTpl = template.Must( + template.New("webFetchDescription"). + Parse(string(webFetchDescriptionTmpl)), +) // NewWebFetchTool creates a simple web fetch tool for sub-agents (no permissions needed). func NewWebFetchTool(workingDir string, client *http.Client) fantasy.AgentTool { @@ -31,7 +37,7 @@ func NewWebFetchTool(workingDir string, client *http.Client) fantasy.AgentTool { return fantasy.NewParallelAgentTool( WebFetchToolName, - webFetchToolDescription, + renderToolDescription(webFetchDescriptionTpl), func(ctx context.Context, params WebFetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) { if params.URL == "" { return fantasy.NewTextErrorResponse("url is required"), nil diff --git a/internal/agent/tools/web_fetch.md b/internal/agent/tools/web_fetch.md deleted file mode 100644 index 51249e574297ddd27139e24d4749eaf20c125851..0000000000000000000000000000000000000000 --- a/internal/agent/tools/web_fetch.md +++ /dev/null @@ -1 +0,0 @@ -Fetch a web URL and return content as markdown; for use inside sub-agents. Large pages (>50KB) are saved to a temp file for grep/view. \ No newline at end of file diff --git a/internal/agent/tools/web_fetch.md.tpl b/internal/agent/tools/web_fetch.md.tpl new file mode 100644 index 0000000000000000000000000000000000000000..fa8f878ed7898c86e63c4c1a4c0ecac605916d0d --- /dev/null +++ b/internal/agent/tools/web_fetch.md.tpl @@ -0,0 +1,2 @@ +Fetch a web URL and return content as markdown; for use inside sub-agents. Large pages (>50KB) are saved to a temp file for grep/view. +{{- 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/web_search.go b/internal/agent/tools/web_search.go index 0d66c60456dc15a65dc8b1ca19a124b97723be7e..22af0c38938cc8d38e013bb1d014b60e5d57cb41 100644 --- a/internal/agent/tools/web_search.go +++ b/internal/agent/tools/web_search.go @@ -3,6 +3,7 @@ package tools import ( "context" _ "embed" + "html/template" "log/slog" "net/http" "time" @@ -10,8 +11,13 @@ import ( "charm.land/fantasy" ) -//go:embed web_search.md -var webSearchToolDescription string +//go:embed web_search.md.tpl +var webSearchDescriptionTmpl []byte + +var webSearchDescriptionTpl = template.Must( + template.New("webSearchDescription"). + Parse(string(webSearchDescriptionTmpl)), +) // NewWebSearchTool creates a web search tool for sub-agents (no permissions needed). func NewWebSearchTool(client *http.Client) fantasy.AgentTool { @@ -29,7 +35,7 @@ func NewWebSearchTool(client *http.Client) fantasy.AgentTool { return fantasy.NewParallelAgentTool( WebSearchToolName, - webSearchToolDescription, + renderToolDescription(webSearchDescriptionTpl), func(ctx context.Context, params WebSearchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) { if params.Query == "" { return fantasy.NewTextErrorResponse("query is required"), nil diff --git a/internal/agent/tools/web_search.md b/internal/agent/tools/web_search.md deleted file mode 100644 index 922bfec3fdf371e763507b553f9a730ab91670a9..0000000000000000000000000000000000000000 --- a/internal/agent/tools/web_search.md +++ /dev/null @@ -1 +0,0 @@ -Search the web via DuckDuckGo; returns titles, URLs, and snippets. Follow up with web_fetch to get full page content. \ No newline at end of file diff --git a/internal/agent/tools/web_search.md.tpl b/internal/agent/tools/web_search.md.tpl new file mode 100644 index 0000000000000000000000000000000000000000..9144ad5014ca9e734aaa2ff9ad0bedf5fa1fcb49 --- /dev/null +++ b/internal/agent/tools/web_search.md.tpl @@ -0,0 +1,2 @@ +Search the web via DuckDuckGo; returns titles, URLs, and snippets. Follow up with web_fetch to get full page content. +{{- if .GhAvailable }} For GitHub searches when an exact repo name, issue, or link is provided, use `gh search` in bash instead.{{- end }}